@base44-preview/sdk 0.8.52-pr.297.c6f2448 → 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,9 +51,9 @@ 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, images, and videos. Pass `{ provider: "typesafe" }` for
56
- * 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`.
57
57
  *
58
58
  * Call `connection()` from a backend function rather than the browser. This
59
59
  * keeps your instructions, tools, and business logic server-side, and lets
@@ -76,26 +76,6 @@ export interface AiGatewayModuleConfig {
76
76
  * `'gemini_3_1_flash_image'` or `'gpt_image_2'`. For request options, the
77
77
  * full model list, and limits, see [Generate images with the AI Gateway](/developers/references/sdk/getting-started/ai-gateway-images).
78
78
  *
79
- * ## Videos
80
- *
81
- * The gateway serves OpenAI's asynchronous video endpoints through the
82
- * `openai` SDK's `videos` client. `videos.create()` starts a job and returns
83
- * its `id` right away; call `videos.retrieve(id)` until `status` is
84
- * `completed` or `failed`. A completed job exposes the stored video at `url`.
85
- * A backend function is a bounded HTTP call, so don't poll inside one: return
86
- * the `id`, then retrieve it from a later call or scheduled run. Video
87
- * requests need a specific model, such as `'veo_3_1_fast'` or
88
- * `'seedance_2_fast'`; there's no `'automatic'` option.
89
- *
90
- * ## Structured evaluations
91
- *
92
- * The `typesafe` provider runs the `jev` evaluation model through
93
- * `@ai-sdk/typesafe-ai` and the `ai` package's `experimental_evaluate`. Give
94
- * it some app state and a set of questions, each a `choice`, `score`, or
95
- * `boolean`, and it returns each answer with its probabilities. Use it
96
- * to classify, score, or route records, then apply your own thresholds and
97
- * actions in code.
98
- *
99
79
  * ## Authentication Modes
100
80
  *
101
81
  * There's no permission difference between modes. Both just determine which
@@ -207,70 +187,6 @@ export interface AiGatewayModule {
207
187
  * const file = new File([image.uint8Array], "lighthouse.png", { type: image.mediaType });
208
188
  * const { file_url } = await base44.integrations.Core.UploadFile({ file });
209
189
  * ```
210
- *
211
- * @example
212
- * ```typescript
213
- * // Start a video job, then check on it from a later call
214
- * import { createClientFromRequest } from "@base44/sdk";
215
- * import OpenAI from "openai";
216
- *
217
- * // Runs inside a backend function
218
- * const base44 = createClientFromRequest(request);
219
- * const { baseURL, token, headers } = base44.aiGateway.connection();
220
- * const { videos } = new OpenAI({ baseURL, apiKey: token, defaultHeaders: headers });
221
- *
222
- * const { videoId } = await request.json();
223
- * if (!videoId) {
224
- * // Avoid automatic replays of billed requests
225
- * const job = await videos.create(
226
- * { model: "veo_3_1_fast", prompt: "A paper boat drifting down a rainy street", seconds: "4" },
227
- * { maxRetries: 0 },
228
- * );
229
- * return Response.json({ videoId: job.id, status: job.status }, { status: 202 });
230
- * }
231
- *
232
- * const video = await videos.retrieve(videoId);
233
- * // `queued` and `in_progress` mean unfinished; `url` is set once `completed`
234
- * return Response.json(video);
235
- * ```
236
- *
237
- * @example
238
- * ```typescript
239
- * // Route a support ticket with a structured evaluation
240
- * import { createClientFromRequest } from "@base44/sdk";
241
- * import { experimental_evaluate as evaluate } from "ai";
242
- * import { createTypeSafeAi } from "@ai-sdk/typesafe-ai";
243
- *
244
- * // Runs inside a backend function
245
- * const base44 = createClientFromRequest(request);
246
- * const { baseURL, token, headers } = base44.aiGateway.connection({ provider: "typesafe" });
247
- * const typesafe = createTypeSafeAi({ baseURL, apiKey: token, headers });
248
- *
249
- * const { answers } = await evaluate({
250
- * model: typesafe.evaluationModel("jev"),
251
- * maxRetries: 0,
252
- * state: { message: "Please refund today's duplicate charge." },
253
- * questions: {
254
- * department: {
255
- * type: "choice",
256
- * instructions: "Which team should handle this?",
257
- * criteria: { billing: "Payments and refunds", support: "Other requests" },
258
- * },
259
- * urgency: {
260
- * type: "score",
261
- * instructions: "How urgent is this?",
262
- * criteria: ["No deadline", "Requested today", "Immediate emergency"],
263
- * },
264
- * refund: { type: "boolean", instructions: "Is a refund explicitly requested?" },
265
- * },
266
- * });
267
- *
268
- * // answers.department → { type: "choice", choice: "billing", probabilities: { billing: 0.97, support: 0.03 } }
269
- * // answers.urgency → { type: "score", score: 1.2, probabilities: { 0: 0.1, 1: 0.6, 2: 0.3 } }
270
- * // answers.refund → { type: "boolean", probability: 0.99 }
271
- * // Pick your own thresholds and act on them in code
272
- * const autoRefund = answers.department.choice === "billing" && answers.refund.probability > 0.9;
273
- * ```
274
190
  */
275
191
  connection(options?: AiGatewayConnectionOptions): AiGatewayConnection;
276
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.c6f2448",
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",