@base44-preview/sdk 0.8.44-pr.264.d98f3ac → 0.8.44-pr.265.790c3c6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createClient, createClientFromRequest, type Base44Client, type CreateClientConfig, type CreateClientOptions } from "./client.js";
2
+ import { createPlatformClient, type CreatePlatformClientConfig, type PlatformClient, type PrincipalClient } from "./platform-client.js";
2
3
  import { Base44Error, type Base44ErrorJSON } from "./utils/axios-client.js";
3
4
  import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl } from "./utils/auth-utils.js";
4
- export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
5
- export type { Base44Client, CreateClientConfig, CreateClientOptions, Base44ErrorJSON, };
5
+ export { createClient, createClientFromRequest, createPlatformClient, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
6
+ export type { Base44Client, CreateClientConfig, CreateClientOptions, Base44ErrorJSON, CreatePlatformClientConfig, PlatformClient, PrincipalClient, };
6
7
  export * from "./types.js";
7
8
  export type { DeleteManyResult, DeleteResult, EntitiesModule, EntityFilterOperators, EntityFilterQuery, EntityFilterValue, EntityHandler, EntityRecord, EntityTypeRegistry, ImportResult, RealtimeEventType, RealtimeEvent, RealtimeCallback, SortField, UpdateManyResult, } from "./modules/entities.types.js";
8
9
  export type { AuthModule, LoginResponse, RegisterParams, VerifyOtpParams, ChangePasswordParams, ResetPasswordParams, User, } from "./modules/auth.types.js";
@@ -13,6 +14,7 @@ export type { AiGatewayModule, AiGatewayConnection, } from "./modules/ai-gateway
13
14
  export type { AppLogsModule } from "./modules/app-logs.types.js";
14
15
  export type { ActorsModule, ActorClient, ActorRef, Connection, ActorSubscription, ActorConnectOptions, ActorNameRegistry, ActorRegistry, } from "./modules/actors.types.js";
15
16
  export type { SsoModule, SsoAccessTokenResponse } from "./modules/sso.types.js";
17
+ export type { PlatformsModule, PrincipalRole, ProvisionPrincipalParams, ServicePrincipal, DeprovisionResult, } from "./modules/platforms.types.js";
16
18
  export { Actor, type Conn } from "./actor.js";
17
19
  export type { ConnectorsModule, UserConnectorsModule, ConnectorApiRequest, ConnectorApiResponse, ConnectorApiResponsePhase, } from "./modules/connectors.types.js";
18
20
  export type { CustomIntegrationsModule, CustomIntegrationCallParams, CustomIntegrationCallResponse, } from "./modules/custom-integrations.types.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createClient, createClientFromRequest, } from "./client.js";
2
+ import { createPlatformClient, } from "./platform-client.js";
2
3
  import { Base44Error } from "./utils/axios-client.js";
3
4
  import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, } from "./utils/auth-utils.js";
