@cboxdk/id-js 0.18.0 → 0.19.0

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.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { A as AuthenticationError, C as CboxIdError, a as ConfigurationError, F as FrontendApiError, I as InvalidStateError } from './errors-D61xljZE.cjs';
2
- export { a as CallbackParams, C as CboxIdClient, S as StoreSecretInput, b as StoredAuthState, V as VaultClient, c as VaultLease, d as VaultSecretRef, e as VerifyStandardWebhookOptions, f as VerifyWebhookOptions, W as WebhookHeaders, v as verifyStandardWebhook, g as verifyWebhook } from './client-BxUHm5hX.cjs';
3
- export { c as AuthorizationPrompt, b as AuthorizationRequest, A as AuthorizationRequestOptions, d as CboxActiveOrganization, e as CboxActor, a as CboxIdConfig, f as CboxOrganization, C as CboxUser, g as ClaimSource, D as DeviceAuthorization, h as DiscoveryDocument, O as OrganizationRole, R as RefreshedTokens, i as TokenResponse, T as TokenTypeHint, j as actor, k as hasPermission, l as hasRole, m as isSupportSession, o as organization, p as permissions, r as roles, s as sessionId } from './types-BYZldock.cjs';
2
+ export { a as CallbackParams, C as CboxIdClient, P as PipeConnectOptions, b as PipeLeaseDeniedError, c as PipeLeaseError, d as PipeNotConnectedError, e as PipeProvider, f as PipeReauthorizationRequiredError, g as PipeTemporarilyUnavailableError, h as PipeToken, S as StoreSecretInput, i as StoredAuthState, V as VaultClient, j as VaultLease, k as VaultSecretRef, l as VerifyStandardWebhookOptions, m as VerifyWebhookOptions, W as WebhookHeaders, p as pipeConnectUrl, v as verifyStandardWebhook, n as verifyWebhook, w as withConnectReturn } from './client-BT-jOm0p.cjs';
3
+ export { c as AuthorizationPrompt, b as AuthorizationRequest, A as AuthorizationRequestOptions, d as CboxActiveOrganization, e as CboxActor, a as CboxIdConfig, f as CboxOrganization, C as CboxUser, g as ClaimSource, D as DeviceAuthorization, h as DiscoveryDocument, F as FEATURE_FLAGS_SCOPE, O as OrganizationRole, R as RefreshedTokens, i as TokenResponse, T as TokenTypeHint, j as actor, k as featureFlags, l as hasFeature, m as hasPermission, n as hasRole, o as isSupportSession, p as organization, q as permissions, r as roles, s as sessionId } from './types-CC1bz68x.cjs';
4
4
 
5
5
  /**
6
6
  * The handler on YOUR side of a legacy migration.
@@ -130,9 +130,35 @@ interface FrontendConfig {
130
130
  end_session: string;
131
131
  jwks: string;
132
132
  };
133
+ /**
134
+ * The social buttons to draw, exactly as the hosted sign-in page would: the environment's
135
+ * own providers, or — with `config({ organization })` — that organization's.
136
+ */
133
137
  social: SocialProvider[];
134
138
  appearance?: Appearance;
139
+ /**
140
+ * Which other sign-in methods are switched on, so a box does not draw a button whose
141
+ * endpoint answers 403. Absent from instances that predate it.
142
+ */
143
+ methods?: {
144
+ passkeys: boolean;
145
+ magic_link: boolean;
146
+ };
147
+ }
148
+ /** Options for {@link CboxIdFrontend.config}. */
149
+ interface FrontendConfigOptions {
150
+ /**
151
+ * The organization the box is for, by id or slug — the way a hosted page is addressed.
152
+ * `social` is then exactly what that organization's sign-in page offers. An organization
153
+ * the environment does not have is answered as no hint at all.
154
+ */
155
+ organization?: string;
135
156
  }
157
+ /**
158
+ * A second factor the person can answer an `mfa_required` with. Kinds only — the server
159
+ * never says which number a text would go to before one is sent.
160
+ */
161
+ type SecondFactorKind = 'totp' | 'sms' | 'recovery_code';
136
162
  /** What a sign-in attempt produced. */
