Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/modules/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ export function createConnectorsModule(
};
},

async getWorkspaceConnection(
connectorId: string
): Promise<ConnectorConnectionResponse> {
if (!connectorId || typeof connectorId !== "string") {
throw new Error("Connector ID is required and must be a string");
}

const response = await axios.get<ConnectorAccessTokenResponse>(
`/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.
*/
Expand Down
29 changes: 29 additions & 0 deletions src/modules/connectors.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,35 @@ export interface ConnectorsModule {
integrationType: ConnectorIntegrationType,
): Promise<ConnectorConnectionResponse>;

/**
* 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<ConnectorConnectionResponse>;

/**
* @internal
* @deprecated Use {@link getCurrentAppUserConnection} instead.
Expand Down
76 changes: 76 additions & 0 deletions tests/unit/connectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createClient>;
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";
Expand Down
Loading