@_mustachio/openauth 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/esm/adapters/memory/session-store.js +22 -0
  2. package/dist/esm/adapters/memory/token-store.js +13 -0
  3. package/dist/esm/client.js +9 -2
  4. package/dist/esm/domain/authorize.js +5 -0
  5. package/dist/esm/domain/callback.js +3 -0
  6. package/dist/esm/domain/discovery.js +44 -2
  7. package/dist/esm/domain/dpop.js +122 -0
  8. package/dist/esm/domain/id-token.js +94 -0
  9. package/dist/esm/domain/introspect.js +5 -1
  10. package/dist/esm/domain/jwt.js +28 -0
  11. package/dist/esm/domain/logout.js +75 -0
  12. package/dist/esm/domain/method-route.js +3 -0
  13. package/dist/esm/domain/par.js +51 -0
  14. package/dist/esm/domain/refresh.js +12 -1
  15. package/dist/esm/domain/register.js +74 -0
  16. package/dist/esm/domain/token-exchange.js +1 -0
  17. package/dist/esm/domain/token.js +48 -7
  18. package/dist/esm/domain/userinfo.js +38 -3
  19. package/dist/esm/http/handlers/authorize.js +90 -3
  20. package/dist/esm/http/handlers/end-session.js +64 -0
  21. package/dist/esm/http/handlers/metadata.js +5 -2
  22. package/dist/esm/http/handlers/par.js +53 -0
  23. package/dist/esm/http/handlers/register.js +46 -0
  24. package/dist/esm/http/handlers/token.js +36 -6
  25. package/dist/esm/http/handlers/userinfo.js +35 -29
  26. package/dist/esm/http/router.js +10 -0
  27. package/dist/esm/http/schemas/authorize.js +7 -1
  28. package/dist/esm/http/schemas/end-session.js +13 -0
  29. package/dist/esm/http/schemas/par.js +9 -0
  30. package/dist/esm/http/schemas/register.js +16 -0
  31. package/dist/esm/index.js +7 -2
  32. package/dist/esm/types/error.js +5 -0
  33. package/dist/types/adapters/memory/session-store.d.ts +3 -1
  34. package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
  35. package/dist/types/adapters/memory/token-store.d.ts +1 -0
  36. package/dist/types/adapters/memory/token-store.d.ts.map +1 -1
  37. package/dist/types/client.d.ts +19 -0
  38. package/dist/types/client.d.ts.map +1 -1
  39. package/dist/types/domain/authorize.d.ts.map +1 -1
  40. package/dist/types/domain/client-credentials.d.ts +2 -0
  41. package/dist/types/domain/client-credentials.d.ts.map +1 -1
  42. package/dist/types/domain/discovery.d.ts +37 -0
  43. package/dist/types/domain/discovery.d.ts.map +1 -1
  44. package/dist/types/domain/dpop.d.ts +61 -0
  45. package/dist/types/domain/dpop.d.ts.map +1 -0
  46. package/dist/types/domain/id-token.d.ts +83 -0
  47. package/dist/types/domain/id-token.d.ts.map +1 -0
  48. package/dist/types/domain/introspect.d.ts +8 -0
  49. package/dist/types/domain/introspect.d.ts.map +1 -1
  50. package/dist/types/domain/jwt.d.ts +27 -1
  51. package/dist/types/domain/jwt.d.ts.map +1 -1
  52. package/dist/types/domain/logout.d.ts +69 -0
  53. package/dist/types/domain/logout.d.ts.map +1 -0
  54. package/dist/types/domain/par.d.ts +62 -0
  55. package/dist/types/domain/par.d.ts.map +1 -0
  56. package/dist/types/domain/refresh.d.ts +9 -0
  57. package/dist/types/domain/refresh.d.ts.map +1 -1
  58. package/dist/types/domain/register.d.ts +33 -0
  59. package/dist/types/domain/register.d.ts.map +1 -0
  60. package/dist/types/domain/token-exchange.d.ts +2 -0
  61. package/dist/types/domain/token-exchange.d.ts.map +1 -1
  62. package/dist/types/domain/token.d.ts +41 -1
  63. package/dist/types/domain/token.d.ts.map +1 -1
  64. package/dist/types/domain/userinfo.d.ts +44 -5
  65. package/dist/types/domain/userinfo.d.ts.map +1 -1
  66. package/dist/types/http/context.d.ts +4 -1
  67. package/dist/types/http/context.d.ts.map +1 -1
  68. package/dist/types/http/handlers/authorize.d.ts.map +1 -1
  69. package/dist/types/http/handlers/end-session.d.ts +3 -0
  70. package/dist/types/http/handlers/end-session.d.ts.map +1 -0
  71. package/dist/types/http/handlers/metadata.d.ts +1 -1
  72. package/dist/types/http/handlers/metadata.d.ts.map +1 -1
  73. package/dist/types/http/handlers/par.d.ts +3 -0
  74. package/dist/types/http/handlers/par.d.ts.map +1 -0
  75. package/dist/types/http/handlers/register.d.ts +3 -0
  76. package/dist/types/http/handlers/register.d.ts.map +1 -0
  77. package/dist/types/http/handlers/token.d.ts.map +1 -1
  78. package/dist/types/http/handlers/userinfo.d.ts.map +1 -1
  79. package/dist/types/http/router.d.ts.map +1 -1
  80. package/dist/types/http/schemas/authorize.d.ts +40 -0
  81. package/dist/types/http/schemas/authorize.d.ts.map +1 -1
  82. package/dist/types/http/schemas/end-session.d.ts +31 -0
  83. package/dist/types/http/schemas/end-session.d.ts.map +1 -0
  84. package/dist/types/http/schemas/par.d.ts +20 -0
  85. package/dist/types/http/schemas/par.d.ts.map +1 -0
  86. package/dist/types/http/schemas/register.d.ts +42 -0
  87. package/dist/types/http/schemas/register.d.ts.map +1 -0
  88. package/dist/types/index.d.ts +3 -3
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/types/ports/audit-log.d.ts +27 -0
  91. package/dist/types/ports/audit-log.d.ts.map +1 -1
  92. package/dist/types/ports/session-store.d.ts +27 -0
  93. package/dist/types/ports/session-store.d.ts.map +1 -1
  94. package/dist/types/ports/token-store.d.ts +12 -0
  95. package/dist/types/ports/token-store.d.ts.map +1 -1
  96. package/dist/types/types/authorization.d.ts +23 -0
  97. package/dist/types/types/authorization.d.ts.map +1 -1
  98. package/dist/types/types/error.d.ts +9 -0
  99. package/dist/types/types/error.d.ts.map +1 -1
  100. package/dist/types/types/flow.d.ts +10 -0
  101. package/dist/types/types/flow.d.ts.map +1 -1
  102. package/dist/types/types/idp.d.ts +91 -1
  103. package/dist/types/types/idp.d.ts.map +1 -1
  104. package/dist/types/types/tenant.d.ts +37 -0
  105. package/dist/types/types/tenant.d.ts.map +1 -1
  106. package/dist/types/types/token.d.ts +128 -0
  107. package/dist/types/types/token.d.ts.map +1 -1
  108. package/package.json +1 -1
  109. package/src/adapters/memory/session-store.ts +44 -1
  110. package/src/adapters/memory/token-store.ts +16 -0
  111. package/src/client.ts +32 -0
  112. package/src/domain/authorize.ts +9 -0
  113. package/src/domain/callback.ts +5 -0
  114. package/src/domain/client-credentials.ts +2 -0
  115. package/src/domain/discovery.ts +80 -2
  116. package/src/domain/dpop.ts +293 -0
  117. package/src/domain/id-token.ts +213 -0
  118. package/src/domain/introspect.ts +10 -0
  119. package/src/domain/jwt.ts +80 -1
  120. package/src/domain/logout.ts +180 -0
  121. package/src/domain/method-route.ts +5 -0
  122. package/src/domain/par.ts +141 -0
  123. package/src/domain/refresh.ts +41 -0
  124. package/src/domain/register.ts +168 -0
  125. package/src/domain/token-exchange.ts +11 -0
  126. package/src/domain/token.ts +134 -8
  127. package/src/domain/userinfo.ts +121 -6
  128. package/src/http/context.ts +4 -0
  129. package/src/http/handlers/authorize.ts +144 -3
  130. package/src/http/handlers/end-session.ts +106 -0
  131. package/src/http/handlers/metadata.ts +7 -2
  132. package/src/http/handlers/par.ts +84 -0
  133. package/src/http/handlers/register.ts +70 -0
  134. package/src/http/handlers/token.ts +56 -0
  135. package/src/http/handlers/userinfo.ts +62 -38
  136. package/src/http/router.ts +19 -0
  137. package/src/http/schemas/authorize.ts +27 -0
  138. package/src/http/schemas/end-session.ts +23 -0
  139. package/src/http/schemas/par.ts +23 -0
  140. package/src/http/schemas/register.ts +29 -0
  141. package/src/index.ts +18 -1
  142. package/src/ports/CONSISTENCY.md +52 -21
  143. package/src/ports/audit-log.ts +29 -0
  144. package/src/ports/session-store.ts +33 -0
  145. package/src/ports/token-store.ts +13 -0
  146. package/src/types/authorization.ts +25 -0
  147. package/src/types/error.ts +23 -0
  148. package/src/types/flow.ts +10 -0
  149. package/src/types/idp.ts +96 -0
  150. package/src/types/tenant.ts +37 -0
  151. package/src/types/token.ts +135 -0