137
163
  type SignInResult = {
138
164
  status: 'ok';
@@ -141,6 +167,11 @@ type SignInResult = {
141
167
  } | {
142
168
  status: 'mfa_required';
143
169
  mfaToken: string;
170
+ /**
171
+ * The factors this person can answer with (laravel-id 1.24+). Absent when the
172
+ * instance predates the list — then only an authenticator or a recovery code works.
173
+ */
174
+ factors?: SecondFactorKind[];
144
175
  } | {
145
176
  status: 'otp_required';
146
177
  mfaToken: string;
@@ -152,6 +183,30 @@ type SignInResult = {
152
183
  status: 'rate_limited';
153
184
  retryAfter: number | undefined;
154
185
  };
186
+ /** What asking for a texted sign-in code produced. */
187
+ type SmsCodeResult =
188
+ /** Sent. `to` is the number masked (`+45 ******78`); the code lasts `expiresIn` seconds. */
189
+ {
190
+ status: 'sent';
191
+ to: string;
192
+ expiresIn: number;
193
+ }
194
+ /** Too soon, or a sending limit was hit. Waiting `retryAfter` seconds may help. */
195
+ | {
196
+ status: 'rate_limited';
197
+ retryAfter: number | undefined;
198
+ }
199
+ /** The text provider failed. Try again shortly, or offer another factor. */
200
+ | {
201
+ status: 'unavailable';
202
+ }
203
+ /**
204
+ * Refused: an unknown or spent token, no number on file, a country texts are not sent
205
+ * to, or no attempts left. One answer for every reason, like a wrong password.
206
+ */
207
+ | {
208
+ status: 'invalid';
209
+ };
155
210
  /** What a browser needs to ask an authenticator for a passkey assertion. */
156
211
  interface PasskeyOptions {
157
212
  challenge: string;
@@ -197,7 +252,7 @@ declare class CboxIdFrontend {
197
252
  * fetches of the same immutable document is a waste the SDK should absorb rather than
198
253
  * ask every consumer to.
199
254
  */
200
- private configPromise;
255
+ private readonly configPromises;
201
256
  constructor(options: FrontendClientOptions);
202
257
  /** Whether this key drives real sign-ins. Handy for a "test mode" badge in your own UI. */
203
258
  get isLive(): boolean;
@@ -207,8 +262,9 @@ declare class CboxIdFrontend {
207
262
  * Fetched once per client instance. The document is small and changes when somebody
208
263
  * edits it in the console, so a long-lived page picks changes up on its next full load
209
264
  * rather than mid-session — which is the right trade for something that decides layout.
265
+ * Each `organization` is fetched and cached on its own.
210
266
  */
211
- config(): Promise<FrontendConfig>;
267
+ config(options?: FrontendConfigOptions): Promise<FrontendConfig>;
212
268
  /**
213
269
  * Who is signed in, given an access token your app already holds.
214
270
  *
@@ -256,8 +312,29 @@ declare class CboxIdFrontend {
256
312
  *
257
313
  * A wrong code costs an attempt, not the sign-in: five are allowed before the token
258
314
  * dies and the person starts from the password again.
315
+ *
316
+ * `method` says which challenge the code answers: `'mfa'` for an authenticator or
317
+ * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent
318
+ * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the
319
+ * server cannot tell them apart by shape — answering one as the other is refused.
320
+ */
321
+ submitSecondFactor(mfaToken: string, code: string, method?: 'mfa' | 'otp' | 'sms'): Promise<SignInResult>;
322
+ /**
323
+ * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.
324
+ * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.
325
+ *
326
+ * Only call it when the person asks ("Text me a code"), never on page load: each send
327
+ * spends one of the token's five attempts — that is what bounds how many texts one
328
+ * proved password can cause — and every text costs the customer money.
329
+ *
330
+ * ```ts
331
+ * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
332
+ * const sent = await frontend.sendSmsCode(result.mfaToken)
333
+ * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)
334
+ * }
335
+ * ```
259
336
  */
260
- submitSecondFactor(mfaToken: string, code: string, method?: 'mfa' | 'otp'): Promise<SignInResult>;
337
+ sendSmsCode(mfaToken: string): Promise<SmsCodeResult>;
261
338
  /**
262
339
  * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.
263
340
  *
@@ -442,4 +519,4 @@ declare function buildManifest(declaration: AuthzDeclaration): Promise<AuthzMani
442
519
  */
443
520
  declare function publishManifest(config: ManifestPublisherConfig, declaration: AuthzDeclaration): Promise<ManifestSyncSummary>;
444
521
 
445
- export { type Appearance, type AuthzDeclaration, type AuthzManifest, CboxIdFrontend, type FrontendClientOptions, type FrontendConfig, type FrontendSession, type LegacyUser, type LegacyVerifierOptions, type ManifestPermission, type ManifestPublisherConfig, type ManifestRole, type ManifestSyncSummary, type PasskeyOptions, type PermissionDefinition, type RoleDefinition, type SignInResult, type SocialProvider, buildManifest, challenge, createLegacyVerifier, createVerifier, defineAuthz, publishManifest, randomToken };
522
+ export { type Appearance, type AuthzDeclaration, type AuthzManifest, CboxIdFrontend, type FrontendClientOptions, type FrontendConfig, type FrontendConfigOptions, type FrontendSession, type LegacyUser, type LegacyVerifierOptions, type ManifestPermission, type ManifestPublisherConfig, type ManifestRole, type ManifestSyncSummary, type PasskeyOptions, type PermissionDefinition, type RoleDefinition, type SecondFactorKind, type SignInResult, type SmsCodeResult, type SocialProvider, buildManifest, challenge, createLegacyVerifier, createVerifier, defineAuthz, publishManifest, randomToken };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { A as AuthenticationError, C as CboxIdError, a as ConfigurationError, F as FrontendApiError, I as InvalidStateError } from './errors-D61xljZE.js';
2
- export { a as CallbackParams, C as CboxIdClient, S as StoreSecretInput, b as StoredAuthState, V as VaultClient, c as VaultLease, d as VaultSecretRef, e as VerifyStandardWebhookOptions, f as VerifyWebhookOptions, W as WebhookHeaders, v as verifyStandardWebhook, g as verifyWebhook } from './client-BSZASrMo.js';
3
- export { c as AuthorizationPrompt, b as AuthorizationRequest, A as AuthorizationRequestOptions, d as CboxActiveOrganization, e as CboxActor, a as CboxIdConfig, f as CboxOrganization, C as CboxUser, g as ClaimSource, D as DeviceAuthorization, h as DiscoveryDocument, O as OrganizationRole, R as RefreshedTokens, i as TokenResponse, T as TokenTypeHint, j as actor, k as hasPermission, l as hasRole, m as isSupportSession, o as organization, p as permissions, r as roles, s as sessionId } from './types-BYZldock.js';
2
+ export { a as CallbackParams, C as CboxIdClient, P as PipeConnectOptions, b as PipeLeaseDeniedError, c as PipeLeaseError, d as PipeNotConnectedError, e as PipeProvider, f as PipeReauthorizationRequiredError, g as PipeTemporarilyUnavailableError, h as PipeToken, S as StoreSecretInput, i as StoredAuthState, V as VaultClient, j as VaultLease, k as VaultSecretRef, l as VerifyStandardWebhookOptions, m as VerifyWebhookOptions, W as WebhookHeaders, p as pipeConnectUrl, v as verifyStandardWebhook, n as verifyWebhook, w as withConnectReturn } from './client-xlEJalu8.js';
3
+ export { c as AuthorizationPrompt, b as AuthorizationRequest, A as AuthorizationRequestOptions, d as CboxActiveOrganization, e as CboxActor, a as CboxIdConfig, f as CboxOrganization, C as CboxUser, g as ClaimSource, D as DeviceAuthorization, h as DiscoveryDocument, F as FEATURE_FLAGS_SCOPE, O as OrganizationRole, R as RefreshedTokens, i as TokenResponse, T as TokenTypeHint, j as actor, k as featureFlags, l as hasFeature, m as hasPermission, n as hasRole, o as isSupportSession, p as organization, q as permissions, r as roles, s as sessionId } from './types-CC1bz68x.js';
4
4
 
5
5
  /**
6
6
  * The handler on YOUR side of a legacy migration.
@@ -130,9 +130,35 @@ interface FrontendConfig {
130
130
  end_session: string;
131
131
  jwks: string;
132
132
  };
133
+ /**
134
+ * The social buttons to draw, exactly as the hosted sign-in page would: the environment's
135
+ * own providers, or — with `config({ organization })` — that organization's.
136
+ */
133
137
  social: SocialProvider[];
134
138
  appearance?: Appearance;
139
+ /**
140
+ * Which other sign-in methods are switched on, so a box does not draw a button whose
141
+ * endpoint answers 403. Absent from instances that predate it.
142
+ */
143
+ methods?: {
144
+ passkeys: boolean;
145
+ magic_link: boolean;
146
+ };
147
+ }
148
+ /** Options for {@link CboxIdFrontend.config}. */
149
+ interface FrontendConfigOptions {
150
+ /**
151
+ * The organization the box is for, by id or slug — the way a hosted page is addressed.
152
+ * `social` is then exactly what that organization's sign-in page offers. An organization
153
+ * the environment does not have is answered as no hint at all.
154
+ */
155
+ organization?: string;
135
156
  }
157
+ /**
158
+ * A second factor the person can answer an `mfa_required` with. Kinds only — the server
159
+ * never says which number a text would go to before one is sent.
160
+ */
161
+ type SecondFactorKind = 'totp' | 'sms' | 'recovery_code';
136
162
  /** What a sign-in attempt produced. */
137
163
  type SignInResult = {
138
164
  status: 'ok';
@@ -141,6 +167,11 @@ type SignInResult = {
141
167
  } | {
142
168
  status: 'mfa_required';
143
169
  mfaToken: string;
170
+ /**
171
+ * The factors this person can answer with (laravel-id 1.24+). Absent when the
172
+ * instance predates the list — then only an authenticator or a recovery code works.
173
+ */
174
+ factors?: SecondFactorKind[];
144
175
  } | {
145
176
  status: 'otp_required';
146
177
  mfaToken: string;
@@ -152,6 +183,30 @@ type SignInResult = {
152
183
  status: 'rate_limited';
153
184
  retryAfter: number | undefined;
154
185
  };
186
+ /** What asking for a texted sign-in code produced. */
187
+ type SmsCodeResult =
188
+ /** Sent. `to` is the number masked (`+45 ******78`); the code lasts `expiresIn` seconds. */
189
+ {
190
+ status: 'sent';
191
+ to: string;
192
+ expiresIn: number;
193
+ }
194
+ /** Too soon, or a sending limit was hit. Waiting `retryAfter` seconds may help. */
195
+ | {
196
+ status: 'rate_limited';
197
+ retryAfter: number | undefined;
198
+ }
199
+ /** The text provider failed. Try again shortly, or offer another factor. */
200
+ | {
201
+ status: 'unavailable';
202
+ }
203
+ /**
204
+ * Refused: an unknown or spent token, no number on file, a country texts are not sent
205
+ * to, or no attempts left. One answer for every reason, like a wrong password.
206
+ */
207
+ | {
208
+ status: 'invalid';
209
+ };
155
210
  /** What a browser needs to ask an authenticator for a passkey assertion. */
156
211
  interface PasskeyOptions {
157
212
  challenge: string;
@@ -197,7 +252,7 @@ declare class CboxIdFrontend {
197
252
  * fetches of the same immutable document is a waste the SDK should absorb rather than
198
253
  * ask every consumer to.
199
254
  */
200
- private configPromise;
255
+ private readonly configPromises;
201
256
  constructor(options: FrontendClientOptions);
202
257
  /** Whether this key drives real sign-ins. Handy for a "test mode" badge in your own UI. */
203
258
  get isLive(): boolean;
@@ -207,8 +262,9 @@ declare class CboxIdFrontend {
207
262
  * Fetched once per client instance. The document is small and changes when somebody
208
263
  * edits it in the console, so a long-lived page picks changes up on its next full load
209
264
  * rather than mid-session — which is the right trade for something that decides layout.
265
+ * Each `organization` is fetched and cached on its own.
210
266
  */
211
- config(): Promise<FrontendConfig>;
267
+ config(options?: FrontendConfigOptions): Promise<FrontendConfig>;
212
268
  /**
213
269
  * Who is signed in, given an access token your app already holds.
214
270
  *
@@ -256,8 +312,29 @@ declare class CboxIdFrontend {
256
312
  *
257
313
  * A wrong code costs an attempt, not the sign-in: five are allowed before the token
258
314
  * dies and the person starts from the password again.
315
+ *
316
+ * `method` says which challenge the code answers: `'mfa'` for an authenticator or
317
+ * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent
318
+ * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the
319
+ * server cannot tell them apart by shape — answering one as the other is refused.
320
+ */
321
+ submitSecondFactor(mfaToken: string, code: string, method?: 'mfa' | 'otp' | 'sms'): Promise<SignInResult>;
322
+ /**
323
+ * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.
324
+ * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.
325
+ *
326
+ * Only call it when the person asks ("Text me a code"), never on page load: each send
327
+ * spends one of the token's five attempts — that is what bounds how many texts one
328
+ * proved password can cause — and every text costs the customer money.
329
+ *
330
+ * ```ts
331
+ * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
332
+ * const sent = await frontend.sendSmsCode(result.mfaToken)
333
+ * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)
334
+ * }
335
+ * ```
259
336
  */
260
- submitSecondFactor(mfaToken: string, code: string, method?: 'mfa' | 'otp'): Promise<SignInResult>;
337
+ sendSmsCode(mfaToken: string): Promise<SmsCodeResult>;
261
338
  /**
262
339
  * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.
263
340
  *
@@ -442,4 +519,4 @@ declare function buildManifest(declaration: AuthzDeclaration): Promise<AuthzMani
442
519
  */
443
520
  declare function publishManifest(config: ManifestPublisherConfig, declaration: AuthzDeclaration): Promise<ManifestSyncSummary>;
444
521
 
445
- export { type Appearance, type AuthzDeclaration, type AuthzManifest, CboxIdFrontend, type FrontendClientOptions, type FrontendConfig, type FrontendSession, type LegacyUser, type LegacyVerifierOptions, type ManifestPermission, type ManifestPublisherConfig, type ManifestRole, type ManifestSyncSummary, type PasskeyOptions, type PermissionDefinition, type RoleDefinition, type SignInResult, type SocialProvider, buildManifest, challenge, createLegacyVerifier, createVerifier, defineAuthz, publishManifest, randomToken };
522
+ export { type Appearance, type AuthzDeclaration, type AuthzManifest, CboxIdFrontend, type FrontendClientOptions, type FrontendConfig, type FrontendConfigOptions, type FrontendSession, type LegacyUser, type LegacyVerifierOptions, type ManifestPermission, type ManifestPublisherConfig, type ManifestRole, type ManifestSyncSummary, type PasskeyOptions, type PermissionDefinition, type RoleDefinition, type SecondFactorKind, type SignInResult, type SmsCodeResult, type SocialProvider, buildManifest, challenge, createLegacyVerifier, createVerifier, defineAuthz, publishManifest, randomToken };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { Discovery } from './chunk-CXGKOJRJ.js';
2
- export { CboxIdClient, VaultClient, actor, challenge, createVerifier, hasPermission, hasRole, isSupportSession, organization, permissions, randomToken, roles, sessionId, verifyStandardWebhook, verifyWebhook } from './chunk-CXGKOJRJ.js';
1
+ import { Discovery } from './chunk-YGLMLIKL.js';
2
+ export { CboxIdClient, FEATURE_FLAGS_SCOPE, PipeLeaseDeniedError, PipeLeaseError, PipeNotConnectedError, PipeReauthorizationRequiredError, PipeTemporarilyUnavailableError, VaultClient, actor, challenge, createVerifier, featureFlags, hasFeature, hasPermission, hasRole, isSupportSession, organization, permissions, pipeConnectUrl, randomToken, roles, sessionId, verifyStandardWebhook, verifyWebhook, withConnectReturn } from './chunk-YGLMLIKL.js';
3
3
  import { assertSecureIssuer } from './chunk-QQW63PGU.js';
4
4
  import { ConfigurationError, FrontendApiError, AuthenticationError } from './chunk-SEUD4TE4.js';
5
5
  export { AuthenticationError, CboxIdError, ConfigurationError, FrontendApiError, InvalidStateError } from './chunk-SEUD4TE4.js';
@@ -98,6 +98,7 @@ function json(body, status) {
98
98
  }
99
99
 
100
100
  // src/frontend.ts
101
+ var SECOND_FACTOR_KINDS = /* @__PURE__ */ new Set(["totp", "sms", "recovery_code"]);
101
102
  var CboxIdFrontend = class {
102
103
  issuer;
103
104
  publishableKey;
@@ -112,7 +113,7 @@ var CboxIdFrontend = class {
112
113
  * fetches of the same immutable document is a waste the SDK should absorb rather than
113
114
  * ask every consumer to.
114
115
  */
115
- configPromise = null;
116
+ configPromises = /* @__PURE__ */ new Map();
116
117
  constructor(options) {
117
118
  if (!options.issuer) {
118
119
  throw new ConfigurationError("issuer is required.");
@@ -139,13 +140,20 @@ var CboxIdFrontend = class {
139
140
  * Fetched once per client instance. The document is small and changes when somebody
140
141
  * edits it in the console, so a long-lived page picks changes up on its next full load
141
142
  * rather than mid-session — which is the right trade for something that decides layout.
143
+ * Each `organization` is fetched and cached on its own.
142
144
  */
143
- async config() {
144
- this.configPromise ??= this.get("/frontend/v1/config", {}, isFrontendConfig).catch((error) => {
145
- this.configPromise = null;
146
- throw error;
147
- });
148
- return this.configPromise;
145
+ async config(options = {}) {
146
+ const organization2 = options.organization ?? "";
147
+ let pending = this.configPromises.get(organization2);
148
+ if (pending === void 0) {
149
+ const path = organization2 === "" ? "/frontend/v1/config" : `/frontend/v1/config?${new URLSearchParams({ organization: organization2 }).toString()}`;
150
+ pending = this.get(path, {}, isFrontendConfig).catch((error) => {
151
+ this.configPromises.delete(organization2);
152
+ throw error;
153
+ });
154
+ this.configPromises.set(organization2, pending);
155
+ }
156
+ return pending;
149
157
  }
150
158
  /**
151
159
  * Who is signed in, given an access token your app already holds.
@@ -207,8 +215,12 @@ var CboxIdFrontend = class {
207
215
  if (body.status === "ok" && typeof body.login_ticket === "string") {
208
216
  return { status: "ok", loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 };
209
217
  }
210
- if ((body.status === "mfa_required" || body.status === "otp_required") && typeof body.mfa_token === "string") {
211
- return { status: body.status, mfaToken: body.mfa_token };
218
+ if (body.status === "mfa_required" && typeof body.mfa_token === "string") {
219
+ const factors = Array.isArray(body.factors) ? body.factors.filter((f) => typeof f === "string" && SECOND_FACTOR_KINDS.has(f)) : void 0;
220
+ return { status: "mfa_required", mfaToken: body.mfa_token, ...factors ? { factors } : {} };
221
+ }
222
+ if (body.status === "otp_required" && typeof body.mfa_token === "string") {
223
+ return { status: "otp_required", mfaToken: body.mfa_token };
212
224
  }
213
225
  if (body.status === "sso_required") {
214
226
  return { status: "sso_required" };
@@ -225,6 +237,11 @@ var CboxIdFrontend = class {
225
237
  *
226
238
  * A wrong code costs an attempt, not the sign-in: five are allowed before the token
227
239
  * dies and the person starts from the password again.
240
+ *
241
+ * `method` says which challenge the code answers: `'mfa'` for an authenticator or
242
+ * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent
243
+ * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the
244
+ * server cannot tell them apart by shape — answering one as the other is refused.
228
245
  */
229
246
  async submitSecondFactor(mfaToken, code, method = "mfa") {
230
247
  const response = await this.post("/frontend/v1/sign-in/factor", { mfa_token: mfaToken, code, method });
@@ -236,6 +253,41 @@ var CboxIdFrontend = class {
236
253
  }
237
254
  return body.status === "ok" && typeof body.login_ticket === "string" ? { status: "ok", loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 } : { status: "invalid" };
238
255
  }
256
+ /**
257
+ * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.
258
+ * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.
259
+ *
260
+ * Only call it when the person asks ("Text me a code"), never on page load: each send
261
+ * spends one of the token's five attempts — that is what bounds how many texts one
262
+ * proved password can cause — and every text costs the customer money.
263
+ *
264
+ * ```ts
265
+ * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
266
+ * const sent = await frontend.sendSmsCode(result.mfaToken)
267
+ * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)
268
+ * }
269
+ * ```
270
+ */
271
+ async sendSmsCode(mfaToken) {
272
+ const response = await this.post("/frontend/v1/sign-in/factor/sms", { mfa_token: mfaToken });
273
+ let body;
274
+ try {
275
+ body = await response.json();
276
+ } catch {
277
+ throw new FrontendApiError("Cbox ID returned a body that is not JSON.", "malformed", response.status);
278
+ }
279
+ if (body.status === "sent" && typeof body.to === "string") {
280
+ return { status: "sent", to: body.to, expiresIn: typeof body.expires_in === "number" ? body.expires_in : 300 };
281
+ }
282
+ if (body.status === "rate_limited" || response.status === 429) {
283
+ const retryAfter = typeof body.retry_after === "number" ? body.retry_after : Number(response.headers.get("Retry-After") ?? "");
284
+ return { status: "rate_limited", retryAfter: Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0 };
285
+ }
286
+ if (body.status === "unavailable") {
287
+ return { status: "unavailable" };
288
+ }
289
+ return { status: "invalid" };
290
+ }
239
291
  /**
240
292
  * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.
241
293
  *