@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 +4 -2
- package/dist/index.js +2 -1
- package/dist/modules/auth.js +12 -7
- package/dist/modules/platforms.d.ts +14 -0
- package/dist/modules/platforms.js +41 -0
- package/dist/modules/platforms.types.d.ts +139 -0
- package/dist/modules/platforms.types.js +1 -0
- package/dist/platform-client.d.ts +52 -0
- package/dist/platform-client.js +130 -0
- package/dist/platform-client.types.d.ts +144 -0
- package/dist/platform-client.types.js +1 -0
- package/dist/utils/principal-tokens.d.ts +64 -0
- package/dist/utils/principal-tokens.js +120 -0
- package/package.json +1 -1
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";
|
package/dist/modules/auth.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
162
|
-
|
|
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
|
+
}
|