diff --git a/src/modules/connectors.ts b/src/modules/connectors.ts index 7755907..2fd902e 100644 --- a/src/modules/connectors.ts +++ b/src/modules/connectors.ts @@ -59,6 +59,24 @@ export function createConnectorsModule( }; }, + async getWorkspaceConnection( + connectorId: string + ): Promise { + if (!connectorId || typeof connectorId !== "string") { + throw new Error("Connector ID is required and must be a string"); + } + + const response = await axios.get( + `/apps/${appId}/external-auth/tokens/connectors/${connectorId}` + ); + + const data = response as unknown as ConnectorAccessTokenResponse; + return { + accessToken: data.access_token, + connectionConfig: data.connection_config ?? null, + }; + }, + /** * @deprecated Use getCurrentAppUserConnection(connectorId) and use the returned accessToken (and connectionConfig when needed) instead. */ diff --git a/src/modules/connectors.types.ts b/src/modules/connectors.types.ts index 84e186c..ae5273d 100644 --- a/src/modules/connectors.types.ts +++ b/src/modules/connectors.types.ts @@ -262,6 +262,35 @@ export interface ConnectorsModule { integrationType: ConnectorIntegrationType, ): Promise; + /** + * Retrieves the OAuth access token and connection configuration for a **workspace-registered** connector + * (a connector backed by an OAuth app registered in the workspace, consented to once by the app builder). + * + * Use this method when the app's backend function needs to use a connector identified by its + * workspace-connector ID rather than a platform integration type. The token returned represents + * the app builder's consent against the workspace's OAuth app and is shared across all app users + * of the app — identical semantics to the platform-shared {@link getConnection} form, + * differing only in which OAuth app was used to produce the token. + * + * @param connectorId - The ID of the workspace connector (the `OrganizationConnector` database ID) as surfaced in the builder chat context. + * @returns Promise resolving to a {@link ConnectorConnectionResponse} with `accessToken` and `connectionConfig`. + * + * @example + * ```typescript + * // Get the connection for a workspace-registered connector + * const { accessToken, connectionConfig } = await base44.asServiceRole.connectors.getWorkspaceConnection( + * 'abc123def', + * ); + * + * const response = await fetch(`https://${connectionConfig?.subdomain}.snowflakecomputing.com/api/v2/statements`, { + * headers: { Authorization: `Bearer ${accessToken}` }, + * }); + * ``` + */ + getWorkspaceConnection( + connectorId: string, + ): Promise; + /** * @internal * @deprecated Use {@link getCurrentAppUserConnection} instead. diff --git a/tests/unit/connectors.test.ts b/tests/unit/connectors.test.ts index 84f2edc..c5dabfc 100644 --- a/tests/unit/connectors.test.ts +++ b/tests/unit/connectors.test.ts @@ -100,6 +100,82 @@ describe("Connectors module – getConnection", () => { }); }); +describe("Connectors module – getWorkspaceConnection", () => { + const appId = "test-app-id"; + const serverUrl = "https://base44.app"; + const serviceToken = "service-token-123"; + let base44: ReturnType; + let scope: nock.Scope; + + beforeEach(() => { + base44 = createClient({ + serverUrl, + appId, + serviceToken, + }); + scope = nock(serverUrl); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + test("extracts accessToken and connectionConfig from connectors endpoint", async () => { + const apiResponse = { + access_token: "builder-oauth-token-xyz789", + integration_type: "snowflake", + connection_config: { subdomain: "xy12345.us-east-1" }, + }; + + scope + .get(`/api/apps/${appId}/external-auth/tokens/connectors/connector-abc`) + .reply(200, apiResponse); + + const connection = + await base44.asServiceRole.connectors.getWorkspaceConnection( + "connector-abc" + ); + + expect(connection.accessToken).toBe("builder-oauth-token-xyz789"); + expect(connection.connectionConfig).toEqual({ + subdomain: "xy12345.us-east-1", + }); + expect(scope.isDone()).toBe(true); + }); + + test("returns connectionConfig as null when API omits connection_config", async () => { + const apiResponse = { + access_token: "token-only", + integration_type: "databricks", + }; + + scope + .get(`/api/apps/${appId}/external-auth/tokens/connectors/conn-2`) + .reply(200, apiResponse); + + const connection = + await base44.asServiceRole.connectors.getWorkspaceConnection("conn-2"); + + expect(connection.accessToken).toBe("token-only"); + expect(connection.connectionConfig).toBeNull(); + expect(scope.isDone()).toBe(true); + }); + + test("throws when connectorId is empty string", async () => { + await expect( + base44.asServiceRole.connectors.getWorkspaceConnection("") + ).rejects.toThrow("Connector ID is required and must be a string"); + }); + + test("throws when connectorId is not a string", async () => { + await expect( + base44.asServiceRole.connectors.getWorkspaceConnection( + null as unknown as string + ) + ).rejects.toThrow("Connector ID is required and must be a string"); + }); +}); + describe("Connectors module – getCurrentAppUserConnection", () => { const appId = "test-app-id"; const serverUrl = "https://base44.app";