@crvouga/mockingbird-service-oauth 1.3.0 → 1.5.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog — @crvouga/mockingbird-service-oauth
2
2
 
3
+ ## 1.5.0 (2026-09-29)
4
+
5
+ ### Features
6
+
7
+ - add hot/warm/cold parity tiers per service ([6621568](https://github.com/crvouga/mockingbird/commit/6621568f6df162838eabab4d3968b5606fe96208))
8
+
9
+ ## 1.4.0 (2026-09-29)
10
+
11
+ ### Features
12
+
13
+ - opt-in seeded credentials so a seeded run replays byte-for-byte ([7a41166](https://github.com/crvouga/mockingbird/commit/7a41166326791b4fc06627fb544d150d3eaab99c))
14
+
15
+ ### Fixes and improvements
16
+
17
+ - make the seeded-credentials example self-contained ([64eef9c](https://github.com/crvouga/mockingbird/commit/64eef9ccbaa555f3bc2420961b7f8fc2a92f5058))
18
+
3
19
  ## 1.3.0 (2026-09-29)
4
20
 
5
21
  ### Features
package/README.md CHANGED
@@ -150,7 +150,7 @@ tapping `oauth-mock-form-post-continue`; with scripts, the page submits the form
150
150
 
151
151
  ### Reproducible provider edge cases
152
152
 
153
- Behavioral randomness is **off by default**. Configure exact scenarios or probabilities; these are test frequencies you choose, not estimates of vendor incidence. OAuth credentials, authorization codes and signing keys always use cryptographic randomness.
153
+ Behavioral randomness is **off by default**. Configure exact scenarios or probabilities; these are test frequencies you choose, not estimates of vendor incidence. OAuth credentials, authorization codes and signing keys use cryptographic randomness unless you opt in to [seeded credentials](#seeded-credentials).
154
154
 
155
155
  ```ts
156
156
  import { createRuntime } from "@crvouga/mockingbird-service-oauth"
@@ -224,6 +224,26 @@ Register an Apple client with `apple.notificationUrl` (an HTTP(S) URL; requires
224
224
 
225
225
  Delivery is awaited by the call that caused it, through `webhooks.fetch` (`createRuntime({ webhooks: { fetch } })`, default global `fetch`), so an in-process suite can route it through its own network model. The admin routes and methods return one `{ clientId, type, delivered, status?, error? }` per notification; a non-2xx response or a thrown error is reported there and never fails the call, and Apple's retries are not modelled. Without an `issuer` option, direct method calls need an `issuer` argument (admin routes derive it from the request). Unknown accounts return 404 from the admin routes and reject from the methods.
226
226
 
227
+ ### Seeded credentials
228
+
229
+ For deterministic simulation tests, opt in so every opaque value the provider mints comes from the seed instead of `crypto`, and the same seed with the same requests replays byte-for-byte. It is **off by default**: without it, codes, tokens, transaction ids, nonces, `kid` and signing keys stay cryptographically random.
230
+
231
+ ```ts
232
+ import { createRuntime } from "@crvouga/mockingbird-service-oauth"
233
+
234
+ const runtime = createRuntime({
235
+ seed: "s1",
236
+ deterministicCredentials: true,
237
+ clients: [{ id: "app", name: "App", redirectUris: ["https://app.test/callback"], secret: "fixture-secret" }],
238
+ accounts: [{ id: "ada", name: "Ada Lovelace", email: "ada@example.test" }],
239
+ })
240
+ ```
241
+
242
+ - Seeded: authorization codes, access and refresh tokens, `transaction` ids, session ids, grant families, Apple notification `jti`s, CSP nonces (on every page and the `form_post` response), and the RS256 signing key with its `kid`, including the never-published key behind `tokens.idTokenSigningKey: "unpublished"`. Values are guessable; use only in tests. Pin the clock (`clock`) as well when you compare ID tokens, whose `iat` / `exp` come from it.
243
+ - Signing keys are a pure function of the seed and the rotation index, so JWKS, the ID-token header and (RS256 being deterministic) the signature replay too. Deriving a 2048-bit RSA key costs roughly half a second to a couple of seconds once per seed per process; it is cached afterwards.
244
+ - The draw counter is part of the namespace state: it is snapshotted with it, and `reset` restarts both the counter and the key sequence, so a reset replays the run. The seed is not mixed with the namespace, so two namespaces or mounts with one seed mint the same values.
245
+ - `createRuntime({ randomBytes: (length) => bytes })` replaces `crypto.getRandomValues` for every minted value instead (it wins over the seeded source), and `signingKeys: [{ privateJwk, publicJwk, kid }, ...]` supplies RS256 keys used in order: the first is current and each `POST /__admin/keys/rotate` takes the next. When the list runs out, rotation falls back to the seeded or random source. The `nonce` option still takes precedence for the `form_post` response.
246
+
227
247
  ### Shared service controls
228
248
 
229
249
  The runtime supplies `/health`, `/__admin/reset`, snapshots, mock clock, request journal, metrics, fault injection and namespace isolation. Use `x-mockingbird-namespace` for in-process tests or `/ns/<name>/…` for complete browser flows. Header-selected namespaces alone cannot persist across ordinary browser navigation. State, grants, sessions and consent live in the shared SQLite abstraction; there are no filesystem or Node imports in the main entry.
@@ -260,7 +280,7 @@ This is a ready-to-use local/test identity provider, **not a production authenti
260
280
 
261
281
  Vendor-hosted Google Identity Services/One Tap, native Apple AuthenticationServices, passkeys, MFA, CAPTCHA, password recovery, email delivery/relay forwarding (only Apple's notification of a forwarding change is sent), app-transfer migration, vendor risk engines, tokeninfo/introspection, logout, GitHub Apps installation/device flows, and Microsoft Graph/tenant administration are not implemented. Other configurable OIDC providers can use the generic profile, but their proprietary scopes and claims are not emulated. Scopes are limited to each profile plus explicitly configured additional scopes. Microsoft uses the configured mock issuer, not real Entra tenant routing. GitHub is the OAuth app login surface, not the full REST API.
262
282
 
263
- No implicit flow, dynamic client registration, wildcard redirect matching, persistent signing-key import, or distributed-session coordination is provided. Private-use redirect schemes work only when their complete URI is explicitly registered; executable/local schemes such as `javascript:`, `data:` and `file:` are rejected. Sign-in and consent pages extend CSP `form-action` with the transaction's redirect origin (or custom scheme) so browsers follow the redirect back to the client. Discovery, JWKS, token, userinfo and revoke answer `OPTIONS` preflights and reflect the request `Origin`, so browser (SPA + PKCE) clients can redeem codes directly; the interaction pages send no CORS headers. Snapshot restore is for the same runtime/instance; signing keys are not serialized. The default backing store is in-memory and state disappears when the process exits. A secure browser context and Web Crypto, Fetch and standard Web APIs are required; Node 22+, Bun and modern browsers provide them.
283
+ No implicit flow, dynamic client registration, wildcard redirect matching, persistent signing-key storage (keys are held in memory; supply them through `signingKeys` or derive them from a seed), or distributed-session coordination is provided. Private-use redirect schemes work only when their complete URI is explicitly registered; executable/local schemes such as `javascript:`, `data:` and `file:` are rejected. Sign-in and consent pages extend CSP `form-action` with the transaction's redirect origin (or custom scheme) so browsers follow the redirect back to the client. Discovery, JWKS, token, userinfo and revoke answer `OPTIONS` preflights and reflect the request `Origin`, so browser (SPA + PKCE) clients can redeem codes directly; the interaction pages send no CORS headers. Snapshot restore is for the same runtime/instance; signing keys are not serialized. The default backing store is in-memory and state disappears when the process exits. A secure browser context and Web Crypto, Fetch and standard Web APIs are required; Node 22+, Bun and modern browsers provide them.
264
284
 
265
285
  References: [Google OpenID Connect](https://developers.google.com/identity/openid-connect/openid-connect), [Apple authorization request](https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server.), and [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html).
266
286
 
@@ -2,7 +2,7 @@ import {
2
2
  OAUTH_SCENARIOS,
3
3
  createMultiRuntime,
4
4
  createRuntime
5
- } from "./chunk-WHI7Y2UA.js";
5
+ } from "./chunk-SYFF4UFD.js";
6
6
 
7
7
  // src/server.ts
8
8
  import { readFile as readFile2 } from "node:fs/promises";
@@ -377,4 +377,4 @@ export {
377
377
  createMultiServer,
378
378
  serveTarget
379
379
  };
380
- //# sourceMappingURL=chunk-MKBPAVYU.js.map
380
+ //# sourceMappingURL=chunk-D4IAZYMT.js.map
@@ -1384,7 +1384,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
1384
1384
  };
1385
1385
 
1386
1386
  // ../core/dist/version.js
1387
- var PACKAGE_VERSION = true ? "1.3.0" : UNRELEASED_VERSION;
1387
+ var PACKAGE_VERSION = true ? "1.5.0" : UNRELEASED_VERSION;
1388
1388
 
1389
1389
  // ../core/dist/webhooks.js
1390
1390
  var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -2246,18 +2246,22 @@ var encoder = new TextEncoder();
2246
2246
  var base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2247
2247
  var random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));
2248
2248
  var hash = async (value) => base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value))));
2249
+ var RSA = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
2249
2250
  var Signer = class {
2250
- pair = crypto.subtle.generateKey(
2251
- {
2252
- name: "RSASSA-PKCS1-v1_5",
2253
- modulusLength: 2048,
2254
- publicExponent: new Uint8Array([1, 0, 1]),
2255
- hash: "SHA-256"
2256
- },
2257
- true,
2258
- ["sign", "verify"]
2259
- );
2260
- kid = random();
2251
+ pair;
2252
+ kid;
2253
+ /** Generates a fresh key pair unless `key` supplies one; `kid` names a generated pair. */
2254
+ constructor(key, kid = random()) {
2255
+ this.kid = key?.kid ?? kid;
2256
+ this.pair = key ? Promise.all([
2257
+ crypto.subtle.importKey("jwk", key.privateJwk, RSA, true, ["sign"]),
2258
+ crypto.subtle.importKey("jwk", key.publicJwk, RSA, true, ["verify"])
2259
+ ]).then(([privateKey, publicKey]) => ({ privateKey, publicKey })) : crypto.subtle.generateKey(
2260
+ { ...RSA, modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]) },
2261
+ true,
2262
+ ["sign", "verify"]
2263
+ );
2264
+ }
2261
2265
  async jwks() {
2262
2266
  const pair = await this.pair;
2263
2267
  return {
@@ -2317,6 +2321,152 @@ async function verifyAppleSecret(token, clientId, apple, now) {
2317
2321
  }
2318
2322
  }
2319
2323
 
2324
+ // src/credentials.ts
2325
+ var seededBytes = (label, length) => {
2326
+ let a = seedFrom(`${label}:0`);
2327
+ let b = seedFrom(`${label}:1`);
2328
+ let c = seedFrom(`${label}:2`);
2329
+ let d = seedFrom(`${label}:3`);
2330
+ const next = () => {
2331
+ const t = (a + b | 0) + d | 0;
2332
+ d = d + 1 | 0;
2333
+ a = b ^ b >>> 9;
2334
+ b = c + (c << 3) | 0;
2335
+ c = (c << 21 | c >>> 11) + t | 0;
2336
+ return t >>> 0;
2337
+ };
2338
+ for (let i = 0; i < 12; i++) next();
2339
+ const out = new Uint8Array(length);
2340
+ for (let i = 0; i < length; i += 4) {
2341
+ const word = next();
2342
+ for (let j = 0; j < 4 && i + j < length; j++) out[i + j] = word >>> 8 * j & 255;
2343
+ }
2344
+ return out;
2345
+ };
2346
+ var toBigInt = (bytes) => BigInt(`0x${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}`);
2347
+ var fromBigInt = (value) => {
2348
+ const hex = value.toString(16);
2349
+ const padded = hex.length % 2 ? `0${hex}` : hex;
2350
+ return Uint8Array.from(padded.match(/../g) ?? [], (h) => Number.parseInt(h, 16));
2351
+ };
2352
+ var modPow = (base, exponent, modulus) => {
2353
+ let result = 1n;
2354
+ let b = base % modulus;
2355
+ for (let e = exponent; e > 0n; e >>= 1n) {
2356
+ if (e & 1n) result = result * b % modulus;
2357
+ b = b * b % modulus;
2358
+ }
2359
+ return result;
2360
+ };
2361
+ var modInverse = (value, modulus) => {
2362
+ let [r0, r1, t0, t1] = [modulus, value % modulus, 0n, 1n];
2363
+ while (r1) {
2364
+ const q = r0 / r1;
2365
+ [r0, r1] = [r1, r0 - q * r1];
2366
+ [t0, t1] = [t1, t0 - q * t1];
2367
+ }
2368
+ return (t0 % modulus + modulus) % modulus;
2369
+ };
2370
+ var SMALL_PRIMES = Array.from({ length: 2e3 }, (_, i) => i + 2).filter((n, _, all) => all.every((m) => m * m > n || n % m)).map(BigInt);
2371
+ var probablePrime = (n) => {
2372
+ if (SMALL_PRIMES.some((p) => n % p === 0n)) return false;
2373
+ let d = n - 1n;
2374
+ let s = 0;
2375
+ while (!(d & 1n)) {
2376
+ d >>= 1n;
2377
+ s++;
2378
+ }
2379
+ return SMALL_PRIMES.slice(0, 16).every((a) => {
2380
+ let x = modPow(a, d, n);
2381
+ if (x === 1n || x === n - 1n) return true;
2382
+ for (let i = 1; i < s; i++) {
2383
+ x = x * x % n;
2384
+ if (x === n - 1n) return true;
2385
+ }
2386
+ return false;
2387
+ });
2388
+ };
2389
+ var E = 65537n;
2390
+ var seededPrime = (label) => {
2391
+ for (let attempt = 0; ; attempt++) {
2392
+ const candidate = toBigInt(seededBytes(`${label}:${attempt}`, 128)) | 3n << 1022n | 1n;
2393
+ if ((candidate - 1n) % E !== 0n && probablePrime(candidate)) return candidate;
2394
+ }
2395
+ };
2396
+ var keyCache = /* @__PURE__ */ new Map();
2397
+ var seededRsa = (label) => {
2398
+ const cached = keyCache.get(label);
2399
+ if (cached) return cached;
2400
+ let p = seededPrime(`${label}:p`);
2401
+ let q = seededPrime(`${label}:q`);
2402
+ for (let i = 0; q === p; i++) q = seededPrime(`${label}:q${i}`);
2403
+ if (p < q) [p, q] = [q, p];
2404
+ const d = modInverse(E, (p - 1n) * (q - 1n));
2405
+ const enc = (v) => base64url(fromBigInt(v));
2406
+ const publicJwk = { kty: "RSA", n: enc(p * q), e: enc(E) };
2407
+ const privateJwk = {
2408
+ ...publicJwk,
2409
+ d: enc(d),
2410
+ p: enc(p),
2411
+ q: enc(q),
2412
+ dp: enc(d % (p - 1n)),
2413
+ dq: enc(d % (q - 1n)),
2414
+ qi: enc(modInverse(q, p))
2415
+ };
2416
+ const pair = { privateJwk, publicJwk };
2417
+ keyCache.set(label, pair);
2418
+ return pair;
2419
+ };
2420
+ var Credentials = class {
2421
+ constructor(sqlite, namespace, seed, options) {
2422
+ this.options = options;
2423
+ this.draws = new Collection(sqlite, namespace, "oauth_credential_draws");
2424
+ this.seed = String(seed ?? 0);
2425
+ }
2426
+ options;
2427
+ draws;
2428
+ seed;
2429
+ keyIndex = 0;
2430
+ bytes(length) {
2431
+ if (this.options.randomBytes) return this.options.randomBytes(length);
2432
+ if (this.options.deterministicCredentials)
2433
+ return seededBytes(`${this.seed}:${this.draws.nextSequence()}`, length);
2434
+ return crypto.getRandomValues(new Uint8Array(length));
2435
+ }
2436
+ /** Codes, access/refresh tokens, transaction ids, session ids and grant families. */
2437
+ token() {
2438
+ return base64url(this.bytes(32));
2439
+ }
2440
+ /** CSP nonce for interaction pages; a UUID unless credentials are controlled. */
2441
+ nonce() {
2442
+ return this.options.randomBytes || this.options.deterministicCredentials ? base64url(this.bytes(16)) : crypto.randomUUID();
2443
+ }
2444
+ /** The next signing key: injected, else derived from the seed, else freshly generated. */
2445
+ signer() {
2446
+ const index = this.keyIndex++;
2447
+ const injected = this.options.signingKeys?.[index];
2448
+ if (injected) return new Signer(injected);
2449
+ if (!this.options.deterministicCredentials || this.options.randomBytes)
2450
+ return new Signer(void 0, this.token());
2451
+ const kid = this.token();
2452
+ return new Signer({ ...seededRsa(`${this.seed}:key:${index}`), kid });
2453
+ }
2454
+ /**
2455
+ * The key that signs ID tokens but is never published. Never taken from `signingKeys` (those are
2456
+ * published); under a seed it is derived without touching the draw counter.
2457
+ */
2458
+ unpublishedSigner() {
2459
+ if (!this.options.deterministicCredentials || this.options.randomBytes)
2460
+ return new Signer(void 0, this.token());
2461
+ const label = `${this.seed}:unpublished`;
2462
+ return new Signer({ ...seededRsa(label), kid: base64url(seededBytes(`${label}:kid`, 32)) });
2463
+ }
2464
+ /** Start the signing-key sequence over; the draw counter is cleared with the namespace. */
2465
+ restart() {
2466
+ this.keyIndex = 0;
2467
+ }
2468
+ };
2469
+
2320
2470
  // src/ui.ts
2321
2471
  var escapeHtml = (value) => value.replace(
2322
2472
  /[&<>"']/g,
@@ -2346,7 +2496,7 @@ function page(title, body, status = 200, action = "'self'", nonce = crypto.rando
2346
2496
  }
2347
2497
  );
2348
2498
  }
2349
- function loginPage(transaction, client, accounts, base, action, signup = false, error = "") {
2499
+ function loginPage(transaction, client, accounts, base, action, signup = false, error = "", nonce) {
2350
2500
  const hidden = `<input type="hidden" name="transaction" value="${escapeHtml(transaction)}">`;
2351
2501
  const endpoint = escapeHtml(`${base}/interaction`);
2352
2502
  const intro = `<span class="eyebrow">Sign in</span><h1 id="title">${signup ? "Create your account" : "Choose an account"}</h1><p>${signup ? "Create an account to continue to" : "Choose an account to continue to"}<br><span class="app">${escapeHtml(client)}</span></p>${error ? `<div class="error" role="alert" data-testid="oauth-mock-${signup ? "signup" : "chooser"}-error">${escapeHtml(error)}</div>` : ""}`;
@@ -2356,11 +2506,11 @@ function loginPage(transaction, client, accounts, base, action, signup = false,
2356
2506
  `${intro}${body}<div class="divider"></div><form method="post" action="${endpoint}">${hidden}<button class="secondary" data-testid="oauth-mock-deny" name="action" value="deny">Cancel sign-in</button></form>`,
2357
2507
  error ? 400 : 200,
2358
2508
  action,
2359
- void 0,
2509
+ nonce,
2360
2510
  signup ? "oauth-mock-signup" : "oauth-mock-chooser"
2361
2511
  );
2362
2512
  }
2363
- function consentPage(transaction, client, account, scopes2, base, action, privacy) {
2513
+ function consentPage(transaction, client, account, scopes2, base, action, privacy, nonce) {
2364
2514
  const labels = {
2365
2515
  openid: "Confirm your identity",
2366
2516
  email: "View your email address",
@@ -2378,7 +2528,7 @@ function consentPage(transaction, client, account, scopes2, base, action, privac
2378
2528
  )}</ul><p>You can cancel now without sharing anything.</p><form method="post" action="${escapeHtml(base)}/interaction"><input type="hidden" name="transaction" value="${escapeHtml(transaction)}">${privacyFields}<button class="primary" data-testid="oauth-mock-allow" name="action" value="allow">Allow &amp; continue</button><button class="secondary" data-testid="oauth-mock-deny" name="action" value="deny">Cancel</button></form>`,
2379
2529
  200,
2380
2530
  action,
2381
- void 0,
2531
+ nonce,
2382
2532
  "oauth-mock-consent"
2383
2533
  );
2384
2534
  }
@@ -2688,6 +2838,8 @@ var OAuthAPI = class {
2688
2838
  options.seed ?? 0,
2689
2839
  options.behavior ?? {}
2690
2840
  );
2841
+ this.credentials = new Credentials(this.sqlite, this.namespace, options.seed, options);
2842
+ this.signer = this.credentials.signer();
2691
2843
  this.seed();
2692
2844
  }
2693
2845
  options;
@@ -2698,7 +2850,8 @@ var OAuthAPI = class {
2698
2850
  tokens;
2699
2851
  sessions;
2700
2852
  consents;
2701
- signer = new Signer();
2853
+ signer;
2854
+ credentials;
2702
2855
  previousSigners = [];
2703
2856
  /** Never published: signs ID tokens for `tokens.idTokenSigningKey: "unpublished"`. */
2704
2857
  unpublishedSigner;
@@ -2755,6 +2908,12 @@ var OAuthAPI = class {
2755
2908
  async reset() {
2756
2909
  clearNamespace(this.sqlite, this.namespace);
2757
2910
  this.behavior.configure(this.options.behavior ?? {});
2911
+ if (this.options.deterministicCredentials || this.options.signingKeys) {
2912
+ this.credentials.restart();
2913
+ this.previousSigners = [];
2914
+ this.unpublishedSigner = void 0;
2915
+ this.signer = this.credentials.signer();
2916
+ }
2758
2917
  this.seed();
2759
2918
  }
2760
2919
  issuer(request) {
@@ -2766,7 +2925,7 @@ var OAuthAPI = class {
2766
2925
  }
2767
2926
  rotateSigningKey(retainPrevious = true) {
2768
2927
  this.previousSigners = retainPrevious ? [this.signer, ...this.previousSigners].slice(0, 4) : [];
2769
- this.signer = new Signer();
2928
+ this.signer = this.credentials.signer();
2770
2929
  return { kid: this.signer.kid };
2771
2930
  }
2772
2931
  /**
@@ -2828,7 +2987,7 @@ var OAuthAPI = class {
2828
2987
  iss: iss.replace(/\/$/, ""),
2829
2988
  aud: client.id,
2830
2989
  iat: Math.floor(this.now() / 1e3),
2831
- jti: random(),
2990
+ jti: this.credentials.token(),
2832
2991
  events: JSON.stringify({
2833
2992
  type,
2834
2993
  sub,
@@ -3037,7 +3196,7 @@ var OAuthAPI = class {
3037
3196
  const skew = tokens?.idTokenClockSkewSeconds ?? 0;
3038
3197
  const iat = Math.floor(this.now() / 1e3) + skew;
3039
3198
  const unpublished = tokens?.idTokenSigningKey === "unpublished";
3040
- if (unpublished) this.unpublishedSigner ??= new Signer();
3199
+ if (unpublished) this.unpublishedSigner ??= this.credentials.unpublishedSigner();
3041
3200
  return (unpublished && this.unpublishedSigner || this.signer).sign({
3042
3201
  ...this.profile(account, grant),
3043
3202
  iss: issuer,
@@ -3240,7 +3399,10 @@ var OAuthAPI = class {
3240
3399
  if (request.method === "GET" && path === "/")
3241
3400
  return page(
3242
3401
  "Identity sandbox",
3243
- '<span class="eyebrow">Mockingbird Identity</span><h1 id="title">Make sign-in<br>feel real.</h1><p>Your identity sandbox is ready. Start sign-in from your application to choose an account, create a new identity, and review access.</p><div class="account"><span class="avatar" aria-hidden="true">\u2713</span><span class="identity"><strong>Ready when you are</strong><small>OAuth 2.0 \xB7 OpenID Connect</small></span></div>'
3402
+ '<span class="eyebrow">Mockingbird Identity</span><h1 id="title">Make sign-in<br>feel real.</h1><p>Your identity sandbox is ready. Start sign-in from your application to choose an account, create a new identity, and review access.</p><div class="account"><span class="avatar" aria-hidden="true">\u2713</span><span class="identity"><strong>Ready when you are</strong><small>OAuth 2.0 \xB7 OpenID Connect</small></span></div>',
3403
+ 200,
3404
+ void 0,
3405
+ this.credentials.nonce()
3244
3406
  );
3245
3407
  return fail("not_found", "Unknown endpoint", 404);
3246
3408
  }
@@ -3336,7 +3498,7 @@ var OAuthAPI = class {
3336
3498
  if (!granted) return error("consent_required", "Consent is required");
3337
3499
  return this.finish({ ...auth, accountId: current.id, authTime: session.authTime }, issuer);
3338
3500
  }
3339
- const id = random();
3501
+ const id = this.credentials.token();
3340
3502
  if (current && session && !prompt.has("login") && !prompt.has("select_account")) {
3341
3503
  auth.accountId = current.id;
3342
3504
  auth.authTime = session.authTime;
@@ -3351,7 +3513,16 @@ var OAuthAPI = class {
3351
3513
  accounts.sort(
3352
3514
  (a, b) => Number(b.email === hint || b.id === hint) - Number(a.email === hint || a.id === hint)
3353
3515
  );
3354
- return loginPage(id, client.name, accounts, issuer, formAction(auth.redirectUri));
3516
+ return loginPage(
3517
+ id,
3518
+ client.name,
3519
+ accounts,
3520
+ issuer,
3521
+ formAction(auth.redirectUri),
3522
+ false,
3523
+ "",
3524
+ this.credentials.nonce()
3525
+ );
3355
3526
  }
3356
3527
  consent(id, client, account, auth, issuer) {
3357
3528
  const existing = this.identities.get(this.identityKey(client, account.id));
@@ -3367,7 +3538,8 @@ var OAuthAPI = class {
3367
3538
  this.provider === "apple" && scopes(auth.scope).has("email") && !existing ? {
3368
3539
  hideEmail: account.privateEmail ?? (this.behavior.config.apple?.emailMode === "hide" || (this.behavior.config.apple?.emailMode ?? "choose") === "choose" && auth.decisions.hideEmail),
3369
3540
  choice: (this.behavior.config.apple?.emailMode ?? "choose") === "choose"
3370
- } : void 0
3541
+ } : void 0,
3542
+ this.credentials.nonce()
3371
3543
  );
3372
3544
  }
3373
3545
  async interact(request, issuer) {
@@ -3384,7 +3556,7 @@ var OAuthAPI = class {
3384
3556
  '<span class="eyebrow">Let\u2019s try again</span><h1 id="title">This sign-in expired</h1><p>Return to your application and start sign-in again.</p>',
3385
3557
  400,
3386
3558
  void 0,
3387
- void 0,
3559
+ this.credentials.nonce(),
3388
3560
  "oauth-mock-error"
3389
3561
  );
3390
3562
  const accounts = () => this.accounts.list({ order: "oldest", where: (a) => !a.disabled }).map((a) => a.value);
@@ -3395,7 +3567,9 @@ var OAuthAPI = class {
3395
3567
  accounts(),
3396
3568
  issuer,
3397
3569
  formAction(auth.redirectUri),
3398
- p.get("screen") === "signup"
3570
+ p.get("screen") === "signup",
3571
+ "",
3572
+ this.credentials.nonce()
3399
3573
  );
3400
3574
  const action = p.get("action");
3401
3575
  if (action === "deny") {
@@ -3415,7 +3589,8 @@ var OAuthAPI = class {
3415
3589
  issuer,
3416
3590
  formAction(auth.redirectUri),
3417
3591
  true,
3418
- "Enter a full name and a valid email address."
3592
+ "Enter a full name and a valid email address.",
3593
+ this.credentials.nonce()
3419
3594
  );
3420
3595
  if (accounts().some((a) => a.email === email) || this.accounts.list().some((a) => a.value.email === email))
3421
3596
  return loginPage(
@@ -3425,9 +3600,15 @@ var OAuthAPI = class {
3425
3600
  issuer,
3426
3601
  formAction(auth.redirectUri),
3427
3602
  true,
3428
- "This email already has an account. Go back to choose it."
3603
+ "This email already has an account. Go back to choose it.",
3604
+ this.credentials.nonce()
3429
3605
  );
3430
- const account = this.seedAccount({ id: random(), email, name, emailVerified: true });
3606
+ const account = this.seedAccount({
3607
+ id: this.credentials.token(),
3608
+ email,
3609
+ name,
3610
+ emailVerified: true
3611
+ });
3431
3612
  auth.accountId = account.id;
3432
3613
  auth.authTime = this.now();
3433
3614
  this.transactions.update(id, auth);
@@ -3443,7 +3624,8 @@ var OAuthAPI = class {
3443
3624
  issuer,
3444
3625
  formAction(auth.redirectUri),
3445
3626
  false,
3446
- "Choose an available account."
3627
+ "Choose an available account.",
3628
+ this.credentials.nonce()
3447
3629
  );
3448
3630
  auth.accountId = account.id;
3449
3631
  auth.authTime = this.now();
@@ -3479,7 +3661,7 @@ var OAuthAPI = class {
3479
3661
  return fail("invalid_request", "Invalid interaction action");
3480
3662
  }
3481
3663
  startSession(result, accountId, authTime, issuer) {
3482
- const session = random();
3664
+ const session = this.credentials.token();
3483
3665
  this.sessions.insert(session, { accountId, authTime, expires: this.now() + 864e5 });
3484
3666
  result.headers.append(
3485
3667
  this.options.cookieHeaders?.response ?? "set-cookie",
@@ -3516,14 +3698,14 @@ var OAuthAPI = class {
3516
3698
  });
3517
3699
  const googleRefresh = behavior.google?.refreshToken ?? "first-consent";
3518
3700
  const issueRefresh = this.provider === "google" ? auth.offline && googleRefresh !== "never" && (googleRefresh === "always" || auth.forceConsent || !previous) : this.provider === "apple" || scopes(scope).has("offline_access");
3519
- const code = random();
3701
+ const code = this.credentials.token();
3520
3702
  const grant = {
3521
3703
  ...auth,
3522
3704
  scope,
3523
3705
  identity,
3524
3706
  issueRefresh,
3525
3707
  expires: this.now() + (behavior.tokens?.codeTtlSeconds ?? 300) * 1e3,
3526
- family: random()
3708
+ family: this.credentials.token()
3527
3709
  };
3528
3710
  this.codes.insert(code, grant);
3529
3711
  this.consents.insert(key, { scope: [...scopes(`${previous?.scope ?? ""} ${scope}`)].join(" ") });
@@ -3552,7 +3734,7 @@ var OAuthAPI = class {
3552
3734
  if (auth.state) values.state = auth.state;
3553
3735
  const redirect = new URL(auth.redirectUri);
3554
3736
  if (auth.responseMode === "form_post") {
3555
- const nonce = this.options.nonce?.() ?? crypto.randomUUID();
3737
+ const nonce = this.options.nonce?.() ?? this.credentials.nonce();
3556
3738
  const result = page(
3557
3739
  "Continue to your app",
3558
3740
  `<span class="eyebrow">All set</span><h1 id="title">Back to your app</h1><p>Your sign-in response is ready.</p><form id="callback" method="post" action="${escapeHtml(auth.redirectUri)}">${Object.entries(
@@ -3696,7 +3878,7 @@ var OAuthAPI = class {
3696
3878
  if (type === "refresh_token")
3697
3879
  this.tokens.update(key, { ...stored, kind: "refresh", lastUsed: this.now() });
3698
3880
  const accessTtl = behavior.tokens?.accessTtlSeconds ?? 3600;
3699
- const access = random();
3881
+ const access = this.credentials.token();
3700
3882
  const result = {
3701
3883
  access_token: access,
3702
3884
  token_type: "Bearer",
@@ -3725,7 +3907,7 @@ var OAuthAPI = class {
3725
3907
  const refreshKey = JSON.stringify([grant.clientId, grant.accountId]);
3726
3908
  const googleEligible = this.provider !== "google" || behavior.google?.refreshToken === "always" || grant.forceConsent || !this.refreshIssued.has(refreshKey);
3727
3909
  if (type === "authorization_code" && grant.issueRefresh && googleEligible || rotating || microsoftRefresh) {
3728
- const refresh = random();
3910
+ const refresh = this.credentials.token();
3729
3911
  const basicScopes = [
3730
3912
  "openid",
3731
3913
  "email",
@@ -3786,4 +3968,4 @@ export {
3786
3968
  createMultiRuntime,
3787
3969
  OAuthAPI
3788
3970
  };
3789
- //# sourceMappingURL=chunk-WHI7Y2UA.js.map
3971
+ //# sourceMappingURL=chunk-SYFF4UFD.js.map