package/src/index.ts CHANGED
@@ -57,8 +57,11 @@ export type {
57
57
 
58
58
  export type {
59
59
  AccessTokenClaims,
60
+ AddressClaim,
60
61
  CodePayload,
62
+ IdTokenClaims,
61
63
  RefreshTokenPayload,
64
+ ScopedProfileClaims,
62
65
  TokenResponse,
63
66
  } from "./types/token"
64
67
 
@@ -74,6 +77,9 @@ export type {
74
77
  IdP,
75
78
  IdPOptions,
76
79
  PersistUpstreamTokens,
80
+ RegisterClient,
81
+ RegisterClientRequest,
82
+ RegisterClientResponse,
77
83
  RenderPicker,
78
84
  SuccessEvent,
79
85
  SuccessMapInput,
@@ -90,7 +96,11 @@ export type {
90
96
  SigningKey,
91
97
  } from "./ports/key-store"
92
98
  export type { MethodStore } from "./ports/method-store"
93
- export type { SessionRecord, SessionStore } from "./ports/session-store"
99
+ export type {
100
+ ParRecord,
101
+ SessionRecord,
102
+ SessionStore,
103
+ } from "./ports/session-store"
94
104
  export type { TokenStore } from "./ports/token-store"
95
105
 
96
106
  /**
@@ -243,6 +253,7 @@ export function createIdP(opts: IdPOptions): IdP {
243
253
  ? { exchangeAudience: opts.exchangeAudience }
244
254
  : {}),
245
255
  ...(opts.renderPicker ? { renderPicker: opts.renderPicker } : {}),
256
+ ...(opts.registerClient ? { registerClient: opts.registerClient } : {}),
246
257
  ...(opts.buildCustomContext
247
258
  ? { buildCustomContext: opts.buildCustomContext }
248
259
  : {}),
@@ -254,6 +265,9 @@ export function createIdP(opts: IdPOptions): IdP {
254
265
  : {}),
255
266
  ...(opts.cookies?.path !== undefined ? { path: opts.cookies.path } : {}),
256
267
  },
268
+ ...(opts.customScopeClaims !== undefined
269
+ ? { customScopeClaims: opts.customScopeClaims }
270
+ : {}),
257
271
  }
258
272
 
259
273
  const app = buildRouter(deps)
@@ -268,5 +282,8 @@ export function createIdP(opts: IdPOptions): IdP {
268
282
  discovery: fetch,
269
283
  revoke: fetch,
270
284
  introspect: fetch,
285
+ endSession: fetch,
286
+ par: fetch,
287
+ register: fetch,
271
288
  }
272
289
  }
@@ -15,27 +15,30 @@ overspecifies adapters that don't need that on the read-eventual paths
15
15
 
16
16
  ## Per-method consistency contracts
17
17
 
18
- | Port | Method | Required | Why |
19
- | -------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
- | `TokenStore` | `saveCode(code, payload, ttl)` | **Strong, atomic.** Payload **encrypted at rest** with a key from `KeyStore`; `ttl ≤ 60 s` (framework refuses larger). | Code must be created exactly once and visible to the next `consumeCode` on any node. Payload may carry upstream tokens — see _Code payload confidentiality_ in plan. |
21
- | `TokenStore` | `consumeCode(code)` | **Strong, CAS.** Returns the **decrypted** payload on success. | Single-use; second consumption must fail deterministically. Race with itself must resolve to one winner. |
22
- | `TokenStore` | `saveRefresh(refresh, payload)` | **Strong, atomic.** | New refresh tokens issued during rotation must be immediately retrievable on the next request. |
23
- | `TokenStore` | `consumeRefresh(refresh, options)` | **Strong, CAS with reuse-detection window** (default 60 s). | Core of refresh-token rotation security. Concurrent presentations resolve to one winner; reuse within the window triggers the revoke chain. |
24
- | `TokenStore` | `peekRefresh(refresh)` | **Eventual acceptable.** Non-destructive read; must never mark the token consumed. | Used by `/revoke` for token-to-client binding (RFC 7009 §2.2) and by the refresh-grant for client auth + audit fields. Race losers fall through to `consumeRefresh`, which is the strong gate, so a stale peek can only delay — not bypass — single-use enforcement. |
25
- | `TokenStore` | `revokeFamily(family)` | Strong preferred; eventual acceptable with documented SLA. | Reuse-detection must invalidate the whole chain. Brief lag is tolerable. |
26
- | `TokenStore` | `revokeBySubject(tenant, subject)` | Strong preferred; eventual acceptable with documented SLA. | Revocation must propagate quickly. A few seconds of lag is tolerable. |
27
- | `SessionStore` | `saveFlow(flowId, payload, ttl)` | **Strong, atomic.** `ttl == expiresAt - createdAt`. | Flow record must be visible on the callback request to `consumeFlow`. |
28
- | `SessionStore` | `updateFlowMethodState(flowId, state)` | **Strong, atomic.** Resolves **before** the user-agent redirect is sent. | Upstream PKCE verifier / nonce must be durably persisted before the redirect, otherwise the callback cannot validate. |
29
- | `SessionStore` | `consumeFlow(flowId)` | **Strong, atomic delete-on-read** that **returns the full `FlowRecord`** (`CAS` or `DELETE … RETURNING`). | Single-use; concurrent consumption resolves to one winner. The record is returned so the framework can snapshot fields into the auth-code payload before disposal. |
30
- | `SessionStore` | `createSession / readSession / revokeSession` (optional long-lived) | **Strong.** | Session creation must be immediately readable on the next request. |
31
- | `KeyStore` | `currentSigningKey()` / `currentEncryptionKey()` | Strong. | Active key must be unambiguous. |
32
- | `KeyStore` | `signingKeys()` (JWKS) | Eventual OK (with TTL). | Verifiers tolerate brief JWKS lag during rotation. |
33
- | `KeyStore` | `getEncryptionKey(kid)` | Strong. | Required to decrypt code payloads encrypted under non-current keys during the overlap window. |
34
- | `ConfigStore` | `getTenantConfig(id)` | **Eventual + bounded staleness (TTL ≤ 60 s).** Invalidation hook fires on update. | Read-heavy, write-rare. Aggressive caching is desirable. |
35
- | `ConfigStore` | `putTenantConfig(config)` | **Strong write + immediate invalidation** of cached entries for the affected `TenantId`. | Subsequent `getTenantConfig` must reflect the new value once the write resolves. |
36
- | `MethodStore` | `getMethodConfig` / `listMethods` | Same as `ConfigStore.getTenantConfig`. | Subset of tenant config. |
37
- | `MethodStore` | `putMethodConfig` / `deleteMethodConfig` | Strong write + immediate invalidation. | Console-driven mutations must take effect on the next request. |
38
- | `AuditLog` | `log(event)` | Append-only, durable. Cross-instance ordering **not** required (consumers sort by `timestamp` + `actor`). | Loss = audit gap; ordering is a UI / SIEM concern. |
18
+ | Port | Method | Required | Why |
19
+ | -------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `TokenStore` | `saveCode(code, payload, ttl)` | **Strong, atomic.** Payload **encrypted at rest** with a key from `KeyStore`; `ttl ≤ 60 s` (framework refuses larger). | Code must be created exactly once and visible to the next `consumeCode` on any node. Payload may carry upstream tokens — see _Code payload confidentiality_ in plan. |
21
+ | `TokenStore` | `consumeCode(code)` | **Strong, CAS.** Returns the **decrypted** payload on success. | Single-use; second consumption must fail deterministically. Race with itself must resolve to one winner. |
22
+ | `TokenStore` | `saveRefresh(refresh, payload)` | **Strong, atomic.** | New refresh tokens issued during rotation must be immediately retrievable on the next request. |
23
+ | `TokenStore` | `consumeRefresh(refresh, options)` | **Strong, CAS with reuse-detection window** (default 60 s). | Core of refresh-token rotation security. Concurrent presentations resolve to one winner; reuse within the window triggers the revoke chain. |
24
+ | `TokenStore` | `peekRefresh(refresh)` | **Eventual acceptable.** Non-destructive read; must never mark the token consumed. | Used by `/revoke` for token-to-client binding (RFC 7009 §2.2) and by the refresh-grant for client auth + audit fields. Race losers fall through to `consumeRefresh`, which is the strong gate, so a stale peek can only delay — not bypass — single-use enforcement. |
25
+ | `TokenStore` | `revokeFamily(family)` | Strong preferred; eventual acceptable with documented SLA. | Reuse-detection must invalidate the whole chain. Brief lag is tolerable. |
26
+ | `TokenStore` | `revokeBySubject(tenant, subject)` | Strong preferred; eventual acceptable with documented SLA. | Revocation must propagate quickly. A few seconds of lag is tolerable. |
27
+ | `SessionStore` | `saveFlow(flowId, payload, ttl)` | **Strong, atomic.** `ttl == expiresAt - createdAt`. | Flow record must be visible on the callback request to `consumeFlow`. |
28
+ | `SessionStore` | `updateFlowMethodState(flowId, state)` | **Strong, atomic.** Resolves **before** the user-agent redirect is sent. | Upstream PKCE verifier / nonce must be durably persisted before the redirect, otherwise the callback cannot validate. |
29
+ | `SessionStore` | `consumeFlow(flowId)` | **Strong, atomic delete-on-read** that **returns the full `FlowRecord`** (`CAS` or `DELETE … RETURNING`). | Single-use; concurrent consumption resolves to one winner. The record is returned so the framework can snapshot fields into the auth-code payload before disposal. |
30
+ | `SessionStore` | `savePar(uri, payload, ttl)` (optional) | **Strong, atomic.** `ttl` default 60 s. | RFC 9126 §3 — the PAR record must be visible to the next `consumePar` on any node. One-shot read. |
31
+ | `SessionStore` | `consumePar(uri)` (optional) | **Strong, atomic delete-on-read.** | RFC 9126 §4 `request_uri` is single-use; concurrent presentations of the same uri resolve to one winner. Same semantics as `consumeFlow`. |
32
+ | `SessionStore` | `createSession / readSession / revokeSession` (optional long-lived) | **Strong.** | Session creation must be immediately readable on the next request. |
33
+ | `TokenStore` | `recordDpopJti(jti, ttlMs)` (optional) | **Strong, atomic record-or-fail.** | RFC 9449 §11.1 replay protection requires single-use enforcement on the jti within the TTL window. A re-presentation must return `invalid_grant` deterministically. Adapters without this method cannot satisfy DPoP and the verifier surfaces `invalid_dpop_proof`. |
34
+ | `KeyStore` | `currentSigningKey()` / `currentEncryptionKey()` | Strong. | Active key must be unambiguous. |
35
+ | `KeyStore` | `signingKeys()` (JWKS) | Eventual OK (with TTL). | Verifiers tolerate brief JWKS lag during rotation. |
36
+ | `KeyStore` | `getEncryptionKey(kid)` | Strong. | Required to decrypt code payloads encrypted under non-current keys during the overlap window. |
37
+ | `ConfigStore` | `getTenantConfig(id)` | **Eventual + bounded staleness (TTL 60 s).** Invalidation hook fires on update. | Read-heavy, write-rare. Aggressive caching is desirable. |
38
+ | `ConfigStore` | `putTenantConfig(config)` | **Strong write + immediate invalidation** of cached entries for the affected `TenantId`. | Subsequent `getTenantConfig` must reflect the new value once the write resolves. |
39
+ | `MethodStore` | `getMethodConfig` / `listMethods` | Same as `ConfigStore.getTenantConfig`. | Subset of tenant config. |
40
+ | `MethodStore` | `putMethodConfig` / `deleteMethodConfig` | Strong write + immediate invalidation. | Console-driven mutations must take effect on the next request. |
41
+ | `AuditLog` | `log(event)` | Append-only, durable. Cross-instance ordering **not** required (consumers sort by `timestamp` + `actor`). | Loss = audit gap; ordering is a UI / SIEM concern. |
39
42
 
40
43
  ## Implications for adapter choice
41
44
 
@@ -86,3 +89,31 @@ The fixture set covers, at minimum:
86
89
  - `ConfigStore` invalidation hook fires within bounded staleness.
87
90
  - JWKS overlap window — keys retired during the verification window remain
88
91
  in `signingKeys()` until removed.
92
+ - **PAR (`supportsPar: true`)** — `savePar` / `consumePar` round-trip,
93
+ one-shot consume atomicity, expiry enforcement, ttl-0 rejection.
94
+ - **DPoP jti (`recordDpopJti`)** — first record succeeds, replay within
95
+ TTL fails with `invalid_grant`, post-TTL the slot is freed for reuse.
96
+
97
+ ## Optional methods and graceful degradation
98
+
99
+ Several recent ports are **optional** because not every adapter has
100
+ caught up yet:
101
+
102
+ - `SessionStore.savePar` / `consumePar` — required for RFC 9126 PAR.
103
+ Without them the framework's `/par` handler returns `invalid_request`
104
+ with a clear "session adapter does not support PAR" description, and
105
+ `/authorize?request_uri=...` returns the same. Memory adapter
106
+ implements; production adapters (Postgres, D1, DO, DynamoDB) follow
107
+ the same `consumeFlow` pattern and should add it.
108
+ - `TokenStore.recordDpopJti` — required for RFC 9449 DPoP. Without it
109
+ the DPoP verifier returns `invalid_dpop_proof: "token-store adapter
110
+ does not support DPoP replay protection"`. Memory adapter
111
+ implements; production adapters add it the same way `consumeRefresh`
112
+ uses an atomic write-once primitive.
113
+
114
+ The framework never advertises a feature in discovery that the wired
115
+ adapters cannot actually serve: `pushed_authorization_request_endpoint`
116
+ and `dpop_signing_alg_values_supported` are advertised unconditionally,
117
+ and clients receive the appropriate error if the adapter doesn't
118
+ support them. Operators who do not run PAR / DPoP should communicate
119
+ that to RPs out-of-band.
@@ -54,6 +54,10 @@ export type AuditEvent =
54
54
  subjectId: string
55
55
  /** Hash of the issued refresh token id — never the token itself. */
56
56
  refreshTokenIdHash?: string
57
+ /** OIDC Core §2 — set when the response carried an `id_token`. */
58
+ idTokenIssued?: boolean
59
+ /** RFC 9449 — set when the access token is DPoP-bound. */
60
+ dpopBound?: boolean
57
61
  }
58
62
  | {
59
63
  kind: "token_refreshed"
@@ -87,6 +91,31 @@ export type AuditEvent =
87
91
  clientId: string
88
92
  family: string
89
93
  }
94
+ | {
95
+ /**
96
+ * Emitted by `/end_session` (OIDC RP-Initiated Logout 1.0 §2) after
97
+ * processing the logout request — regardless of whether a
98
+ * `post_logout_redirect_uri` was supplied. `subjectId` is present
99
+ * when the request carried an `id_token_hint` that successfully
100
+ * verified; absent otherwise.
101
+ */
102
+ kind: "session_logout"
103
+ tenantId: TenantId
104
+ clientId?: string
105
+ subjectId?: string
106
+ }
107
+ | {
108
+ /**
109
+ * Emitted when a DPoP proof's `jti` is presented within the replay
110
+ * window (RFC 9449 §11.1). The request was rejected with
111
+ * `invalid_dpop_proof`. Operators / SIEM use this to spot
112
+ * stolen-key replay attempts.
113
+ */
114
+ kind: "dpop_replay_detected"
115
+ tenantId: TenantId | null
116
+ /** First-half of the offending jti so logs can correlate without storing it. */
117
+ jtiPrefix: string
118
+ }
90
119
  | {
91
120
  kind: "flow_replay_attempt"
92
121
  tenantId: TenantId | null
@@ -83,6 +83,39 @@ export type SessionStore = {
83
83
  ): Promise<Result<void>>
84
84
  readSession?(sessionId: string): Promise<Result<SessionRecord>>
85
85
  revokeSession?(sessionId: string): Promise<Result<void>>
86
+
87
+ /**
88
+ * Optional: Pushed Authorization Request (RFC 9126) storage. Persists the
89
+ * pre-parsed `/authorize` parameters under an opaque `request_uri` for
90
+ * one-shot retrieval at `/authorize` time. Strong consistency + atomic
91
+ * delete-on-read — same semantics as `saveFlow` / `consumeFlow`.
92
+ *
93
+ * Adapters without these methods cannot satisfy `/par`; the framework's
94
+ * `/par` handler returns `invalid_request` when called against such a
95
+ * store. Implement both methods together (the framework only exposes
96
+ * the endpoint when both are present).
97
+ */
98
+ savePar?(
99
+ requestUri: string,
100
+ payload: ParRecord,
101
+ ttl: number,
102
+ ): Promise<Result<void>>
103
+ consumePar?(requestUri: string): Promise<Result<ParRecord>>
104
+ }
105
+
106
+ /**
107
+ * Stored PAR payload. The `params` blob is the raw form/query record from
108
+ * `POST /par`, kept verbatim so the `/authorize` rehydrate path can feed
109
+ * it through the same Zod parser the direct path uses.
110
+ */
111
+ export type ParRecord = {
112
+ requestUri: string
113
+ /** Raw key/value record as posted to `/par` (excluding auth fields). */
114
+ params: Record<string, string>
115
+ clientId: string
116
+ /** Wall-clock issuance + absolute expiry (ms). */
117
+ issuedAt: number
118
+ expiresAt: number
86
119
  }
87
120
 
88
121
  /** Optional long-lived session payload (used by `createSession` family). */
@@ -100,4 +100,17 @@ export type TokenStore = {
100
100
  * preferred; documented eventual lag acceptable.
101
101
  */
102
102
  revokeBySubject(tenantId: TenantId, subjectId: string): Promise<Result<void>>
103
+
104
+ /**
105
+ * Optional: DPoP proof replay protection (RFC 9449 §11.1). Atomically
106
+ * record the proof's `jti` and reject if it's been seen within the TTL
107
+ * window. Returns `invalid_grant` (with `reuseSignal` omitted) when a
108
+ * `jti` is already present.
109
+ *
110
+ * Adapters without this method cannot satisfy DPoP at the token /
111
+ * userinfo endpoints; the framework's DPoP verifier returns
112
+ * `invalid_dpop_proof` when called against such a store. Memory adapter
113
+ * implements; production adapters follow in subsequent commits.
114
+ */
115
+ recordDpopJti?(jti: string, ttlMs: number): Promise<Result<void>>
103
116
  }
@@ -37,6 +37,31 @@ export type AuthorizationRequest = {
37
37
  uiLocales?: string[]
38
38
  /** Standard OIDC. */
39
39
  nonce?: string
40
+ /**
41
+ * OIDC Core §5.5 — RP-requested claims, parsed from the `claims`
42
+ * parameter's JSON value. The library currently honors claim **names**
43
+ * (additive, bypasses scope gating) but does not enforce the
44
+ * `essential` / `value` / `values` qualifiers.
45
+ */
46
+ claimsRequest?: ClaimsRequest
47
+ }
48
+
49
+ /**
50
+ * OIDC Core §5.5 `claims` parameter. Each section maps claim name to an
51
+ * optional qualifier object (`{essential, value, values}`) or `null`
52
+ * meaning "just request the claim without qualifiers".
53
+ */
54
+ export type ClaimsRequest = {
55
+ /** Claims to include in the `/userinfo` response. */
56
+ userinfo?: Record<string, ClaimRequestEntry | null>
57
+ /** Claims to include in the `id_token`. */
58
+ id_token?: Record<string, ClaimRequestEntry | null>
59
+ }
60
+
61
+ export type ClaimRequestEntry = {
62
+ essential?: boolean
63
+ value?: unknown
64
+ values?: unknown[]
40
65
  }
41
66
 
42
67
  /**
@@ -50,6 +50,21 @@ export type AuthError =
50
50
  // endpoints. Use for control-flow signalling that should never escape to a
51
51
  // standards-compliant client.
52
52
  | { code: "internal_error"; description: string; cause?: unknown }
53
+ // RFC 9449 §5.2 — DPoP proof verification failed (bad signature, htm/htu
54
+ // mismatch, iat outside window, replayed jti, missing/mismatched cnf.jkt).
55
+ // Returned as a 400 with `error="invalid_dpop_proof"` on form-body
56
+ // endpoints; on resource-server endpoints it becomes a 401 with
57
+ // `WWW-Authenticate: DPoP error="invalid_dpop_proof"`.
58
+ //
59
+ // `replaySignal`, when present, indicates `recordDpopJti` reported a
60
+ // jti already seen within the replay window. The HTTP layer uses this
61
+ // to emit a `dpop_replay_detected` audit event distinct from other
62
+ // proof failures.
63
+ | {
64
+ code: "invalid_dpop_proof"
65
+ description: string
66
+ replaySignal?: { jti: string }
67
+ }
53
68
 
54
69
  export type AuthErrorCode = AuthError["code"]
55
70
 
@@ -120,4 +135,12 @@ export const authError = {
120
135
  description,
121
136
  ...(cause !== undefined ? { cause } : {}),
122
137
  }),
138
+ invalidDpopProof: (
139
+ description: string,
140
+ replaySignal?: { jti: string },
141
+ ): AuthError => ({
142
+ code: "invalid_dpop_proof",
143
+ description,
144
+ ...(replaySignal !== undefined ? { replaySignal } : {}),
145
+ }),
123
146
  }
package/src/types/flow.ts CHANGED
@@ -19,6 +19,7 @@
19
19
  * needed at `/token` into the auth-code payload; `methodState` is
20
20
  * dropped.
21
21
  */
22
+ import type { ClaimsRequest } from "./authorization"
22
23
  import type { TenantId } from "./tenant"
23
24
 
24
25
  export type FlowRecord = {
@@ -56,6 +57,15 @@ export type FlowRecord = {
56
57
  * `appState`. Compared at callback against the consumed record.
57
58
  */
58
59
  nonce: string
60
+ /**
61
+ * Relying party's OIDC `nonce` parameter (OIDC Core §3.1.2.1). Distinct
62
+ * from `nonce` (which is the framework's CSRF nonce for state-MAC
63
+ * binding). When present, must be echoed in the issued `id_token`
64
+ * (OIDC Core §2). Snapshotted into `CodePayload.appNonce` at success.
65
+ */
66
+ appNonce?: string
67
+ /** OIDC Core §5.5 — RP-requested claims, parsed at `/authorize`. */
68
+ claimsRequest?: ClaimsRequest
59
69
  /**
60
70
  * Relying-party → IdP PKCE. The RP generates the verifier and sends the
61
71
  * challenge to `/authorize`. Verified at `/token`.
package/src/types/idp.ts CHANGED
@@ -15,6 +15,7 @@ import type { AnyAuthMethodFactory } from "./method"
15
15
  import type { Result } from "./result"
16
16
  import type { SubjectClaim, SubjectSchema } from "./subject"
17
17
  import type {
18
+ ClientConfig,
18
19
  StateKeyRing,
19
20
  TenantContext,
20
21
  TenantId,
@@ -119,6 +120,69 @@ export type ExchangeAudience = (
119
120
  },
120
121
  ) => Promise<SubjectClaim | AuthError>
121
122
 
123
+ /**
124
+ * RFC 7591 Dynamic Client Registration request body, normalized to the
125
+ * library's type system. The framework's `/register` handler parses the
126
+ * raw JSON, validates structure, then calls the host's `registerClient`
127
+ * hook with this shape. The host owns persistence — writing through its
128
+ * own `ConfigStore` — and returns the final `ClientConfig`.
129
+ */
130
+ export type RegisterClientRequest = {
131
+ client_name?: string
132
+ redirect_uris: string[]
133
+ /** RFC 7591 §2 — defaults to `["authorization_code"]`. */
134
+ grant_types?: string[]
135
+ /** RFC 7591 §2 — defaults to `["code"]`. */
136
+ response_types?: string[]
137
+ /** RFC 7591 §2 — `"none"` = public client, otherwise confidential. */
138
+ token_endpoint_auth_method?:
139
+ | "none"
140
+ | "client_secret_basic"
141
+ | "client_secret_post"
142
+ scope?: string
143
+ /** OIDC RP-Initiated Logout 1.0 §2. */
144
+ post_logout_redirect_uris?: string[]
145
+ /** OIDC Core §8.1. */
146
+ sector_identifier_uri?: string
147
+ /** Free-form metadata the host may interpret. */
148
+ contacts?: string[]
149
+ }
150
+
151
+ /**
152
+ * RFC 7591 §3.2.1 response body. Returned verbatim from `/register` when
153
+ * the host hook produces a `ClientConfig`. `client_secret` is included
154
+ * only for confidential clients; public clients omit it.
155
+ */
156
+ export type RegisterClientResponse = {
157
+ client_id: string
158
+ client_secret?: string
159
+ client_id_issued_at: number
160
+ client_secret_expires_at?: number
161
+ redirect_uris: string[]
162
+ grant_types?: string[]
163
+ response_types?: string[]
164
+ token_endpoint_auth_method?: string
165
+ client_name?: string
166
+ }
167
+
168
+ /**
169
+ * Optional Dynamic Client Registration hook. Hosts that want to expose
170
+ * RFC 7591 client provisioning supply this; the framework validates the
171
+ * wire format, then defers persistence to the host. If absent, the
172
+ * `/register` endpoint returns `invalid_request` so RPs receive a clear
173
+ * "not enabled" signal rather than a 404.
174
+ *
175
+ * The hook receives the parsed request, the resolved tenant, and the
176
+ * plaintext client secret (if any) the framework minted — hosts hash it
177
+ * with `hashClientSecret` before storing on `ClientConfig.secretHash`,
178
+ * then return the final `ClientConfig` along with the secret in the
179
+ * `RegisterClientResponse` so the RP can record it.
180
+ */
181
+ export type RegisterClient = (input: {
182
+ tenant: TenantContext
183
+ request: RegisterClientRequest
184
+ }) => Promise<Result<{ client: ClientConfig; secret?: string }, AuthError>>
185
+
122
186
  /**
123
187
  * Optional override for the default provider picker shown when an
124
188
  * `/authorize` request has multiple enabled methods and no `method_id`.
@@ -213,6 +277,13 @@ export type IdPOptions = {
213
277
  */
214
278
  renderPicker?: RenderPicker
215
279
 
280
+ /**
281
+ * Optional RFC 7591 Dynamic Client Registration hook. See
282
+ * `RegisterClient` for the contract. When absent, `/register` rejects
283
+ * with `invalid_request: "dynamic client registration is not enabled"`.
284
+ */
285
+ registerClient?: RegisterClient
286
+
216
287
  /**
217
288
  * Optional hook that builds the `TenantContext.request.custom` blob for
218
289
  * each request the framework processes (the initial `/authorize`, the
@@ -234,6 +305,25 @@ export type IdPOptions = {
234
305
  req: Request,
235
306
  ) => Record<string, unknown> | Promise<Record<string, unknown>>
236
307
 
308
+ /**
309
+ * Optional vendor scope → claim-names map. Merged on top of OIDC Core
310
+ * §5.4 when building the id_token + `/userinfo` response. Lets a host
311
+ * expose its own identity vocabulary (`tenant_id`, `org_role`, etc.)
312
+ * via custom scope names. The standard §5.4 mapping always wins on
313
+ * collision, so an entry for `email` is silently ignored.
314
+ *
315
+ * Per-client scope allowlist still applies — a client must list a
316
+ * custom scope in `ClientConfig.scopes` to be allowed to request it.
317
+ *
318
+ * ```ts
319
+ * customScopeClaims: {
320
+ * tenant: ["tenant_id", "tenant_role", "tenant_roles"],
321
+ * org: ["organization_id", "org_role"],
322
+ * }
323
+ * ```
324
+ */
325
+ customScopeClaims?: Record<string, ReadonlyArray<string>>
326
+
237
327
  /**
238
328
  * Optional override for the framework's `Set-Cookie` defaults.
239
329
  *
@@ -266,4 +356,10 @@ export type IdP = {
266
356
  discovery: (req: Request) => Promise<Response>
267
357
  revoke: (req: Request) => Promise<Response>
268
358
  introspect: (req: Request) => Promise<Response>
359
+ /** OIDC RP-Initiated Logout 1.0. */
360
+ endSession: (req: Request) => Promise<Response>
361
+ /** RFC 9126 Pushed Authorization Requests. */
362
+ par: (req: Request) => Promise<Response>
363
+ /** RFC 7591 Dynamic Client Registration. */
364
+ register: (req: Request) => Promise<Response>
269
365
  }
@@ -128,6 +128,26 @@ export type PublicClientConfig = {
128
128
  scopes: string[]
129
129
  /** Must be `true` for public clients per OAuth 2.1 §2.1.1. */
130
130
  pkceRequired: true
131
+ /**
132
+ * Registered URIs to which `/end_session` may redirect after RP-initiated
133
+ * logout (OIDC RP-Initiated Logout 1.0 §2). Exact-match. If absent, the
134
+ * `/end_session` endpoint refuses any `post_logout_redirect_uri`.
135
+ */
136
+ postLogoutRedirectUris?: string[]
137
+ /**
138
+ * RFC 9126 §2: when `true`, the client MUST use Pushed Authorization
139
+ * Requests; a direct `GET /authorize` call without `request_uri` is
140
+ * rejected with `invalid_request`.
141
+ */
142
+ requirePushedAuthorizationRequests?: boolean
143
+ /**
144
+ * OIDC Core §8.1 — when set, the subject identifier (`sub`) is derived
145
+ * as a pairwise pseudonym keyed by this string. Two clients sharing
146
+ * the same `sectorIdentifier` will see the same `sub`; different
147
+ * values yield different `sub`s for the same end user. Absent =
148
+ * public subject (sub is identical across all RPs).
149
+ */
150
+ sectorIdentifier?: string
131
151
  /** Phase 8. */
132
152
  dpopRequired?: boolean
133
153
  }
@@ -146,6 +166,23 @@ export type ConfidentialClientConfig = {
146
166
  * strongly discouraged; OAuth 2.1 §2.1.1 recommends PKCE for every client.
147
167
  */
148
168
  pkceRequired: boolean
169
+ /**
170
+ * Registered URIs to which `/end_session` may redirect after RP-initiated
171
+ * logout (OIDC RP-Initiated Logout 1.0 §2). Exact-match.
172
+ */
173
+ postLogoutRedirectUris?: string[]
174
+ /**
175
+ * RFC 9126 §2: when `true`, the client MUST use Pushed Authorization
176
+ * Requests; a direct `GET /authorize` call without `request_uri` is
177
+ * rejected with `invalid_request`.
178
+ */
179
+ requirePushedAuthorizationRequests?: boolean
180
+ /**
181
+ * OIDC Core §8.1 — see `PublicClientConfig.sectorIdentifier` for
182
+ * semantics. Same field; duplicated on each branch because the
183
+ * `ClientConfig` discriminated union doesn't share optional fields.
184
+ */
185
+ sectorIdentifier?: string
149
186
  /** Phase 8. */
150
187
  dpopRequired?: boolean
151
188
  }