@base44-preview/sdk 0.8.52-pr.297.7590c31 → 0.8.52-pr.298.d08e0ba

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/README.md CHANGED
@@ -151,3 +151,5 @@ npm run create-docs
151
151
  cd docs
152
152
  mintlify dev
153
153
  ```
154
+
155
+ See [experimental app-user MFA](MFA.md) for challenge handling and managed continuation.
package/dist/index.d.ts CHANGED
@@ -18,3 +18,4 @@ export { Actor, type Conn } from "./actor.js";
18
18
  export type { ConnectorsModule, UserConnectorsModule, ConnectorApiRequest, ConnectorApiResponse, ConnectorApiResponsePhase, } from "./modules/connectors.types.js";
19
19
  export type { CustomIntegrationsModule, CustomIntegrationCallParams, CustomIntegrationCallResponse, } from "./modules/custom-integrations.types.js";
20
20
  export type { GetAccessTokenOptions, SaveAccessTokenOptions, RemoveAccessTokenOptions, GetLoginUrlOptions, } from "./utils/auth-utils.types.js";
21
+ export type { MfaChallenge, MfaModule, MfaProof, LoginResult, MfaEnrollmentResult } from "./modules/mfa.js";
@@ -51,13 +51,15 @@ export interface AiGatewayModuleConfig {
51
51
  * against the provider directly, no separate account, API key, or billing
52
52
  * setup with the underlying model provider required.
53
53
  *
54
- * By default, `connection()` uses the OpenAI-compatible provider, which serves
55
- * chat and images. Pass `{ provider: "typesafe" }` for structured evaluations.
54
+ * By default, `connection()` uses the OpenAI-compatible provider. Pass
55
+ * `{ provider: "typesafe" }` to connect to TypeSafe for structured evaluations
56
+ * with `@ai-sdk/typesafe-ai`.
56
57
  *
57
58
  * Call `connection()` from a backend function rather than the browser. This
58
59
  * keeps your instructions, tools, and business logic server-side, and lets
59
- * you enforce your own auth, rate, and spend limits around the call. Which
60
- * token it returns depends on the authentication mode, described below.
60
+ * you enforce your own auth, rate, and spend limits around the call. The
61
+ * `token` it returns is the caller's regular session token, the same one
62
+ * used for every other SDK call.
61
63
  *
62
64
  * ## OpenAI-compatible models
63
65
  *
@@ -74,23 +76,13 @@ export interface AiGatewayModuleConfig {
74
76
  * `'gemini_3_1_flash_image'` or `'gpt_image_2'`. For request options, the
75
77
  * full model list, and limits, see [Generate images with the AI Gateway](/developers/references/sdk/getting-started/ai-gateway-images).
76
78
  *
77
- * ## Structured evaluations
78
- *
79
- * The `typesafe` provider runs the `jev` evaluation model through
80
- * `@ai-sdk/typesafe-ai` and `experimental_evaluate` from the `ai` package,
81
- * version 7.0.105 or later. Pass `evaluationModel("jev")`; the gateway rejects
82
- * other model IDs, such as `"jev-latest"`. Give it some app state and a set of
83
- * questions, each a `choice`, `score`, or `boolean`. Choice and score answers
84
- * come back with `probabilities` per option; boolean answers with a single
85
- * `probability` of `true`. Use it to classify, score, or route records, then
86
- * apply your own thresholds and actions in code.
87
- *
88
79
  * ## Authentication Modes
89
80
  *
90
- * The mode determines which token `connection()` returns:
81
+ * There's no permission difference between modes. Both just determine which
82
+ * token `connection()` returns:
91
83
  *
92
- * - **Service role authentication** (`base44.asServiceRole.aiGateway`): Returns the service-role token. Use this from backend functions, including scheduled automations with no signed-in user. Every call spends the app's credits, so check the caller with `base44.auth.me()` first when a user triggers the function.
93
- * - **User authentication** (`base44.aiGateway`): Returns the signed-in app user's token. When an app restricts Core integrations, which is the default for new apps, public apps reject gateway calls made with a user token unless the AI Gateway is allowed.
84
+ * - **User authentication** (`base44.aiGateway`): Returns the signed-in app user's token.
85
+ * - **Service role authentication** (`base44.asServiceRole.aiGateway`): Returns the service-role token instead, for calling the gateway when there's no signed-in user, such as from a scheduled automation.
94
86
  *
95
87
  * ## Billing and limits
96
88
  *
@@ -119,7 +111,7 @@ export interface AiGatewayModule {
119
111
  *
120
112
  * // Runs inside a backend function
121
113
  * const base44 = createClientFromRequest(request);
122
- * const { baseURL, token, headers } = base44.asServiceRole.aiGateway.connection();
114
+ * const { baseURL, token, headers } = base44.aiGateway.connection();
123
115
  * const openai = new OpenAI({ baseURL, apiKey: token, defaultHeaders: headers });
124
116
  *
125
117
  * const response = await openai.chat.completions.create({
@@ -141,7 +133,7 @@ export interface AiGatewayModule {
141
133
  * // Runs inside a backend function, reviewing a return request
142
134
  * const base44 = createClientFromRequest(request);
143
135
  * const returnRequest = await base44.entities.ReturnRequest.get(returnId);
144
- * const { baseURL, token, headers } = base44.asServiceRole.aiGateway.connection();
136
+ * const { baseURL, token, headers } = base44.aiGateway.connection();
145
137
  * // Point any OpenAI-compatible client at `baseURL` with `apiKey: token` and `headers`.
146
138
  * const models = createOpenAICompatible({ name: "base44", baseURL, apiKey: token, headers });
147
139
  *
@@ -182,7 +174,7 @@ export interface AiGatewayModule {
182
174
  *
183
175
  * // Runs inside a backend function
184
176
  * const base44 = createClientFromRequest(request);
185
- * const { baseURL, token, headers } = base44.asServiceRole.aiGateway.connection();
177
+ * const { baseURL, token, headers } = base44.aiGateway.connection();
186
178
  * const models = createOpenAICompatible({ name: "base44", baseURL, apiKey: token, headers });
187
179
  *
188
180
  * const { image } = await generateImage({
@@ -195,45 +187,6 @@ export interface AiGatewayModule {
195
187
  * const file = new File([image.uint8Array], "lighthouse.png", { type: image.mediaType });
196
188
  * const { file_url } = await base44.integrations.Core.UploadFile({ file });
197
189
  * ```
