@crvouga/mockingbird-service-oauth 1.1.0 → 1.2.1

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,18 @@
1
1
  # Changelog — @crvouga/mockingbird-service-oauth
2
2
 
3
+ ## 1.2.1 (2026-09-29)
4
+
5
+ ### Fixes and improvements
6
+
7
+ - sign and retry webhooks on the injected clock ([1091bde](https://github.com/crvouga/mockingbird/commit/1091bde0df69d9cc3eba2062d8523755e6caca14))
8
+
9
+ ## 1.2.0 (2026-09-29)
10
+
11
+ ### Features
12
+
13
+ - add id token faults for issuer clock skew and unpublished signing key ([7a014f2](https://github.com/crvouga/mockingbird/commit/7a014f2d0e5beeb458a056f092b1b62ccbf504d4))
14
+ - send Sign in with Apple server-to-server notifications ([681348e](https://github.com/crvouga/mockingbird/commit/681348e5ef0b4cf87067f013e3d0703339d608c1))
15
+
3
16
  ## 1.1.0 (2026-09-29)
4
17
 
5
18
  ### Features
package/README.md CHANGED
@@ -186,9 +186,9 @@ Identity/consent decisions are sampled once per authorization and kept with the
186
186
  | GitHub | OAuth app endpoints, JSON or form token responses, no ID token, and a nullable `/user.email` even with email scope. `/user/emails` returns primary/secondary and verified/unverified addresses and requires `user:email` or `user`. Resource responses expose `X-OAuth-Scopes` and `X-Accepted-OAuth-Scopes`. An unverified primary account fails token exchange with `unverified_user_email`. Incorrect credentials/code/redirect produce GitHub error names. |
187
187
  | Any | Missing/unverified email, missing names, denied consent, partial scopes, configurable token/code expiry, transient token failures, revoked grants, strict refresh rotation/reuse detection, and signing-key rotation. Non-Apple account email changes retain the subject. Additional scopes can be accepted via `additionalScopes`; associated resource APIs are not implied. |
188
188
 
189
- The complete typed controls are `OAuthBehavior`. `probabilities` accepts `hideEmail`, `omitEmail`, `omitName`, `unverifiedEmail`, `denyConsent`, `tokenUnavailable`, and `invalidGrant`, each in `[0,1]`. Static `claims` flags force omissions or unverified email. `consent.error` supports `access_denied`, `interaction_required`, or `temporarily_unavailable`. `tokens` accepts positive integer `accessTtlSeconds`, `codeTtlSeconds`, `refreshTtlSeconds`, `refreshRotation: "reuse" | "rotate"`, and `refreshError`. These controls are local testing overrides, not claims that all providers implement every variation.
189
+ The complete typed controls are `OAuthBehavior`. `probabilities` accepts `hideEmail`, `omitEmail`, `omitName`, `unverifiedEmail`, `denyConsent`, `tokenUnavailable`, and `invalidGrant`, each in `[0,1]`. Static `claims` flags force omissions or unverified email. `consent.error` supports `access_denied`, `interaction_required`, or `temporarily_unavailable`. `tokens` accepts positive integer `accessTtlSeconds`, `codeTtlSeconds`, `refreshTtlSeconds`, `refreshRotation: "reuse" | "rotate"`, `refreshError`, and two ID-token faults for testing a relying party's verification: `idTokenClockSkewSeconds` (an integer, forward or backward) sets `iat = now + skew`, `exp = iat + accessTtlSeconds` and offsets `auth_time` by the same skew, and `idTokenSigningKey: "unpublished"` signs ID tokens with a key whose `kid` no JWKS response ever contains, even after `rotateSigningKey`. Both apply to authorization-code, refresh-grant and `code id_token` ID tokens and compose with each other. These controls are local testing overrides, not claims that all providers implement every variation.
190
190
 
191
- `OAUTH_SCENARIOS` supplies: `apple_private_relay`, `apple_share_email`, `apple_returning_user`, `apple_boolean_claims`, `microsoft_missing_email`, `microsoft_spa_expiry`, `github_unverified_email`, `missing_email`, `missing_name`, `unverified_email`, `google_no_refresh_token`, `google_reauthentication`, `revoked_refresh_token`, `rotating_refresh_tokens`, `short_lived_tokens`, `consent_denied`, `intermittent_token_failure`. Explicit fields override the chosen preset's fields. Unknown keys and invalid values fail validation.
191
+ `OAUTH_SCENARIOS` supplies: `apple_private_relay`, `apple_share_email`, `apple_returning_user`, `apple_boolean_claims`, `microsoft_missing_email`, `microsoft_spa_expiry`, `github_unverified_email`, `missing_email`, `missing_name`, `unverified_email`, `google_no_refresh_token`, `google_reauthentication`, `revoked_refresh_token`, `rotating_refresh_tokens`, `short_lived_tokens`, `consent_denied`, `intermittent_token_failure`, `id_token_clock_ahead` (+600 s), `id_token_stale` (-7200 s, expired an hour ago), `id_token_unknown_key`. Explicit fields override the chosen preset's fields. Unknown keys and invalid values fail validation.
192
192
 
193
193
  ```sh
194
194
  npx mockingbird-oauth serve --provider apple --seed regression-42 --scenario apple_private_relay
@@ -198,17 +198,33 @@ curl -X PUT http://localhost:8810/__admin/behavior -H 'content-type: application
198
198
  curl http://localhost:8810/__admin/behavior
199
199
  curl -X POST http://localhost:8810/__admin/consents/revoke -H 'content-type: application/json' \
200
200
  -d '{"clientId":"app","accountId":"ada"}'
201
+ curl -X POST http://localhost:8810/__admin/accounts/delete -H 'content-type: application/json' \
202
+ -d '{"accountId":"ada"}'
203
+ curl -X POST http://localhost:8810/__admin/relay-forwarding -H 'content-type: application/json' \
204
+ -d '{"accountId":"ada","forwarding":false}'
201
205
  curl -X POST http://localhost:8810/__admin/keys/rotate -H 'content-type: application/json' \
202
206
  -d '{"retainPrevious":true}'
203
207
  ```
204
208
 
205
- These routes use the shared admin-key and namespace controls. `revokeConsent(clientId, accountId)` removes that client's grants and resets first-use disclosure; it does not disable the account. `rotateSigningKey(true)` retains up to four previous public keys so existing tokens still verify; `false` withdraws them to test stale JWKS caches. Keys themselves are not included in snapshots, so restoring state does not undo a key rotation.
209
+ These routes use the shared admin-key and namespace controls. `revokeConsent(clientId, accountId)` removes that client's grants and resets first-use disclosure; it does not disable the account. It is async because it can deliver a [server-to-server notification](#sign-in-with-apple-server-to-server-notifications). `rotateSigningKey(true)` retains up to four previous public keys so existing tokens still verify; `false` withdraws them to test stale JWKS caches. Keys themselves are not included in snapshots, so restoring state does not undo a key rotation.
210
+
211
+ ### Sign in with Apple server-to-server notifications
212
+
213
+ Register an Apple client with `apple.notificationUrl` (an HTTP(S) URL; requires the JWT `apple` client form) and the mock behaves like Apple's [server-to-server notification endpoint](https://developer.apple.com/documentation/signinwithapplerestapi/processing-changes-for-sign-in-with-apple-accounts): it POSTs `{"payload": "<JWS>"}` as JSON. The JWS is signed by the current signing key (verify it against the JWKS) with `iss` = the issuer, `aud` = the client id, `iat` from the mock clock and a random `jti`. `events` is a JSON string of `{ type, sub, event_time }`, where `sub` is the client's identity for the account and `event_time` is milliseconds from the mock clock.
214
+
215
+ | Trigger | Event `type` | Sent to |
216
+ | --- | --- | --- |
217
+ | `revokeConsent(clientId, accountId)` / `POST /__admin/consents/revoke` | `consent-revoked` | that client, when it held the consent |
218
+ | `deleteAccount(accountId)` / `POST /__admin/accounts/delete {"accountId"}` | `account-delete` | every client holding a consent; then the account, its grants, sessions and pending sign-ins are removed |
219
+ | `setRelayForwarding(accountId, forwarding)` / `POST /__admin/relay-forwarding {"accountId","forwarding"}` | `email-disabled` / `email-enabled`, with `email` (the relay address) and `is_private_email: "true"` | every client holding a consent whose identity is a private relay address |
220
+
221
+ 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.
206
222
 
207
223
  ### Shared service controls
208
224
 
209
225
  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.
210
226
 
211
- `OAUTH_PRESETS` includes `token_unavailable` and `access_denied`. Fault rules can also target a provider-specific path, e.g. `POST /__admin/faults` with `{"pathPrefix":"/auth/token","status":503,"body":{"error":"temporarily_unavailable"}}`. No outbound webhooks are modeled. Journals contain request metadata, never passwords or request bodies.
227
+ `OAUTH_PRESETS` includes `token_unavailable` and `access_denied`. Fault rules can also target a provider-specific path, e.g. `POST /__admin/faults` with `{"pathPrefix":"/auth/token","status":503,"body":{"error":"temporarily_unavailable"}}`. The only outbound requests are the Apple notifications above. Journals contain request metadata, never passwords or request bodies.
212
228
 
213
229
  ## API
214
230
 
@@ -238,7 +254,7 @@ references below.
238
254
 
239
255
  This is a ready-to-use local/test identity provider, **not a production authentication server or a claim that every proprietary provider feature is implemented**. Its ready tier covers the documented OAuth/OIDC login, identity, consent, token, provider-edge-case, and UI surface. Applications should still run a small final check against each real provider before release.
240
256
 
241
- Vendor-hosted Google Identity Services/One Tap, native Apple AuthenticationServices, passkeys, MFA, CAPTCHA, password recovery, email delivery/relay forwarding, 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.
257
+ 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.
242
258
 
243
259
  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.
244
260
 
@@ -2,7 +2,7 @@ import {
2
2
  OAUTH_SCENARIOS,
3
3
  createMultiRuntime,
4
4
  createRuntime
5
- } from "./chunk-JA62N2QU.js";
5
+ } from "./chunk-XP2AEPEJ.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-FZKITSDL.js.map
380
+ //# sourceMappingURL=chunk-2JIADQOV.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.1.0" : UNRELEASED_VERSION;
1387
+ var PACKAGE_VERSION = true ? "1.2.1" : 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" } });
@@ -2069,7 +2069,10 @@ var OAUTH_SCENARIOS = {
2069
2069
  rotating_refresh_tokens: { tokens: { refreshRotation: "rotate" } },
2070
2070
  short_lived_tokens: { tokens: { accessTtlSeconds: 5, codeTtlSeconds: 5, refreshTtlSeconds: 30 } },
2071
2071
  consent_denied: { consent: { error: "access_denied" } },
2072
- intermittent_token_failure: { probabilities: { tokenUnavailable: 0.25 } }
2072
+ intermittent_token_failure: { probabilities: { tokenUnavailable: 0.25 } },
2073
+ id_token_clock_ahead: { tokens: { idTokenClockSkewSeconds: 600 } },
2074
+ id_token_stale: { tokens: { idTokenClockSkewSeconds: -7200 } },
2075
+ id_token_unknown_key: { tokens: { idTokenSigningKey: "unpublished" } }
2073
2076
  };
2074
2077
  var object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
2075
2078
  var keys = (value, allowed) => {
@@ -2154,10 +2157,16 @@ function validateBehavior(input) {
2154
2157
  "codeTtlSeconds",
2155
2158
  "refreshTtlSeconds",
2156
2159
  "refreshRotation",
2157
- "refreshError"
2160
+ "refreshError",
2161
+ "idTokenClockSkewSeconds",
2162
+ "idTokenSigningKey"
2158
2163
  ]);
2159
2164
  enumeration(tokens.refreshRotation, ["reuse", "rotate"]);
2160
2165
  enumeration(tokens.refreshError, ["invalid_grant", "invalid_rapt"]);
2166
+ enumeration(tokens.idTokenSigningKey, ["published", "unpublished"]);
2167
+ const skew = tokens.idTokenClockSkewSeconds;
2168
+ if (skew !== void 0 && (typeof skew !== "number" || !Number.isSafeInteger(skew) || Math.abs(skew) > 31536e4))
2169
+ throw new Error("idTokenClockSkewSeconds must be an integer in [-315360000,315360000]");
2161
2170
  for (const key of ["accessTtlSeconds", "codeTtlSeconds", "refreshTtlSeconds"]) {
2162
2171
  const v = tokens[key];
2163
2172
  if (v !== void 0 && (typeof v !== "number" || !Number.isSafeInteger(v) || v < 1 || v > 31536e4))
@@ -2424,12 +2433,44 @@ function createRuntime2(options = {}) {
2424
2433
  return Response.json({ error: String(error) }, { status: 400 });
2425
2434
  }
2426
2435
  },
2427
- "POST /consents/revoke": ({ namespace, body }) => {
2436
+ "POST /consents/revoke": async ({ namespace, body, request }) => {
2428
2437
  const input = body;
2429
2438
  if (!input || typeof input.clientId !== "string" || typeof input.accountId !== "string")
2430
2439
  return Response.json({ error: "clientId and accountId are required" }, { status: 400 });
2431
- runtime.instance(namespace).revokeConsent(input.clientId, input.accountId);
2432
- return Response.json({ revoked: true });
2440
+ const api = runtime.instance(namespace);
2441
+ const notifications = await api.revokeConsent(
2442
+ input.clientId,
2443
+ input.accountId,
2444
+ api.issuer(request)
2445
+ );
2446
+ return Response.json({ revoked: true, notifications });
2447
+ },
2448
+ "POST /accounts/delete": async ({ namespace, body, request }) => {
2449
+ const input = body;
2450
+ if (!input || typeof input.accountId !== "string")
2451
+ return Response.json({ error: "accountId is required" }, { status: 400 });
2452
+ const api = runtime.instance(namespace);
2453
+ if (!api.accounts.has(input.accountId))
2454
+ return Response.json({ error: "Unknown account" }, { status: 404 });
2455
+ const notifications = await api.deleteAccount(input.accountId, api.issuer(request));
2456
+ return Response.json({ deleted: true, notifications });
2457
+ },
2458
+ "POST /relay-forwarding": async ({ namespace, body, request }) => {
2459
+ const input = body;
2460
+ if (!input || typeof input.accountId !== "string" || typeof input.forwarding !== "boolean")
2461
+ return Response.json(
2462
+ { error: "accountId and boolean forwarding are required" },
2463
+ { status: 400 }
2464
+ );
2465
+ const api = runtime.instance(namespace);
2466
+ if (!api.accounts.has(input.accountId))
2467
+ return Response.json({ error: "Unknown account" }, { status: 404 });
2468
+ const notifications = await api.setRelayForwarding(
2469
+ input.accountId,
2470
+ input.forwarding,
2471
+ api.issuer(request)
2472
+ );
2473
+ return Response.json({ forwarding: input.forwarding, notifications });
2433
2474
  },
2434
2475
  "POST /keys/rotate": ({ namespace, body }) => {
2435
2476
  const input = body;
@@ -2641,6 +2682,8 @@ var OAuthAPI = class {
2641
2682
  consents;
2642
2683
  signer = new Signer();
2643
2684
  previousSigners = [];
2685
+ /** Never published: signs ID tokens for `tokens.idTokenSigningKey: "unpublished"`. */
2686
+ unpublishedSigner;
2644
2687
  behavior;
2645
2688
  identities;
2646
2689
  appleDisclosures;
@@ -2684,6 +2727,10 @@ var OAuthAPI = class {
2684
2727
  throw new Error(
2685
2728
  "Apple client requires teamId, keyId and a public P-256 JWK, without a static secret"
2686
2729
  );
2730
+ if (client.apple?.notificationUrl !== void 0 && !(typeof client.apple.notificationUrl === "string" && safeRedirect(client.apple.notificationUrl) && WEB_SCHEMES.has(new URL(client.apple.notificationUrl).protocol)))
2731
+ throw new Error(
2732
+ "apple.notificationUrl must be an HTTP(S) URL without fragment or credentials"
2733
+ );
2687
2734
  this.clients.insert(client.id, structuredClone(client));
2688
2735
  return client;
2689
2736
  }
@@ -2704,7 +2751,95 @@ var OAuthAPI = class {
2704
2751
  this.signer = new Signer();
2705
2752
  return { kid: this.signer.kid };
2706
2753
  }
2707
- revokeConsent(clientId, accountId) {
2754
+ /**
2755
+ * Withdraws one client's consent. When the client registered `apple.notificationUrl` and held
2756
+ * the consent, Apple's `consent-revoked` notification is delivered before this resolves.
2757
+ */
2758
+ async revokeConsent(clientId, accountId, issuer) {
2759
+ const targets = this.notifiable(accountId).filter((t) => t.client.id === clientId);
2760
+ this.forget(clientId, accountId);
2761
+ return this.notify(targets, "consent-revoked", issuer);
2762
+ }
2763
+ /**
2764
+ * Deletes an account with its grants and sessions, after telling every client that held a
2765
+ * consent (`account-delete`).
2766
+ */
2767
+ async deleteAccount(accountId, issuer) {
2768
+ if (!this.accounts.has(accountId)) throw new Error("Unknown account");
2769
+ const targets = this.notifiable(accountId);
2770
+ for (const { value: client } of this.clients.list()) this.forget(client.id, accountId);
2771
+ for (const row of this.codes.list({ where: (c) => c.accountId === accountId }))
2772
+ this.codes.delete(row.id);
2773
+ for (const row of this.tokens.list({ where: (t) => t.accountId === accountId }))
2774
+ this.tokens.delete(row.id);
2775
+ for (const row of this.transactions.list({ where: (t) => t.accountId === accountId }))
2776
+ this.transactions.delete(row.id);
2777
+ for (const row of this.sessions.list({ where: (s) => s.accountId === accountId }))
2778
+ this.sessions.delete(row.id);
2779
+ this.accounts.delete(accountId);
2780
+ return this.notify(targets, "account-delete", issuer);
2781
+ }
2782
+ /**
2783
+ * Tells every client whose identity for the account is a private relay address that the user
2784
+ * turned forwarding off (`email-disabled`) or on (`email-enabled`).
2785
+ */
2786
+ async setRelayForwarding(accountId, forwarding, issuer) {
2787
+ if (!this.accounts.has(accountId)) throw new Error("Unknown account");
2788
+ return this.notify(
2789
+ this.notifiable(accountId).filter((t) => t.relay),
2790
+ forwarding ? "email-enabled" : "email-disabled",
2791
+ issuer,
2792
+ true
2793
+ );
2794
+ }
2795
+ /** Clients with a notification endpoint that hold a consent for the account. */
2796
+ notifiable(accountId) {
2797
+ return this.clients.list({ order: "oldest" }).flatMap(({ value: client }) => {
2798
+ const identity = this.identities.get(this.identityKey(client, accountId));
2799
+ return client.apple?.notificationUrl && identity && this.consents.has(JSON.stringify([client.id, accountId])) ? [{ client, sub: identity.sub, email: identity.email, relay: identity.privateEmail }] : [];
2800
+ });
2801
+ }
2802
+ async notify(targets, type, issuer, withEmail = false) {
2803
+ if (!targets.length) return [];
2804
+ const iss = issuer ?? this.options.issuer;
2805
+ if (!iss) throw new Error("A notification needs the issuer option or an issuer argument");
2806
+ const send = this.options.webhooks?.fetch ?? ((r) => fetch(r));
2807
+ const deliveries = [];
2808
+ for (const { client, sub, email } of targets) {
2809
+ const payload = await this.signer.sign({
2810
+ iss: iss.replace(/\/$/, ""),
2811
+ aud: client.id,
2812
+ iat: Math.floor(this.now() / 1e3),
2813
+ jti: random(),
2814
+ events: JSON.stringify({
2815
+ type,
2816
+ sub,
2817
+ ...withEmail ? { email, is_private_email: "true" } : {},
2818
+ event_time: this.now()
2819
+ })
2820
+ });
2821
+ try {
2822
+ const response = await send(
2823
+ new Request(client.apple?.notificationUrl ?? "", {
2824
+ method: "POST",
2825
+ headers: { "content-type": "application/json" },
2826
+ body: JSON.stringify({ payload })
2827
+ })
2828
+ );
2829
+ deliveries.push({
2830
+ clientId: client.id,
2831
+ type,
2832
+ delivered: response.ok,
2833
+ status: response.status
2834
+ });
2835
+ } catch (error) {
2836
+ deliveries.push({ clientId: client.id, type, delivered: false, error: String(error) });
2837
+ }
2838
+ }
2839
+ return deliveries;
2840
+ }
2841
+ /** Drops one client's grant for an account: consent, identity, first-use disclosure, codes, tokens. */
2842
+ forget(clientId, accountId) {
2708
2843
  const key = JSON.stringify([clientId, accountId]);
2709
2844
  this.consents.delete(key);
2710
2845
  this.refreshIssued.delete(key);
@@ -2847,13 +2982,18 @@ var OAuthAPI = class {
2847
2982
  async idToken(grant, issuer, extra = {}) {
2848
2983
  const account = this.accounts.get(grant.accountId);
2849
2984
  if (!account) throw new Error("Account no longer exists");
2850
- return this.signer.sign({
2985
+ const tokens = this.behavior.config.tokens;
2986
+ const skew = tokens?.idTokenClockSkewSeconds ?? 0;
2987
+ const iat = Math.floor(this.now() / 1e3) + skew;
2988
+ const unpublished = tokens?.idTokenSigningKey === "unpublished";
2989
+ if (unpublished) this.unpublishedSigner ??= new Signer();
2990
+ return (unpublished && this.unpublishedSigner || this.signer).sign({
2851
2991
  ...this.profile(account, grant),
2852
2992
  iss: issuer,
2853
2993
  aud: grant.clientId,
2854
- iat: Math.floor(this.now() / 1e3),
2855
- exp: Math.floor(this.now() / 1e3) + (this.behavior.config.tokens?.accessTtlSeconds ?? 3600),
2856
- auth_time: Math.floor(grant.authTime / 1e3),
2994
+ iat,
2995
+ exp: iat + (tokens?.accessTtlSeconds ?? 3600),
2996
+ auth_time: Math.floor(grant.authTime / 1e3) + skew,
2857
2997
  ...grant.nonce ? { nonce: grant.nonce } : {},
2858
2998
  ...extra
2859
2999
  });
@@ -3595,4 +3735,4 @@ export {
3595
3735
  createMultiRuntime,
3596
3736
  OAuthAPI
3597
3737
  };
3598
- //# sourceMappingURL=chunk-JA62N2QU.js.map
3738
+ //# sourceMappingURL=chunk-XP2AEPEJ.js.map