@cboxdk/id-js 0.19.0 → 0.21.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/CHANGELOG.md +213 -0
- package/README.md +51 -3
- package/RELEASE-CANDIDATE.md +11 -0
- package/dist/agent.cjs +613 -0
- package/dist/agent.cjs.map +1 -0
- package/dist/agent.d.cts +400 -0
- package/dist/agent.d.ts +400 -0
- package/dist/agent.js +527 -0
- package/dist/agent.js.map +1 -0
- package/dist/api-keys-BE3R3q_A.d.ts +105 -0
- package/dist/api-keys-C8-Q4eFA.d.cts +105 -0
- package/dist/chunk-EVZ6RB3K.js +98 -0
- package/dist/chunk-EVZ6RB3K.js.map +1 -0
- package/dist/{chunk-YGLMLIKL.js → chunk-T5LCQOLS.js} +4 -22
- package/dist/chunk-T5LCQOLS.js.map +1 -0
- package/dist/chunk-VGQASHTI.js +22 -0
- package/dist/chunk-VGQASHTI.js.map +1 -0
- package/dist/decisions-C84Fzah4.d.ts +23 -0
- package/dist/decisions-DNd9UXwi.d.cts +23 -0
- package/dist/index.cjs +127 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +27 -6
- package/dist/index.js.map +1 -1
- package/dist/management.cjs +352 -19
- package/dist/management.cjs.map +1 -1
- package/dist/management.d.cts +994 -42
- package/dist/management.d.ts +994 -42
- package/dist/management.js +352 -19
- package/dist/management.js.map +1 -1
- package/dist/nextjs.d.cts +1 -1
- package/dist/nextjs.d.ts +1 -1
- package/dist/nextjs.js +2 -1
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +103 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -105
- package/dist/server.d.ts +3 -105
- package/dist/server.js +1 -0
- package/package.json +16 -3
- package/dist/chunk-YGLMLIKL.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
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
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.
|
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
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
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.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Discovery } from './chunk-
|
|
2
|
-
export { CboxIdClient, FEATURE_FLAGS_SCOPE, PipeLeaseDeniedError, PipeLeaseError, PipeNotConnectedError, PipeReauthorizationRequiredError, PipeTemporarilyUnavailableError, VaultClient, actor,
|
|
1
|
+
import { Discovery } from './chunk-T5LCQOLS.js';
|
|
2
|
+
export { CboxIdClient, FEATURE_FLAGS_SCOPE, PipeLeaseDeniedError, PipeLeaseError, PipeNotConnectedError, PipeReauthorizationRequiredError, PipeTemporarilyUnavailableError, VaultClient, actor, featureFlags, hasFeature, hasPermission, hasRole, isSupportSession, organization, permissions, pipeConnectUrl, roles, sessionId, verifyStandardWebhook, verifyWebhook, withConnectReturn } from './chunk-T5LCQOLS.js';
|
|
3
|
+
export { PermissionDecision, checkPermissions } from './chunk-EVZ6RB3K.js';
|
|
4
|
+
export { challenge, createVerifier, randomToken } from './chunk-VGQASHTI.js';
|
|
3
5
|
import { assertSecureIssuer } from './chunk-QQW63PGU.js';
|
|
4
6
|
import { ConfigurationError, FrontendApiError, AuthenticationError } from './chunk-SEUD4TE4.js';
|
|
5
7
|
export { AuthenticationError, CboxIdError, ConfigurationError, FrontendApiError, InvalidStateError } from './chunk-SEUD4TE4.js';
|
|
@@ -487,6 +489,11 @@ async function publishManifest(config, declaration) {
|
|
|
487
489
|
throw new ConfigurationError("Publishing a manifest needs issuer, clientId and clientSecret.");
|
|
488
490
|
}
|
|
489
491
|
const timeoutMs = config.timeoutMs ?? 1e4;
|
|
492
|
+
assertSecureIssuer(config.issuer);
|
|
493
|
+
const issuer = new URL(config.issuer);
|
|
494
|
+
if (issuer.username || issuer.password || issuer.search || issuer.hash) {
|
|
495
|
+
throw new ConfigurationError("The manifest issuer must not contain credentials, a query or a fragment.");
|
|
496
|
+
}
|
|
490
497
|
const manifest = await buildManifest(declaration);
|
|
491
498
|
const discovery = new Discovery(config.issuer, timeoutMs, 36e5);
|
|
492
499
|
const token = await mintToken(config, discovery, timeoutMs);
|
|
@@ -494,6 +501,7 @@ async function publishManifest(config, declaration) {
|
|
|
494
501
|
`${config.issuer.replace(/\/$/, "")}/api/v1/apps/manifest`,
|
|
495
502
|
{
|
|
496
503
|
method: "POST",
|
|
504
|
+
redirect: "error",
|
|
497
505
|
headers: {
|
|
498
506
|
authorization: `Bearer ${token}`,
|
|
499
507
|
accept: "application/json",
|
|
@@ -504,10 +512,20 @@ async function publishManifest(config, declaration) {
|
|
|
504
512
|
timeoutMs
|
|
505
513
|
);
|
|
506
514
|
if (!response.ok) {
|
|
507
|
-
throw new AuthenticationError(`Manifest push failed: HTTP ${response.status}
|
|
515
|
+
throw new AuthenticationError(`Manifest push failed: HTTP ${response.status}`);
|
|
516
|
+
}
|
|
517
|
+
const json2 = await response.json().catch(() => {
|
|
518
|
+
throw new AuthenticationError("Cbox ID returned an invalid manifest acknowledgement.");
|
|
519
|
+
});
|
|
520
|
+
const checksum = await sha256Hex(canonicalManifestJson(declaration.permissions ?? [], declaration.roles ?? []));
|
|
521
|
+
if (json2 === null || typeof json2 !== "object" || Array.isArray(json2)) {
|
|
522
|
+
throw new AuthenticationError("Cbox ID did not acknowledge the authorization manifest.");
|
|
508
523
|
}
|
|
509
|
-
const
|
|
510
|
-
|
|
524
|
+
const summary = json2;
|
|
525
|
+
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)) {
|
|
526
|
+
throw new AuthenticationError("Cbox ID did not acknowledge this app and its complete authorization manifest.");
|
|
527
|
+
}
|
|
528
|
+
return summary;
|
|
511
529
|
}
|
|
512
530
|
async function mintToken(config, discovery, timeoutMs) {
|
|
513
531
|
const response = await fetchWithTimeout(
|
|
@@ -515,6 +533,7 @@ async function mintToken(config, discovery, timeoutMs) {
|
|
|
515
533
|
{
|
|
516
534
|
method: "POST",
|
|
517
535
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
536
|
+
redirect: "error",
|
|
518
537
|
body: new URLSearchParams({
|
|
519
538
|
grant_type: "client_credentials",
|
|
520
539
|
client_id: config.clientId,
|
|
@@ -525,7 +544,7 @@ async function mintToken(config, discovery, timeoutMs) {
|
|
|
525
544
|
timeoutMs
|
|
526
545
|
);
|
|
527
546
|
const json2 = await response.json().catch(() => ({}));
|
|
528
|
-
if (typeof json2.access_token !== "string" || json2.access_token === "") {
|
|
547
|
+
if (!response.ok || typeof json2.access_token !== "string" || json2.access_token === "") {
|
|
529
548
|
throw new AuthenticationError(
|
|
530
549
|
"Could not obtain an apps.manifest access token \u2014 check the client credentials and that the client holds the apps.manifest scope."
|
|
531
550
|
);
|
|
@@ -647,6 +666,8 @@ async function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
647
666
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
648
667
|
try {
|
|
649
668
|
return await fetch(url, { ...init, signal: controller.signal });
|
|
669
|
+
} catch {
|
|
670
|
+
throw new AuthenticationError("Cbox ID manifest publication could not be verified.");
|
|
650
671
|
} finally {
|
|
651
672
|
clearTimeout(timer);
|
|
652
673
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/legacy.ts","../src/frontend.ts","../src/authz.ts"],"names":["key","organization","permissions","roles","json"],"mappings":"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","file":"index.js","sourcesContent":["import { ConfigurationError } from './errors.js';\n\n/**\n * The handler on YOUR side of a legacy migration.\n *\n * Cbox ID offers it an email and a password it has never seen, and your code answers with\n * the person or with nothing. On a yes they are created in Cbox ID as a result of the\n * login that just succeeded, and from their next sign-in the old system is never asked\n * again.\n *\n * WHY THIS IS A FACTORY AND NOT A DOCUMENT. The wire format is simple enough to describe\n * in a paragraph — a signed POST, JSON in, JSON out — and describing it would mean every\n * customer writes the signature check themselves. That check is exactly the part people\n * get wrong: `===` instead of a constant-time compare, no freshness window, no thought\n * about replay. So the factory owns all of it and you write the one function that knows\n * your own database.\n *\n * It is deliberately framework-agnostic: it takes a `Request` and returns a `Response`,\n * which is what Next.js route handlers, Remix, Hono, Bun and Deno all speak natively.\n */\n\n/** What your handler answers with. Returning `null` means \"no\", and is not an error. */\nexport interface LegacyUser {\n email: string;\n name?: string | null;\n emailVerified?: boolean;\n /**\n * Your stored hash, when you can expose it.\n *\n * Passing it lets the person keep their password in Cbox ID verbatim — it is stored as\n * is and upgraded to the platform hasher on their next login, exactly like a\n * bulk-imported user. Omit it and Cbox ID hashes the password they just proved they\n * know, which is the honest option when your store cannot hand a hash over.\n */\n passwordHash?: string;\n}\n\nexport interface LegacyVerifierOptions {\n /** The same secret you declared in your manifest, or configured in the console. */\n secret: string;\n /**\n * Decide whether these credentials are good, and say who they belong to.\n *\n * Return `null` for no. THROWING IS NOT THE SAME as returning null: a throw means your\n * store could not decide, and is answered with a 503 so Cbox ID refuses the sign-in\n * rather than treating an outage as a wrong password.\n */\n verify: (email: string, password: string) => Promise<LegacyUser | null>;\n /**\n * How far out of date a request may be, in seconds. Defaults to 300.\n *\n * A window is what makes a captured request stop working. Too wide and a recording is\n * useful for hours; too narrow and ordinary clock skew starts refusing real traffic.\n */\n toleranceSeconds?: number;\n}\n\n/**\n * Build the request handler.\n *\n * ```ts\n * // app/api/cbox-legacy/route.ts\n * export const POST = createLegacyVerifier({\n * secret: process.env.CBOX_LEGACY_SECRET!,\n * async verify(email, password) {\n * const row = await db.users.findByEmail(email)\n * if (!row || !(await argon2.verify(row.password, password))) return null\n * return { email: row.email, name: row.name, emailVerified: !!row.confirmedAt }\n * },\n * })\n * ```\n */\nexport function createLegacyVerifier(options: LegacyVerifierOptions): (request: Request) => Promise<Response> {\n if (!options.secret || options.secret.length < 32) {\n throw new ConfigurationError(\n 'A legacy verifier secret must be at least 32 characters; it is the only thing proving a request came from Cbox ID.',\n );\n }\n\n const tolerance = options.toleranceSeconds ?? 300;\n\n return async function handle(request: Request): Promise<Response> {\n // Read once. The signature covers the RAW body, so re-serializing parsed JSON would\n // produce different bytes and every valid request would be refused.\n const raw = await request.text();\n\n if (!(await isSignedByCboxId(raw, request.headers.get('X-Cbox-Signature'), options.secret, tolerance))) {\n // No detail. A caller who cannot sign is not owed help distinguishing \"bad\n // signature\" from \"stale timestamp\" — both mean the same thing to anyone\n // legitimate, and the difference is a hint to anyone else.\n return json({ error: 'unauthorized' }, 401);\n }\n\n let payload: { email?: unknown; password?: unknown };\n\n try {\n payload = JSON.parse(raw) as typeof payload;\n } catch {\n return json({ error: 'invalid_request' }, 400);\n }\n\n const { email, password } = payload;\n\n if (typeof email !== 'string' || email === '') {\n return json({ error: 'invalid_request' }, 400);\n }\n\n // No password means Cbox ID is asking \"do you know this address\" — the tooling path,\n // used for a dry run before cutover. It never reaches here from a sign-in.\n if (typeof password !== 'string') {\n return json({ user: null }, 200);\n }\n\n let user: LegacyUser | null;\n\n try {\n user = await options.verify(email, password);\n } catch {\n // 503, not 401. Your store could not decide, and Cbox ID must refuse the sign-in\n // rather than read an outage as a wrong password — but it should also be able to\n // tell the two apart in its own logs, which a 401 would hide.\n return json({ error: 'unavailable' }, 503);\n }\n\n if (!user) {\n return json({ user: null }, 200);\n }\n\n return json(\n {\n email: user.email,\n name: user.name ?? null,\n email_verified: user.emailVerified === true,\n ...(user.passwordHash ? { password_hash: user.passwordHash } : {}),\n },\n 200,\n );\n };\n}\n\n/**\n * Whether this body was signed by Cbox ID, recently.\n *\n * Both halves matter. The HMAC proves it came from something holding the secret; the\n * freshness window is what stops a captured request working forever. Compared in constant\n * time, because a comparison that returns early leaks how much of a guess was right.\n */\nasync function isSignedByCboxId(\n body: string,\n header: string | null,\n secret: string,\n toleranceSeconds: number,\n): Promise<boolean> {\n if (!header) {\n return false;\n }\n\n const parts: Record<string, string> = {};\n\n for (const piece of header.split(',')) {\n const [key, value] = piece.split('=', 2);\n if (key && value) {\n parts[key.trim()] = value.trim();\n }\n }\n\n const timestamp = parts['t'] ?? '';\n const signature = parts['v1'] ?? '';\n\n if (!/^\\d+$/.test(timestamp) || signature === '') {\n return false;\n }\n\n if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > toleranceSeconds) {\n return false;\n }\n\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${timestamp}.${body}`));\n const expected = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');\n\n return timingSafeEqual(expected, signature);\n}\n\n/** Constant-time string compare. Length is not secret; content is. */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n let diff = 0;\n\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return diff === 0;\n}\n\nfunction json(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n}\n","import { assertSecureIssuer } from './issuer.js';\nimport { ConfigurationError, FrontendApiError } from './errors.js';\n\n/**\n * The browser-safe half of the SDK.\n *\n * Everything else in this package assumes a server: it holds a client secret, it exchanges\n * codes, it verifies webhooks. None of that may run in a page, which is why the UI\n * components have always been redirect shells — a page that cannot authenticate itself to\n * the identity provider cannot ask it anything, so it sends the person away and waits.\n *\n * A publishable key changes that. It is public on purpose: it ships in your bundle, it is\n * visible in devtools, and it is safe there because it only works from the origins you\n * registered. What it buys is that a page can read its OWN sign-in configuration — the\n * endpoints, the social buttons, your theme — and render a form itself.\n *\n * This client deliberately has no secret, no token storage and no crypto. It reads two\n * documents. Anything that mints or holds a credential still belongs on your server.\n */\n\n/** How a page identifies itself. Public — put it in your bundle. */\nexport interface FrontendClientOptions {\n /** Your identity provider's origin, e.g. `https://id.acme.com`. */\n issuer: string;\n /** `pk_live_…` or `pk_test_…`. Safe to publish. */\n publishableKey: string;\n /** Abort after this long. Defaults to 10s. */\n timeoutMs?: number;\n /**\n * How many times to retry a transient failure. Defaults to 2 (so three attempts).\n *\n * Only the network and 5xx are retried. A refusal is a configuration problem and\n * retrying it just delays the developer finding out; a 429 is honoured rather than\n * hammered.\n */\n retries?: number;\n /** Injectable for tests and for runtimes with a non-global fetch. */\n fetch?: typeof globalThis.fetch;\n}\n\n/** A social button to draw. Name and provider only — never an internal id. */\nexport interface SocialProvider {\n provider: string;\n name: string;\n}\n\n/** The customer's theme, when they have set one. Absent means \"use your own defaults\". */\nexport interface Appearance {\n preset: string;\n radius: string;\n font: string;\n light: Record<string, string>;\n dark: Record<string, string>;\n}\n\n/** Everything needed to draw a sign-in box, and nothing that identifies anybody. */\nexport interface FrontendConfig {\n mode: 'test' | 'live' | null;\n issuer: string;\n endpoints: {\n authorization: string;\n token: string;\n userinfo: string;\n end_session: string;\n jwks: string;\n };\n /**\n * The social buttons to draw, exactly as the hosted sign-in page would: the environment's\n * own providers, or — with `config({ organization })` — that organization's.\n */\n social: SocialProvider[];\n appearance?: Appearance;\n /**\n * Which other sign-in methods are switched on, so a box does not draw a button whose\n * endpoint answers 403. Absent from instances that predate it.\n */\n methods?: { passkeys: boolean; magic_link: boolean };\n}\n\n/** Options for {@link CboxIdFrontend.config}. */\nexport interface FrontendConfigOptions {\n /**\n * The organization the box is for, by id or slug — the way a hosted page is addressed.\n * `social` is then exactly what that organization's sign-in page offers. An organization\n * the environment does not have is answered as no hint at all.\n */\n organization?: string;\n}\n\n/**\n * A second factor the person can answer an `mfa_required` with. Kinds only — the server\n * never says which number a text would go to before one is sent.\n */\nexport type SecondFactorKind = 'totp' | 'sms' | 'recovery_code';\n\n/** What a sign-in attempt produced. */\nexport type SignInResult =\n | { status: 'ok'; loginTicket: string; expiresIn: number }\n | {\n status: 'mfa_required';\n mfaToken: string;\n /**\n * The factors this person can answer with (laravel-id 1.24+). Absent when the\n * instance predates the list — then only an authenticator or a recovery code works.\n */\n factors?: SecondFactorKind[];\n }\n | { status: 'otp_required'; mfaToken: string }\n | { status: 'sso_required' }\n | { status: 'invalid' }\n | { status: 'rate_limited'; retryAfter: number | undefined };\n\n/** What asking for a texted sign-in code produced. */\nexport type SmsCodeResult =\n /** Sent. `to` is the number masked (`+45 ******78`); the code lasts `expiresIn` seconds. */\n | { status: 'sent'; to: string; expiresIn: number }\n /** Too soon, or a sending limit was hit. Waiting `retryAfter` seconds may help. */\n | { status: 'rate_limited'; retryAfter: number | undefined }\n /** The text provider failed. Try again shortly, or offer another factor. */\n | { status: 'unavailable' }\n /**\n * Refused: an unknown or spent token, no number on file, a country texts are not sent\n * to, or no attempts left. One answer for every reason, like a wrong password.\n */\n | { status: 'invalid' };\n\nconst SECOND_FACTOR_KINDS: ReadonlySet<string> = new Set<SecondFactorKind>(['totp', 'sms', 'recovery_code']);\n\n/** What a browser needs to ask an authenticator for a passkey assertion. */\nexport interface PasskeyOptions {\n challenge: string;\n /** Carries the challenge to the second request, in place of a session cookie. */\n challenge_token: string;\n rpId: string;\n userVerification: string;\n allowCredentials: unknown[];\n timeout: number;\n}\n\n/** Who the browser is signed in as, or nobody. */\nexport interface FrontendSession {\n user: { id: string; email: string; name: string | null } | null;\n}\n\n/**\n * Reads the public configuration and the current session from a browser.\n *\n * ```ts\n * const frontend = new CboxIdFrontend({\n * issuer: 'https://id.acme.com',\n * publishableKey: 'pk_live_…',\n * })\n *\n * const config = await frontend.config() // cached for you; safe to call per component\n * const { user } = await frontend.session(accessToken)\n * ```\n */\nexport class CboxIdFrontend {\n private readonly issuer: string;\n private readonly publishableKey: string;\n private readonly timeoutMs: number;\n private readonly retries: number;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n /**\n * The config document, and the in-flight promise for it.\n *\n * Deduplicated rather than merely cached: a page with a sign-in form, a user button and\n * an organization switcher mounts three components in the same tick, and three parallel\n * fetches of the same immutable document is a waste the SDK should absorb rather than\n * ask every consumer to.\n */\n private readonly configPromises = new Map<string, Promise<FrontendConfig>>();\n\n constructor(options: FrontendClientOptions) {\n if (!options.issuer) {\n throw new ConfigurationError('issuer is required.');\n }\n\n // The browser channel carries login tickets, one-time codes and the session the user\n // is in the middle of establishing. Over `http` a network attacker on the same café\n // Wi-Fi reads the ticket and redeems it — and the sign-in box the user is looking at\n // is served by the attacker, so nothing about the page tells them.\n assertSecureIssuer(options.issuer);\n\n // Caught here rather than at the first request, because the failure otherwise arrives\n // as an opaque 401 from the network tab and the cause — a secret key pasted into a\n // browser bundle — is exactly the mistake worth naming out loud.\n if (!options.publishableKey?.startsWith('pk_')) {\n throw new ConfigurationError(\n 'publishableKey must be a pk_test_… or pk_live_… key. Client secrets and API keys must never be put in a browser.',\n );\n }\n\n this.issuer = options.issuer.replace(/\\/$/, '');\n this.publishableKey = options.publishableKey;\n this.timeoutMs = options.timeoutMs ?? 10_000;\n this.retries = Math.max(0, options.retries ?? 2);\n this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n /** Whether this key drives real sign-ins. Handy for a \"test mode\" badge in your own UI. */\n get isLive(): boolean {\n return this.publishableKey.startsWith('pk_live_');\n }\n\n /**\n * The public sign-in configuration.\n *\n * Fetched once per client instance. The document is small and changes when somebody\n * edits it in the console, so a long-lived page picks changes up on its next full load\n * rather than mid-session — which is the right trade for something that decides layout.\n * Each `organization` is fetched and cached on its own.\n */\n async config(options: FrontendConfigOptions = {}): Promise<FrontendConfig> {\n const organization = options.organization ?? '';\n let pending = this.configPromises.get(organization);\n\n if (pending === undefined) {\n const path = organization === ''\n ? '/frontend/v1/config'\n : `/frontend/v1/config?${new URLSearchParams({ organization }).toString()}`;\n\n pending = this.get<FrontendConfig>(path, {}, isFrontendConfig).catch((error: unknown) => {\n // Cleared so a transient network failure does not poison the instance for the\n // lifetime of the page.\n this.configPromises.delete(organization);\n throw error;\n });\n this.configPromises.set(organization, pending);\n }\n\n return pending;\n }\n\n /**\n * Who is signed in, given an access token your app already holds.\n *\n * Returns `{ user: null }` rather than throwing when nobody is signed in. That is a\n * state, not an error: a user button renders on pages nobody has signed in on, and\n * making callers treat a rejection as a state is how flash-of-signed-out bugs happen.\n *\n * The publishable key grants nothing here — the token is the entire authority.\n */\n async session(accessToken?: string): Promise<FrontendSession> {\n if (!accessToken) {\n return { user: null };\n }\n\n return this.get<FrontendSession>(\n '/frontend/v1/session',\n { Authorization: `Bearer ${accessToken}` },\n isFrontendSession,\n );\n }\n\n /**\n * Sign in with a password, from a form your page drew.\n *\n * WHAT COMES BACK IS A TICKET, NEVER A TOKEN. Handing tokens to a page that proved a\n * password is the implicit grant, which OAuth 2.1 removes: tokens in a URL, in history,\n * in `Referer`, with no client authentication and no PKCE binding. Spend the ticket by\n * sending the browser to `/oauth/authorize` with your own PKCE challenge and\n * `login_ticket` in the query — the ordinary flow, which then issues an ordinary code.\n *\n * It expires in sixty seconds. It is for one redirect, not for storing.\n *\n * ```ts\n * const result = await frontend.signIn(email, password)\n *\n * if (result.status === 'ok') {\n * window.location.href = `${config.endpoints.authorization}?${new URLSearchParams({\n * client_id, redirect_uri, response_type: 'code', code_challenge, code_challenge_method: 'S256',\n * login_ticket: result.loginTicket,\n * })}`\n * }\n * ```\n *\n * EVERY REFUSAL IS THE SAME REFUSAL. `invalid` covers a wrong password, an unknown\n * address and a locked account, because distinguishing them is the enumeration oracle\n * the server refuses to build — do not present them differently in your UI either.\n */\n async signIn(email: string, password: string): Promise<SignInResult> {\n const response = await this.post('/frontend/v1/sign-in', { email, password });\n\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get('Retry-After') ?? '');\n\n return { status: 'rate_limited', retryAfter: Number.isFinite(retryAfter) ? retryAfter : undefined };\n }\n\n let body: { status?: string; login_ticket?: string; mfa_token?: string; expires_in?: number; factors?: unknown };\n\n try {\n body = (await response.json()) as typeof body;\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n if (body.status === 'ok' && typeof body.login_ticket === 'string') {\n return { status: 'ok', loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 };\n }\n\n if (body.status === 'mfa_required' && typeof body.mfa_token === 'string') {\n // Unknown kinds are dropped rather than passed on: a page that draws a button for a\n // factor this SDK cannot complete strands the person on it.\n const factors = Array.isArray(body.factors)\n ? body.factors.filter((f): f is SecondFactorKind => typeof f === 'string' && SECOND_FACTOR_KINDS.has(f))\n : undefined;\n\n return { status: 'mfa_required', mfaToken: body.mfa_token, ...(factors ? { factors } : {}) };\n }\n\n if (body.status === 'otp_required' && typeof body.mfa_token === 'string') {\n return { status: 'otp_required', mfaToken: body.mfa_token };\n }\n\n if (body.status === 'sso_required') {\n return { status: 'sso_required' };\n }\n\n return { status: 'invalid' };\n }\n\n /**\n * Finish a sign-in that owed a second factor.\n *\n * The token from `signIn()` carries the pending state, because a cross-origin page has\n * no session cookie to carry it in. A TOTP code or a recovery code both work — an\n * embedded form that could not accept a recovery code would strand exactly the people\n * that escape hatch exists for.\n *\n * A wrong code costs an attempt, not the sign-in: five are allowed before the token\n * dies and the person starts from the password again.\n *\n * `method` says which challenge the code answers: `'mfa'` for an authenticator or\n * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent\n * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the\n * server cannot tell them apart by shape — answering one as the other is refused.\n */\n async submitSecondFactor(mfaToken: string, code: string, method: 'mfa' | 'otp' | 'sms' = 'mfa'): Promise<SignInResult> {\n const response = await this.post('/frontend/v1/sign-in/factor', { mfa_token: mfaToken, code, method });\n\n let body: { status?: string; login_ticket?: string; expires_in?: number };\n\n try {\n body = (await response.json()) as typeof body;\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n return body.status === 'ok' && typeof body.login_ticket === 'string'\n ? { status: 'ok', loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 }\n : { status: 'invalid' };\n }\n\n /**\n * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.\n * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.\n *\n * Only call it when the person asks (\"Text me a code\"), never on page load: each send\n * spends one of the token's five attempts — that is what bounds how many texts one\n * proved password can cause — and every text costs the customer money.\n *\n * ```ts\n * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {\n * const sent = await frontend.sendSmsCode(result.mfaToken)\n * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)\n * }\n * ```\n */\n async sendSmsCode(mfaToken: string): Promise<SmsCodeResult> {\n const response = await this.post('/frontend/v1/sign-in/factor/sms', { mfa_token: mfaToken });\n\n let body: { status?: string; to?: unknown; expires_in?: unknown; retry_after?: unknown };\n\n try {\n body = (await response.json()) as typeof body;\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n if (body.status === 'sent' && typeof body.to === 'string') {\n return { status: 'sent', to: body.to, expiresIn: typeof body.expires_in === 'number' ? body.expires_in : 300 };\n }\n\n if (body.status === 'rate_limited' || response.status === 429) {\n const retryAfter = typeof body.retry_after === 'number' ? body.retry_after : Number(response.headers.get('Retry-After') ?? '');\n\n return { status: 'rate_limited', retryAfter: Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : undefined };\n }\n\n if (body.status === 'unavailable') {\n return { status: 'unavailable' };\n }\n\n return { status: 'invalid' };\n }\n\n /**\n * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.\n *\n * The handle exists because a cross-origin page has no session cookie to keep the\n * challenge in between the two requests WebAuthn needs. Pass it back to\n * {@link signInWithPasskey} with whatever `navigator.credentials.get()` returns.\n *\n * `allowCredentials` comes back empty on purpose: this is a discoverable-credential\n * flow, so the authenticator offers whichever passkey it holds and the page never has to\n * say who is signing in — asking for an email first, to look up which credentials exist,\n * would be an enumeration oracle.\n */\n async passkeyOptions(): Promise<PasskeyOptions> {\n const response = await this.post('/frontend/v1/sign-in/passkey/options', {});\n\n if (!response.ok) {\n throw new FrontendApiError(\n `Cbox ID refused the request (${response.status}). Check that this page's origin is on the key's allow-list.`,\n 'origin_not_allowed',\n response.status,\n );\n }\n\n return (await response.json()) as PasskeyOptions;\n }\n\n /**\n * Complete a passkey sign-in with the assertion the authenticator produced.\n *\n * `assertion` is the credential from `navigator.credentials.get()`, serialised — the\n * server verifies the raw body, so pass it through unchanged rather than rebuilding it.\n */\n async signInWithPasskey(challengeToken: string, assertion: Record<string, unknown>): Promise<SignInResult> {\n const response = await this.post('/frontend/v1/sign-in/passkey', { ...assertion, challenge_token: challengeToken });\n\n let body: { status?: string; login_ticket?: string; expires_in?: number };\n\n try {\n body = (await response.json()) as typeof body;\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n return body.status === 'ok' && typeof body.login_ticket === 'string'\n ? { status: 'ok', loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 }\n : { status: 'invalid' };\n }\n\n private async post(path: string, payload: Record<string, unknown>): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n try {\n return await this.fetchImpl(`${this.issuer}${path}`, {\n method: 'POST',\n headers: {\n 'X-Cbox-Publishable-Key': this.publishableKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n } catch {\n throw new FrontendApiError(\n `Could not reach Cbox ID at ${this.issuer}. If this is a browser, check that this page's origin is on the key's allow-list — a refused request fails as a network error by design.`,\n 'unavailable',\n );\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async get<T>(\n path: string,\n extraHeaders: Record<string, string> = {},\n validate?: (body: unknown) => body is T,\n ): Promise<T> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.retries; attempt++) {\n // Linear, not exponential, and only between attempts. This sits inside a page\n // render: a caller waiting on their sign-in box would rather fail in a second than\n // succeed in eight.\n if (attempt > 0) {\n await new Promise((resolve) => setTimeout(resolve, 150 * attempt));\n }\n\n try {\n return await this.attempt<T>(path, extraHeaders, validate);\n } catch (error) {\n lastError = error;\n\n // Retry only what a retry can fix. A refusal is a configuration problem and\n // retrying it delays the developer finding out; a rate limit is honoured rather\n // than hammered; a malformed body will be malformed again.\n if (!(error instanceof FrontendApiError) || error.code !== 'unavailable') {\n throw error;\n }\n }\n }\n\n throw lastError;\n }\n\n private async attempt<T>(\n path: string,\n extraHeaders: Record<string, string>,\n validate?: (body: unknown) => body is T,\n ): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n let response: Response;\n\n try {\n response = await this.fetchImpl(`${this.issuer}${path}`, {\n headers: {\n // A header, never a query string: a query string puts the key in server logs,\n // in `Referer` on every outbound link, and in browser history.\n 'X-Cbox-Publishable-Key': this.publishableKey,\n Accept: 'application/json',\n ...extraHeaders,\n },\n signal: controller.signal,\n });\n } catch (cause) {\n // Includes the abort on timeout, and — in a browser — a refusal, because a refused\n // request deliberately carries no CORS headers and so never becomes a Response.\n throw new FrontendApiError(\n `Could not reach Cbox ID at ${this.issuer}. If this is a browser, check that this page's origin is on the key's allow-list — a refused request fails as a network error by design.`,\n 'unavailable',\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get('Retry-After') ?? '');\n\n throw new FrontendApiError(\n (await described(response)) ?? 'Cbox ID is rate limiting this key.',\n 'rate_limited',\n 429,\n Number.isFinite(retryAfter) ? retryAfter : undefined,\n );\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new FrontendApiError(\n // THE SERVER'S OWN REASON FIRST. It answers precisely — \"No publishable key was\n // presented\" is a different problem from \"That publishable key cannot be used\n // from this origin\" — and substituting a guess for it made this client less\n // precise than the API it wraps. The fallback still stands for the browser case,\n // where a refusal never becomes a readable Response at all.\n (await described(response)) ??\n `Cbox ID refused the request (${response.status}). Check that this page's origin is on the key's allow-list, and that the key is not revoked.`,\n 'origin_not_allowed',\n response.status,\n );\n }\n\n if (!response.ok) {\n throw new FrontendApiError(\n (await described(response)) ?? `Cbox ID returned ${response.status}.`,\n 'unavailable',\n response.status,\n );\n }\n\n let body: unknown;\n\n try {\n body = await response.json();\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n // A 200 whose body is not the document we expect means something between the page and\n // us is rewriting responses — a proxy, an extension, a captive portal. Caught here so\n // it surfaces as that, rather than as `undefined` deep inside somebody's component.\n if (validate && !validate(body)) {\n throw new FrontendApiError(\n 'Cbox ID returned a body that is not the expected document.',\n 'malformed',\n response.status,\n );\n }\n\n return body as T;\n }\n}\n\n/**\n * The server's own `error_description`, when it sent one.\n *\n * Read from a CLONE: the caller may still want the body, and a Response body can only be\n * consumed once — a helper that quietly drained it would turn a precise error into a\n * different, stranger one.\n */\nasync function described(response: Response): Promise<string | undefined> {\n try {\n const body: unknown = await response.clone().json();\n\n if (typeof body === 'object' && body !== null && 'error_description' in body) {\n const description = (body as { error_description: unknown }).error_description;\n\n return typeof description === 'string' && description !== '' ? description : undefined;\n }\n } catch {\n // A refusal without a readable body is the normal case in a browser.\n }\n\n return undefined;\n}\n\n/** The shape check for the config document. Structural, not exhaustive. */\nfunction isFrontendConfig(body: unknown): body is FrontendConfig {\n if (typeof body !== 'object' || body === null) {\n return false;\n }\n\n const doc = body as Partial<FrontendConfig>;\n\n return typeof doc.issuer === 'string' && typeof doc.endpoints === 'object' && doc.endpoints !== null;\n}\n\n/** The shape check for the session document. `user: null` is valid and expected. */\nfunction isFrontendSession(body: unknown): body is FrontendSession {\n return typeof body === 'object' && body !== null && 'user' in body;\n}\n","import { Discovery } from './discovery.js';\nimport { AuthenticationError, ConfigurationError } from './errors.js';\n\n/**\n * A permission your app declares. `key` is a `feature:action` slug (e.g.\n * `invoices:create`); `description` is human-facing copy for the Cbox ID console.\n */\nexport interface PermissionDefinition {\n key: string;\n description?: string;\n /**\n * Whether an organization's own administrators may hand this permission out in a custom\n * role they build themselves (self-serve). Defaults to `false`: a permission is internal\n * unless you opt it in, so a key added in a hurry never becomes something every customer\n * can grant. Sent as `tenant_assignable: true`.\n */\n tenantAssignable?: boolean;\n}\n\n/**\n * A role your app declares. `permissions` lists the permission keys the role\n * grants — each must reference a declared {@link PermissionDefinition}.\n */\nexport interface RoleDefinition {\n key: string;\n name: string;\n description?: string;\n permissions: string[];\n /**\n * Whether an organization's administrators may assign this role to their members.\n * Defaults to `true`.\n *\n * Set `false` for a STAFF role — your own support or operations people, held\n * environment-wide across every customer (typically the role that grants\n * `support:impersonate`). Cbox ID never lists or accepts a staff role on the\n * organization plane; only an environment administrator can grant it. Sent as\n * `tenant_assignable: false`.\n */\n tenantAssignable?: boolean;\n}\n\n/** A permission as it is sent to Cbox ID (snake_case, the manifest's wire format). */\nexport interface ManifestPermission {\n key: string;\n description?: string;\n /** Present only for a self-serve permission. See {@link PermissionDefinition.tenantAssignable}. */\n tenant_assignable?: true;\n}\n\n/** A role as it is sent to Cbox ID (snake_case, the manifest's wire format). */\nexport interface ManifestRole {\n key: string;\n name: string;\n description?: string;\n permissions: string[];\n /** Present only for a staff role. See {@link RoleDefinition.tenantAssignable}. */\n tenant_assignable?: false;\n}\n\n/**\n * The roles + permissions an app declares in code. Either list may be omitted.\n * Pass this to {@link defineAuthz} (to validate up front) or straight to\n * {@link buildManifest} / {@link publishManifest}.\n */\nexport interface AuthzDeclaration {\n permissions?: PermissionDefinition[];\n roles?: RoleDefinition[];\n /**\n * Where this app's OLD login lives, while you are migrating off it.\n *\n * Declared here for the same reason roles are: it is a fact about the app, it belongs\n * with the deploy, and the alternative is a URL pasted into a console and a secret\n * pasted into an env file that quietly disagree six weeks later.\n *\n * IT DOES NOT TAKE EFFECT ON ITS OWN. Unlike a role — which affects only the app that\n * declared it — this names a URL that every unknown email and the password typed with\n * it will be offered to, on the environment's whole sign-in path. So it arrives as a\n * proposal and an operator has to approve it in the console. Changing the url later\n * drops that approval, on purpose.\n */\n legacyLogin?: LegacyLoginDeclaration;\n}\n\n/** Where an app's old login lives. See {@link AuthzDeclaration.legacyLogin}. */\nexport interface LegacyLoginDeclaration {\n /** Must be https — a password in the clear is readable by everything on the path. */\n url: string;\n /** At least 32 characters. It is the only thing proving a request came from Cbox ID. */\n secret: string;\n}\n\n/**\n * The manifest as it is sent to Cbox ID: the declared permissions + roles plus a\n * content-derived `version`, so republishing an unchanged catalog is a no-op.\n */\nexport interface AuthzManifest {\n version: string;\n permissions: ManifestPermission[];\n roles: ManifestRole[];\n legacy_login?: LegacyLoginDeclaration;\n}\n\n/** What {@link publishManifest} needs to authenticate the push. */\nexport interface ManifestPublisherConfig {\n /** Base URL (issuer) of the Cbox ID instance — the `token_endpoint` is discovered from it. */\n issuer: string;\n /** Your OAuth client id. The client must hold the `apps.manifest` scope. */\n clientId: string;\n /** Your OAuth client secret. The push uses the client-credentials grant. */\n clientSecret: string;\n /** Timeout (ms) for back-channel HTTP calls. Defaults to `10000`. */\n timeoutMs?: number;\n}\n\n/**\n * The server's sync summary. The named fields are the ones the SDK/CLI reads;\n * the index signature keeps any extra fields the server returns.\n */\nexport interface ManifestSyncSummary {\n unchanged?: boolean;\n roles_declared?: number;\n permissions_declared?: number;\n version?: string;\n [key: string]: unknown;\n}\n\n/**\n * Validate a roles/permissions declaration and return it normalized (both lists\n * always present). Call this where you declare your catalog so mistakes — a role\n * that grants an undeclared permission, a duplicate key — surface immediately\n * rather than on deploy.\n *\n * @throws ConfigurationError on a missing key/name, a duplicate, or a role that\n * references a permission that was never declared.\n */\nexport function defineAuthz(\n declaration: AuthzDeclaration,\n): AuthzDeclaration & { permissions: PermissionDefinition[]; roles: RoleDefinition[] } {\n const permissions = declaration.permissions ?? [];\n const roles = declaration.roles ?? [];\n assertDeclaration(permissions, roles);\n\n // The legacy login is validated here too, so a bad one fails in the deploy that\n // introduced it rather than as a 4xx from a manifest push somebody has to go and read.\n if (declaration.legacyLogin) {\n assertLegacyLogin(declaration.legacyLogin);\n }\n\n // `Required<>` no longer fits: the catalog is always present after this, and the legacy\n // login legitimately is not. Returning the declaration's own optionality keeps a caller\n // from having to invent an empty one.\n return { ...declaration, permissions, roles };\n}\n\n/**\n * Build the manifest — the declared catalog plus a stable content hash `version`\n * (first 16 hex chars of the sha256 of the canonicalized catalog). The catalog is\n * sorted by key first, so declaration order never changes the version.\n */\nexport async function buildManifest(declaration: AuthzDeclaration): Promise<AuthzManifest> {\n const permissions = declaration.permissions ?? [];\n const roles = declaration.roles ?? [];\n assertDeclaration(permissions, roles);\n\n // The version hashes the CATALOG only, and the legacy login is deliberately outside it:\n // this canonicalization is a cross-SDK contract — id-js, id-python, id-go and the PHP\n // reference all produce the same bytes, and a shared fixture exists to keep them from\n // drifting. The server compares a declared url separately for the same reason.\n const version = (await sha256Hex(canonicalManifestJson(permissions, roles))).slice(0, 16);\n\n const manifest: AuthzManifest = {\n version,\n permissions: [...permissions].sort((a, b) => byteCompare(a.key, b.key)).map(wirePermission),\n roles: [...roles].sort((a, b) => byteCompare(a.key, b.key)).map(wireRole),\n };\n\n if (declaration.legacyLogin) {\n assertLegacyLogin(declaration.legacyLogin);\n manifest.legacy_login = declaration.legacyLogin;\n }\n\n return manifest;\n}\n\n/**\n * Publish this app's authorization manifest (its declared roles + permissions) to\n * Cbox ID. The app owns what a role means; Cbox ID owns identity and who holds it.\n *\n * It mints a client-credentials token with the `apps.manifest` scope (token\n * endpoint discovered from the issuer's OIDC configuration), then POSTs the\n * manifest to `{issuer}/api/v1/apps/manifest`. Run it on deploy or from a script;\n * an unchanged catalog is a server-side no-op.\n *\n * @returns the server's sync summary (`unchanged`, `roles_declared`, …).\n * @throws ConfigurationError when config or the declaration is invalid.\n * @throws AuthenticationError when the token cannot be minted or the push fails.\n */\nexport async function publishManifest(\n config: ManifestPublisherConfig,\n declaration: AuthzDeclaration,\n): Promise<ManifestSyncSummary> {\n if (!config.issuer || !config.clientId || !config.clientSecret) {\n throw new ConfigurationError('Publishing a manifest needs issuer, clientId and clientSecret.');\n }\n\n const timeoutMs = config.timeoutMs ?? 10_000;\n const manifest = await buildManifest(declaration);\n const discovery = new Discovery(config.issuer, timeoutMs, 3_600_000);\n\n const token = await mintToken(config, discovery, timeoutMs);\n\n const response = await fetchWithTimeout(\n `${config.issuer.replace(/\\/$/, '')}/api/v1/apps/manifest`,\n {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify(manifest),\n },\n timeoutMs,\n );\n\n if (!response.ok) {\n throw new AuthenticationError(`Manifest push failed: HTTP ${response.status} ${await response.text()}`);\n }\n\n const json = (await response.json()) as unknown;\n return json !== null && typeof json === 'object' ? (json as ManifestSyncSummary) : {};\n}\n\nasync function mintToken(\n config: ManifestPublisherConfig,\n discovery: Discovery,\n timeoutMs: number,\n): Promise<string> {\n const response = await fetchWithTimeout(\n await discovery.endpoint('token_endpoint'),\n {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: config.clientId,\n client_secret: config.clientSecret,\n scope: 'apps.manifest',\n }),\n },\n timeoutMs,\n );\n\n const json = (await response.json().catch(() => ({}))) as { access_token?: unknown };\n if (typeof json.access_token !== 'string' || json.access_token === '') {\n throw new AuthenticationError(\n 'Could not obtain an apps.manifest access token — check the client credentials and that the client holds the apps.manifest scope.',\n );\n }\n return json.access_token;\n}\n\n/**\n * A `feature:action` (or bare `feature`) key: lowercase, with dot/colon-separated segments.\n * The server's own pattern, restated so a bad key fails where it was written rather than\n * as a 4xx from a manifest push on deploy.\n */\nconst KEY_PATTERN = /^[a-z][a-z0-9_-]*(?:[.:][a-z0-9_-]+)*$/;\n\nfunction assertDeclaration(permissions: PermissionDefinition[], roles: RoleDefinition[]): void {\n const permissionKeys = new Set<string>();\n for (const permission of permissions) {\n if (!permission.key) {\n throw new ConfigurationError('Every permission needs a non-empty `key`.');\n }\n assertKey(permission.key, 'Permission');\n if (permissionKeys.has(permission.key)) {\n throw new ConfigurationError(`Permission \"${permission.key}\" is declared more than once.`);\n }\n assertFlag(permission.tenantAssignable, `Permission \"${permission.key}\"`);\n permissionKeys.add(permission.key);\n }\n\n const roleKeys = new Set<string>();\n for (const role of roles) {\n if (!role.key) {\n throw new ConfigurationError('Every role needs a non-empty `key`.');\n }\n assertKey(role.key, 'Role');\n if (!role.name) {\n throw new ConfigurationError(`Role \"${role.key}\" needs a non-empty \\`name\\`.`);\n }\n if (roleKeys.has(role.key)) {\n throw new ConfigurationError(`Role \"${role.key}\" is declared more than once.`);\n }\n assertFlag(role.tenantAssignable, `Role \"${role.key}\"`);\n roleKeys.add(role.key);\n for (const reference of role.permissions) {\n if (!permissionKeys.has(reference)) {\n throw new ConfigurationError(\n `Role \"${role.key}\" references permission \"${reference}\", which is not declared.`,\n );\n }\n }\n }\n}\n\nfunction assertKey(key: string, what: 'Permission' | 'Role'): void {\n if (!KEY_PATTERN.test(key)) {\n throw new ConfigurationError(\n `${what} key \"${key}\" is not a lowercase \\`feature:action\\` slug (e.g. \\`invoices:create\\`).`,\n );\n }\n}\n\n/**\n * `tenantAssignable`, when given, must be a real boolean.\n *\n * The type says so, but a catalog loaded from YAML or JSON arrives untyped, and the\n * string `\"false\"` is truthy: the one value that has to mean \"staff only\" would read as\n * \"every customer may grant this\". The server refuses a non-boolean role flag for the\n * same reason; refusing it here puts the failure in the deploy that introduced it.\n */\nfunction assertFlag(value: unknown, owner: string): void {\n if (value !== undefined && typeof value !== 'boolean') {\n throw new ConfigurationError(`${owner} \\`tenantAssignable\\` must be true or false.`);\n }\n}\n\n/**\n * A permission in the manifest's wire format: fixed key order, no `undefined` fields.\n *\n * `tenant_assignable` is written only when it is `true`, the non-default. The server reads\n * the snake_case key and nothing else, so this mapping is the whole difference between a\n * self-serve permission and an internal one — `tenantAssignable` sent as-is would be\n * ignored.\n */\nfunction wirePermission(permission: PermissionDefinition): ManifestPermission {\n return {\n key: permission.key,\n ...(permission.description === undefined ? {} : { description: permission.description }),\n ...(permission.tenantAssignable === true ? { tenant_assignable: true as const } : {}),\n };\n}\n\n/**\n * A role in the manifest's wire format: fixed key order, de-duplicated and sorted\n * permission refs, no `undefined` fields.\n *\n * `tenant_assignable: false` is written only for a staff role. THIS IS THE LINE THAT\n * KEEPS A STAFF ROLE STAFF-ONLY: the server defaults an absent key to assignable, so a\n * camelCase `tenantAssignable: false` passed straight through would publish your support\n * role as one every customer's administrator can hand out.\n */\nfunction wireRole(role: RoleDefinition): ManifestRole {\n return {\n key: role.key,\n name: role.name,\n ...(role.description === undefined ? {} : { description: role.description }),\n permissions: uniqueSorted(role.permissions),\n ...(role.tenantAssignable === false ? { tenant_assignable: false as const } : {}),\n };\n}\n\n/**\n * Serialize {permissions, roles} to the exact canonical JSON the PHP reference hashes,\n * so the `version` is byte-for-byte identical across every Cbox ID SDK. Matches PHP\n * `json_encode` defaults: object keys in insertion order, permissions and roles sorted\n * by key, each role's permission refs de-duplicated and sorted, an absent-or-empty\n * description emitted as `null`, forward slashes escaped as `\\/`, and every non-ASCII\n * code unit as `\\uXXXX`.\n *\n * Each `tenant_assignable` flag appears ONLY in its non-default state — `true` on a\n * permission, `false` on a role — exactly as the reference writes it. That keeps every\n * catalog that never mentions the flag hashing to the bytes it always has, while a\n * change to it still changes the version (an unchanged version is a skipped sync).\n */\nexport function canonicalManifestJson(\n permissions: PermissionDefinition[],\n roles: RoleDefinition[],\n): string {\n const canonical = {\n permissions: [...permissions]\n .sort((a, b) => byteCompare(a.key, b.key))\n .map((p) => ({\n key: p.key,\n description: emptyToNull(p.description),\n ...(p.tenantAssignable === true ? { tenant_assignable: true } : {}),\n })),\n roles: [...roles]\n .sort((a, b) => byteCompare(a.key, b.key))\n .map((r) => ({\n key: r.key,\n name: r.name,\n description: emptyToNull(r.description),\n permissions: uniqueSorted(r.permissions),\n ...(r.tenantAssignable === false ? { tenant_assignable: false } : {}),\n })),\n };\n return escapeLikePhp(JSON.stringify(canonical));\n}\n\n/**\n * Permission refs as the server stores them: each once, sorted. The server drops a\n * repeated ref before it hashes, so hashing the repeat here would give a `version` that\n * no server ever computes.\n */\nfunction uniqueSorted(values: readonly string[]): string[] {\n return [...new Set(values)].sort(byteCompare);\n}\n\n/** PHP treats an absent or empty description as `null` in the hashed catalog. */\nfunction emptyToNull(value: string | undefined): string | null {\n return value === undefined || value === '' ? null : value;\n}\n\n/** Byte-wise (code-unit) comparison — matches PHP `strcmp` on the ASCII-only keys. */\nfunction byteCompare(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/** Apply PHP `json_encode`'s default `\\/` slash and `\\uXXXX` non-ASCII escaping. */\nfunction escapeLikePhp(json: string): string {\n let out = '';\n for (let i = 0; i < json.length; i++) {\n const char = json.charAt(i);\n const code = json.charCodeAt(i);\n if (char === '/') {\n out += '\\\\/';\n } else if (code > 0x7f) {\n out += '\\\\u' + code.toString(16).padStart(4, '0');\n } else {\n out += char;\n }\n }\n return out;\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n}\n\nasync function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Refuse a legacy login declaration the server would refuse anyway.\n *\n * Checked in the SDK so it fails at build time, in the deploy that introduced it, rather\n * than as a 4xx from a manifest push somebody has to go and read. Both rules are the\n * server's and are restated here rather than inferred: https because a password in the\n * clear is readable by everything on the path, and a real secret because it is the only\n * thing proving a request came from Cbox ID.\n */\nfunction assertLegacyLogin(legacy: LegacyLoginDeclaration): void {\n if (!legacy.url.startsWith('https://')) {\n throw new ConfigurationError('legacyLogin.url must be https — a password must never cross the network in the clear.');\n }\n\n if (!legacy.secret || legacy.secret.length < 32) {\n throw new ConfigurationError('legacyLogin.secret must be at least 32 characters; it is what proves a request came from Cbox ID.');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/legacy.ts","../src/frontend.ts","../src/authz.ts"],"names":["key","organization","permissions","roles","json"],"mappings":"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","file":"index.js","sourcesContent":["import { ConfigurationError } from './errors.js';\n\n/**\n * The handler on YOUR side of a legacy migration.\n *\n * Cbox ID offers it an email and a password it has never seen, and your code answers with\n * the person or with nothing. On a yes they are created in Cbox ID as a result of the\n * login that just succeeded, and from their next sign-in the old system is never asked\n * again.\n *\n * WHY THIS IS A FACTORY AND NOT A DOCUMENT. The wire format is simple enough to describe\n * in a paragraph — a signed POST, JSON in, JSON out — and describing it would mean every\n * customer writes the signature check themselves. That check is exactly the part people\n * get wrong: `===` instead of a constant-time compare, no freshness window, no thought\n * about replay. So the factory owns all of it and you write the one function that knows\n * your own database.\n *\n * It is deliberately framework-agnostic: it takes a `Request` and returns a `Response`,\n * which is what Next.js route handlers, Remix, Hono, Bun and Deno all speak natively.\n */\n\n/** What your handler answers with. Returning `null` means \"no\", and is not an error. */\nexport interface LegacyUser {\n email: string;\n name?: string | null;\n emailVerified?: boolean;\n /**\n * Your stored hash, when you can expose it.\n *\n * Passing it lets the person keep their password in Cbox ID verbatim — it is stored as\n * is and upgraded to the platform hasher on their next login, exactly like a\n * bulk-imported user. Omit it and Cbox ID hashes the password they just proved they\n * know, which is the honest option when your store cannot hand a hash over.\n */\n passwordHash?: string;\n}\n\nexport interface LegacyVerifierOptions {\n /** The same secret you declared in your manifest, or configured in the console. */\n secret: string;\n /**\n * Decide whether these credentials are good, and say who they belong to.\n *\n * Return `null` for no. THROWING IS NOT THE SAME as returning null: a throw means your\n * store could not decide, and is answered with a 503 so Cbox ID refuses the sign-in\n * rather than treating an outage as a wrong password.\n */\n verify: (email: string, password: string) => Promise<LegacyUser | null>;\n /**\n * How far out of date a request may be, in seconds. Defaults to 300.\n *\n * A window is what makes a captured request stop working. Too wide and a recording is\n * useful for hours; too narrow and ordinary clock skew starts refusing real traffic.\n */\n toleranceSeconds?: number;\n}\n\n/**\n * Build the request handler.\n *\n * ```ts\n * // app/api/cbox-legacy/route.ts\n * export const POST = createLegacyVerifier({\n * secret: process.env.CBOX_LEGACY_SECRET!,\n * async verify(email, password) {\n * const row = await db.users.findByEmail(email)\n * if (!row || !(await argon2.verify(row.password, password))) return null\n * return { email: row.email, name: row.name, emailVerified: !!row.confirmedAt }\n * },\n * })\n * ```\n */\nexport function createLegacyVerifier(options: LegacyVerifierOptions): (request: Request) => Promise<Response> {\n if (!options.secret || options.secret.length < 32) {\n throw new ConfigurationError(\n 'A legacy verifier secret must be at least 32 characters; it is the only thing proving a request came from Cbox ID.',\n );\n }\n\n const tolerance = options.toleranceSeconds ?? 300;\n\n return async function handle(request: Request): Promise<Response> {\n // Read once. The signature covers the RAW body, so re-serializing parsed JSON would\n // produce different bytes and every valid request would be refused.\n const raw = await request.text();\n\n if (!(await isSignedByCboxId(raw, request.headers.get('X-Cbox-Signature'), options.secret, tolerance))) {\n // No detail. A caller who cannot sign is not owed help distinguishing \"bad\n // signature\" from \"stale timestamp\" — both mean the same thing to anyone\n // legitimate, and the difference is a hint to anyone else.\n return json({ error: 'unauthorized' }, 401);\n }\n\n let payload: { email?: unknown; password?: unknown };\n\n try {\n payload = JSON.parse(raw) as typeof payload;\n } catch {\n return json({ error: 'invalid_request' }, 400);\n }\n\n const { email, password } = payload;\n\n if (typeof email !== 'string' || email === '') {\n return json({ error: 'invalid_request' }, 400);\n }\n\n // No password means Cbox ID is asking \"do you know this address\" — the tooling path,\n // used for a dry run before cutover. It never reaches here from a sign-in.\n if (typeof password !== 'string') {\n return json({ user: null }, 200);\n }\n\n let user: LegacyUser | null;\n\n try {\n user = await options.verify(email, password);\n } catch {\n // 503, not 401. Your store could not decide, and Cbox ID must refuse the sign-in\n // rather than read an outage as a wrong password — but it should also be able to\n // tell the two apart in its own logs, which a 401 would hide.\n return json({ error: 'unavailable' }, 503);\n }\n\n if (!user) {\n return json({ user: null }, 200);\n }\n\n return json(\n {\n email: user.email,\n name: user.name ?? null,\n email_verified: user.emailVerified === true,\n ...(user.passwordHash ? { password_hash: user.passwordHash } : {}),\n },\n 200,\n );\n };\n}\n\n/**\n * Whether this body was signed by Cbox ID, recently.\n *\n * Both halves matter. The HMAC proves it came from something holding the secret; the\n * freshness window is what stops a captured request working forever. Compared in constant\n * time, because a comparison that returns early leaks how much of a guess was right.\n */\nasync function isSignedByCboxId(\n body: string,\n header: string | null,\n secret: string,\n toleranceSeconds: number,\n): Promise<boolean> {\n if (!header) {\n return false;\n }\n\n const parts: Record<string, string> = {};\n\n for (const piece of header.split(',')) {\n const [key, value] = piece.split('=', 2);\n if (key && value) {\n parts[key.trim()] = value.trim();\n }\n }\n\n const timestamp = parts['t'] ?? '';\n const signature = parts['v1'] ?? '';\n\n if (!/^\\d+$/.test(timestamp) || signature === '') {\n return false;\n }\n\n if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > toleranceSeconds) {\n return false;\n }\n\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${timestamp}.${body}`));\n const expected = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('');\n\n return timingSafeEqual(expected, signature);\n}\n\n/** Constant-time string compare. Length is not secret; content is. */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n let diff = 0;\n\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return diff === 0;\n}\n\nfunction json(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n}\n","import { assertSecureIssuer } from './issuer.js';\nimport { ConfigurationError, FrontendApiError } from './errors.js';\n\n/**\n * The browser-safe half of the SDK.\n *\n * Everything else in this package assumes a server: it holds a client secret, it exchanges\n * codes, it verifies webhooks. None of that may run in a page, which is why the UI\n * components have always been redirect shells — a page that cannot authenticate itself to\n * the identity provider cannot ask it anything, so it sends the person away and waits.\n *\n * A publishable key changes that. It is public on purpose: it ships in your bundle, it is\n * visible in devtools, and it is safe there because it only works from the origins you\n * registered. What it buys is that a page can read its OWN sign-in configuration — the\n * endpoints, the social buttons, your theme — and render a form itself.\n *\n * This client deliberately has no secret, no token storage and no crypto. It reads two\n * documents. Anything that mints or holds a credential still belongs on your server.\n */\n\n/** How a page identifies itself. Public — put it in your bundle. */\nexport interface FrontendClientOptions {\n /** Your identity provider's origin, e.g. `https://id.acme.com`. */\n issuer: string;\n /** `pk_live_…` or `pk_test_…`. Safe to publish. */\n publishableKey: string;\n /** Abort after this long. Defaults to 10s. */\n timeoutMs?: number;\n /**\n * How many times to retry a transient failure. Defaults to 2 (so three attempts).\n *\n * Only the network and 5xx are retried. A refusal is a configuration problem and\n * retrying it just delays the developer finding out; a 429 is honoured rather than\n * hammered.\n */\n retries?: number;\n /** Injectable for tests and for runtimes with a non-global fetch. */\n fetch?: typeof globalThis.fetch;\n}\n\n/** A social button to draw. Name and provider only — never an internal id. */\nexport interface SocialProvider {\n provider: string;\n name: string;\n}\n\n/** The customer's theme, when they have set one. Absent means \"use your own defaults\". */\nexport interface Appearance {\n preset: string;\n radius: string;\n font: string;\n light: Record<string, string>;\n dark: Record<string, string>;\n}\n\n/** Everything needed to draw a sign-in box, and nothing that identifies anybody. */\nexport interface FrontendConfig {\n mode: 'test' | 'live' | null;\n issuer: string;\n endpoints: {\n authorization: string;\n token: string;\n userinfo: string;\n end_session: string;\n jwks: string;\n };\n /**\n * The social buttons to draw, exactly as the hosted sign-in page would: the environment's\n * own providers, or — with `config({ organization })` — that organization's.\n */\n social: SocialProvider[];\n appearance?: Appearance;\n /**\n * Which other sign-in methods are switched on, so a box does not draw a button whose\n * endpoint answers 403. Absent from instances that predate it.\n */\n methods?: { passkeys: boolean; magic_link: boolean };\n}\n\n/** Options for {@link CboxIdFrontend.config}. */\nexport interface FrontendConfigOptions {\n /**\n * The organization the box is for, by id or slug — the way a hosted page is addressed.\n * `social` is then exactly what that organization's sign-in page offers. An organization\n * the environment does not have is answered as no hint at all.\n */\n organization?: string;\n}\n\n/**\n * A second factor the person can answer an `mfa_required` with. Kinds only — the server\n * never says which number a text would go to before one is sent.\n */\nexport type SecondFactorKind = 'totp' | 'sms' | 'recovery_code';\n\n/** What a sign-in attempt produced. */\nexport type SignInResult =\n | { status: 'ok'; loginTicket: string; expiresIn: number }\n | {\n status: 'mfa_required';\n mfaToken: string;\n /**\n * The factors this person can answer with (laravel-id 1.24+). Absent when the\n * instance predates the list — then only an authenticator or a recovery code works.\n */\n factors?: SecondFactorKind[];\n }\n | { status: 'otp_required'; mfaToken: string }\n | { status: 'sso_required' }\n | { status: 'invalid' }\n | { status: 'rate_limited'; retryAfter: number | undefined };\n\n/** What asking for a texted sign-in code produced. */\nexport type SmsCodeResult =\n /** Sent. `to` is the number masked (`+45 ******78`); the code lasts `expiresIn` seconds. */\n | { status: 'sent'; to: string; expiresIn: number }\n /** Too soon, or a sending limit was hit. Waiting `retryAfter` seconds may help. */\n | { status: 'rate_limited'; retryAfter: number | undefined }\n /** The text provider failed. Try again shortly, or offer another factor. */\n | { status: 'unavailable' }\n /**\n * Refused: an unknown or spent token, no number on file, a country texts are not sent\n * to, or no attempts left. One answer for every reason, like a wrong password.\n */\n | { status: 'invalid' };\n\nconst SECOND_FACTOR_KINDS: ReadonlySet<string> = new Set<SecondFactorKind>(['totp', 'sms', 'recovery_code']);\n\n/** What a browser needs to ask an authenticator for a passkey assertion. */\nexport interface PasskeyOptions {\n challenge: string;\n /** Carries the challenge to the second request, in place of a session cookie. */\n challenge_token: string;\n rpId: string;\n userVerification: string;\n allowCredentials: unknown[];\n timeout: number;\n}\n\n/** Who the browser is signed in as, or nobody. */\nexport interface FrontendSession {\n user: { id: string; email: string; name: string | null } | null;\n}\n\n/**\n * Reads the public configuration and the current session from a browser.\n *\n * ```ts\n * const frontend = new CboxIdFrontend({\n * issuer: 'https://id.acme.com',\n * publishableKey: 'pk_live_…',\n * })\n *\n * const config = await frontend.config() // cached for you; safe to call per component\n * const { user } = await frontend.session(accessToken)\n * ```\n */\nexport class CboxIdFrontend {\n private readonly issuer: string;\n private readonly publishableKey: string;\n private readonly timeoutMs: number;\n private readonly retries: number;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n /**\n * The config document, and the in-flight promise for it.\n *\n * Deduplicated rather than merely cached: a page with a sign-in form, a user button and\n * an organization switcher mounts three components in the same tick, and three parallel\n * fetches of the same immutable document is a waste the SDK should absorb rather than\n * ask every consumer to.\n */\n private readonly configPromises = new Map<string, Promise<FrontendConfig>>();\n\n constructor(options: FrontendClientOptions) {\n if (!options.issuer) {\n throw new ConfigurationError('issuer is required.');\n }\n\n // The browser channel carries login tickets, one-time codes and the session the user\n // is in the middle of establishing. Over `http` a network attacker on the same café\n // Wi-Fi reads the ticket and redeems it — and the sign-in box the user is looking at\n // is served by the attacker, so nothing about the page tells them.\n assertSecureIssuer(options.issuer);\n\n // Caught here rather than at the first request, because the failure otherwise arrives\n // as an opaque 401 from the network tab and the cause — a secret key pasted into a\n // browser bundle — is exactly the mistake worth naming out loud.\n if (!options.publishableKey?.startsWith('pk_')) {\n throw new ConfigurationError(\n 'publishableKey must be a pk_test_… or pk_live_… key. Client secrets and API keys must never be put in a browser.',\n );\n }\n\n this.issuer = options.issuer.replace(/\\/$/, '');\n this.publishableKey = options.publishableKey;\n this.timeoutMs = options.timeoutMs ?? 10_000;\n this.retries = Math.max(0, options.retries ?? 2);\n this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n /** Whether this key drives real sign-ins. Handy for a \"test mode\" badge in your own UI. */\n get isLive(): boolean {\n return this.publishableKey.startsWith('pk_live_');\n }\n\n /**\n * The public sign-in configuration.\n *\n * Fetched once per client instance. The document is small and changes when somebody\n * edits it in the console, so a long-lived page picks changes up on its next full load\n * rather than mid-session — which is the right trade for something that decides layout.\n * Each `organization` is fetched and cached on its own.\n */\n async config(options: FrontendConfigOptions = {}): Promise<FrontendConfig> {\n const organization = options.organization ?? '';\n let pending = this.configPromises.get(organization);\n\n if (pending === undefined) {\n const path = organization === ''\n ? '/frontend/v1/config'\n : `/frontend/v1/config?${new URLSearchParams({ organization }).toString()}`;\n\n pending = this.get<FrontendConfig>(path, {}, isFrontendConfig).catch((error: unknown) => {\n // Cleared so a transient network failure does not poison the instance for the\n // lifetime of the page.\n this.configPromises.delete(organization);\n throw error;\n });\n this.configPromises.set(organization, pending);\n }\n\n return pending;\n }\n\n /**\n * Who is signed in, given an access token your app already holds.\n *\n * Returns `{ user: null }` rather than throwing when nobody is signed in. That is a\n * state, not an error: a user button renders on pages nobody has signed in on, and\n * making callers treat a rejection as a state is how flash-of-signed-out bugs happen.\n *\n * The publishable key grants nothing here — the token is the entire authority.\n */\n async session(accessToken?: string): Promise<FrontendSession> {\n if (!accessToken) {\n return { user: null };\n }\n\n return this.get<FrontendSession>(\n '/frontend/v1/session',\n { Authorization: `Bearer ${accessToken}` },\n isFrontendSession,\n );\n }\n\n /**\n * Sign in with a password, from a form your page drew.\n *\n * WHAT COMES BACK IS A TICKET, NEVER A TOKEN. Handing tokens to a page that proved a\n * password is the implicit grant, which OAuth 2.1 removes: tokens in a URL, in history,\n * in `Referer`, with no client authentication and no PKCE binding. Spend the ticket by\n * sending the browser to `/oauth/authorize` with your own PKCE challenge and\n * `login_ticket` in the query — the ordinary flow, which then issues an ordinary code.\n *\n * It expires in sixty seconds. It is for one redirect, not for storing.\n *\n * ```ts\n * const result = await frontend.signIn(email, password)\n *\n * if (result.status === 'ok') {\n * window.location.href = `${config.endpoints.authorization}?${new URLSearchParams({\n * client_id, redirect_uri, response_type: 'code', code_challenge, code_challenge_method: 'S256',\n * login_ticket: result.loginTicket,\n * })}`\n * }\n * ```\n *\n * EVERY REFUSAL IS THE SAME REFUSAL. `invalid` covers a wrong password, an unknown\n * address and a locked account, because distinguishing them is the enumeration oracle\n * the server refuses to build — do not present them differently in your UI either.\n */\n async signIn(email: string, password: string): Promise<SignInResult> {\n const response = await this.post('/frontend/v1/sign-in', { email, password });\n\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get('Retry-After') ?? '');\n\n return { status: 'rate_limited', retryAfter: Number.isFinite(retryAfter) ? retryAfter : undefined };\n }\n\n let body: { status?: string; login_ticket?: string; mfa_token?: string; expires_in?: number; factors?: unknown };\n\n try {\n body = (await response.json()) as typeof body;\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n if (body.status === 'ok' && typeof body.login_ticket === 'string') {\n return { status: 'ok', loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 };\n }\n\n if (body.status === 'mfa_required' && typeof body.mfa_token === 'string') {\n // Unknown kinds are dropped rather than passed on: a page that draws a button for a\n // factor this SDK cannot complete strands the person on it.\n const factors = Array.isArray(body.factors)\n ? body.factors.filter((f): f is SecondFactorKind => typeof f === 'string' && SECOND_FACTOR_KINDS.has(f))\n : undefined;\n\n return { status: 'mfa_required', mfaToken: body.mfa_token, ...(factors ? { factors } : {}) };\n }\n\n if (body.status === 'otp_required' && typeof body.mfa_token === 'string') {\n return { status: 'otp_required', mfaToken: body.mfa_token };\n }\n\n if (body.status === 'sso_required') {\n return { status: 'sso_required' };\n }\n\n return { status: 'invalid' };\n }\n\n /**\n * Finish a sign-in that owed a second factor.\n *\n * The token from `signIn()` carries the pending state, because a cross-origin page has\n * no session cookie to carry it in. A TOTP code or a recovery code both work — an\n * embedded form that could not accept a recovery code would strand exactly the people\n * that escape hatch exists for.\n *\n * A wrong code costs an attempt, not the sign-in: five are allowed before the token\n * dies and the person starts from the password again.\n *\n * `method` says which challenge the code answers: `'mfa'` for an authenticator or\n * recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent\n * with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the\n * server cannot tell them apart by shape — answering one as the other is refused.\n */\n async submitSecondFactor(mfaToken: string, code: string, method: 'mfa' | 'otp' | 'sms' = 'mfa'): Promise<SignInResult> {\n const response = await this.post('/frontend/v1/sign-in/factor', { mfa_token: mfaToken, code, method });\n\n let body: { status?: string; login_ticket?: string; expires_in?: number };\n\n try {\n body = (await response.json()) as typeof body;\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n return body.status === 'ok' && typeof body.login_ticket === 'string'\n ? { status: 'ok', loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 }\n : { status: 'invalid' };\n }\n\n /**\n * Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.\n * Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.\n *\n * Only call it when the person asks (\"Text me a code\"), never on page load: each send\n * spends one of the token's five attempts — that is what bounds how many texts one\n * proved password can cause — and every text costs the customer money.\n *\n * ```ts\n * if (result.status === 'mfa_required' && result.factors?.includes('sms')) {\n * const sent = await frontend.sendSmsCode(result.mfaToken)\n * if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)\n * }\n * ```\n */\n async sendSmsCode(mfaToken: string): Promise<SmsCodeResult> {\n const response = await this.post('/frontend/v1/sign-in/factor/sms', { mfa_token: mfaToken });\n\n let body: { status?: string; to?: unknown; expires_in?: unknown; retry_after?: unknown };\n\n try {\n body = (await response.json()) as typeof body;\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n if (body.status === 'sent' && typeof body.to === 'string') {\n return { status: 'sent', to: body.to, expiresIn: typeof body.expires_in === 'number' ? body.expires_in : 300 };\n }\n\n if (body.status === 'rate_limited' || response.status === 429) {\n const retryAfter = typeof body.retry_after === 'number' ? body.retry_after : Number(response.headers.get('Retry-After') ?? '');\n\n return { status: 'rate_limited', retryAfter: Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : undefined };\n }\n\n if (body.status === 'unavailable') {\n return { status: 'unavailable' };\n }\n\n return { status: 'invalid' };\n }\n\n /**\n * The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.\n *\n * The handle exists because a cross-origin page has no session cookie to keep the\n * challenge in between the two requests WebAuthn needs. Pass it back to\n * {@link signInWithPasskey} with whatever `navigator.credentials.get()` returns.\n *\n * `allowCredentials` comes back empty on purpose: this is a discoverable-credential\n * flow, so the authenticator offers whichever passkey it holds and the page never has to\n * say who is signing in — asking for an email first, to look up which credentials exist,\n * would be an enumeration oracle.\n */\n async passkeyOptions(): Promise<PasskeyOptions> {\n const response = await this.post('/frontend/v1/sign-in/passkey/options', {});\n\n if (!response.ok) {\n throw new FrontendApiError(\n `Cbox ID refused the request (${response.status}). Check that this page's origin is on the key's allow-list.`,\n 'origin_not_allowed',\n response.status,\n );\n }\n\n return (await response.json()) as PasskeyOptions;\n }\n\n /**\n * Complete a passkey sign-in with the assertion the authenticator produced.\n *\n * `assertion` is the credential from `navigator.credentials.get()`, serialised — the\n * server verifies the raw body, so pass it through unchanged rather than rebuilding it.\n */\n async signInWithPasskey(challengeToken: string, assertion: Record<string, unknown>): Promise<SignInResult> {\n const response = await this.post('/frontend/v1/sign-in/passkey', { ...assertion, challenge_token: challengeToken });\n\n let body: { status?: string; login_ticket?: string; expires_in?: number };\n\n try {\n body = (await response.json()) as typeof body;\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n return body.status === 'ok' && typeof body.login_ticket === 'string'\n ? { status: 'ok', loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 }\n : { status: 'invalid' };\n }\n\n private async post(path: string, payload: Record<string, unknown>): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n try {\n return await this.fetchImpl(`${this.issuer}${path}`, {\n method: 'POST',\n headers: {\n 'X-Cbox-Publishable-Key': this.publishableKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n } catch {\n throw new FrontendApiError(\n `Could not reach Cbox ID at ${this.issuer}. If this is a browser, check that this page's origin is on the key's allow-list — a refused request fails as a network error by design.`,\n 'unavailable',\n );\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async get<T>(\n path: string,\n extraHeaders: Record<string, string> = {},\n validate?: (body: unknown) => body is T,\n ): Promise<T> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.retries; attempt++) {\n // Linear, not exponential, and only between attempts. This sits inside a page\n // render: a caller waiting on their sign-in box would rather fail in a second than\n // succeed in eight.\n if (attempt > 0) {\n await new Promise((resolve) => setTimeout(resolve, 150 * attempt));\n }\n\n try {\n return await this.attempt<T>(path, extraHeaders, validate);\n } catch (error) {\n lastError = error;\n\n // Retry only what a retry can fix. A refusal is a configuration problem and\n // retrying it delays the developer finding out; a rate limit is honoured rather\n // than hammered; a malformed body will be malformed again.\n if (!(error instanceof FrontendApiError) || error.code !== 'unavailable') {\n throw error;\n }\n }\n }\n\n throw lastError;\n }\n\n private async attempt<T>(\n path: string,\n extraHeaders: Record<string, string>,\n validate?: (body: unknown) => body is T,\n ): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n let response: Response;\n\n try {\n response = await this.fetchImpl(`${this.issuer}${path}`, {\n headers: {\n // A header, never a query string: a query string puts the key in server logs,\n // in `Referer` on every outbound link, and in browser history.\n 'X-Cbox-Publishable-Key': this.publishableKey,\n Accept: 'application/json',\n ...extraHeaders,\n },\n signal: controller.signal,\n });\n } catch (cause) {\n // Includes the abort on timeout, and — in a browser — a refusal, because a refused\n // request deliberately carries no CORS headers and so never becomes a Response.\n throw new FrontendApiError(\n `Could not reach Cbox ID at ${this.issuer}. If this is a browser, check that this page's origin is on the key's allow-list — a refused request fails as a network error by design.`,\n 'unavailable',\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get('Retry-After') ?? '');\n\n throw new FrontendApiError(\n (await described(response)) ?? 'Cbox ID is rate limiting this key.',\n 'rate_limited',\n 429,\n Number.isFinite(retryAfter) ? retryAfter : undefined,\n );\n }\n\n if (response.status === 401 || response.status === 403) {\n throw new FrontendApiError(\n // THE SERVER'S OWN REASON FIRST. It answers precisely — \"No publishable key was\n // presented\" is a different problem from \"That publishable key cannot be used\n // from this origin\" — and substituting a guess for it made this client less\n // precise than the API it wraps. The fallback still stands for the browser case,\n // where a refusal never becomes a readable Response at all.\n (await described(response)) ??\n `Cbox ID refused the request (${response.status}). Check that this page's origin is on the key's allow-list, and that the key is not revoked.`,\n 'origin_not_allowed',\n response.status,\n );\n }\n\n if (!response.ok) {\n throw new FrontendApiError(\n (await described(response)) ?? `Cbox ID returned ${response.status}.`,\n 'unavailable',\n response.status,\n );\n }\n\n let body: unknown;\n\n try {\n body = await response.json();\n } catch {\n throw new FrontendApiError('Cbox ID returned a body that is not JSON.', 'malformed', response.status);\n }\n\n // A 200 whose body is not the document we expect means something between the page and\n // us is rewriting responses — a proxy, an extension, a captive portal. Caught here so\n // it surfaces as that, rather than as `undefined` deep inside somebody's component.\n if (validate && !validate(body)) {\n throw new FrontendApiError(\n 'Cbox ID returned a body that is not the expected document.',\n 'malformed',\n response.status,\n );\n }\n\n return body as T;\n }\n}\n\n/**\n * The server's own `error_description`, when it sent one.\n *\n * Read from a CLONE: the caller may still want the body, and a Response body can only be\n * consumed once — a helper that quietly drained it would turn a precise error into a\n * different, stranger one.\n */\nasync function described(response: Response): Promise<string | undefined> {\n try {\n const body: unknown = await response.clone().json();\n\n if (typeof body === 'object' && body !== null && 'error_description' in body) {\n const description = (body as { error_description: unknown }).error_description;\n\n return typeof description === 'string' && description !== '' ? description : undefined;\n }\n } catch {\n // A refusal without a readable body is the normal case in a browser.\n }\n\n return undefined;\n}\n\n/** The shape check for the config document. Structural, not exhaustive. */\nfunction isFrontendConfig(body: unknown): body is FrontendConfig {\n if (typeof body !== 'object' || body === null) {\n return false;\n }\n\n const doc = body as Partial<FrontendConfig>;\n\n return typeof doc.issuer === 'string' && typeof doc.endpoints === 'object' && doc.endpoints !== null;\n}\n\n/** The shape check for the session document. `user: null` is valid and expected. */\nfunction isFrontendSession(body: unknown): body is FrontendSession {\n return typeof body === 'object' && body !== null && 'user' in body;\n}\n","import { Discovery } from './discovery.js';\nimport { AuthenticationError, ConfigurationError } from './errors.js';\nimport { assertSecureIssuer } from './issuer.js';\n\n/**\n * A permission your app declares. `key` is a `feature:action` slug (e.g.\n * `invoices:create`); `description` is human-facing copy for the Cbox ID console.\n */\nexport interface PermissionDefinition {\n key: string;\n description?: string;\n /**\n * Whether an organization's own administrators may hand this permission out in a custom\n * role they build themselves (self-serve). Defaults to `false`: a permission is internal\n * unless you opt it in, so a key added in a hurry never becomes something every customer\n * can grant. Sent as `tenant_assignable: true`.\n */\n tenantAssignable?: boolean;\n}\n\n/**\n * A role your app declares. `permissions` lists the permission keys the role\n * grants — each must reference a declared {@link PermissionDefinition}.\n */\nexport interface RoleDefinition {\n key: string;\n name: string;\n description?: string;\n permissions: string[];\n /**\n * Whether an organization's administrators may assign this role to their members.\n * Defaults to `true`.\n *\n * Set `false` for a STAFF role — your own support or operations people, held\n * environment-wide across every customer (typically the role that grants\n * `support:impersonate`). Cbox ID never lists or accepts a staff role on the\n * organization plane; only an environment administrator can grant it. Sent as\n * `tenant_assignable: false`.\n */\n tenantAssignable?: boolean;\n}\n\n/** A permission as it is sent to Cbox ID (snake_case, the manifest's wire format). */\nexport interface ManifestPermission {\n key: string;\n description?: string;\n /** Present only for a self-serve permission. See {@link PermissionDefinition.tenantAssignable}. */\n tenant_assignable?: true;\n}\n\n/** A role as it is sent to Cbox ID (snake_case, the manifest's wire format). */\nexport interface ManifestRole {\n key: string;\n name: string;\n description?: string;\n permissions: string[];\n /** Present only for a staff role. See {@link RoleDefinition.tenantAssignable}. */\n tenant_assignable?: false;\n}\n\n/**\n * The roles + permissions an app declares in code. Either list may be omitted.\n * Pass this to {@link defineAuthz} (to validate up front) or straight to\n * {@link buildManifest} / {@link publishManifest}.\n */\nexport interface AuthzDeclaration {\n permissions?: PermissionDefinition[];\n roles?: RoleDefinition[];\n /**\n * Where this app's OLD login lives, while you are migrating off it.\n *\n * Declared here for the same reason roles are: it is a fact about the app, it belongs\n * with the deploy, and the alternative is a URL pasted into a console and a secret\n * pasted into an env file that quietly disagree six weeks later.\n *\n * IT DOES NOT TAKE EFFECT ON ITS OWN. Unlike a role — which affects only the app that\n * declared it — this names a URL that every unknown email and the password typed with\n * it will be offered to, on the environment's whole sign-in path. So it arrives as a\n * proposal and an operator has to approve it in the console. Changing the url later\n * drops that approval, on purpose.\n */\n legacyLogin?: LegacyLoginDeclaration;\n}\n\n/** Where an app's old login lives. See {@link AuthzDeclaration.legacyLogin}. */\nexport interface LegacyLoginDeclaration {\n /** Must be https — a password in the clear is readable by everything on the path. */\n url: string;\n /** At least 32 characters. It is the only thing proving a request came from Cbox ID. */\n secret: string;\n}\n\n/**\n * The manifest as it is sent to Cbox ID: the declared permissions + roles plus a\n * content-derived `version`, so republishing an unchanged catalog is a no-op.\n */\nexport interface AuthzManifest {\n version: string;\n permissions: ManifestPermission[];\n roles: ManifestRole[];\n legacy_login?: LegacyLoginDeclaration;\n}\n\n/** What {@link publishManifest} needs to authenticate the push. */\nexport interface ManifestPublisherConfig {\n /** Base URL (issuer) of the Cbox ID instance — the `token_endpoint` is discovered from it. */\n issuer: string;\n /** Your OAuth client id. The client must hold the `apps.manifest` scope. */\n clientId: string;\n /** Your OAuth client secret. The push uses the client-credentials grant. */\n clientSecret: string;\n /** Timeout (ms) for back-channel HTTP calls. Defaults to `10000`. */\n timeoutMs?: number;\n}\n\n/**\n * The server's sync summary. The named fields are the ones the SDK/CLI reads;\n * the index signature keeps any extra fields the server returns.\n */\nexport interface ManifestSyncSummary {\n unchanged?: boolean;\n roles_declared?: number;\n permissions_declared?: number;\n version?: string;\n [key: string]: unknown;\n}\n\n/**\n * Validate a roles/permissions declaration and return it normalized (both lists\n * always present). Call this where you declare your catalog so mistakes — a role\n * that grants an undeclared permission, a duplicate key — surface immediately\n * rather than on deploy.\n *\n * @throws ConfigurationError on a missing key/name, a duplicate, or a role that\n * references a permission that was never declared.\n */\nexport function defineAuthz(\n declaration: AuthzDeclaration,\n): AuthzDeclaration & { permissions: PermissionDefinition[]; roles: RoleDefinition[] } {\n const permissions = declaration.permissions ?? [];\n const roles = declaration.roles ?? [];\n assertDeclaration(permissions, roles);\n\n // The legacy login is validated here too, so a bad one fails in the deploy that\n // introduced it rather than as a 4xx from a manifest push somebody has to go and read.\n if (declaration.legacyLogin) {\n assertLegacyLogin(declaration.legacyLogin);\n }\n\n // `Required<>` no longer fits: the catalog is always present after this, and the legacy\n // login legitimately is not. Returning the declaration's own optionality keeps a caller\n // from having to invent an empty one.\n return { ...declaration, permissions, roles };\n}\n\n/**\n * Build the manifest — the declared catalog plus a stable content hash `version`\n * (first 16 hex chars of the sha256 of the canonicalized catalog). The catalog is\n * sorted by key first, so declaration order never changes the version.\n */\nexport async function buildManifest(declaration: AuthzDeclaration): Promise<AuthzManifest> {\n const permissions = declaration.permissions ?? [];\n const roles = declaration.roles ?? [];\n assertDeclaration(permissions, roles);\n\n // The version hashes the CATALOG only, and the legacy login is deliberately outside it:\n // this canonicalization is a cross-SDK contract — id-js, id-python, id-go and the PHP\n // reference all produce the same bytes, and a shared fixture exists to keep them from\n // drifting. The server compares a declared url separately for the same reason.\n const version = (await sha256Hex(canonicalManifestJson(permissions, roles))).slice(0, 16);\n\n const manifest: AuthzManifest = {\n version,\n permissions: [...permissions].sort((a, b) => byteCompare(a.key, b.key)).map(wirePermission),\n roles: [...roles].sort((a, b) => byteCompare(a.key, b.key)).map(wireRole),\n };\n\n if (declaration.legacyLogin) {\n assertLegacyLogin(declaration.legacyLogin);\n manifest.legacy_login = declaration.legacyLogin;\n }\n\n return manifest;\n}\n\n/**\n * Publish this app's authorization manifest (its declared roles + permissions) to\n * Cbox ID. The app owns what a role means; Cbox ID owns identity and who holds it.\n *\n * It mints a client-credentials token with the `apps.manifest` scope (token\n * endpoint discovered from the issuer's OIDC configuration), then POSTs the\n * manifest to `{issuer}/api/v1/apps/manifest`. Run it on deploy or from a script;\n * an unchanged catalog is a server-side no-op.\n *\n * @returns the server's sync summary (`unchanged`, `roles_declared`, …).\n * @throws ConfigurationError when config or the declaration is invalid.\n * @throws AuthenticationError when the token cannot be minted or the push fails.\n */\nexport async function publishManifest(\n config: ManifestPublisherConfig,\n declaration: AuthzDeclaration,\n): Promise<ManifestSyncSummary> {\n if (!config.issuer || !config.clientId || !config.clientSecret) {\n throw new ConfigurationError('Publishing a manifest needs issuer, clientId and clientSecret.');\n }\n\n const timeoutMs = config.timeoutMs ?? 10_000;\n assertSecureIssuer(config.issuer);\n const issuer = new URL(config.issuer);\n if (issuer.username || issuer.password || issuer.search || issuer.hash) {\n throw new ConfigurationError('The manifest issuer must not contain credentials, a query or a fragment.');\n }\n const manifest = await buildManifest(declaration);\n const discovery = new Discovery(config.issuer, timeoutMs, 3_600_000);\n\n const token = await mintToken(config, discovery, timeoutMs);\n\n const response = await fetchWithTimeout(\n `${config.issuer.replace(/\\/$/, '')}/api/v1/apps/manifest`,\n {\n method: 'POST',\n redirect: 'error',\n headers: {\n authorization: `Bearer ${token}`,\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify(manifest),\n },\n timeoutMs,\n );\n\n if (!response.ok) {\n throw new AuthenticationError(`Manifest push failed: HTTP ${response.status}`);\n }\n\n const json: unknown = await response.json().catch(() => {\n throw new AuthenticationError('Cbox ID returned an invalid manifest acknowledgement.');\n });\n const checksum = await sha256Hex(canonicalManifestJson(declaration.permissions ?? [], declaration.roles ?? []));\n if (json === null || typeof json !== 'object' || Array.isArray(json)) {\n throw new AuthenticationError('Cbox ID did not acknowledge the authorization manifest.');\n }\n const summary = json as ManifestSyncSummary;\n if (summary.client_id !== config.clientId || summary.version !== manifest.version || summary.checksum !== checksum\n || typeof summary.unchanged !== 'boolean' || !Number.isInteger(summary.roles_declared)\n || !Number.isInteger(summary.permissions_declared)\n || (summary.roles_declared ?? -1) < 0 || (summary.permissions_declared ?? -1) < 0\n || (!summary.unchanged && (summary.roles_declared !== manifest.roles.length\n || summary.permissions_declared !== manifest.permissions.length))) {\n throw new AuthenticationError('Cbox ID did not acknowledge this app and its complete authorization manifest.');\n }\n return summary;\n}\n\nasync function mintToken(\n config: ManifestPublisherConfig,\n discovery: Discovery,\n timeoutMs: number,\n): Promise<string> {\n const response = await fetchWithTimeout(\n await discovery.endpoint('token_endpoint'),\n {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n redirect: 'error',\n body: new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: config.clientId,\n client_secret: config.clientSecret,\n scope: 'apps.manifest',\n }),\n },\n timeoutMs,\n );\n\n const json = (await response.json().catch(() => ({}))) as { access_token?: unknown };\n if (!response.ok || typeof json.access_token !== 'string' || json.access_token === '') {\n throw new AuthenticationError(\n 'Could not obtain an apps.manifest access token — check the client credentials and that the client holds the apps.manifest scope.',\n );\n }\n return json.access_token;\n}\n\n/**\n * A `feature:action` (or bare `feature`) key: lowercase, with dot/colon-separated segments.\n * The server's own pattern, restated so a bad key fails where it was written rather than\n * as a 4xx from a manifest push on deploy.\n */\nconst KEY_PATTERN = /^[a-z][a-z0-9_-]*(?:[.:][a-z0-9_-]+)*$/;\n\nfunction assertDeclaration(permissions: PermissionDefinition[], roles: RoleDefinition[]): void {\n const permissionKeys = new Set<string>();\n for (const permission of permissions) {\n if (!permission.key) {\n throw new ConfigurationError('Every permission needs a non-empty `key`.');\n }\n assertKey(permission.key, 'Permission');\n if (permissionKeys.has(permission.key)) {\n throw new ConfigurationError(`Permission \"${permission.key}\" is declared more than once.`);\n }\n assertFlag(permission.tenantAssignable, `Permission \"${permission.key}\"`);\n permissionKeys.add(permission.key);\n }\n\n const roleKeys = new Set<string>();\n for (const role of roles) {\n if (!role.key) {\n throw new ConfigurationError('Every role needs a non-empty `key`.');\n }\n assertKey(role.key, 'Role');\n if (!role.name) {\n throw new ConfigurationError(`Role \"${role.key}\" needs a non-empty \\`name\\`.`);\n }\n if (roleKeys.has(role.key)) {\n throw new ConfigurationError(`Role \"${role.key}\" is declared more than once.`);\n }\n assertFlag(role.tenantAssignable, `Role \"${role.key}\"`);\n roleKeys.add(role.key);\n for (const reference of role.permissions) {\n if (!permissionKeys.has(reference)) {\n throw new ConfigurationError(\n `Role \"${role.key}\" references permission \"${reference}\", which is not declared.`,\n );\n }\n }\n }\n}\n\nfunction assertKey(key: string, what: 'Permission' | 'Role'): void {\n if (!KEY_PATTERN.test(key)) {\n throw new ConfigurationError(\n `${what} key \"${key}\" is not a lowercase \\`feature:action\\` slug (e.g. \\`invoices:create\\`).`,\n );\n }\n}\n\n/**\n * `tenantAssignable`, when given, must be a real boolean.\n *\n * The type says so, but a catalog loaded from YAML or JSON arrives untyped, and the\n * string `\"false\"` is truthy: the one value that has to mean \"staff only\" would read as\n * \"every customer may grant this\". The server refuses a non-boolean role flag for the\n * same reason; refusing it here puts the failure in the deploy that introduced it.\n */\nfunction assertFlag(value: unknown, owner: string): void {\n if (value !== undefined && typeof value !== 'boolean') {\n throw new ConfigurationError(`${owner} \\`tenantAssignable\\` must be true or false.`);\n }\n}\n\n/**\n * A permission in the manifest's wire format: fixed key order, no `undefined` fields.\n *\n * `tenant_assignable` is written only when it is `true`, the non-default. The server reads\n * the snake_case key and nothing else, so this mapping is the whole difference between a\n * self-serve permission and an internal one — `tenantAssignable` sent as-is would be\n * ignored.\n */\nfunction wirePermission(permission: PermissionDefinition): ManifestPermission {\n return {\n key: permission.key,\n ...(permission.description === undefined ? {} : { description: permission.description }),\n ...(permission.tenantAssignable === true ? { tenant_assignable: true as const } : {}),\n };\n}\n\n/**\n * A role in the manifest's wire format: fixed key order, de-duplicated and sorted\n * permission refs, no `undefined` fields.\n *\n * `tenant_assignable: false` is written only for a staff role. THIS IS THE LINE THAT\n * KEEPS A STAFF ROLE STAFF-ONLY: the server defaults an absent key to assignable, so a\n * camelCase `tenantAssignable: false` passed straight through would publish your support\n * role as one every customer's administrator can hand out.\n */\nfunction wireRole(role: RoleDefinition): ManifestRole {\n return {\n key: role.key,\n name: role.name,\n ...(role.description === undefined ? {} : { description: role.description }),\n permissions: uniqueSorted(role.permissions),\n ...(role.tenantAssignable === false ? { tenant_assignable: false as const } : {}),\n };\n}\n\n/**\n * Serialize {permissions, roles} to the exact canonical JSON the PHP reference hashes,\n * so the `version` is byte-for-byte identical across every Cbox ID SDK. Matches PHP\n * `json_encode` defaults: object keys in insertion order, permissions and roles sorted\n * by key, each role's permission refs de-duplicated and sorted, an absent-or-empty\n * description emitted as `null`, forward slashes escaped as `\\/`, and every non-ASCII\n * code unit as `\\uXXXX`.\n *\n * Each `tenant_assignable` flag appears ONLY in its non-default state — `true` on a\n * permission, `false` on a role — exactly as the reference writes it. That keeps every\n * catalog that never mentions the flag hashing to the bytes it always has, while a\n * change to it still changes the version (an unchanged version is a skipped sync).\n */\nexport function canonicalManifestJson(\n permissions: PermissionDefinition[],\n roles: RoleDefinition[],\n): string {\n const canonical = {\n permissions: [...permissions]\n .sort((a, b) => byteCompare(a.key, b.key))\n .map((p) => ({\n key: p.key,\n description: emptyToNull(p.description),\n ...(p.tenantAssignable === true ? { tenant_assignable: true } : {}),\n })),\n roles: [...roles]\n .sort((a, b) => byteCompare(a.key, b.key))\n .map((r) => ({\n key: r.key,\n name: r.name,\n description: emptyToNull(r.description),\n permissions: uniqueSorted(r.permissions),\n ...(r.tenantAssignable === false ? { tenant_assignable: false } : {}),\n })),\n };\n return escapeLikePhp(JSON.stringify(canonical));\n}\n\n/**\n * Permission refs as the server stores them: each once, sorted. The server drops a\n * repeated ref before it hashes, so hashing the repeat here would give a `version` that\n * no server ever computes.\n */\nfunction uniqueSorted(values: readonly string[]): string[] {\n return [...new Set(values)].sort(byteCompare);\n}\n\n/** PHP treats an absent or empty description as `null` in the hashed catalog. */\nfunction emptyToNull(value: string | undefined): string | null {\n return value === undefined || value === '' ? null : value;\n}\n\n/** Byte-wise (code-unit) comparison — matches PHP `strcmp` on the ASCII-only keys. */\nfunction byteCompare(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/** Apply PHP `json_encode`'s default `\\/` slash and `\\uXXXX` non-ASCII escaping. */\nfunction escapeLikePhp(json: string): string {\n let out = '';\n for (let i = 0; i < json.length; i++) {\n const char = json.charAt(i);\n const code = json.charCodeAt(i);\n if (char === '/') {\n out += '\\\\/';\n } else if (code > 0x7f) {\n out += '\\\\u' + code.toString(16).padStart(4, '0');\n } else {\n out += char;\n }\n }\n return out;\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('');\n}\n\nasync function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } catch {\n throw new AuthenticationError('Cbox ID manifest publication could not be verified.');\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Refuse a legacy login declaration the server would refuse anyway.\n *\n * Checked in the SDK so it fails at build time, in the deploy that introduced it, rather\n * than as a 4xx from a manifest push somebody has to go and read. Both rules are the\n * server's and are restated here rather than inferred: https because a password in the\n * clear is readable by everything on the path, and a real secret because it is the only\n * thing proving a request came from Cbox ID.\n */\nfunction assertLegacyLogin(legacy: LegacyLoginDeclaration): void {\n if (!legacy.url.startsWith('https://')) {\n throw new ConfigurationError('legacyLogin.url must be https — a password must never cross the network in the clear.');\n }\n\n if (!legacy.secret || legacy.secret.length < 32) {\n throw new ConfigurationError('legacyLogin.secret must be at least 32 characters; it is what proves a request came from Cbox ID.');\n }\n}\n"]}
|