198
- *
199
- * @example
200
- * ```typescript
201
- * // Route a support ticket with a structured evaluation
202
- * import { createClientFromRequest } from "@base44/sdk";
203
- * import { experimental_evaluate as evaluate } from "ai";
204
- * import { createTypeSafeAi } from "@ai-sdk/typesafe-ai";
205
- *
206
- * // Runs inside a backend function
207
- * const base44 = createClientFromRequest(request);
208
- * const { baseURL, token, headers } = base44.asServiceRole.aiGateway.connection({ provider: "typesafe" });
209
- * const typesafe = createTypeSafeAi({ baseURL, apiKey: token, headers });
210
- *
211
- * const { answers, providerMetadata } = await evaluate({
212
- * model: typesafe.evaluationModel("jev"),
213
- * maxRetries: 0, // Avoid automatic replays of billed requests
214
- * state: { message: "Please refund today's duplicate charge." },
215
- * questions: {
216
- * department: {
217
- * type: "choice",
218
- * instructions: "Which team should handle this?",
219
- * criteria: { billing: "Payments and refunds", support: "Other requests" },
220
- * },
221
- * urgency: {
222
- * type: "score",
223
- * instructions: "How urgent is this?",
224
- * criteria: ["No deadline", "Requested today", "Immediate emergency"],
225
- * },
226
- * refund: { type: "boolean", instructions: "Is a refund explicitly requested?" },
227
- * },
228
- * });
229
- *
230
- * // answers.department → { type: "choice", choice: "billing", probabilities: { billing: 0.97, support: 0.03 } }
231
- * // answers.urgency → { type: "score", score: 1.2, probabilities: { 0: 0.1, 1: 0.6, 2: 0.3 } }
232
- * // answers.refund → { type: "boolean", probability: 0.99 }
233
- * // Choice and score confidence is in providerMetadata.typesafe.confidence
234
- * // Pick your own thresholds and act on them in code
235
- * const autoRefund = answers.department.choice === "billing" && answers.refund.probability > 0.9;
236
- * ```
237
190
  */
