@aooth/auth 0.1.46 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/atscript-db.cjs +3 -1
  2. package/dist/atscript-db.d.cts +4 -3
  3. package/dist/atscript-db.d.mts +4 -3
  4. package/dist/atscript-db.mjs +3 -1
  5. package/dist/auth-credential-C3qebKuy.d.mts +245 -0
  6. package/dist/auth-credential-CEijxoGU.d.cts +245 -0
  7. package/dist/authz.cjs +124 -35
  8. package/dist/authz.d.cts +78 -24
  9. package/dist/authz.d.mts +78 -24
  10. package/dist/authz.mjs +121 -37
  11. package/dist/{store-BG6m6oSJ.d.mts → clock-DJ_eroHW.d.cts} +50 -1
  12. package/dist/{store-BG6m6oSJ.d.cts → clock-DJ_eroHW.d.mts} +50 -1
  13. package/dist/{dynamic-client-store-DLRfntHr.mjs → dynamic-client-store-B1JwFHdP.mjs} +1 -0
  14. package/dist/{dynamic-client-store-DzqdUwnw.d.cts → dynamic-client-store-DJgtMjPw.d.mts} +58 -9
  15. package/dist/{dynamic-client-store-oHEsQaJg.d.mts → dynamic-client-store-DbiSLOj0.d.cts} +58 -9
  16. package/dist/{dynamic-client-store-DAStgv2j.cjs → dynamic-client-store-DykM9QOT.cjs} +1 -0
  17. package/dist/index.cjs +39 -26
  18. package/dist/index.d.cts +12 -218
  19. package/dist/index.d.mts +12 -218
  20. package/dist/index.mjs +39 -27
  21. package/dist/opaque-token-BaMOrC1A.mjs +13 -0
  22. package/dist/opaque-token-CaZgD2i6.cjs +18 -0
  23. package/dist/redis.d.cts +2 -2
  24. package/dist/redis.d.mts +2 -2
  25. package/dist/{store-N9daSmHS.d.cts → store-BYnpA7VK.d.cts} +1 -1
  26. package/dist/{store-jFgby4bK.d.mts → store-pKh0ZaGG.d.mts} +1 -1
  27. package/package.json +8 -8
  28. package/src/atscript-db/auth-credential.as +16 -0
  29. package/src/atscript-db/auth-credential.as.d.ts +4 -1
  30. package/src/atscript-db/dynamic-client.as +11 -1
  31. package/src/atscript-db/dynamic-client.as.d.ts +3 -0
  32. package/dist/clock-BjXa0LXb.d.cts +0 -14
  33. package/dist/clock-BjXa0LXb.d.mts +0 -14
package/dist/authz.mjs CHANGED
@@ -1,7 +1,37 @@
1
1
  import { t as defaultClock } from "./clock-Bdsep_1j.mjs";
2
- import { c as PendingAuthorizationStoreMemory, i as AuthCodeStoreMemory, n as DynamicClientStoreMemory, o as DEFAULT_PENDING_TTL_MS, r as AuthCodeStore, s as PendingAuthorizationStore, t as DynamicClientStore } from "./dynamic-client-store-DLRfntHr.mjs";
3
- import { timingSafeEqual } from "node:crypto";
2
+ import { t as generateOpaqueToken } from "./opaque-token-BaMOrC1A.mjs";
3
+ import { c as PendingAuthorizationStoreMemory, i as AuthCodeStoreMemory, n as DynamicClientStoreMemory, o as DEFAULT_PENDING_TTL_MS, r as AuthCodeStore, s as PendingAuthorizationStore, t as DynamicClientStore } from "./dynamic-client-store-B1JwFHdP.mjs";
4
+ import { createHash, timingSafeEqual } from "node:crypto";
4
5
  import { SignJWT, exportJWK, importPKCS8, importSPKI } from "jose";
6
+ //#region src/authz/token-policy.ts
7
+ /**
8
+ * Default refresh-token (grant) lifetime for {@link TokenPolicy.refresh} when
9
+ * no `ttl` is given: 30 days. Deliberately independent of the session tier's
10
+ * `AuthCredentialOptions.refresh.ttl` — an authz grant's lifetime is the
11
+ * policy's decision, never inherited from browser-session posture.
12
+ */
13
+ const DEFAULT_AUTHZ_REFRESH_TTL_MS = 720 * 60 * 6e4;
14
+ /**
15
+ * Flatten a {@link TokenPolicy} into the `AuthCredential.issue()` options a
16
+ * token endpoint forwards. The refresh dimension applies only to client-bound
17
+ * grants — the family is stamped with `metadata.authzClientId`, the binding
18
+ * the `refresh_token` grant enforces — so a clientless (loopback) grant
19
+ * ignores `policy.refresh`. `refresh: false` otherwise: an authz mint never
20
+ * rides the instance-level session refresh config (no orphaned refresh row
21
+ * for a policy that didn't opt in). Lives HERE, next to the policy type and
22
+ * its default ttl, so every HTTP adapter applies the same semantics.
23
+ */
24
+ function tokenPolicyToIssueOptions(policy, clientId) {
25
+ const withRefresh = policy.refresh !== void 0 && clientId !== void 0;
26
+ return {
27
+ ...policy.payload,
28
+ ...policy.kind !== void 0 && { kind: policy.kind },
29
+ ...policy.ttl !== void 0 && { ttl: policy.ttl },
30
+ refresh: withRefresh ? { ttl: policy.refresh?.ttl ?? 2592e6 } : false,
31
+ ...withRefresh && { metadata: { authzClientId: clientId } }
32
+ };
33
+ }
34
+ //#endregion
5
35
  //#region src/authz/authz-errors.ts
6
36
  /** A typed authorization-server failure. */
7
37
  var AuthorizeError = class extends Error {
@@ -59,6 +89,15 @@ var LoopbackClientPolicy = class {
59
89
  }
60
90
  };
