Skip to content

Commit 4ff862b

Browse files
committed
feat(connectors): add getConnection({ connectorId }) overload for BYO_SHARED
Backend functions can now retrieve the OAuth access token + connection config for a workspace-registered connector (backed by an OrganizationConnector that the app builder consented to) via a new overload on getConnection: await base44.asServiceRole.connectors.getConnection({ connectorId: "..." }); Existing string-arg callers are unchanged — they continue to resolve to the platform-SHARED path. The overload dispatches on argument shape (string vs object) and hits a new server route GET /apps/{app_id}/external-auth/tokens/by-connector/{connector_id} for the workspace-connector lookup.
1 parent 5a14f31 commit 4ff862b

3 files changed

Lines changed: 111 additions & 5 deletions

File tree

src/modules/connectors.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,24 @@ export function createConnectorsModule(
4242
},
4343

4444
async getConnection(
45-
integrationType: ConnectorIntegrationType
45+
arg: ConnectorIntegrationType | { connectorId: string }
4646
): Promise<ConnectorConnectionResponse> {
47-
if (!integrationType || typeof integrationType !== "string") {
47+
let url: string;
48+
if (typeof arg === "string") {
49+
if (!arg) {
50+
throw new Error("Integration type is required and must be a string");
51+
}
52+
url = `/apps/${appId}/external-auth/tokens/${arg}`;
53+
} else if (arg && typeof arg === "object" && typeof arg.connectorId === "string") {
54+
if (!arg.connectorId) {
55+
throw new Error("Connector ID is required and must be a string");
56+
}
57+
url = `/apps/${appId}/external-auth/tokens/by-connector/${arg.connectorId}`;
58+
} else {
4859
throw new Error("Integration type is required and must be a string");
4960
}
5061

51-
const response = await axios.get<ConnectorAccessTokenResponse>(
52-
`/apps/${appId}/external-auth/tokens/${integrationType}`
53-
);
62+
const response = await axios.get<ConnectorAccessTokenResponse>(url);
5463

5564
const data = response as unknown as ConnectorAccessTokenResponse;
5665
return {

src/modules/connectors.types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,35 @@ export interface ConnectorsModule {
243243
integrationType: ConnectorIntegrationType,
244244
): Promise<ConnectorConnectionResponse>;
245245

246+
/**
247+
* Retrieves the OAuth access token and connection configuration for a **workspace-registered** connector
248+
* (a connector backed by an OAuth app registered in the workspace, consented to once by the app builder).
249+
*
250+
* Use this overload when the app's backend function needs to use a connector identified by its
251+
* workspace-connector ID rather than a platform integration type. The token returned represents
252+
* the app builder's consent against the workspace's OAuth app and is shared across all app users
253+
* of the app — identical semantics to the platform-shared `getConnection(integrationType)` form,
254+
* differing only in which OAuth app was used to produce the token.
255+
*
256+
* @param opts - An object with `connectorId` — the ID of the workspace connector (the `OrganizationConnector` database ID) as surfaced in the builder chat context.
257+
* @returns Promise resolving to a {@link ConnectorConnectionResponse} with `accessToken` and `connectionConfig`.
258+
*
259+
* @example
260+
* ```typescript
261+
* // Get the connection for a workspace-registered connector
262+
* const { accessToken, connectionConfig } = await base44.asServiceRole.connectors.getConnection({
263+
* connectorId: 'abc123def',
264+
* });
265+
*
266+
* const response = await fetch(`https://${connectionConfig?.subdomain}.snowflakecomputing.com/api/v2/statements`, {
267+
* headers: { Authorization: `Bearer ${accessToken}` },
268+
* });
269+
* ```
270+
*/
271+
getConnection(
272+
opts: { connectorId: string },
273+
): Promise<ConnectorConnectionResponse>;
274+
246275
/**
247276
* Retrieves an OAuth access token for an end user's connection to a specific connector.
248277
*

tests/unit/connectors.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,74 @@ describe("Connectors module – getConnection", () => {
100100
});
101101
});
102102

103+
describe("Connectors module – getConnection({ connectorId })", () => {
104+
const appId = "test-app-id";
105+
const serverUrl = "https://base44.app";
106+
const serviceToken = "service-token-123";
107+
let base44: ReturnType<typeof createClient>;
108+
let scope: nock.Scope;
109+
110+
beforeEach(() => {
111+
base44 = createClient({
112+
serverUrl,
113+
appId,
114+
serviceToken,
115+
});
116+
scope = nock(serverUrl);
117+
});
118+
119+
afterEach(() => {
120+
nock.cleanAll();
121+
});
122+
123+
test("extracts accessToken and connectionConfig from by-connector endpoint", async () => {
124+
const apiResponse = {
125+
access_token: "builder-oauth-token-xyz789",
126+
integration_type: "snowflake",
127+
connection_config: { subdomain: "xy12345.us-east-1" },
128+
};
129+
130+
scope
131+
.get(`/api/apps/${appId}/external-auth/tokens/by-connector/connector-abc`)
132+
.reply(200, apiResponse);
133+
134+
const connection = await base44.asServiceRole.connectors.getConnection({
135+
connectorId: "connector-abc",
136+
});
137+
138+
expect(connection.accessToken).toBe("builder-oauth-token-xyz789");
139+
expect(connection.connectionConfig).toEqual({
140+
subdomain: "xy12345.us-east-1",
141+
});
142+
expect(scope.isDone()).toBe(true);
143+
});
144+
145+
test("returns connectionConfig as null when API omits connection_config", async () => {
146+
const apiResponse = {
147+
access_token: "token-only",
148+
integration_type: "databricks",
149+
};
150+
151+
scope
152+
.get(`/api/apps/${appId}/external-auth/tokens/by-connector/conn-2`)
153+
.reply(200, apiResponse);
154+
155+
const connection = await base44.asServiceRole.connectors.getConnection({
156+
connectorId: "conn-2",
157+
});
158+
159+
expect(connection.accessToken).toBe("token-only");
160+
expect(connection.connectionConfig).toBeNull();
161+
expect(scope.isDone()).toBe(true);
162+
});
163+
164+
test("throws when connectorId is empty string", async () => {
165+
await expect(
166+
base44.asServiceRole.connectors.getConnection({ connectorId: "" })
167+
).rejects.toThrow("Connector ID is required and must be a string");
168+
});
169+
});
170+
103171
describe("Connectors module – getCurrentAppUserConnection", () => {
104172
const appId = "test-app-id";
105173
const serverUrl = "https://base44.app";

0 commit comments

Comments
 (0)