4
- export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
5
+ export { createClient, createClientFromRequest, createPlatformClient, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
5
6
  export * from "./types.js";
6
7
  export { Actor } from "./actor.js";
@@ -1,4 +1,3 @@
1
- import { removeAccessToken } from "../utils/auth-utils.js";
2
1
  import { resetAnalyticsSessionContext } from "./analytics.js";
3
2
  function isInsideIframe() {
4
3
  if (typeof window === "undefined")
@@ -116,10 +115,7 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
116
115
  : window.location.href;
117
116
  // Build the login URL
118
117
  const loginUrl = `${options.appBaseUrl}/login?from_url=${encodeURIComponent(redirectUrl)}`;
119
- // The login page must not boot with the token that just got us here,
120
- // or a rejected token redirects to /login forever.
121
- removeAccessToken({});
122
- removeAccessToken({ storageKey: "token" });
118
+ // Redirect to the login page
123
119
  window.location.href = loginUrl;
124
120
  },
125
121
  // Redirects the user to a provider's login page
@@ -158,8 +154,17 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
158
154
  hasAccessToken = false;
159
155
  // Only do the rest if in a browser environment
160
156
  if (typeof window !== "undefined") {
161
- removeAccessToken({});
162
- removeAccessToken({ storageKey: "token" });
157
+ // Remove token from localStorage
158
+ if (window.localStorage) {
159
+ try {
160
+ window.localStorage.removeItem("base44_access_token");
161
+ // Remove "token" that is set by the built-in SDK of platform version 2
162
+ window.localStorage.removeItem("token");
163
+ }
164
+ catch (e) {
165
+ console.error("Failed to remove token from localStorage:", e);
166
+ }
167
+ }
163
168
  // Determine the from_url parameter
164
169
  const fromUrl = redirectUrl || window.location.href;
165
170
  // Redirect to server-side logout endpoint to clear HTTP-only cookies
@@ -0,0 +1,14 @@
1
+ import type { AxiosInstance } from "axios";
2
+ import type { PlatformsModule } from "./platforms.types.js";
3
+ /**
4
+ * Creates the platforms module.
5
+ *
6
+ * Takes the *provision* client specifically, not the mint client. The two keys
7
+ * are separated so that no code path holding the hot-path key can create or
8
+ * destroy principals, and passing one client here is what keeps that true.
9
+ *
10
+ * @param axios - An Axios instance carrying the `service_users:provision` key.
11
+ * @returns The platforms module.
12
+ * @internal
13
+ */
14
+ export declare function createPlatformsModule(axios: AxiosInstance): PlatformsModule;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Creates the platforms module.
3
+ *
4
+ * Takes the *provision* client specifically, not the mint client. The two keys
5
+ * are separated so that no code path holding the hot-path key can create or
6
+ * destroy principals, and passing one client here is what keeps that true.
7
+ *
8
+ * @param axios - An Axios instance carrying the `service_users:provision` key.
9
+ * @returns The platforms module.
10
+ * @internal
11
+ */
12
+ export function createPlatformsModule(axios) {
13
+ return {
14
+ async provisionPrincipal(params) {
15
+ const body = {
16
+ service_external_id: params.externalId,
17
+ };
18
+ // Omitted rather than sent as null: the server applies its own defaults
19
+ // for both, and `role` in particular is clamped to a ceiling.
20
+ if (params.displayName !== undefined)
21
+ body.display_name = params.displayName;
22
+ if (params.role !== undefined)
23
+ body.role = params.role;
24
+ const response = await axios.post("/api/service/users", body);
25
+ return {
26
+ externalId: response.service_external_id,
27
+ userId: response.user_id,
28
+ email: response.email,
29
+ role: response.role,
30
+ created: response.created,
31
+ };
32
+ },
33
+ async deprovisionPrincipal(externalId) {
34
+ const response = await axios.delete(`/api/service/users/${encodeURIComponent(externalId)}`);
35
+ return {
36
+ externalId: response.service_external_id,
37
+ removed: response.removed,
38
+ };
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The role a service principal holds in the workspace.
3
+ *
4
+ * Capped on the server: a principal is never an owner or an admin, whatever a
5
+ * caller asks for. Passing anything outside this set is clamped down rather than
6
+ * rejected, so a typo cannot quietly grant more than intended.
7
+ */
8
+ export type PrincipalRole = "editor" | "viewer";
9
+ /**
10
+ * Parameters for provisioning a service principal.
11
+ */
12
+ export interface ProvisionPrincipalParams {
13
+ /**
14
+ * Your own identifier for the person this principal acts for — whatever your
15
+ * platform already calls them (`"user_42"`, a UUID, a tenant-scoped handle).
16
+ *
17
+ * It is opaque to Base44 and is the only thing that addresses this principal
18
+ * afterwards, so it must be stable for the life of the account. Never an
19
+ * email, and never an SSO identity.
20
+ */
21
+ externalId: string;
22
+ /**
23
+ * A human-readable name, shown wherever the principal appears in Base44.
24
+ *
25
+ * Cosmetic only: nothing resolves a principal by name.
26
+ */
27
+ displayName?: string;
28
+ /**
29
+ * The workspace role to create the principal with.
30
+ *
31
+ * @defaultValue `"editor"`
32
+ */
33
+ role?: PrincipalRole;
34
+ }
35
+ /**
36
+ * A provisioned service principal.
37
+ */
38
+ export interface ServicePrincipal {
39
+ /** The identifier you provisioned it under. */
40
+ externalId: string;
41
+ /** Base44's own user id for the principal. */
42
+ userId: string;
43
+ /**
44
+ * The synthetic address Base44 generated for it.
45
+ *
46
+ * In a reserved, non-routable domain that can never receive mail — a
47
+ * principal is a robot identity, not a person with an inbox.
48
+ */
49
+ email: string;
50
+ /** The role it actually holds, after the server's ceiling is applied. */
51
+ role: string;
52
+ /**
53
+ * Whether this call created the principal.
54
+ *
55
+ * `false` means it already existed and was returned unchanged. Provisioning is
56
+ * idempotent, so this is informational — not an error to handle.
57
+ */
58
+ created: boolean;
59
+ }
60
+ /**
61
+ * The outcome of deprovisioning a principal.
62
+ */
63
+ export interface DeprovisionResult {
64
+ /** The identifier that was addressed. */
65
+ externalId: string;
66
+ /**
67
+ * Whether this call actually tore a principal down.
68
+ *
69
+ * `false` means nothing matched. Repeating a deprovision is safe and still
70
+ * succeeds, so a `false` on the *first* call is the interesting one: it means
71
+ * the id is wrong and some live principal is still holding vended tokens.
72
+ */
73
+ removed: boolean;
74
+ }
75
+ /**
76
+ * Platform-level operations, scoped to a workspace rather than to one app.
77
+ *
78
+ * Reached through {@link createPlatformClient | createPlatformClient()}, which is
79
+ * the only client that holds workspace keys. This module manages *who* your
80
+ * platform acts as; {@link PlatformClient.asPrincipal | asPrincipal()} is how you
81
+ * then act as one.
82
+ */
83
+ export interface PlatformsModule {
84
+ /**
85
+ * Creates a service principal for one of your users, or returns the existing one.
86
+ *
87
+ * Idempotent per `(workspace, externalId)`, so the intended use is to call it
88
+ * on every request that needs a principal rather than tracking which of your
89
+ * users you have provisioned. The second call is a plain lookup.
90
+ *
91
+ * The principal is created with **no credential of its own** — nothing can log
92
+ * in as it. The only way to act as it is
93
+ * {@link PlatformClient.asPrincipal | asPrincipal()}, which needs your
94
+ * workspace's mint key.
95
+ *
96
+ * Requires the `service_users:provision` key.
97
+ *
98
+ * @param params - The principal to provision.
99
+ * @returns The principal, whether it was just created or already existed.
100
+ *
101
+ * @throws {Base44Error} 409 if the generated address collides with an existing
102
+ * account, 403 if service principals are not enabled for the workspace.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // Safe to call on every request — the second call is just a lookup.
107
+ * const principal = await base44.platforms.provisionPrincipal({
108
+ * externalId: 'user_42',
109
+ * displayName: 'Dana',
110
+ * });
111
+ *
112
+ * const asDana = base44.asPrincipal('user_42');
113
+ * ```
114
+ */
115
+ provisionPrincipal(params: ProvisionPrincipalParams): Promise<ServicePrincipal>;
116
+ /**
117
+ * Removes a service principal from the workspace.
118
+ *
119
+ * This is the offboarding lever, and it bites immediately: every vended token
120
+ * re-checks the workspace membership on each request, so tokens already handed
121
+ * out stop working now rather than when they expire.
122
+ *
123
+ * Requires the `service_users:provision` key — which is why that key should not
124
+ * be reachable from a request path.
125
+ *
126
+ * Deprovisioning does not delete the apps the principal built; it owns them,
127
+ * and they outlive it.
128
+ *
129
+ * @param externalId - The identifier the principal was provisioned under.
130
+ * @returns Whether a principal was actually torn down.
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * // A user closed their account.
135
+ * await base44.platforms.deprovisionPrincipal('user_42');
136
+ * ```
137
+ */
138
+ deprovisionPrincipal(externalId: string): Promise<DeprovisionResult>;
139
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import type { CreatePlatformClientConfig, PlatformClient, PrincipalClient } from "./platform-client.types.js";
2
+ export type { CreatePlatformClientConfig, PlatformClient, PrincipalClient };
3
+ /**
4
+ * Creates a workspace-scoped Base44 client, for platforms that build apps on
5
+ * behalf of their own users.
6
+ *
7
+ * This is the third client factory, and it exists for the one case neither of
8
+ * the others covers. {@linkcode createClient | createClient()} is scoped to one
9
+ * app and one user; {@linkcode createClientFromRequest | createClientFromRequest()}
10
+ * runs inside a Base44-hosted function. A platform is scoped to a **workspace**:
11
+ * it has many users, none of whom have a Base44 account, and it needs each of
12
+ * them to own the apps they build.
13
+ *
14
+ * The model is a *service principal* — a synthetic member of your workspace,
15
+ * created with no credential of its own, that you act as. Your users never see
16
+ * Base44; you keep your own accounts and map each one to a principal.
17
+ *
18
+ * Two steps, and the first is idempotent:
19
+ *
20
+ * 1. {@linkcode PlatformsModule.provisionPrincipal | provisionPrincipal()} to
21
+ * make sure a principal exists for your user.
22
+ * 2. {@linkcode PlatformClient.asPrincipal | asPrincipal()} to act as them.
23
+ *
24
+ * **Server-side only.** This client holds workspace API keys, which authorize
25
+ * every app in the workspace. Never construct one in a browser, and never send
26
+ * either key to one — a browser gets a short-lived token vended *for it*, which
27
+ * is what {@linkcode PrincipalClient.getToken | getToken()} is for.
28
+ *
29
+ * @param config - Configuration object for the platform client.
30
+ * @returns A configured platform client.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { createPlatformClient } from '@base44/sdk';
35
+ *
36
+ * const base44 = createPlatformClient({
37
+ * mintKey: process.env.BASE44_MINT_KEY, // user_tokens:mint
38
+ * provisionKey: process.env.BASE44_PROVISION_KEY, // service_users:provision
39
+ * });
40
+ *
41
+ * // On a request from one of your users:
42
+ * await base44.platforms.provisionPrincipal({
43
+ * externalId: 'user_42',
44
+ * displayName: 'Dana',
45
+ * });
46
+ *
47
+ * const asDana = base44.asPrincipal('user_42');
48
+ * const app = await asDana.forApp(appId);
49
+ * const todos = await app.entities.Todo.list();
50
+ * ```
51
+ */
52
+ export declare function createPlatformClient(config: CreatePlatformClientConfig): PlatformClient;
@@ -0,0 +1,130 @@
1
+ import { createAxiosClient } from "./utils/axios-client.js";
2
+ import { createPlatformsModule } from "./modules/platforms.js";
3
+ import { createPrincipalTokenStore } from "./utils/principal-tokens.js";
4
+ import { createClient } from "./client.js";
5
+ /**
6
+ * Creates a workspace-scoped Base44 client, for platforms that build apps on
7
+ * behalf of their own users.
8
+ *
9
+ * This is the third client factory, and it exists for the one case neither of
10
+ * the others covers. {@linkcode createClient | createClient()} is scoped to one
11
+ * app and one user; {@linkcode createClientFromRequest | createClientFromRequest()}
12
+ * runs inside a Base44-hosted function. A platform is scoped to a **workspace**:
13
+ * it has many users, none of whom have a Base44 account, and it needs each of
14
+ * them to own the apps they build.
15
+ *
16
+ * The model is a *service principal* — a synthetic member of your workspace,
17
+ * created with no credential of its own, that you act as. Your users never see
18
+ * Base44; you keep your own accounts and map each one to a principal.
19
+ *
20
+ * Two steps, and the first is idempotent:
21
+ *
22
+ * 1. {@linkcode PlatformsModule.provisionPrincipal | provisionPrincipal()} to
23
+ * make sure a principal exists for your user.
24
+ * 2. {@linkcode PlatformClient.asPrincipal | asPrincipal()} to act as them.
25
+ *
26
+ * **Server-side only.** This client holds workspace API keys, which authorize
27
+ * every app in the workspace. Never construct one in a browser, and never send
28
+ * either key to one — a browser gets a short-lived token vended *for it*, which
29
+ * is what {@linkcode PrincipalClient.getToken | getToken()} is for.
30
+ *
31
+ * @param config - Configuration object for the platform client.
32
+ * @returns A configured platform client.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { createPlatformClient } from '@base44/sdk';
37
+ *
38
+ * const base44 = createPlatformClient({
39
+ * mintKey: process.env.BASE44_MINT_KEY, // user_tokens:mint
40
+ * provisionKey: process.env.BASE44_PROVISION_KEY, // service_users:provision
41
+ * });
42
+ *
43
+ * // On a request from one of your users:
44
+ * await base44.platforms.provisionPrincipal({
45
+ * externalId: 'user_42',
46
+ * displayName: 'Dana',
47
+ * });
48
+ *
49
+ * const asDana = base44.asPrincipal('user_42');
50
+ * const app = await asDana.forApp(appId);
51
+ * const todos = await app.entities.Todo.list();
52
+ * ```
53
+ */
54
+ export function createPlatformClient(config) {
55
+ const { serverUrl = "https://base44.app", mintKey, provisionKey = mintKey, options, } = config;
56
+ // Three clients, because they carry three different credentials — and the
57
+ // separation is the security property, not tidiness. Nothing reachable from a
58
+ // request should be able to create a principal, and nothing at all should
59
+ // present a workspace key to an endpoint that authenticates a refresh token.
60
+ const mintAxios = createAxiosClient({
61
+ baseURL: serverUrl,
62
+ token: mintKey,
63
+ onError: options === null || options === void 0 ? void 0 : options.onError,
64
+ });
65
+ const provisionAxios = createAxiosClient({
66
+ baseURL: serverUrl,
67
+ token: provisionKey,
68
+ onError: options === null || options === void 0 ? void 0 : options.onError,
69
+ });
70
+ const oauthAxios = createAxiosClient({
71
+ baseURL: serverUrl,
72
+ onError: options === null || options === void 0 ? void 0 : options.onError,
73
+ });
74
+ const tokens = createPrincipalTokenStore({ mintAxios, oauthAxios });
75
+ const platforms = createPlatformsModule(provisionAxios);
76
+ // One principal handle per external id, and one app client per (principal,
77
+ // app). Both are addressed by stable ids and both wrap cached state, so
78
+ // rebuilding them per request would throw away the token cache that makes the
79
+ // mint rate limit survivable.
80
+ const principals = new Map();
81
+ const buildPrincipal = (externalId) => {
82
+ // The token each client was last given, so a rotation can be detected. Held
83
+ // beside the client rather than read back off it because a client does not
84
+ // expose its credential.
85
+ const apps = new Map();
86
+ const getToken = async () => (await tokens.get(externalId)).accessToken;
87
+ return {
88
+ externalId,
89
+ getToken,
90
+ async forApp(appId) {
91
+ const token = await getToken();
92
+ const held = apps.get(appId);
93
+ if (held) {
94
+ // Only on an actual rotation. `setToken` treats the call as an
95
+ // identity change — it discards the in-flight `me()` other callers are
96
+ // awaiting and resets the analytics session — so applying it on every
97
+ // request would undo work the client does on the caller's behalf.
98
+ if (held.token !== token) {
99
+ held.client.setToken(token);
100
+ held.token = token;
101
+ }
102
+ return held.client;
103
+ }
104
+ const client = createClient({ appId, serverUrl, token, options });
105
+ apps.set(appId, { client, token });
106
+ return client;
107
+ },
108
+ async revokeToken() {
109
+ await tokens.revoke(externalId);
110
+ for (const { client } of apps.values())
111
+ client.cleanup();
112
+ apps.clear();
113
+ },
114
+ };
115
+ };
116
+ return {
117
+ platforms,
118
+ asPrincipal(externalId) {
119
+ let principal = principals.get(externalId);
120
+ if (!principal) {
121
+ principal = buildPrincipal(externalId);
122
+ principals.set(externalId, principal);
123
+ }
124
+ return principal;
125
+ },
126
+ getConfig() {
127
+ return { serverUrl };
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,144 @@
1
+ import type { Base44Client, CreateClientOptions } from "./client.types.js";
2
+ import type { PlatformsModule } from "./modules/platforms.types.js";
3
+ /**
4
+ * Configuration for creating a Base44 platform client.
5
+ */
6
+ export interface CreatePlatformClientConfig {
7
+ /**
8
+ * The workspace API key used to vend tokens, holding the `user_tokens:mint` scope.
9
+ *
10
+ * This is the hot-path key: it is presented on every request that needs to act
11
+ * as one of your users. A mint-only key can vend tokens for principals that
12
+ * already exist but cannot *create* one, which is what stops it from being an
13
+ * impersonate-anyone primitive if it leaks.
14
+ */
15
+ mintKey: string;
16
+ /**
17
+ * The workspace API key used to create and remove principals, holding the
18
+ * `service_users:provision` scope.
19
+ *
20
+ * Keep it separate from `mintKey`, and keep it off any code path a request can
21
+ * reach. That separation is what makes deprovisioning stick: with a
22
+ * provision-capable key on the hot path, a removed user can be re-provisioned
23
+ * by the next request that mentions them, quietly undoing the offboarding.
24
+ *
25
+ * @defaultValue `mintKey`, for a single-key deployment. Workable, but weaker
26
+ * for the reason above.
27
+ */
28
+ provisionKey?: string;
29
+ /**
30
+ * The Base44 server URL.
31
+ *
32
+ * @defaultValue `"https://base44.app"`
33
+ */
34
+ serverUrl?: string;
35
+ /**
36
+ * Additional client options.
37
+ */
38
+ options?: CreateClientOptions;
39
+ }
40
+ /**
41
+ * A view of Base44 that acts as one of your users.
42
+ *
43
+ * Obtained from {@link PlatformClient.asPrincipal | asPrincipal()}. Holding one
44
+ * costs nothing — no token is vended until you ask for something.
45
+ */
46
+ export interface PrincipalClient {
47
+ /** The identifier this principal was provisioned under. */
48
+ readonly externalId: string;
49
+ /**
50
+ * A Base44 client for one app, acting as this principal.
51
+ *
52
+ * This is the bridge to the rest of the SDK: the returned client is an ordinary
53
+ * {@link Base44Client}, so `entities`, `agents`, `functions` and the rest work
54
+ * exactly as documented — scoped to what this principal may see, which is
55
+ * usually far less than a service role.
56
+ *
57
+ * Cheap to call repeatedly. The token behind it is cached and renewed for you,
58
+ * and the same app gets the same client back, so a long-lived worker can hold
59
+ * one and a serverless handler can ask for one per request.
60
+ *
61
+ * @param appId - The app to act on.
62
+ * @returns A client scoped to that app, authenticated as this principal.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const asDana = base44.asPrincipal('user_42');
67
+ * const app = await asDana.forApp(appId);
68
+ *
69
+ * // Every module, with Dana's permissions rather than the workspace's.
70
+ * const todos = await app.entities.Todo.list();
71
+ * ```
72
+ */
73
+ forApp(appId: string): Promise<Base44Client>;
74
+ /**
75
+ * The raw access token for this principal, minting or renewing as needed.
76
+ *
77
+ * Most code should use {@link PrincipalClient.forApp | forApp()} instead. Reach
78
+ * for this when you need to authenticate a request the SDK does not make for
79
+ * you.
80
+ *
81
+ * Do not cache what this returns — it is already cached, and holding a copy is
82
+ * how a caller ends up using a token the store has since replaced.
83
+ *
84
+ * @returns A currently-valid access token.
85
+ *
86
+ * @throws {Base44Error} 404 if no principal matches, which is what an
87
+ * un-provisioned `externalId` looks like: minting never creates one.
88
+ */
89
+ getToken(): Promise<string>;
90
+ /**
91
+ * Forgets this principal's cached token and revokes its refresh token.
92
+ *
93
+ * Use when a user signs out of *your* platform. It does not remove the
94
+ * principal — the apps it built belong to it, and signing out should not hand
95
+ * them to the workspace owner. Removing it is
96
+ * {@link PlatformsModule.deprovisionPrincipal | deprovisionPrincipal()}.
97
+ *
98
+ * Only the refresh token can be revoked; a live access token remains valid for
99
+ * the rest of its hour. Deprovisioning is the lever that cuts one off
100
+ * immediately.
101
+ */
102
+ revokeToken(): Promise<void>;
103
+ }
104
+ /**
105
+ * A workspace-scoped Base44 client, for platforms that build apps on behalf of
106
+ * their own users.
107
+ *
108
+ * The third client factory, beside {@link createClient | createClient()} (one
109
+ * app, one user) and {@link createClientFromRequest | createClientFromRequest()}
110
+ * (inside a Base44 function). This one is scoped to a *workspace*: it manages the
111
+ * identities your users act as, and hands you a client per identity.
112
+ */
113
+ export interface PlatformClient {
114
+ /** {@link PlatformsModule | Platforms module} for managing service principals. */
115
+ platforms: PlatformsModule;
116
+ /**
117
+ * Acts as one of your users.
118
+ *
119
+ * Mirrors `base44.asServiceRole` in shape — the same SDK, different
120
+ * permissions — but parameterised, because a platform has many identities
121
+ * rather than one privileged one.
122
+ *
123
+ * The principal must already exist;
124
+ * {@link PlatformsModule.provisionPrincipal | provisionPrincipal()} is
125
+ * idempotent and safe to call first on every request.
126
+ *
127
+ * @param externalId - The identifier the principal was provisioned under.
128
+ * @returns A view of Base44 that acts as that principal.
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * await base44.platforms.provisionPrincipal({ externalId: 'user_42' });
133
+ * const asDana = base44.asPrincipal('user_42');
134
+ * ```
135
+ */
136
+ asPrincipal(externalId: string): PrincipalClient;
137
+ /**
138
+ * Gets the current client configuration.
139
+ * @internal
140
+ */
141
+ getConfig(): {
142
+ serverUrl: string;
143
+ };
144
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import type { AxiosInstance } from "axios";
2
+ /**
3
+ * The OAuth client id every service-principal token is issued under.
4
+ *
5
+ * Deliberately not one of Base44's MCP client prefixes — a token whose client id
6
+ * starts with one of those is rejected everywhere except `/mcp`.
7
+ */
8
+ export declare const SERVICE_CLIENT_ID = "svc_delegate";
9
+ /**
10
+ * Re-mint this long before a token's stated expiry.
11
+ *
12
+ * Access tokens are vended with an explicit one-hour lifetime, so this is a few
13
+ * percent of the token's life — enough that a slow call cannot land after the
14
+ * token it was authorized with has expired.
15
+ */
16
+ export declare const REFRESH_SKEW_MS: number;
17
+ /** A vended access token and what is needed to renew it. */
18
+ export interface VendedToken {
19
+ accessToken: string;
20
+ refreshToken?: string;
21
+ /** Epoch milliseconds at which the access token stops being accepted. */
22
+ expiresAt: number;
23
+ }
24
+ interface TokenStoreConfig {
25
+ /** Carries the `user_tokens:mint` key. Used for minting and nothing else. */
26
+ mintAxios: AxiosInstance;
27
+ /**
28
+ * Carries no credential at all.
29
+ *
30
+ * `/oauth/token` and `/oauth/revoke` authenticate the *refresh token*, not the
31
+ * caller, so presenting a workspace key there would be sending a
32
+ * long-lived secret somewhere it is neither wanted nor checked.
33
+ */
34
+ oauthAxios: AxiosInstance;
35
+ refreshSkewMs?: number;
36
+ }
37
+ /**
38
+ * Caches one vended token per principal, and renews it before it lapses.
39
+ *
40
+ * Caching is not an optimization here. Minting is rate-limited **per workspace**,
41
+ * so a platform that mints on every request spends one shared budget on behalf of
42
+ * every user at once and starts failing under exactly the load it was built for.
43
+ * A vended token is good for an hour; this holds it for that hour.
44
+ *
45
+ * @internal
46
+ */
47
+ export declare function createPrincipalTokenStore({ mintAxios, oauthAxios, refreshSkewMs, }: TokenStoreConfig): {
48
+ /**
49
+ * A currently-valid token for the principal, minting or renewing only when
50
+ * the held one is spent.
51
+ */
52
+ get(externalId: string): Promise<VendedToken>;
53
+ /**
54
+ * Drops the held token and asks Base44 to revoke its refresh token.
55
+ *
56
+ * Only the refresh half is revocable — the access token is self-contained and
57
+ * stays valid until it expires. To cut a principal off *now*, deprovision it:
58
+ * the workspace membership is re-checked on every request.
59
+ */
60
+ revoke(externalId: string): Promise<void>;
61
+ };
62
+ /** @internal */
63
+ export type PrincipalTokenStore = ReturnType<typeof createPrincipalTokenStore>;
64
+ export {};
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The OAuth client id every service-principal token is issued under.
3
+ *
4
+ * Deliberately not one of Base44's MCP client prefixes — a token whose client id
5
+ * starts with one of those is rejected everywhere except `/mcp`.
6
+ */
7
+ export const SERVICE_CLIENT_ID = "svc_delegate";
8
+ /**
9
+ * Re-mint this long before a token's stated expiry.
10
+ *
11
+ * Access tokens are vended with an explicit one-hour lifetime, so this is a few
12
+ * percent of the token's life — enough that a slow call cannot land after the
13
+ * token it was authorized with has expired.
14
+ */
15
+ export const REFRESH_SKEW_MS = 5 * 60 * 1000;
16
+ /**
17
+ * Caches one vended token per principal, and renews it before it lapses.
18
+ *
19
+ * Caching is not an optimization here. Minting is rate-limited **per workspace**,
20
+ * so a platform that mints on every request spends one shared budget on behalf of
21
+ * every user at once and starts failing under exactly the load it was built for.
22
+ * A vended token is good for an hour; this holds it for that hour.
23
+ *
24
+ * @internal
25
+ */
26
+ export function createPrincipalTokenStore({ mintAxios, oauthAxios, refreshSkewMs = REFRESH_SKEW_MS, }) {
27
+ const cache = new Map();
28
+ // One entry per principal currently being fetched. Without this, N concurrent
29
+ // requests for a user whose token just lapsed each fire their own mint — a
30
+ // self-inflicted burst against the very limit the cache exists to respect.
31
+ const inFlight = new Map();
32
+ const toVended = (response) => {
33
+ var _a;
34
+ const lifetimeMs = Math.max(Number(response.expires_in) || 0, 0) * 1000;
35
+ // Never let the skew consume the whole lifetime: a token considered stale on
36
+ // arrival would mint again on the next call, and again, turning the cache
37
+ // into a mint-per-request loop against a shared budget.
38
+ const skew = Math.min(refreshSkewMs, lifetimeMs / 2);
39
+ return {
40
+ accessToken: response.access_token,
41
+ refreshToken: (_a = response.refresh_token) !== null && _a !== void 0 ? _a : undefined,
42
+ expiresAt: Date.now() + lifetimeMs - skew,
43
+ };
44
+ };
45
+ const mint = async (externalId) => {
46
+ const response = await mintAxios.post("/api/service/user-tokens", { service_external_id: externalId });
47
+ return toVended(response);
48
+ };
49
+ const renew = async (refreshToken) => {
50
+ const response = await oauthAxios.post("/oauth/token", new URLSearchParams({
51
+ grant_type: "refresh_token",
52
+ refresh_token: refreshToken,
53
+ client_id: SERVICE_CLIENT_ID,
54
+ }), { headers: { "Content-Type": "application/x-www-form-urlencoded" } });
55
+ return toVended(response);
56
+ };
57
+ const fetchToken = async (externalId) => {
58
+ const held = cache.get(externalId);
59
+ if (held === null || held === void 0 ? void 0 : held.refreshToken) {
60
+ try {
61
+ return await renew(held.refreshToken);
62
+ }
63
+ catch (_a) {
64
+ // A refresh token can be revoked, expired, or invalidated by a role
65
+ // change. Minting is the recovery, and it re-checks everything the
66
+ // refresh would have — so falling through is not a way around a
67
+ // revocation, it just costs one extra round trip.
68
+ }
69
+ }
70
+ return mint(externalId);
71
+ };
72
+ return {
73
+ /**
74
+ * A currently-valid token for the principal, minting or renewing only when
75
+ * the held one is spent.
76
+ */
77
+ async get(externalId) {
78
+ const held = cache.get(externalId);
79
+ if (held && held.expiresAt > Date.now())
80
+ return held;
81
+ const pending = inFlight.get(externalId);
82
+ if (pending)
83
+ return pending;
84
+ const request = fetchToken(externalId)
85
+ .then((token) => {
86
+ cache.set(externalId, token);
87
+ return token;
88
+ })
89
+ .finally(() => {
90
+ inFlight.delete(externalId);
91
+ });
92
+ inFlight.set(externalId, request);
93
+ return request;
94
+ },
95
+ /**
96
+ * Drops the held token and asks Base44 to revoke its refresh token.
97
+ *
98
+ * Only the refresh half is revocable — the access token is self-contained and
99
+ * stays valid until it expires. To cut a principal off *now*, deprovision it:
100
+ * the workspace membership is re-checked on every request.
101
+ */
102
+ async revoke(externalId) {
103
+ const held = cache.get(externalId);
104
+ cache.delete(externalId);
105
+ if (!(held === null || held === void 0 ? void 0 : held.refreshToken))
106
+ return;
107
+ // Best effort: a failed revoke must not leave a caller unable to forget a
108
+ // principal locally.
109
+ try {
110
+ await oauthAxios.post("/oauth/revoke", new URLSearchParams({
111
+ token: held.refreshToken,
112
+ client_id: SERVICE_CLIENT_ID,
113
+ }), { headers: { "Content-Type": "application/x-www-form-urlencoded" } });
114
+ }
115
+ catch (_a) {
116
+ /* the local record is already gone, which is the part that matters */
117
+ }
118
+ },
119
+ };
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.44-pr.264.d98f3ac",
3
+ "version": "0.8.44-pr.265.790c3c6",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",