238
191
  connection(options?: AiGatewayConnectionOptions): AiGatewayConnection;
239
192
  }
@@ -1,3 +1,4 @@
1
+ import { createMfaModule, prepareMfaPkce } from "./mfa.js";
1
2
  import { resetAnalyticsSessionContext } from "./analytics.js";
2
3
  function isInsideIframe() {
3
4
  if (typeof window === "undefined")
@@ -19,9 +20,9 @@ function loginViaPopup(url, redirectUrl, expectedOrigin) {
19
20
  const height = 600;
20
21
  const left = Math.round(window.screenX + (window.outerWidth - width) / 2);
21
22
  const top = Math.round(window.screenY + (window.outerHeight - height) / 2);
22
- const popup = window.open(url, "base44_auth", `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`);
23
+ const popup = window.open("about:blank", "base44_auth", `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`);
23
24
  if (!popup) {
24
- return;
25
+ return Promise.resolve();
25
26
  }
26
27
  const cleanup = () => {
27
28
  window.removeEventListener("message", onMessage);
@@ -52,6 +53,7 @@ function loginViaPopup(url, redirectUrl, expectedOrigin) {
52
53
  cleanup();
53
54
  }, 500);
54
55
  window.addEventListener("message", onMessage);
56
+ return url.then(value => { popup.location.href = value; }, error => { cleanup(); throw error; });
55
57
  }
56
58
  /**
57
59
  * Creates the auth module for the Base44 SDK.
@@ -82,7 +84,8 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
82
84
  // to the identity transitions below (`setToken`, `logout`) instead of to the
83
85
  // header a caller may have set on the instance directly.
84
86
  let hasAccessToken = Boolean(options.token);
85
- return {
87
+ const module = {
88
+ mfa: createMfaModule(axios, appId, token => module.setToken(token)),
86
89
  hasToken() {
87
90
  return hasAccessToken;
88
91
  },
@@ -119,9 +122,10 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
119
122
  window.location.href = loginUrl;
120
123
  },
121
124
  // Redirects the user to a provider's login page
122
- loginWithProvider(provider, fromUrl = "/") {
125
+ async loginWithProvider(provider, fromUrl = "/") {
123
126
  // Build the full redirect URL
124
127
  const redirectUrl = new URL(fromUrl, window.location.origin).toString();
128
+ const mfaPkce = provider === "sso" ? Promise.resolve("") : prepareMfaPkce(appId).then(value => `&app_mfa_code_challenge=${value}`);
125
129
  const queryParams = `app_id=${appId}&from_url=${encodeURIComponent(redirectUrl)}`;
126
130
  // SSO uses a different URL structure with appId in the path
127
131
  let authPath;
@@ -137,11 +141,11 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
137
141
  // When running inside an iframe, use a popup to avoid OAuth providers
138
142
  // blocking iframe navigation.
139
143
  if (isInsideIframe()) {
140
- const popupLoginUrl = `${loginUrl}&popup_origin=${encodeURIComponent(window.location.origin)}`;
144
+ const popupLoginUrl = mfaPkce.then(pkce => `${loginUrl}${pkce}&popup_origin=${encodeURIComponent(window.location.origin)}`);
141
145
  return loginViaPopup(popupLoginUrl, redirectUrl, window.location.origin);
142
146
  }
143
147
  // Default: full-page redirect
144
- window.location.href = loginUrl;
148
+ window.location.href = loginUrl + await mfaPkce;
145
149
  },
146
150
  // Logout the current user
147
151
  logout(redirectUrl) {
@@ -207,6 +211,8 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
207
211
  password,
208
212
  ...(turnstileToken && { turnstile_token: turnstileToken }),
209
213
  });
214
+ if (response.mfa_required)
215
+ return response;
210
216
  const { access_token, user } = response;
211
217
  if (access_token) {
212
218
  this.setToken(access_token);
@@ -278,4 +284,5 @@ export function createAuthModule(axios, functionsAxiosClient, appId, options) {
278
284
  });
279
285
  },
280
286
  };
287
+ return module;
281
288
  }
@@ -1,3 +1,4 @@
1
+ import type { LoginResult, MfaModule } from "./mfa.js";
1
2
  /**
2
3
  * An authenticated user.
3
4
  */
@@ -37,6 +38,7 @@ export interface User {
37
38
  * Response from login endpoints containing user information and access token.
38
39
  */
39
40
  export interface LoginResponse {
41
+ mfa_required?: false;
40
42
  /** JWT access token for authentication. */
41
43
  access_token: string;
42
44
  /** User information. */
@@ -117,6 +119,8 @@ export interface AuthModuleOptions {
117
119
  * The auth module is only available in user authentication mode (`base44.auth`).
118
120
  */
119
121
  export interface AuthModule {
122
+ /** Multi-factor enrollment and login for this app. */
123
+ mfa: MfaModule;
120
124
  /**
121
125
  * Gets the current authenticated user's information.
122
126
  *
@@ -219,7 +223,7 @@ export interface AuthModule {
219
223
  * ```
220
224
  *
221
225
  */
222
- loginWithProvider(provider: string, fromUrl?: string): void;
226
+ loginWithProvider(provider: string, fromUrl?: string): Promise<void>;
223
227
  /**
224
228
  * Logs out the current user.
225
229
  *
@@ -302,7 +306,7 @@ export interface AuthModule {
302
306
  * );
303
307
  * ```
304
308
  */
305
- loginViaEmailPassword(email: string, password: string, turnstileToken?: string): Promise<LoginResponse>;
309
+ loginViaEmailPassword(email: string, password: string, turnstileToken?: string): Promise<LoginResult>;
306
310
  /**
307
311
  * Checks if the current user is authenticated.
308
312
  *
@@ -427,7 +431,7 @@ export interface AuthModule {
427
431
  * );
428
432
  * ```
429
433
  */
430
- verifyOtp(params: VerifyOtpParams): Promise<any>;
434
+ verifyOtp(params: VerifyOtpParams): Promise<LoginResult>;
431
435
  /**
432
436
  * Resends an OTP code to the user's email address.
433
437
  *
@@ -0,0 +1,52 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ import type { LoginResponse } from './auth.types.js';
3
+ /** A restricted, short-lived login outcome. It is never an access token. */
4
+ export interface MfaChallenge {
5
+ mfa_required: true;
6
+ next_step: 'enroll' | 'verify';
7
+ mfa_methods: ('totp' | 'sms')[];
8
+ mfa_session_token: string;
9
+ expires_in_seconds: number;
10
+ phone_hint?: string;
11
+ access_token?: never;
12
+ user?: never;
13
+ }
14
+ export type LoginResult = LoginResponse | MfaChallenge;
15
+ export interface MfaEnrollmentResult extends LoginResponse {
16
+ backup_codes?: string[];
17
+ }
18
+ export interface MfaProof {
19
+ code: string;
20
+ method: 'totp' | 'sms';
21
+ remember_device?: boolean;
22
+ }
23
+ /** App-scoped MFA. Pass the challenge credential explicitly; do not set it as the SDK token. */
24
+ export interface MfaModule {
25
+ getStatus(): Promise<{
26
+ mfa_enabled: boolean;
27
+ methods: ('totp' | 'sms')[];
28
+ backup_codes_remaining: number;
29
+ policy: 'off' | 'required';
30
+ }>;
31
+ /** Requires an app admin session; resets a user in this app only. */
32
+ resetForUser(appUserId: string): Promise<{
33
+ success: true;
34
+ }>;
35
+ /** Open managed settings with fresh primary authentication and factor verification. */
36
+ openSecurity(returnTo?: string): void;
37
+ setupTotp(challenge: MfaChallenge): Promise<{
38
+ secret: string;
39
+ secret_formatted: string;
40
+ qr_code: string;
41
+ }>;
42
+ sendSms(challenge: MfaChallenge, phoneNumber: string): Promise<unknown>;
43
+ enrollTotp(challenge: MfaChallenge, code: string, rememberDevice?: boolean): Promise<MfaEnrollmentResult>;
44
+ enrollSms(challenge: MfaChallenge, code: string, rememberDevice?: boolean): Promise<MfaEnrollmentResult>;
45
+ verify(challenge: MfaChallenge, proof: MfaProof): Promise<LoginResponse>;
46
+ verifyRecoveryCode(challenge: MfaChallenge, code: string, rememberDevice?: boolean): Promise<LoginResponse>;
47
+ resendSms(challenge: MfaChallenge): Promise<unknown>;
48
+ /** Open Base44's managed MFA flow on this app's origin. */
49
+ continueLogin(challenge: MfaChallenge, returnTo?: string): Promise<void>;
50
+ }
51
+ export declare function prepareMfaPkce(appId: string): Promise<string>;
52
+ export declare function createMfaModule(axios: AxiosInstance, appId: string, setToken: (token: string) => void): MfaModule;
@@ -0,0 +1,43 @@
1
+ export async function prepareMfaPkce(appId) {
2
+ const encode = (value) => btoa(String.fromCharCode(...value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
3
+ const verifier = encode(crypto.getRandomValues(new Uint8Array(32)));
4
+ sessionStorage.setItem(`app-mfa-pkce:${appId}`, verifier);
5
+ return encode(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));
6
+ }
7
+ export function createMfaModule(axios, appId, setToken) {
8
+ const post = async (challenge, path, payload = {}) => {
9
+ const response = await axios.post(`/apps/${appId}/auth/mfa${path}`, payload, {
10
+ headers: { Authorization: `Bearer ${challenge.mfa_session_token}` }, withCredentials: true,
11
+ });
12
+ const token = response.access_token;
13
+ if (token)
14
+ setToken(token);
15
+ return response;
16
+ };
17
+ return {
18
+ getStatus: () => axios.get(`/apps/${appId}/auth/mfa/status`),
19
+ resetForUser: appUserId => axios.post(`/apps/${appId}/auth/mfa/reset`, { app_user_id: appUserId }),
20
+ openSecurity(returnTo = '/') {
21
+ const target = new URL(returnTo, window.location.origin);
22
+ if (target.origin !== window.location.origin)
23
+ throw new Error('MFA must return to this app.');
24
+ window.location.href = `/auth/mfa?${new URLSearchParams({ security: '1', from_url: target.pathname + target.search + target.hash })}`;
25
+ },
26
+ setupTotp: challenge => post(challenge, '/totp/setup'),
27
+ sendSms: (challenge, phoneNumber) => post(challenge, '/sms/send-code', { phone_number: phoneNumber }),
28
+ enrollTotp: (challenge, code, rememberDevice = false) => post(challenge, '/totp/verify', { code, remember_device: rememberDevice }),
29
+ enrollSms: (challenge, code, rememberDevice = false) => post(challenge, '/sms/verify', { code, remember_device: rememberDevice }),
30
+ verify: (challenge, proof) => post(challenge, '/verify', proof),
31
+ verifyRecoveryCode: (challenge, code, rememberDevice = false) => post(challenge, '/verify-backup', { backup_code: code, remember_device: rememberDevice }),
32
+ resendSms: challenge => post(challenge, '/resend'),
33
+ async continueLogin(challenge, returnTo = '/') {
34
+ const target = new URL(returnTo, window.location.origin);
35
+ if (target.origin !== window.location.origin)
36
+ throw new Error('MFA must return to this app.');
37
+ const codeChallenge = await prepareMfaPkce(appId);
38
+ const handoff = await post(challenge, '/handoff', { code_challenge: codeChallenge });
39
+ const query = new URLSearchParams({ app_id: appId, mfa_code: handoff.code, from_url: target.pathname + target.search + target.hash });
40
+ window.location.href = `/auth/mfa?${query}`;
41
+ },
42
+ };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.8.52-pr.297.7590c31",
3
+ "version": "0.8.52-pr.298.d08e0ba",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",