61
91
  //#endregion
92
+ //#region src/utils/timing-safe.ts
93
+ /** Constant-time string compare that also fails closed on a length mismatch. */
94
+ function timingSafeEqualStr(a, b) {
95
+ const ab = Buffer.from(a, "utf8");
96
+ const bb = Buffer.from(b, "utf8");
97
+ if (ab.length !== bb.length) return false;
98
+ return timingSafeEqual(ab, bb);
99
+ }
100
+ //#endregion
62
101
  //#region src/authz/oidc-claims-resolver.ts
63
102
  /**
64
103
  * Resolves the OIDC profile claims to embed in an `id_token` for a given user +
@@ -156,13 +195,6 @@ var RegisteredClientPolicy = class {
156
195
  return granted.length > 0 ? granted.join(" ") : void 0;
157
196
  }
158
197
  };
159
- /** Constant-time string compare that also fails closed on a length mismatch. */
160
- function timingSafeEqualStr(a, b) {
161
- const ab = Buffer.from(a, "utf8");
162
- const bb = Buffer.from(b, "utf8");
163
- if (ab.length !== bb.length) return false;
164
- return timingSafeEqual(ab, bb);
165
- }
166
198
  //#endregion
167
199
  //#region src/authz/composite-client-policy.ts
168
200
  /**
@@ -212,6 +244,30 @@ var CompositeClientPolicy = class {
212
244
  }
213
245
  };
214
246
  //#endregion
247
+ //#region src/authz/client-secret.ts
248
+ /**
249
+ * Mint a confidential dynamic client's secret (RFC 7591 `client_secret`) — a
250
+ * plain {@link generateOpaqueToken} mint. Returned to the registrant ONCE in
251
+ * the registration response; only its {@link hashClientSecret} digest is
252
+ * stored.
253
+ */
254
+ function mintClientSecret() {
255
+ return generateOpaqueToken();
256
+ }
257
+ /**
258
+ * Storage digest for a client secret — plain SHA-256 (hex). No KDF/salt on
259
+ * purpose: the secret is high-entropy server-minted material (never a human
260
+ * password), so brute-forcing the digest is infeasible and a per-row salt
261
+ * buys nothing.
262
+ */
263
+ function hashClientSecret(secret) {
264
+ return createHash("sha256").update(secret, "utf8").digest("hex");
265
+ }
266
+ /** Constant-time check of a presented secret against a stored digest. */
267
+ function verifyClientSecret(secret, hash) {
268
+ return timingSafeEqualStr(hashClientSecret(secret), hash);
269
+ }
270
+ //#endregion
215
271
  //#region src/authz/client-registration.ts
216
272
  /** A typed RFC 7591 registration failure; `message` becomes `error_description`. */
217
273
  var ClientRegistrationError = class extends Error {
@@ -222,9 +278,11 @@ var ClientRegistrationError = class extends Error {
222
278
  this.code = code;
223
279
  }
224
280
  };
225
- /** Grant/response types the v1 authorization server supports (auth-code + PKCE only). */
226
- const SUPPORTED_GRANT_TYPES = ["authorization_code"];
281
+ /** Grant/response types the authorization server supports (auth-code + PKCE, plus refresh). */
282
+ const SUPPORTED_GRANT_TYPES = ["authorization_code", "refresh_token"];
227
283
  const SUPPORTED_RESPONSE_TYPES = ["code"];
284
+ /** Token-endpoint auth methods DCR accepts (both advertised in the RFC 8414 document). */
285
+ const SUPPORTED_TOKEN_ENDPOINT_AUTH_METHODS = ["none", "client_secret_post"];
228
286
  /** RFC 6749 §3.3 scope-token charset: printable ASCII minus space, `"` and `\`. */
229
287
  const SCOPE_TOKEN_RE = /^[\x21\x23-\x5B\x5D-\x7E]+$/u;
230
288
  const DEFAULT_MAX_REDIRECT_URIS = 5;
