@cboxdk/id-js 0.18.0 → 0.20.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/README.md +121 -4
  3. package/RELEASE-CANDIDATE.md +11 -0
  4. package/dist/api-keys-BE3R3q_A.d.ts +105 -0
  5. package/dist/api-keys-C8-Q4eFA.d.cts +105 -0
  6. package/dist/chunk-EVZ6RB3K.js +98 -0
  7. package/dist/chunk-EVZ6RB3K.js.map +1 -0
  8. package/dist/{chunk-CXGKOJRJ.js → chunk-YGLMLIKL.js} +144 -9
  9. package/dist/chunk-YGLMLIKL.js.map +1 -0
  10. package/dist/{client-BxUHm5hX.d.cts → client-BT-jOm0p.d.cts} +126 -2
  11. package/dist/{client-BSZASrMo.d.ts → client-xlEJalu8.d.ts} +126 -2
  12. package/dist/decisions-C84Fzah4.d.ts +23 -0
  13. package/dist/decisions-DNd9UXwi.d.cts +23 -0
  14. package/dist/index.cjs +338 -28
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +84 -6
  17. package/dist/index.d.ts +84 -6
  18. package/dist/index.js +87 -15
  19. package/dist/index.js.map +1 -1
  20. package/dist/management.cjs +1115 -62
  21. package/dist/management.cjs.map +1 -1
  22. package/dist/management.d.cts +3618 -135
  23. package/dist/management.d.ts +3618 -135
  24. package/dist/management.js +1115 -63
  25. package/dist/management.js.map +1 -1
  26. package/dist/nextjs.cjs +136 -5
  27. package/dist/nextjs.cjs.map +1 -1
  28. package/dist/nextjs.d.cts +4 -3
  29. package/dist/nextjs.d.ts +4 -3
  30. package/dist/nextjs.js +1 -1
  31. package/dist/server.cjs +103 -9
  32. package/dist/server.cjs.map +1 -1
  33. package/dist/server.d.cts +3 -105
  34. package/dist/server.d.ts +3 -105
  35. package/dist/server.js +1 -0
  36. package/dist/{types-BYZldock.d.cts → types-CC1bz68x.d.cts} +30 -1
  37. package/dist/{types-BYZldock.d.ts → types-CC1bz68x.d.ts} +30 -1
  38. package/package.json +8 -3
  39. package/dist/chunk-CXGKOJRJ.js.map +0 -1
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
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
+ export { P as PermissionDecision, a as PermissionDecisionConfig, c as checkPermissions } from './decisions-DNd9UXwi.cjs';
4
5
 
5
6
  /**
6
7
  * The handler on YOUR side of a legacy migration.
@@ -130,9 +131,35 @@ interface FrontendConfig {
130
131
  end_session: string;
131
132
  jwks: string;
132
133
  };
134
+ /**
135
+ * The social buttons to draw, exactly as the hosted sign-in page would: the environment's
136
+ * own providers, or — with `config({ organization })` — that organization's.
137
+ */
133
138
  social: SocialProvider[];
134
139
  appearance?: Appearance;
140
+ /**
141
+ * Which other sign-in methods are switched on, so a box does not draw a button whose
142
+ * endpoint answers 403. Absent from instances that predate it.
143
+ */
144
+ methods?: {
145
+ passkeys: boolean;
146
+ magic_link: boolean;
147
+ };
148
+ }
149
+ /** Options for {@link CboxIdFrontend.config}. */
150
+ interface FrontendConfigOptions {
151
+ /**
152
+ * The organization the box is for, by id or slug — the way a hosted page is addressed.
153
+ * `social` is then exactly what that organization's sign-in page offers. An organization
154
+ * the environment does not have is answered as no hint at all.
155
+ */
156
+ organization?: string;
135
157
  }
158
+ /**
159
+ * A second factor the person can answer an `mfa_required` with. Kinds only — the server
160
+ * never says which number a text would go to before one is sent.
161
+ */
162
+ type SecondFactorKind = 'totp' | 'sms' | 'recovery_code';
136
163
  /** What a sign-in attempt produced. */
