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
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,10 @@ export function extractSignatureInfo(
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Match function signature: > **methodName**(...): `returnType` or `returnType`\<`generic`\>
// Method-level generics appear between the bold name and arguments.
// Handle both simple types and generic types like `Promise`\<`any`\> or `Promise`\<[`TypeName`](link)\>
const sigMatch = line.match(
/^>\s*\*\*(\w+)\*\*\([^)]*\):\s*`([^`]+)`(?:\\<(.+?)\\>)?/
/^>\s*\*\*(\w+)\*\*(?:\\<.*?\\>)?\([^)]*\):\s*`([^`]+)`(?:\\<(.+?)\\>)?/
);
if (sigMatch) {
const methodName = sigMatch[1];
Expand Down Expand Up @@ -368,7 +369,9 @@ function rewriteReturnSections(content, options) {
let sigLineIdx = i - 2; // Go back past the Returns heading
while (
sigLineIdx >= 0 &&
!lines[sigLineIdx].match(/^>\s*\*\*\w+\*\*\(/)
!lines[sigLineIdx].match(
/^>\s*\*\*\w+\*\*(?:\\<.*?\\>)?\(/
)
) {
sigLineIdx--;
}
Expand Down Expand Up @@ -467,7 +470,9 @@ function rewriteReturnSections(content, options) {
let sigLineIdx = i - 2; // Go back past the Returns heading
while (
sigLineIdx >= 0 &&
!lines[sigLineIdx].match(/^>\s*\*\*\w+\*\*\(/)
!lines[sigLineIdx].match(
/^>\s*\*\*\w+\*\*(?:\\<.*?\\>)?\(/
)
) {
sigLineIdx--;
}
Expand Down
3 changes: 3 additions & 0 deletions scripts/mintlify-post-processing/types-to-expose.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"AnalyticsModule",
"AppLogsModule",
"AuthModule",
"ConnectorApiRequest",
"ConnectorApiResponse",
"ConnectorApiResponsePhase",
"ConnectorIntegrationType",
"ConnectorIntegrationTypeRegistry",
"ConnectorsModule",
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ export { Actor, type Conn } from "./actor.js";
export type {
ConnectorsModule,
UserConnectorsModule,
ConnectorApiRequest,
ConnectorApiResponse,
ConnectorApiResponsePhase,
} from "./modules/connectors.types.js";

export type {
Expand Down
74 changes: 74 additions & 0 deletions src/modules/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@ import { AxiosInstance } from "axios";
import {
ConnectorIntegrationType,
ConnectorAccessTokenResponse,
ConnectorApiRequest,
ConnectorApiResponse,
ConnectorConnectionResponse,
ConnectorProxyRawResponse,
AppUserConnectorConnectionResponse,
ConnectorsModule,
UserConnectorsModule,
} from "./connectors.types.js";

const CONNECTOR_API_METHODS = new Set([
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"HEAD",
]);

/**
* Creates the Connectors module for the Base44 SDK.
*
Expand Down Expand Up @@ -112,6 +124,68 @@ export function createConnectorsModule(
connectionConfig: data.connection_config ?? null,
};
},

async callApi<T = unknown>(
integrationType: ConnectorIntegrationType,
request: ConnectorApiRequest
): Promise<ConnectorApiResponse<T>> {
assertNonEmptyString(integrationType, "Integration type");
return proxyCall<T>(
axios,
`/apps/${appId}/connectors/${integrationType}/call`,
request
);
},
};
}

function assertNonEmptyString(value: unknown, label: string): void {
if (!value || typeof value !== "string") {
throw new Error(`${label} is required and must be a string`);
}
}

/**
* POST a request to the connector proxy and normalize the response.
*
* The proxy reports upstream outcomes in the body rather than as HTTP status, so
* a provider 4xx/5xx arrives here as a resolved response with `success: false` —
* only Base44-side failures reject through the axios error interceptor.
*
* @internal
*/
async function proxyCall<T>(
axios: AxiosInstance,
url: string,
request: ConnectorApiRequest
): Promise<ConnectorApiResponse<T>> {
if (!request || typeof request !== "object") {
throw new Error("Request is required and must be an object");
}
assertNonEmptyString(request.path, "Request path");
const method = request.method ?? "GET";
if (!CONNECTOR_API_METHODS.has(method)) {
throw new Error(
"Request method must be one of GET, POST, PUT, PATCH, DELETE, or HEAD"
);
}

const response = await axios.post(url, {
method,
path: request.path,
query: request.query ?? {},
headers: request.headers ?? {},
body: request.body ?? null,
});

const data = response as unknown as ConnectorProxyRawResponse;
return {
success: data.success,
phase: data.phase,
status: data.status_code ?? null,
data: data.data as T,
headers: data.headers ?? {},
creditsCharged: data.credits_charged ?? 0,
};
}

Expand Down
116 changes: 116 additions & 0 deletions src/modules/connectors.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,71 @@ export interface AppUserConnectorConnectionResponse {
connectionConfig: Record<string, string> | null;
}

/**
* How far a metered connector call progressed through the Base44 proxy.
*
* Only `not_sent` proves that the provider did not execute the request.
* `timed_out` and `sent_unconfirmed` may have executed upstream, so do not
* automatically retry non-idempotent requests based on those phases.
*/
export type ConnectorApiResponsePhase =
| "not_sent"
| "responded"
| "timed_out"
| "sent_unconfirmed";

/**
* A request to forward to a metered connector's API through the Base44 proxy.
*/
export interface ConnectorApiRequest {
/** HTTP method for the upstream request. Defaults to `'GET'`. */
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
/**
* Path relative to the connector's API root, starting with `/`, such as `'/2/tweets'`.
*
* Must not be an absolute URL. Query parameters may be included here or passed
* separately as {@link query}; either way they are forwarded and priced identically.
*/
path: string;
/** Query parameters. Merged into the request URL alongside any already present in {@link path}. */
query?: Record<string, string | number | boolean | Array<string | number>>;
/** Extra request headers. Only headers the connector explicitly allows are forwarded; the rest are dropped. */
headers?: Record<string, string>;
/** JSON request body. Ignored for `GET`, `HEAD`, and `DELETE`. */
body?: unknown;
}

/**
* The upstream API's response, as returned by the Base44 connector proxy.
*/
export interface ConnectorApiResponse<T = unknown> {
/** `true` only when the upstream API returned a 2xx status. Proxy and upstream errors are `false`. */
success: boolean;
/** How far the call progressed. Only `not_sent` proves the provider did not execute it. */
phase: ConnectorApiResponsePhase;
/** The upstream HTTP status code, or `null` when no response was received. */
status: number | null;
/** The parsed upstream response body, or proxy error details when no response was received. */
data: T;
/** The subset of upstream response headers the connector exposes, typically rate-limit counters. */
headers: Record<string, string>;
/** Integration credits billed to the workspace for this call. */
creditsCharged: number;
}

/**
* Raw proxy response shape. Mapped to {@link ConnectorApiResponse} before being returned.
* @internal
*/
export interface ConnectorProxyRawResponse {
success: boolean;
phase: ConnectorApiResponsePhase;
status_code: number | null;
data: unknown;
headers: Record<string, string>;
credits_charged: number;
}

/**
* Connectors module for managing OAuth tokens for external services.
*
Expand Down Expand Up @@ -78,6 +143,18 @@ export interface AppUserConnectorConnectionResponse {
* 3. In a backend function, call {@linkcode getCurrentAppUserConnection | getCurrentAppUserConnection()} using the service role client (`base44.asServiceRole.connectors`) with the connector ID to retrieve the app user's token.
* 4. Use the returned `accessToken` to call the external service's API directly. Some connectors also return a `connectionConfig` with additional values such as a subdomain for building the API URL.
*
* ## Metered connectors
*
* A few [platform connectors](#shared-connectors) are backed by paid third-party APIs that charge Base44 per call. For those, the OAuth token is **not** available to your code — {@linkcode getConnection | getConnection()} rejects with a `403`. Call them with {@linkcode callApi | callApi()} instead: Base44 attaches the credential server-side, forwards the request, and bills your workspace's integration credits for the call.
*
* This applies to platform connectors only. A workspace-registered or app user connector runs on **your own** OAuth app, so the provider invoices you directly and there is nothing for Base44 to meter — those keep normal token access via {@linkcode getWorkspaceConnection | getWorkspaceConnection()} and {@linkcode getCurrentAppUserConnection | getCurrentAppUserConnection()}.
*
* Two things to keep in mind when writing against a metered connector:
*
* - **Cost varies by endpoint, sometimes sharply.** The same connector can charge two orders of magnitude more for one endpoint than another, so avoid putting an expensive call inside a loop and batch wherever the provider supports it. Each response reports what it actually cost as `creditsCharged`.
* - **Provider and transport outcomes are returned, not thrown.** A provider `4xx`/`5xx` or a connection failure comes back as `success: false` with its `phase`; authorization, quota, and invalid proxy requests reject the promise.
* - **Only `phase: 'not_sent'` proves the provider did not execute the request.** A timeout or in-flight failure may have executed upstream, so do not automatically retry a non-idempotent call unless the provider supports an idempotency key.
*
* ## Available connectors
*
* The connectors below can be used as shared connectors or as app user connectors. For a shared platform connector, pass the integration type string to {@linkcode getConnection | getConnection()}. For a connector you register in Workspace Settings with your own OAuth app, use the connector ID with {@linkcode getWorkspaceConnection | getWorkspaceConnection()} for a shared token, or with {@linkcode getCurrentAppUserConnection | getCurrentAppUserConnection()} for a per-user token.
Expand Down Expand Up @@ -345,6 +422,45 @@ export interface ConnectorsModule {
getCurrentAppUserConnection(
connectorId: string,
): Promise<AppUserConnectorConnectionResponse>;

/**
* Calls a [metered connector's](#metered-connectors) API through the Base44 proxy.
*
* Use this for a shared platform connector identified by an integration type. Base44 adds the OAuth credential to the outgoing request, forwards it, and bills the workspace for the call, so you never handle the token yourself.
*
* @param integrationType - The type of integration, such as `'x'`. See [Available connectors](#available-connectors).
* @param request - The upstream request to forward. See {@link ConnectorApiRequest}.
* @returns Promise resolving to a {@link ConnectorApiResponse}. Note that an upstream error is reported in `success` and `status`, not thrown — only Base44-side failures reject.
*
* @example
* ```typescript
* // Post to X
* const res = await base44.asServiceRole.connectors.callApi('x', {
* method: 'POST',
* path: '/2/tweets',
* body: { text: 'Shipped!' },
* });
*
* if (!res.success) {
* console.error('X rejected the post', res.status, res.data);
* }
* ```
*
* @example
* ```typescript
* // Read, with query parameters and a look at what the call cost
* const res = await base44.asServiceRole.connectors.callApi('x', {
* path: '/2/tweets/search/recent',
* query: { query: 'base44', max_results: 10 },
* });
*
* console.log(`${res.creditsCharged} credits`, res.data);
* ```
*/
callApi<T = unknown>(
integrationType: ConnectorIntegrationType,
request: ConnectorApiRequest,
): Promise<ConnectorApiResponse<T>>;
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/utils/axios-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,9 @@ export function createAxiosClient({
const base44Error = new Base44Error(
message,
error.response?.status,
error.response?.data?.code,
error.response?.data?.code ??
error.response?.headers?.get?.("x-base44-connector-error") ??
error.response?.headers?.["x-base44-connector-error"],
error.response?.data,
error
);
Expand Down
31 changes: 31 additions & 0 deletions tests/types/connectors.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {
ConnectorApiRequest,
ConnectorApiResponse,
ConnectorApiResponsePhase,
} from "../../src/index.js";

const phase = "sent_unconfirmed" satisfies ConnectorApiResponsePhase;

const request = {
method: "POST",
path: "/2/tweets",
} satisfies ConnectorApiRequest;

const response = {
success: false,
phase,
status: null,
data: { error: "request outcome unknown" },
headers: {},
creditsCharged: 3,
} satisfies ConnectorApiResponse;

const rejectsLowercaseMethod = {
// @ts-expect-error Connector methods use the uppercase wire values.
method: "post",
path: "/2/tweets",
} satisfies ConnectorApiRequest;

void request;
void response;
void rejectsLowercaseMethod;
Loading
Loading