@@ -273,13 +331,14 @@ function isAcceptableRedirectUri(uri) {
273
331
  * Normalization is allowed by RFC 7591 §2 (the server MAY replace requested
274
332
  * metadata with its own values) and is load-bearing for real connectors:
275
333
  * `grant_types` / `response_types` are INTERSECTED with what the server
276
- * supports (a connector requesting `["authorization_code", "refresh_token"]`
277
- * registers with `["authorization_code"]` — the 201 echo of the narrowed set
278
- * is the contract) rather than rejected. `token_endpoint_auth_method` defaults
279
- * to `"none"` when absent (v1 is public-clients-only, so the RFC's
280
- * `client_secret_basic` default is unsupported), but an EXPLICIT non-`"none"`
281
- * ask is rejected — never silently downgrade a client that asked for a secret.
282
- * Unknown fields are ignored and never echoed.
334
+ * supports (an unsupported grant is dropped from the echo — the 201 echo of
335
+ * the narrowed set is the contract) rather than rejected, but the result must
336
+ * still include `authorization_code` (it is the only way to establish a grant;
337
+ * `refresh_token` alone mints nothing). `token_endpoint_auth_method` defaults
338
+ * to `"none"` when absent (the RFC's `client_secret_basic` default is
339
+ * unsupported — secrets travel in the POST form body); an EXPLICIT ask outside
340
+ * the allowed set is rejected — never silently downgrade a client that asked
341
+ * for a secret. Unknown fields are ignored and never echoed.
283
342
  */
284
343
  function validateClientRegistration(body, opts) {
285
344
  if (typeof body !== "object" || body === null || Array.isArray(body)) throw new ClientRegistrationError("invalid_client_metadata", "registration request must be a JSON object");
@@ -291,9 +350,11 @@ function validateClientRegistration(body, opts) {
291
350
  if (redirectUris.length === 0) throw new ClientRegistrationError("invalid_redirect_uri", "redirect_uris must be non-empty");
292
351
  if (redirectUris.length > maxUris) throw new ClientRegistrationError("invalid_redirect_uri", `redirect_uris accepts at most ${maxUris} entries`);
293
352
  for (const uri of redirectUris) if (uri.length > maxUriLength || !isAcceptableRedirectUri(uri)) throw new ClientRegistrationError("invalid_redirect_uri", "each redirect_uri must be an https URL with an explicit host or a loopback literal, without a fragment");
294
- if ((req.token_endpoint_auth_method ?? "none") !== "none") throw new ClientRegistrationError("invalid_client_metadata", "only token_endpoint_auth_method \"none\" is supported");
353
+ const allowedAuthMethods = (opts?.allowedTokenEndpointAuthMethods ?? SUPPORTED_TOKEN_ENDPOINT_AUTH_METHODS).filter((m) => SUPPORTED_TOKEN_ENDPOINT_AUTH_METHODS.includes(m));
354
+ const authMethod = req.token_endpoint_auth_method ?? "none";
355
+ if (typeof authMethod !== "string" || !allowedAuthMethods.includes(authMethod)) throw new ClientRegistrationError("invalid_client_metadata", `only token_endpoint_auth_method ${allowedAuthMethods.map((m) => `"${m}"`).join(" / ")} is supported`);
295
356
  const grantTypes = req.grant_types === void 0 ? [...SUPPORTED_GRANT_TYPES] : asStringArray(req.grant_types, "grant_types").filter((g) => SUPPORTED_GRANT_TYPES.includes(g));
296
- if (grantTypes.length === 0) throw new ClientRegistrationError("invalid_client_metadata", "grant_types must include authorization_code");
357
+ if (!grantTypes.includes("authorization_code")) throw new ClientRegistrationError("invalid_client_metadata", "grant_types must include authorization_code");
297
358
  const responseTypes = req.response_types === void 0 ? [...SUPPORTED_RESPONSE_TYPES] : asStringArray(req.response_types, "response_types").filter((r) => SUPPORTED_RESPONSE_TYPES.includes(r));
298
359
  if (responseTypes.length === 0) throw new ClientRegistrationError("invalid_client_metadata", "response_types must include code");
299
360
  let clientName;
@@ -312,7 +373,7 @@ function validateClientRegistration(body, opts) {
312
373
  }
313
374
  return {
314
375
  redirectUris,
315
- tokenEndpointAuthMethod: "none",
376
+ tokenEndpointAuthMethod: authMethod,
316
377
  grantTypes,
317
378
  responseTypes,
318
379
  ...clientName !== void 0 && { clientName },
@@ -323,9 +384,9 @@ const DEFAULT_MAX_CLIENTS = 1e3;
323
384
  /**
324
385
  * The RFC 7591 registration operation behind `POST {issuer}/register`
325
386
  * (OAUTH.md R2): validate → guard → lazy GC of never-used rows → hard cap →
326
- * persist. Framework-free — `@aooth/auth-moost`'s controller endpoint is a
327
- * thin HTTP adapter over `register()`, and non-moost servers can call it
328
- * directly.
387
+ * mint secret (confidential) → persist. Framework-free — `@aooth/auth-moost`'s
388
+ * controller endpoint is a thin HTTP adapter over `register()`, and non-moost
389
+ * servers can call it directly.
329
390
  */
330
391
  var DynamicClientRegistration = class {
331
392
  store;
@@ -342,12 +403,28 @@ var DynamicClientRegistration = class {
342
403
  this.validation = opts.validation;
343
404
  this.clock = opts.clock ?? defaultClock;
344
405
  }
345
- /** Validate and persist a registration request body; returns the minted client. */
406
+ /**
407
+ * Validate and persist a registration request body; returns the minted
408
+ * client. For a `client_secret_post` registration the returned record
409
+ * additionally carries the plaintext `clientSecret` (the ONE disclosure —
410
+ * only its SHA-256 digest is stored).
411
+ */
346
412
  async register(body) {
347
413
  const metadata = validateClientRegistration(body, this.validation);
348
414
  await this.guard?.({ metadata });
349
415
  if (this.unusedClientTtlMs !== void 0) await this.store.deleteUnusedBefore(this.clock.now() - this.unusedClientTtlMs);
350
416
  if (await this.store.count() >= this.maxClients) throw new ClientRegistrationError("invalid_client_metadata", "registration limit reached");
417
+ if (metadata.tokenEndpointAuthMethod === "client_secret_post") {
418
+ const clientSecret = mintClientSecret();
419
+ return {
420
+ ...await this.store.create({
421
+ ...metadata,
422
+ clientSecretHash: hashClientSecret(clientSecret)
423
+ }),
424
+ clientSecret,
425
+ clientSecretExpiresAt: 0
426
+ };
427
+ }
351
428
  return this.store.create(metadata);
352
429
  }
353
430
  };
@@ -358,13 +435,14 @@ const DEFAULT_DYNAMIC_TOKEN_POLICY = {
358
435
  ttl: 720 * 60 * 6e4
359
436
  };
360
437
  /**
361
- * Policy for RFC 7591 dynamically-registered public clients (OAUTH.md R2) on
362
- * the `ClientRedirectPolicy` seam. `resolveClient` authorizes the client +
438
+ * Policy for RFC 7591 dynamically-registered clients (OAUTH.md R2) on the
439
+ * `ClientRedirectPolicy` seam. `resolveClient` authorizes the client +
363
440
  * `redirect_uri` against ITS registered allowlist and resolves the granted
364
441
  * scope + token policy; `authenticateClient` re-checks existence at `/token`
365
- * (`token_endpoint_auth_method: "none"` ⇒ no secret — PKCE is the binding, and
366
- * a registration garbage-collected mid-flight fails closed). Dynamic clients
367
- * receive an access token only — NO `id_token` in v1 (OAUTH.md R6).
442
+ * (a registration garbage-collected mid-flight fails closed) and, for a
443
+ * `client_secret_post` registration, validates the presented secret against
444
+ * the stored digest (public `"none"` clients: PKCE is the binding). Dynamic
445
+ * clients receive an access token only — NO `id_token` in v1 (OAUTH.md R6).
368
446
  *
369
447
  * INVARIANT: the returned {@link ResolvedClient} always carries `clientId`, so
370
448
  * the minted code records it and the token endpoint's symmetric client binding
@@ -400,12 +478,16 @@ var DynamicClientPolicy = class {
400
478
  }
401
479
  /**
402
480
  * `/token`-side check: the client must still exist (fail closed when the
403
- * registration was deleted/GC'd between authorize and redemption). No secret
404
- * is checked — public client, PKCE is the binding; a spurious
405
- * `client_secret` is ignored.
481
+ * registration was deleted/GC'd between authorize and redemption). A client
482
+ * registered with `token_endpoint_auth_method: "client_secret_post"` must
483
+ * additionally present its minted `client_secret` (constant-time check
484
+ * against the stored digest). For a public (`"none"`) client no secret is
485
+ * checked — PKCE is the binding; a spurious `client_secret` is ignored.
406
486
  */
407
487
  async authenticateClient(args) {
408
- await this.requireClient(args.clientId);
488
+ const client = await this.requireClient(args.clientId);
489
+ if (client.tokenEndpointAuthMethod !== "client_secret_post") return;
490
+ if (client.clientSecretHash === void 0 || args.clientSecret === void 0 || !verifyClientSecret(args.clientSecret, client.clientSecretHash)) throw new AuthorizeError("invalid_client", "client authentication failed");
409
491
  }
410
492
  /** Known-ness probe for `CompositeClientPolicy` dispatch. */
411
493
  async hasClient(clientId) {
@@ -536,7 +618,9 @@ function canonicalizeIssuer(issuer) {
536
618
  *
537
619
  * Capability fields are fixed to what the server actually implements:
538
620
  * `response_types_supported` `["code"]`, `grant_types_supported`
539
- * `["authorization_code"]`, `code_challenge_methods_supported` `["S256"]`
621
+ * `["authorization_code", "refresh_token"]` (the grant is implemented server-
622
+ * wide; whether a given client's grant mints a refresh token is per-policy —
623
+ * `TokenPolicy.refresh`), `code_challenge_methods_supported` `["S256"]`
540
624
  * (PKCE is mandatory — it is the binding for public clients). Optional fields
541
625
  * are omitted entirely (no `undefined` keys) so the serialized JSON carries
542
626
  * only what was configured.
@@ -551,7 +635,7 @@ function buildAuthorizationServerMetadata(opts) {
551
635
  ...opts.registrationEndpoint !== void 0 && { registration_endpoint: opts.registrationEndpoint },
552
636
  ...opts.jwksUri !== void 0 && { jwks_uri: opts.jwksUri },
553
637
  response_types_supported: ["code"],
554
- grant_types_supported: ["authorization_code"],
638
+ grant_types_supported: ["authorization_code", "refresh_token"],
555
639
  code_challenge_methods_supported: ["S256"],
556
640
  token_endpoint_auth_methods_supported: authMethods.includes("none") ? authMethods : ["none", ...authMethods],
557
641
  ...opts.scopesSupported !== void 0 && { scopes_supported: opts.scopesSupported }
@@ -606,4 +690,4 @@ function buildWwwAuthenticateBearerChallenge(opts) {
606
690
  return params.length === 0 ? "Bearer" : `Bearer ${params.join(", ")}`;
607
691
  }
608
692
  //#endregion
609
- export { AuthCodeStore, AuthCodeStoreMemory, AuthorizeError, ClientRegistrationError, CompositeClientPolicy, DEFAULT_PENDING_TTL_MS, DynamicClientPolicy, DynamicClientRegistration, DynamicClientStore, DynamicClientStoreMemory, IdTokenSigner, LoopbackClientPolicy, NoopOidcClaimsResolver, OidcClaimsResolver, PendingAuthorizationStore, PendingAuthorizationStoreMemory, RegisteredClientPolicy, buildAuthorizationServerMetadata, buildProtectedResourceMetadata, buildWwwAuthenticateBearerChallenge, canonicalizeIssuer, isLoopbackRedirectUri, scopeGrants, validateClientRegistration };
693
+ export { AuthCodeStore, AuthCodeStoreMemory, AuthorizeError, ClientRegistrationError, CompositeClientPolicy, DEFAULT_AUTHZ_REFRESH_TTL_MS, DEFAULT_PENDING_TTL_MS, DynamicClientPolicy, DynamicClientRegistration, DynamicClientStore, DynamicClientStoreMemory, IdTokenSigner, LoopbackClientPolicy, NoopOidcClaimsResolver, OidcClaimsResolver, PendingAuthorizationStore, PendingAuthorizationStoreMemory, RegisteredClientPolicy, buildAuthorizationServerMetadata, buildProtectedResourceMetadata, buildWwwAuthenticateBearerChallenge, canonicalizeIssuer, hashClientSecret, isLoopbackRedirectUri, mintClientSecret, scopeGrants, tokenPolicyToIssueOptions, validateClientRegistration, verifyClientSecret };
@@ -50,6 +50,34 @@ interface CredentialMetadata {
50
50
  * the `listSessions({ kind })` filter. Absent ⇒ an ordinary interactive session.
51
51
  */
52
52
  credentialKind?: string;
53
+ /**
54
+ * OAuth `client_id` this token family was minted for by the authorization
55
+ * server's token endpoint (`@aooth/auth-moost`'s `AuthorizeController`) —
56
+ * the binding the `refresh_token` grant enforces: a refresh token redeems
57
+ * only for the client it was issued to, and a family WITHOUT this stamp
58
+ * (e.g. a browser session) is never redeemable at the OAuth token endpoint.
59
+ * Written by the authz tier, carried across rotation like all metadata.
60
+ */
61
+ authzClientId?: string;
62
+ /**
63
+ * Per-family access-token lifetime in ms — stamped by `issue()` when a
64
+ * refresh token is minted with a per-mint `ttl` override, and consulted on
65
+ * every refresh so rotated access tokens keep the authority fixed at mint
66
+ * time (a 15-min-policy token must not come back as the instance default
67
+ * after a refresh). Absent ⇒ refresh mints with the instance `accessTtl`.
68
+ */
69
+ accessTtl?: number;
70
+ /**
71
+ * Per-family rotation semantics — stamped `"always"` by `issue()` whenever a
72
+ * refresh token is minted via the per-mint `IssueOptions.refresh` object, and
73
+ * honored by `refresh()` OVER the instance config. Same mint-time-authority
74
+ * mechanism as {@link accessTtl}: a per-mint family (e.g. an OAuth grant with
75
+ * a 60-day ceiling) keeps its fixed ceiling even on an instance whose
76
+ * browser sessions rotate `'sliding'` — rotation must never extend the
77
+ * grant's lifetime. Absent ⇒ the instance rotation applies (ordinary
78
+ * sessions are unaffected).
79
+ */
80
+ refreshRotation?: "none" | "always" | "sliding";
53
81
  }
54
82
  /**
55
83
  * Persisted state of a credential — the fixed **envelope**. A consumer's
@@ -145,6 +173,14 @@ interface IssueResult {
145
173
  accessExpiresAt: number;
146
174
  refreshExpiresAt?: number;
147
175
  }
176
+ /**
177
+ * Result of redeeming a refresh token: the fresh pair plus the id of the user
178
+ * whose family was rotated — the caller (e.g. the OAuth `refresh_token` grant)
179
+ * presented only an opaque token, so this is where it learns the subject.
180
+ */
181
+ interface RefreshResult extends IssueResult {
182
+ userId: string;
183
+ }
148
184
  /**
149
185
  * Refresh token configuration.
150
186
  */
@@ -260,4 +296,17 @@ interface DenylistStore {
260
296
  cleanup(): Promise<number>;
261
297
  }
262
298
  //#endregion
263
- export { CredentialState as a, RefreshConfig as c, CredentialMetadata as i, SessionEnricher as l, DenylistStore as n, EnrichedSession as o, AuthContext as r, IssueResult as s, CredentialStore as t, SessionInfo as u };
299
+ //#region src/utils/clock.d.ts
300
+ /**
301
+ * Minimal time abstraction shared across stores and orchestrators.
302
+ *
303
+ * Defaults to wall-clock; tests inject a fake clock to deterministically
304
+ * advance time. Kept tiny (single `now()` method) so any timing primitive
305
+ * — Date, performance.now-ish, monotonic, etc. — can be plugged in.
306
+ */
307
+ interface Clock {
308
+ now(): number;
309
+ }
310
+ declare const defaultClock: Clock;
311
+ //#endregion
312
+ export { AuthContext as a, EnrichedSession as c, RefreshResult as d, SessionEnricher as f, DenylistStore as i, IssueResult as l, defaultClock as n, CredentialMetadata as o, SessionInfo as p, CredentialStore as r, CredentialState as s, Clock as t, RefreshConfig as u };
@@ -50,6 +50,34 @@ interface CredentialMetadata {
50
50
  * the `listSessions({ kind })` filter. Absent ⇒ an ordinary interactive session.
51
51
  */
52
52
  credentialKind?: string;
53
+ /**
54
+ * OAuth `client_id` this token family was minted for by the authorization
55
+ * server's token endpoint (`@aooth/auth-moost`'s `AuthorizeController`) —
56
+ * the binding the `refresh_token` grant enforces: a refresh token redeems
57
+ * only for the client it was issued to, and a family WITHOUT this stamp
58
+ * (e.g. a browser session) is never redeemable at the OAuth token endpoint.
59
+ * Written by the authz tier, carried across rotation like all metadata.
60
+ */
61
+ authzClientId?: string;
62
+ /**
63
+ * Per-family access-token lifetime in ms — stamped by `issue()` when a
64
+ * refresh token is minted with a per-mint `ttl` override, and consulted on
65
+ * every refresh so rotated access tokens keep the authority fixed at mint
66
+ * time (a 15-min-policy token must not come back as the instance default
67
+ * after a refresh). Absent ⇒ refresh mints with the instance `accessTtl`.
68
+ */
69
+ accessTtl?: number;
70
+ /**
71
+ * Per-family rotation semantics — stamped `"always"` by `issue()` whenever a
72
+ * refresh token is minted via the per-mint `IssueOptions.refresh` object, and
73
+ * honored by `refresh()` OVER the instance config. Same mint-time-authority
74
+ * mechanism as {@link accessTtl}: a per-mint family (e.g. an OAuth grant with
75
+ * a 60-day ceiling) keeps its fixed ceiling even on an instance whose
76
+ * browser sessions rotate `'sliding'` — rotation must never extend the
77
+ * grant's lifetime. Absent ⇒ the instance rotation applies (ordinary
78
+ * sessions are unaffected).
79
+ */
80
+ refreshRotation?: "none" | "always" | "sliding";
53
81
  }
54
82
  /**
55
83
  * Persisted state of a credential — the fixed **envelope**. A consumer's
@@ -145,6 +173,14 @@ interface IssueResult {
145
173
  accessExpiresAt: number;
146
174
  refreshExpiresAt?: number;
147
175
  }
176
+ /**
177
+ * Result of redeeming a refresh token: the fresh pair plus the id of the user
178
+ * whose family was rotated — the caller (e.g. the OAuth `refresh_token` grant)
179
+ * presented only an opaque token, so this is where it learns the subject.
180
+ */
181
+ interface RefreshResult extends IssueResult {
182
+ userId: string;
183
+ }
148
184
  /**
149
185
  * Refresh token configuration.
150
186
  */
@@ -260,4 +296,17 @@ interface DenylistStore {
260
296
  cleanup(): Promise<number>;
261
297
  }
262
298
  //#endregion
263
- export { CredentialState as a, RefreshConfig as c, CredentialMetadata as i, SessionEnricher as l, DenylistStore as n, EnrichedSession as o, AuthContext as r, IssueResult as s, CredentialStore as t, SessionInfo as u };
299
+ //#region src/utils/clock.d.ts
300
+ /**
301
+ * Minimal time abstraction shared across stores and orchestrators.
302
+ *
303
+ * Defaults to wall-clock; tests inject a fake clock to deterministically
304
+ * advance time. Kept tiny (single `now()` method) so any timing primitive
305
+ * — Date, performance.now-ish, monotonic, etc. — can be plugged in.
306
+ */
307
+ interface Clock {
308
+ now(): number;
309
+ }
310
+ declare const defaultClock: Clock;
311
+ //#endregion
312
+ export { AuthContext as a, EnrichedSession as c, RefreshResult as d, SessionEnricher as f, DenylistStore as i, IssueResult as l, defaultClock as n, CredentialMetadata as o, SessionInfo as p, CredentialStore as r, CredentialState as s, Clock as t, RefreshConfig as u };
@@ -150,6 +150,7 @@ var DynamicClientStoreMemory = class extends DynamicClientStore {
150
150
  responseTypes: [...rec.responseTypes],
151
151
  createdAt: this.clock.now(),
152
152
  ...rec.clientName !== void 0 && { clientName: rec.clientName },
153
+ ...rec.clientSecretHash !== void 0 && { clientSecretHash: rec.clientSecretHash },
153
154
  ...rec.scope !== void 0 && { scope: rec.scope }
154
155
  };
155
156
  this.store.set(row.clientId, structuredClone(row));
@@ -1,4 +1,5 @@
1
- import { t as Clock } from "./clock-BjXa0LXb.cjs";
1
+ import { t as Clock } from "./clock-DJ_eroHW.mjs";
2
+ import { r as IssueOptions } from "./auth-credential-C3qebKuy.mjs";
2
3
 
3
4
  //#region src/authz/token-policy.d.ts
4
5
  /**
@@ -24,7 +25,41 @@ interface TokenPolicy {
24
25
  * is persisted on the pending-authorization + auth-code records.
25
26
  */
26
27
  payload?: Record<string, unknown>;
28
+ /**
29
+ * Opt-in refresh dimension (OAuth 2.1 `refresh_token` grant): when set, the
30
+ * token endpoint mints a rotating refresh token alongside the access token
31
+ * and redeems `grant_type=refresh_token` for the family. `ttl` is the
32
+ * refresh-token (i.e. grant) lifetime in ms — defaults to
33
+ * {@link DEFAULT_AUTHZ_REFRESH_TTL_MS} (30 days). Absent ⇒ today's behavior:
34
+ * access token only, re-consent at expiry.
35
+ *
36
+ * Honored only for grants bound to a registered `client_id` (Tier-2 /
37
+ * dynamic clients) — the family is stamped with `metadata.authzClientId` and
38
+ * a refresh token redeems only for that client. A clientless (Tier-1
39
+ * loopback) grant has nothing to bind to, so the flag is IGNORED there.
40
+ */
41
+ refresh?: {
42
+ ttl?: number;
43
+ };
27
44
  }
45
+ /**
46
+ * Default refresh-token (grant) lifetime for {@link TokenPolicy.refresh} when
47
+ * no `ttl` is given: 30 days. Deliberately independent of the session tier's
48
+ * `AuthCredentialOptions.refresh.ttl` — an authz grant's lifetime is the
49
+ * policy's decision, never inherited from browser-session posture.
50
+ */
51
+ declare const DEFAULT_AUTHZ_REFRESH_TTL_MS: number;
52
+ /**
53
+ * Flatten a {@link TokenPolicy} into the `AuthCredential.issue()` options a
54
+ * token endpoint forwards. The refresh dimension applies only to client-bound
55
+ * grants — the family is stamped with `metadata.authzClientId`, the binding
56
+ * the `refresh_token` grant enforces — so a clientless (loopback) grant
57
+ * ignores `policy.refresh`. `refresh: false` otherwise: an authz mint never
58
+ * rides the instance-level session refresh config (no orphaned refresh row
59
+ * for a policy that didn't opt in). Lives HERE, next to the policy type and
60
+ * its default ttl, so every HTTP adapter applies the same semantics.
61
+ */
62
+ declare function tokenPolicyToIssueOptions(policy: TokenPolicy, clientId: string | undefined): IssueOptions;
28
63
  //#endregion
29
64
  //#region src/authz/pending-authorization-store.d.ts
30
65
  /**
@@ -239,13 +274,15 @@ declare class AuthCodeStoreMemory extends AuthCodeStore {
239
274
  }
240
275
  //#endregion
241
276
  //#region src/authz/dynamic-client-store.d.ts
277
+ /** Token-endpoint auth methods a dynamic client may register with. */
278
+ type DynamicClientAuthMethod = "none" | "client_secret_post";
242
279
  /**
243
280
  * One dynamically-registered OAuth client (RFC 7591) — the record behind a
244
- * connector-style public client that self-registered at `POST /register`.
281
+ * connector-style client that self-registered at `POST /register`.
245
282
  * Everything here is **registrant-supplied** (post-validation) except
246
- * `clientId` and the timestamps: treat `clientName` as untrusted display text
247
- * and `redirectUris` as the exact-match delivery allowlist that
248
- * `DynamicClientPolicy` enforces at `/authorize`.
283
+ * `clientId`, `clientSecretHash` and the timestamps: treat `clientName` as
284
+ * untrusted display text and `redirectUris` as the exact-match delivery
285
+ * allowlist that `DynamicClientPolicy` enforces at `/authorize`.
249
286
  */
250
287
  interface DynamicClient {
251
288
  /** Store-minted opaque client identifier (the DCR response `client_id`). */
@@ -254,8 +291,18 @@ interface DynamicClient {
254
291
  clientName?: string;
255
292
  /** Validated redirect allowlist — https exact-match entries and/or loopback literals. */
256
293
  redirectUris: string[];
257
- /** v1 supports public clients only — PKCE is the binding, no secret exists. */
258
- tokenEndpointAuthMethod: "none";
294
+ /**
295
+ * `"none"` (public — PKCE is the binding) or `"client_secret_post"`
296
+ * (confidential — a server-minted secret is additionally checked at `/token`).
297
+ */
298
+ tokenEndpointAuthMethod: DynamicClientAuthMethod;
299
+ /**
300
+ * SHA-256 hex digest of the minted `client_secret` — present iff
301
+ * `tokenEndpointAuthMethod` is `"client_secret_post"`. The plaintext secret
302
+ * is returned ONCE in the registration response and never stored; see
303
+ * `hashClientSecret` / `verifyClientSecret`.
304
+ */
305
+ clientSecretHash?: string;
259
306
  /** Registered grant types (narrowed to what the server supports). */
260
307
  grantTypes: string[];
261
308
  /** Registered response types (narrowed to what the server supports). */
@@ -275,7 +322,9 @@ interface DynamicClient {
275
322
  interface NewDynamicClient {
276
323
  clientName?: string;
277
324
  redirectUris: string[];
278
- tokenEndpointAuthMethod: "none";
325
+ tokenEndpointAuthMethod: DynamicClientAuthMethod;
326
+ /** Digest of the minted secret (confidential clients) — see {@link DynamicClient.clientSecretHash}. */
327
+ clientSecretHash?: string;
279
328
  grantTypes: string[];
280
329
  responseTypes: string[];
281
330
  scope?: string;
@@ -327,4 +376,4 @@ declare class DynamicClientStoreMemory extends DynamicClientStore {
327
376
  deleteUnusedBefore(cutoff: number): Promise<number>;
328
377
  }
329
378
  //#endregion
330
- export { TokenPolicy as _, NewDynamicClient as a, AuthCodeStoreMemory as c, DEFAULT_PENDING_TTL_MS as d, NewPendingAuthorization as f, PendingAuthorizationStoreMemoryOptions as g, PendingAuthorizationStoreMemory as h, DynamicClientStoreMemoryOptions as i, AuthCodeStoreMemoryOptions as l, PendingAuthorizationStore as m, DynamicClientStore as n, AuthCode as o, PendingAuthorization as p, DynamicClientStoreMemory as r, AuthCodeStore as s, DynamicClient as t, NewAuthCode as u };
379
+ export { PendingAuthorizationStoreMemoryOptions as _, DynamicClientStoreMemoryOptions as a, tokenPolicyToIssueOptions as b, AuthCodeStore as c, NewAuthCode as d, DEFAULT_PENDING_TTL_MS as f, PendingAuthorizationStoreMemory as g, PendingAuthorizationStore as h, DynamicClientStoreMemory as i, AuthCodeStoreMemory as l, PendingAuthorization as m, DynamicClientAuthMethod as n, NewDynamicClient as o, NewPendingAuthorization as p, DynamicClientStore as r, AuthCode as s, DynamicClient as t, AuthCodeStoreMemoryOptions as u, DEFAULT_AUTHZ_REFRESH_TTL_MS as v, TokenPolicy as y };
@@ -1,4 +1,5 @@
1
- import { t as Clock } from "./clock-BjXa0LXb.mjs";
1
+ import { t as Clock } from "./clock-DJ_eroHW.cjs";
2
+ import { r as IssueOptions } from "./auth-credential-CEijxoGU.cjs";
2
3
 
3
4
  //#region src/authz/token-policy.d.ts
4
5
  /**
@@ -24,7 +25,41 @@ interface TokenPolicy {
24
25
  * is persisted on the pending-authorization + auth-code records.
25
26
  */
26
27
  payload?: Record<string, unknown>;
28
+ /**
29
+ * Opt-in refresh dimension (OAuth 2.1 `refresh_token` grant): when set, the
30
+ * token endpoint mints a rotating refresh token alongside the access token
31
+ * and redeems `grant_type=refresh_token` for the family. `ttl` is the
32
+ * refresh-token (i.e. grant) lifetime in ms — defaults to
33
+ * {@link DEFAULT_AUTHZ_REFRESH_TTL_MS} (30 days). Absent ⇒ today's behavior:
34
+ * access token only, re-consent at expiry.
35
+ *
36
+ * Honored only for grants bound to a registered `client_id` (Tier-2 /
37
+ * dynamic clients) — the family is stamped with `metadata.authzClientId` and
38
+ * a refresh token redeems only for that client. A clientless (Tier-1
39
+ * loopback) grant has nothing to bind to, so the flag is IGNORED there.
40
+ */
41
+ refresh?: {
42
+ ttl?: number;
43
+ };
27
44
  }
45
+ /**
46
+ * Default refresh-token (grant) lifetime for {@link TokenPolicy.refresh} when
47
+ * no `ttl` is given: 30 days. Deliberately independent of the session tier's
48
+ * `AuthCredentialOptions.refresh.ttl` — an authz grant's lifetime is the
49
+ * policy's decision, never inherited from browser-session posture.
50
+ */
51
+ declare const DEFAULT_AUTHZ_REFRESH_TTL_MS: number;
52
+ /**
53
+ * Flatten a {@link TokenPolicy} into the `AuthCredential.issue()` options a
54
+ * token endpoint forwards. The refresh dimension applies only to client-bound
55
+ * grants — the family is stamped with `metadata.authzClientId`, the binding
56
+ * the `refresh_token` grant enforces — so a clientless (loopback) grant
57
+ * ignores `policy.refresh`. `refresh: false` otherwise: an authz mint never
58
+ * rides the instance-level session refresh config (no orphaned refresh row
59
+ * for a policy that didn't opt in). Lives HERE, next to the policy type and
60
+ * its default ttl, so every HTTP adapter applies the same semantics.
61
+ */
62
+ declare function tokenPolicyToIssueOptions(policy: TokenPolicy, clientId: string | undefined): IssueOptions;
28
63
  //#endregion
29
64
  //#region src/authz/pending-authorization-store.d.ts
30
65
  /**
@@ -239,13 +274,15 @@ declare class AuthCodeStoreMemory extends AuthCodeStore {
239
274
  }
240
275
  //#endregion
241
276
  //#region src/authz/dynamic-client-store.d.ts
277
+ /** Token-endpoint auth methods a dynamic client may register with. */
278
+ type DynamicClientAuthMethod = "none" | "client_secret_post";
242
279
  /**
243
280
  * One dynamically-registered OAuth client (RFC 7591) — the record behind a
244
- * connector-style public client that self-registered at `POST /register`.
281
+ * connector-style client that self-registered at `POST /register`.
245
282
  * Everything here is **registrant-supplied** (post-validation) except
246
- * `clientId` and the timestamps: treat `clientName` as untrusted display text
247
- * and `redirectUris` as the exact-match delivery allowlist that
248
- * `DynamicClientPolicy` enforces at `/authorize`.
283
+ * `clientId`, `clientSecretHash` and the timestamps: treat `clientName` as
284
+ * untrusted display text and `redirectUris` as the exact-match delivery
285
+ * allowlist that `DynamicClientPolicy` enforces at `/authorize`.
249
286
  */
250
287
  interface DynamicClient {
251
288
  /** Store-minted opaque client identifier (the DCR response `client_id`). */
@@ -254,8 +291,18 @@ interface DynamicClient {
254
291
  clientName?: string;
255
292
  /** Validated redirect allowlist — https exact-match entries and/or loopback literals. */
256
293
  redirectUris: string[];
257
- /** v1 supports public clients only — PKCE is the binding, no secret exists. */
258
- tokenEndpointAuthMethod: "none";
294
+ /**
295
+ * `"none"` (public — PKCE is the binding) or `"client_secret_post"`
296
+ * (confidential — a server-minted secret is additionally checked at `/token`).
297
+ */
298
+ tokenEndpointAuthMethod: DynamicClientAuthMethod;
299
+ /**
300
+ * SHA-256 hex digest of the minted `client_secret` — present iff
301
+ * `tokenEndpointAuthMethod` is `"client_secret_post"`. The plaintext secret
302
+ * is returned ONCE in the registration response and never stored; see
303
+ * `hashClientSecret` / `verifyClientSecret`.
304
+ */
305
+ clientSecretHash?: string;
259
306
  /** Registered grant types (narrowed to what the server supports). */
260
307
  grantTypes: string[];
261
308
  /** Registered response types (narrowed to what the server supports). */
@@ -275,7 +322,9 @@ interface DynamicClient {
275
322
  interface NewDynamicClient {
276
323
  clientName?: string;
277
324
  redirectUris: string[];
278
- tokenEndpointAuthMethod: "none";
325
+ tokenEndpointAuthMethod: DynamicClientAuthMethod;
326
+ /** Digest of the minted secret (confidential clients) — see {@link DynamicClient.clientSecretHash}. */
327
+ clientSecretHash?: string;
279
328
  grantTypes: string[];
280
329
  responseTypes: string[];
281
330
  scope?: string;
@@ -327,4 +376,4 @@ declare class DynamicClientStoreMemory extends DynamicClientStore {
327
376
  deleteUnusedBefore(cutoff: number): Promise<number>;
328
377
  }
329
378
  //#endregion
330
- export { TokenPolicy as _, NewDynamicClient as a, AuthCodeStoreMemory as c, DEFAULT_PENDING_TTL_MS as d, NewPendingAuthorization as f, PendingAuthorizationStoreMemoryOptions as g, PendingAuthorizationStoreMemory as h, DynamicClientStoreMemoryOptions as i, AuthCodeStoreMemoryOptions as l, PendingAuthorizationStore as m, DynamicClientStore as n, AuthCode as o, PendingAuthorization as p, DynamicClientStoreMemory as r, AuthCodeStore as s, DynamicClient as t, NewAuthCode as u };
379
+ export { PendingAuthorizationStoreMemoryOptions as _, DynamicClientStoreMemoryOptions as a, tokenPolicyToIssueOptions as b, AuthCodeStore as c, NewAuthCode as d, DEFAULT_PENDING_TTL_MS as f, PendingAuthorizationStoreMemory as g, PendingAuthorizationStore as h, DynamicClientStoreMemory as i, AuthCodeStoreMemory as l, PendingAuthorization as m, DynamicClientAuthMethod as n, NewDynamicClient as o, NewPendingAuthorization as p, DynamicClientStore as r, AuthCode as s, DynamicClient as t, AuthCodeStoreMemoryOptions as u, DEFAULT_AUTHZ_REFRESH_TTL_MS as v, TokenPolicy as y };
@@ -150,6 +150,7 @@ var DynamicClientStoreMemory = class extends DynamicClientStore {
150
150
  responseTypes: [...rec.responseTypes],
151
151
  createdAt: this.clock.now(),
152
152
  ...rec.clientName !== void 0 && { clientName: rec.clientName },
153
+ ...rec.clientSecretHash !== void 0 && { clientSecretHash: rec.clientSecretHash },
153
154
  ...rec.scope !== void 0 && { scope: rec.scope }
154
155
  };
155
156
  this.store.set(row.clientId, structuredClone(row));