137
164
  type SignInResult = {
138
165
  status: 'ok';
@@ -141,6 +168,11 @@ type SignInResult = {
141
168
  } | {
142
169
  status: 'mfa_required';
143
170
  mfaToken: string;
171
+ /**
172
+ * The factors this person can answer with (laravel-id 1.24+). Absent when the
173
+ * instance predates the list — then only an authenticator or a recovery code works.
174
+ */
175
+ factors?: SecondFactorKind[];
144
176
  } | {
145
177
  status: 'otp_required';
146
178
  mfaToken: string;
@@ -152,6 +184,30 @@ type SignInResult = {
152
184
  status: 'rate_limited';
153
185
  retryAfter: number | undefined;
154
186
  };
187
+ /** What asking for a texted sign-in code produced. */
188
+ type SmsCodeResult =
189
+ /** Sent. `to` is the number masked (`+45 ******78`); the code lasts `expiresIn` seconds. */
190
+ {
191
+ status: 'sent';
192
+ to: string;
193
+ expiresIn: number;
194
+ }
195
+ /** Too soon, or a sending limit was hit. Waiting `retryAfter` seconds may help. */
196
+ | {
197
+ status: 'rate_limited';
198
+ retryAfter: number | undefined;
199
+ }
200
+ /** The text provider failed. Try again shortly, or offer another factor. */
201
+ | {
202
+ status: 'unavailable';
203
+ }
204
+ /**
205
+ * Refused: an unknown or spent token, no number on file, a country texts are not sent
206
+ * to, or no attempts left. One answer for every reason, like a wrong password.
207
+ */
208
+ | {
209
+ status: 'invalid';
210
+ };
155
211
  /** What a browser needs to ask an authenticator for a passkey assertion. */
156
212
  interface PasskeyOptions {
157
213
  challenge: string;
@@ -197,7 +253,7 @@ declare class CboxIdFrontend {
197
253
  * fetches of the same immutable document is a waste the SDK should absorb rather than
198
254
  * ask every consumer to.
199
255
  */
200
- private configPromise;
256
+ private readonly configPromises;
201
257
  constructor(options: FrontendClientOptions);
202
258
  /** Whether this key drives real sign-ins. Handy for a "test mode" badge in your own UI. */
203
259
  get isLive(): boolean;
@@ -207,8 +263,9 @@ declare class CboxIdFrontend {
207
263
  * Fetched once per client instance. The document is small and changes when somebody
208
264
  * edits it in the console, so a long-lived page picks changes up on its next full load
209
265
  * rather than mid-session — which is the right trade for something that decides layout.
266
+ * Each `organization` is fetched and cached on its own.
210
267
  */
211
- config(): Promise<FrontendConfig>;
268
+ config(options?: FrontendConfigOptions): Promise<FrontendConfig>;
212
269
  /**
213
270
  * Who is signed in, given an access token your app already holds.
214
271
  *
@@ -256,8 +313,29 @@ declare class CboxIdFrontend {
256
313
  *
257
314
  * A wrong code costs an attempt, not the sign-in: five are allowed before the token
258
315
  * dies and the person starts from the password again.
316
+ *
317
+ * `method` says which challenge the code answers: `'mfa'` for an authenticator or
318
+ * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent
319
+ * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the
320
+ * server cannot tell them apart by shape — answering one as the other is refused.
321
+ */
322
+ submitSecondFactor(mfaToken: string, code: string, method?: 'mfa' | 'otp' | 'sms'): Promise<SignInResult>;
323
+ /**
324
+ * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.
325
+ * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.
326
+ *
327
+ * Only call it when the person asks ("Text me a code"), never on page load: each send
328
+ * spends one of the token's five attempts — that is what bounds how many texts one
329
+ * proved password can cause — and every text costs the customer money.
330
+ *
331
+ * ```ts
332
+ * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
333
+ * const sent = await frontend.sendSmsCode(result.mfaToken)
334
+ * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)
335
+ * }
336
+ * ```
259
337
  */
260
- submitSecondFactor(mfaToken: string, code: string, method?: 'mfa' | 'otp'): Promise<SignInResult>;
338
+ sendSmsCode(mfaToken: string): Promise<SmsCodeResult>;
261
339
  /**
262
340
  * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.
263
341
  *
@@ -442,4 +520,4 @@ declare function buildManifest(declaration: AuthzDeclaration): Promise<AuthzMani
442
520
  */
443
521
  declare function publishManifest(config: ManifestPublisherConfig, declaration: AuthzDeclaration): Promise<ManifestSyncSummary>;
444
522
 
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 };
523
+ 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,7 @@
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
+ export { P as PermissionDecision, a as PermissionDecisionConfig, c as checkPermissions } from './decisions-C84Fzah4.js';
4
5
 
5
6
  /**
6
7
  * The handler on YOUR side of a legacy migration.
@@ -130,9 +131,35 @@ interface FrontendConfig {
130
131
  end_session: string;
131
132
  jwks: string;
132
133
  };
134
+ /**
135
+ * The social buttons to draw, exactly as the hosted sign-in page would: the environment's
136
+ * own providers, or — with `config({ organization })` — that organization's.
137
+ */
133
138
  social: SocialProvider[];
134
139
  appearance?: Appearance;
140
+ /**
141
+ * Which other sign-in methods are switched on, so a box does not draw a button whose
142
+ * endpoint answers 403. Absent from instances that predate it.
143
+ */
144
+ methods?: {
145
+ passkeys: boolean;
146
+ magic_link: boolean;
147
+ };
148
+ }
149
+ /** Options for {@link CboxIdFrontend.config}. */
150
+ interface FrontendConfigOptions {
151
+ /**
152
+ * The organization the box is for, by id or slug — the way a hosted page is addressed.
153
+ * `social` is then exactly what that organization's sign-in page offers. An organization
154
+ * the environment does not have is answered as no hint at all.
155
+ */
156
+ organization?: string;
135
157
  }
158
+ /**
159
+ * A second factor the person can answer an `mfa_required` with. Kinds only — the server
160
+ * never says which number a text would go to before one is sent.
161
+ */
162
+ type SecondFactorKind = 'totp' | 'sms' | 'recovery_code';
136
163
  /** What a sign-in attempt produced. */
137
164
  type SignInResult = {
138
165
  status: 'ok';
@@ -141,6 +168,11 @@ type SignInResult = {
141
168
  } | {
142
169
  status: 'mfa_required';
143
170
  mfaToken: string;
171
+ /**
172
+ * The factors this person can answer with (laravel-id 1.24+). Absent when the
173
+ * instance predates the list — then only an authenticator or a recovery code works.
174
+ */
175
+ factors?: SecondFactorKind[];
144
176
  } | {
145
177
  status: 'otp_required';
146
178
  mfaToken: string;
@@ -152,6 +184,30 @@ type SignInResult = {
152
184
  status: 'rate_limited';
153
185
  retryAfter: number | undefined;
154
186
  };
187
+ /** What asking for a texted sign-in code produced. */
188
+ type SmsCodeResult =
189
+ /** Sent. `to` is the number masked (`+45 ******78`); the code lasts `expiresIn` seconds. */
190
+ {
191
+ status: 'sent';
192
+ to: string;
193
+ expiresIn: number;
194
+ }
195
+ /** Too soon, or a sending limit was hit. Waiting `retryAfter` seconds may help. */
196
+ | {
197
+ status: 'rate_limited';
198
+ retryAfter: number | undefined;
199
+ }
200
+ /** The text provider failed. Try again shortly, or offer another factor. */
201
+ | {
202
+ status: 'unavailable';
203
+ }
204
+ /**
205
+ * Refused: an unknown or spent token, no number on file, a country texts are not sent
206
+ * to, or no attempts left. One answer for every reason, like a wrong password.
207
+ */
208
+ | {
209
+ status: 'invalid';
210
+ };
155
211
  /** What a browser needs to ask an authenticator for a passkey assertion. */
156
212
  interface PasskeyOptions {
157
213
  challenge: string;
@@ -197,7 +253,7 @@ declare class CboxIdFrontend {
197
253
  * fetches of the same immutable document is a waste the SDK should absorb rather than
198
254
  * ask every consumer to.
199
255
  */
200
- private configPromise;
256
+ private readonly configPromises;
201
257
  constructor(options: FrontendClientOptions);
202
258
  /** Whether this key drives real sign-ins. Handy for a "test mode" badge in your own UI. */
203
259
  get isLive(): boolean;
@@ -207,8 +263,9 @@ declare class CboxIdFrontend {
207
263
  * Fetched once per client instance. The document is small and changes when somebody
208
264
  * edits it in the console, so a long-lived page picks changes up on its next full load
209
265
  * rather than mid-session — which is the right trade for something that decides layout.
266
+ * Each `organization` is fetched and cached on its own.
210
267
  */
211
- config(): Promise<FrontendConfig>;
268
+ config(options?: FrontendConfigOptions): Promise<FrontendConfig>;
212
269
  /**
213
270
  * Who is signed in, given an access token your app already holds.
214
271
  *
@@ -256,8 +313,29 @@ declare class CboxIdFrontend {
256
313
  *
257
314
  * A wrong code costs an attempt, not the sign-in: five are allowed before the token
258
315
  * dies and the person starts from the password again.
316
+ *
317
+ * `method` says which challenge the code answers: `'mfa'` for an authenticator or
318
+ * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent
319
+ * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the
320
+ * server cannot tell them apart by shape — answering one as the other is refused.
321
+ */
322
+ submitSecondFactor(mfaToken: string, code: string, method?: 'mfa' | 'otp' | 'sms'): Promise<SignInResult>;
323
+ /**
324
+ * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.
325
+ * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.
326
+ *
327
+ * Only call it when the person asks ("Text me a code"), never on page load: each send
328
+ * spends one of the token's five attempts — that is what bounds how many texts one
329
+ * proved password can cause — and every text costs the customer money.
330
+ *
331
+ * ```ts
332
+ * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
333
+ * const sent = await frontend.sendSmsCode(result.mfaToken)
334
+ * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)
335
+ * }
336
+ * ```
259
337
  */
260
- submitSecondFactor(mfaToken: string, code: string, method?: 'mfa' | 'otp'): Promise<SignInResult>;
338
+ sendSmsCode(mfaToken: string): Promise<SmsCodeResult>;
261
339
  /**
262
340
  * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.
263
341
  *
@@ -442,4 +520,4 @@ declare function buildManifest(declaration: AuthzDeclaration): Promise<AuthzMani
442
520
  */
443
521
  declare function publishManifest(config: ManifestPublisherConfig, declaration: AuthzDeclaration): Promise<ManifestSyncSummary>;
444
522
 
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 };
523
+ 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,6 @@
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
+ export { PermissionDecision, checkPermissions } from './chunk-EVZ6RB3K.js';
3
4
  import { assertSecureIssuer } from './chunk-QQW63PGU.js';
4
5
  import { ConfigurationError, FrontendApiError, AuthenticationError } from './chunk-SEUD4TE4.js';
5
6
  export { AuthenticationError, CboxIdError, ConfigurationError, FrontendApiError, InvalidStateError } from './chunk-SEUD4TE4.js';
@@ -98,6 +99,7 @@ function json(body, status) {
98
99
  }
99
100
 
100
101
  // src/frontend.ts
102
+ var SECOND_FACTOR_KINDS = /* @__PURE__ */ new Set(["totp", "sms", "recovery_code"]);
101
103
  var CboxIdFrontend = class {
102
104
  issuer;
103
105
  publishableKey;
@@ -112,7 +114,7 @@ var CboxIdFrontend = class {
112
114
  * fetches of the same immutable document is a waste the SDK should absorb rather than
113
115
  * ask every consumer to.
114
116
  */
115
- configPromise = null;
117
+ configPromises = /* @__PURE__ */ new Map();
116
118
  constructor(options) {
117
119
  if (!options.issuer) {
118
120
  throw new ConfigurationError("issuer is required.");
@@ -139,13 +141,20 @@ var CboxIdFrontend = class {
139
141
  * Fetched once per client instance. The document is small and changes when somebody
140
142
  * edits it in the console, so a long-lived page picks changes up on its next full load
141
143
  * rather than mid-session — which is the right trade for something that decides layout.
144
+ * Each `organization` is fetched and cached on its own.
142
145
  */
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;
146
+ async config(options = {}) {
147
+ const organization2 = options.organization ?? "";
148
+ let pending = this.configPromises.get(organization2);
149
+ if (pending === void 0) {
150
+ const path = organization2 === "" ? "/frontend/v1/config" : `/frontend/v1/config?${new URLSearchParams({ organization: organization2 }).toString()}`;
151
+ pending = this.get(path, {}, isFrontendConfig).catch((error) => {
152
+ this.configPromises.delete(organization2);
153
+ throw error;
154
+ });
155
+ this.configPromises.set(organization2, pending);
156
+ }
157
+ return pending;
149
158
  }
150
159
  /**
151
160
  * Who is signed in, given an access token your app already holds.
@@ -207,8 +216,12 @@ var CboxIdFrontend = class {
207
216
  if (body.status === "ok" && typeof body.login_ticket === "string") {
208
217
  return { status: "ok", loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 };
209
218
  }
210
- if ((body.status === "mfa_required" || body.status === "otp_required") && typeof body.mfa_token === "string") {
211
- return { status: body.status, mfaToken: body.mfa_token };
219
+ if (body.status === "mfa_required" && typeof body.mfa_token === "string") {
220
+ const factors = Array.isArray(body.factors) ? body.factors.filter((f) => typeof f === "string" && SECOND_FACTOR_KINDS.has(f)) : void 0;
221
+ return { status: "mfa_required", mfaToken: body.mfa_token, ...factors ? { factors } : {} };
222
+ }
223
+ if (body.status === "otp_required" && typeof body.mfa_token === "string") {
224
+ return { status: "otp_required", mfaToken: body.mfa_token };
212
225
  }
213
226
  if (body.status === "sso_required") {
214
227
  return { status: "sso_required" };
@@ -225,6 +238,11 @@ var CboxIdFrontend = class {
225
238
  *
226
239
  * A wrong code costs an attempt, not the sign-in: five are allowed before the token
227
240
  * dies and the person starts from the password again.
241
+ *
242
+ * `method` says which challenge the code answers: `'mfa'` for an authenticator or
243
+ * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent
244
+ * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the
245
+ * server cannot tell them apart by shape — answering one as the other is refused.
228
246
  */
229
247
  async submitSecondFactor(mfaToken, code, method = "mfa") {
230
248
  const response = await this.post("/frontend/v1/sign-in/factor", { mfa_token: mfaToken, code, method });
@@ -236,6 +254,41 @@ var CboxIdFrontend = class {
236
254
  }
237
255
  return body.status === "ok" && typeof body.login_ticket === "string" ? { status: "ok", loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 } : { status: "invalid" };
238
256
  }
257
+ /**
258
+ * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.
259
+ * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.
260
+ *
261
+ * Only call it when the person asks ("Text me a code"), never on page load: each send
262
+ * spends one of the token's five attempts — that is what bounds how many texts one
263
+ * proved password can cause — and every text costs the customer money.
264
+ *
265
+ * ```ts
266
+ * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
267
+ * const sent = await frontend.sendSmsCode(result.mfaToken)
268
+ * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)
269
+ * }
270
+ * ```
271
+ */
272
+ async sendSmsCode(mfaToken) {
273
+ const response = await this.post("/frontend/v1/sign-in/factor/sms", { mfa_token: mfaToken });
274
+ let body;
275
+ try {
276
+ body = await response.json();
277
+ } catch {
278
+ throw new FrontendApiError("Cbox ID returned a body that is not JSON.", "malformed", response.status);
279
+ }
280
+ if (body.status === "sent" && typeof body.to === "string") {
281
+ return { status: "sent", to: body.to, expiresIn: typeof body.expires_in === "number" ? body.expires_in : 300 };
282
+ }
283
+ if (body.status === "rate_limited" || response.status === 429) {
284
+ const retryAfter = typeof body.retry_after === "number" ? body.retry_after : Number(response.headers.get("Retry-After") ?? "");
285
+ return { status: "rate_limited", retryAfter: Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0 };
286
+ }
287
+ if (body.status === "unavailable") {
288
+ return { status: "unavailable" };
289
+ }
290
+ return { status: "invalid" };
291
+ }
239
292
  /**
240
293
  * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.
241
294
  *
@@ -435,6 +488,11 @@ async function publishManifest(config, declaration) {
435
488
  throw new ConfigurationError("Publishing a manifest needs issuer, clientId and clientSecret.");
436
489
  }
437
490
  const timeoutMs = config.timeoutMs ?? 1e4;
491
+ assertSecureIssuer(config.issuer);
492
+ const issuer = new URL(config.issuer);
493
+ if (issuer.username || issuer.password || issuer.search || issuer.hash) {
494
+ throw new ConfigurationError("The manifest issuer must not contain credentials, a query or a fragment.");
495
+ }
438
496
  const manifest = await buildManifest(declaration);
439
497
  const discovery = new Discovery(config.issuer, timeoutMs, 36e5);
440
498
  const token = await mintToken(config, discovery, timeoutMs);
@@ -442,6 +500,7 @@ async function publishManifest(config, declaration) {
442
500
  `${config.issuer.replace(/\/$/, "")}/api/v1/apps/manifest`,
443
501
  {
444
502
  method: "POST",
503
+ redirect: "error",
445
504
  headers: {
446
505
  authorization: `Bearer ${token}`,
447
506
  accept: "application/json",
@@ -452,10 +511,20 @@ async function publishManifest(config, declaration) {
452
511
  timeoutMs
453
512
  );
454
513
  if (!response.ok) {
455
- throw new AuthenticationError(`Manifest push failed: HTTP ${response.status} ${await response.text()}`);
514
+ throw new AuthenticationError(`Manifest push failed: HTTP ${response.status}`);
515
+ }
516
+ const json2 = await response.json().catch(() => {
517
+ throw new AuthenticationError("Cbox ID returned an invalid manifest acknowledgement.");
518
+ });
519
+ const checksum = await sha256Hex(canonicalManifestJson(declaration.permissions ?? [], declaration.roles ?? []));
520
+ if (json2 === null || typeof json2 !== "object" || Array.isArray(json2)) {
521
+ throw new AuthenticationError("Cbox ID did not acknowledge the authorization manifest.");
522
+ }
523
+ const summary = json2;
524
+ if (summary.client_id !== config.clientId || summary.version !== manifest.version || summary.checksum !== checksum || typeof summary.unchanged !== "boolean" || !Number.isInteger(summary.roles_declared) || !Number.isInteger(summary.permissions_declared) || (summary.roles_declared ?? -1) < 0 || (summary.permissions_declared ?? -1) < 0 || !summary.unchanged && (summary.roles_declared !== manifest.roles.length || summary.permissions_declared !== manifest.permissions.length)) {
525
+ throw new AuthenticationError("Cbox ID did not acknowledge this app and its complete authorization manifest.");
456
526
  }
457
- const json2 = await response.json();
458
- return json2 !== null && typeof json2 === "object" ? json2 : {};
527
+ return summary;
459
528
  }
460
529
  async function mintToken(config, discovery, timeoutMs) {
461
530
  const response = await fetchWithTimeout(
@@ -463,6 +532,7 @@ async function mintToken(config, discovery, timeoutMs) {
463
532
  {
464
533
  method: "POST",
465
534
  headers: { "content-type": "application/x-www-form-urlencoded" },
535
+ redirect: "error",
466
536
  body: new URLSearchParams({
467
537
  grant_type: "client_credentials",
468
538
  client_id: config.clientId,
@@ -473,7 +543,7 @@ async function mintToken(config, discovery, timeoutMs) {
473
543
  timeoutMs
474
544
  );
475
545
  const json2 = await response.json().catch(() => ({}));
476
- if (typeof json2.access_token !== "string" || json2.access_token === "") {
546
+ if (!response.ok || typeof json2.access_token !== "string" || json2.access_token === "") {
477
547
  throw new AuthenticationError(
478
548
  "Could not obtain an apps.manifest access token \u2014 check the client credentials and that the client holds the apps.manifest scope."
479
549
  );
@@ -595,6 +665,8 @@ async function fetchWithTimeout(url, init, timeoutMs) {
595
665
  const timer = setTimeout(() => controller.abort(), timeoutMs);
596
666
  try {
597
667
  return await fetch(url, { ...init, signal: controller.signal });
668
+ } catch {
669
+ throw new AuthenticationError("Cbox ID manifest publication could not be verified.");
598
670
  } finally {
599
671
  clearTimeout(timer);
600
672
  }