@_mustachio/openauth 0.13.3 → 0.15.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 (61) hide show
  1. package/dist/esm/client.js +61 -62
  2. package/dist/esm/domain/authorize.js +10 -0
  3. package/dist/esm/domain/callback.js +21 -19
  4. package/dist/esm/domain/client-credentials.js +16 -0
  5. package/dist/esm/domain/method-route.js +10 -0
  6. package/dist/esm/domain/refresh.js +16 -5
  7. package/dist/esm/domain/register.js +8 -2
  8. package/dist/esm/domain/state-envelope.js +19 -0
  9. package/dist/esm/domain/subject.js +35 -0
  10. package/dist/esm/domain/token.js +28 -0
  11. package/dist/esm/http/handlers/token.js +6 -0
  12. package/dist/esm/http/middleware/tenant.js +5 -20
  13. package/dist/esm/index.js +2 -0
  14. package/dist/types/client.d.ts +61 -40
  15. package/dist/types/client.d.ts.map +1 -1
  16. package/dist/types/domain/authorize.d.ts.map +1 -1
  17. package/dist/types/domain/callback.d.ts.map +1 -1
  18. package/dist/types/domain/client-credentials.d.ts +6 -2
  19. package/dist/types/domain/client-credentials.d.ts.map +1 -1
  20. package/dist/types/domain/method-route.d.ts.map +1 -1
  21. package/dist/types/domain/refresh.d.ts +3 -0
  22. package/dist/types/domain/refresh.d.ts.map +1 -1
  23. package/dist/types/domain/register.d.ts.map +1 -1
  24. package/dist/types/domain/state-envelope.d.ts +22 -0
  25. package/dist/types/domain/state-envelope.d.ts.map +1 -1
  26. package/dist/types/domain/subject.d.ts +48 -0
  27. package/dist/types/domain/subject.d.ts.map +1 -0
  28. package/dist/types/domain/token-exchange.d.ts +3 -1
  29. package/dist/types/domain/token-exchange.d.ts.map +1 -1
  30. package/dist/types/domain/token.d.ts +11 -2
  31. package/dist/types/domain/token.d.ts.map +1 -1
  32. package/dist/types/http/context.d.ts +6 -0
  33. package/dist/types/http/context.d.ts.map +1 -1
  34. package/dist/types/http/handlers/token.d.ts.map +1 -1
  35. package/dist/types/http/middleware/tenant.d.ts.map +1 -1
  36. package/dist/types/http/schemas/revocation.d.ts +4 -4
  37. package/dist/types/http/schemas/token.d.ts +12 -12
  38. package/dist/types/index.d.ts +1 -1
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/ports/audit-log.d.ts +25 -1
  41. package/dist/types/ports/audit-log.d.ts.map +1 -1
  42. package/dist/types/types/idp.d.ts +105 -38
  43. package/dist/types/types/idp.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/client.ts +145 -129
  46. package/src/domain/authorize.ts +10 -0
  47. package/src/domain/callback.ts +22 -29
  48. package/src/domain/client-credentials.ts +27 -2
  49. package/src/domain/method-route.ts +10 -0
  50. package/src/domain/refresh.ts +42 -10
  51. package/src/domain/register.ts +27 -8
  52. package/src/domain/state-envelope.ts +40 -0
  53. package/src/domain/subject.ts +103 -0
  54. package/src/domain/token-exchange.ts +3 -1
  55. package/src/domain/token.ts +54 -2
  56. package/src/http/context.ts +6 -0
  57. package/src/http/handlers/token.ts +6 -0
  58. package/src/http/middleware/tenant.ts +5 -24
  59. package/src/index.ts +3 -2
  60. package/src/ports/audit-log.ts +26 -1
  61. package/src/types/idp.ts +107 -41
package/src/types/idp.ts CHANGED
@@ -46,30 +46,6 @@ export type SuccessMapInput = {
46
46
  context: Record<string, unknown> | null
47
47
  }
48
48
 
49
- /**
50
- * Optional observation hook payload — fires after the subject claim has
51
- * already been minted. **Does not** influence the issued subject; use it
52
- * for audit, analytics, side effects only.
53
- */
54
- export type SuccessEvent = SuccessMapInput & {
55
- /** The final subject claim that became the JWT `sub`. */
56
- claim: SubjectClaim
57
- }
58
-
59
- /**
60
- * Optional observation hook payload — fires on a failed auth attempt.
61
- * Carries enough id information for operators to find the offending flow
62
- * / config row without leaking secrets.
63
- */
64
- export type FailureEvent = {
65
- tenantId: TenantId | null
66
- clientId: string | null
67
- methodId?: string
68
- methodKind?: string
69
- flowId?: string
70
- error: AuthError
71
- }
72
-
73
49
  /**
74
50
  * Input to the optional `IdPOptions.onLogout` hook.
75
51
  *
@@ -120,6 +96,63 @@ export type LogoutEventInput = {
120
96
  */
121
97
  export type LogoutHookResult = { revokeSubject?: string } | void
122
98
 
99
+ /**
100
+ * Fired when tokens are minted, naming the derived subject id.
101
+ *
102
+ * This is the only way a host can learn `subjectId` — the value the
103
+ * library signs as `sub`, and the key `TokenStore.revokeBySubject` and
104
+ * `revokeAllForSubject` take. It is derived inside the library from the
105
+ * claim and the receiving client's `sectorIdentifier`, so without this
106
+ * hook a host holds a revocation primitive it can never call, and
107
+ * `onLogout`'s `revokeSubject` has no map to resolve against.
108
+ *
109
+ * That matters because refresh rotation never re-consults the host:
110
+ * `refreshTokens` mints from the claim captured on the stored payload.
111
+ * Revoking the chain is the only way to stop an offboarded user's tokens
112
+ * from being renewed indefinitely.
113
+ *
114
+ * **A principal maps to many subject ids, not one.** Record them; do not
115
+ * overwrite. Two reasons, both silent if you assume otherwise:
116
+ *
117
+ * - **Pairwise** — with `ClientConfig.sectorIdentifier` set, derivation
118
+ * mixes the sector in (OIDC Core §8.1), so the same person has a
119
+ * distinct `sub` per sector. Revoking one leaves the others minting.
120
+ * - **Claim contents** — derivation hashes `claim.properties`. If your
121
+ * `success()` returns anything mutable (an email, a role), the subject
122
+ * id changes when that value changes, and chains issued under the old
123
+ * one survive. Returning a single immutable id is the way to avoid
124
+ * this; OIDC Core §2 wants `sub` never reassigned.
125
+ *
126
+ * Runs **before** the refresh token is persisted, so throwing aborts
127
+ * issuance with `server_error` and leaves no chain behind. That is
128
+ * deliberate: recording the mapping after the token is durable would let
129
+ * a hook failure produce exactly the unrevokable token this exists to
130
+ * prevent. Like `success` and `persistUpstreamTokens`, and unlike
131
+ * `AuditLog`, a failure here stops the grant.
132
+ */
133
+ export type OnTokenIssued = (input: {
134
+ tenant: TenantContext
135
+ /** Client the tokens were minted for. */
136
+ clientId: string
137
+ /**
138
+ * The `sub` of the issued access token — the key `revokeBySubject` and
139
+ * `revokeAllForSubject` take.
140
+ */
141
+ subjectId: string
142
+ /**
143
+ * The claim your `success()` (or token-exchange audience mapping)
144
+ * returned, already validated against `subjects`. Use it to attribute
145
+ * `subjectId` to one of your own principals.
146
+ */
147
+ claim: SubjectClaim
148
+ /**
149
+ * Refresh-token family, for `TokenStore.revokeFamily`. Absent when the
150
+ * grant issued no refresh token (`client_credentials`, RFC 6749
151
+ * §4.4.3), because then there is no chain to revoke.
152
+ */
153
+ family?: string
154
+ }) => Promise<void> | void
155
+
123
156
  /**
124
157
  * Optional hook called at `/token` time, after PKCE has succeeded and
125
158
  * after the `success` callback has produced a `SubjectClaim`, but
@@ -217,19 +250,41 @@ export type RegisterClientResponse = {
217
250
  /**
218
251
  * Optional Dynamic Client Registration hook. Hosts that want to expose
219
252
  * RFC 7591 client provisioning supply this; the framework validates the
220
- * wire format, then defers persistence to the host. If absent, the
221
- * `/register` endpoint returns `invalid_request` so RPs receive a clear
222
- * "not enabled" signal rather than a 404.
253
+ * wire format and mints credentials, then defers **persistence** to the
254
+ * host. If absent, the `/register` endpoint returns `invalid_request` so
255
+ * RPs receive a clear "not enabled" signal rather than a 404.
256
+ *
257
+ * The library owns credential generation — entropy, hashing, and the
258
+ * `ClientConfig` discriminated union, which requires `pkceRequired: true`
259
+ * as a literal on public clients and a `secretHash` on confidential ones.
260
+ * Those are protocol and security concerns, and making every host
261
+ * reimplement them is how they get done wrong. The host owns the table:
262
+ * write `client` through your own `ConfigStore` and return it.
263
+ *
264
+ * Before 0.14.0 this hook received only `{ tenant, request }` and the
265
+ * framework discarded what it had generated, so hosts had to mint their
266
+ * own — contradicting both this doc comment and `ARCHITECTURE.md`.
223
267
  *
224
- * The hook receives the parsed request, the resolved tenant, and the
225
- * plaintext client secret (if any) the framework minted — hosts hash it
226
- * with `hashClientSecret` before storing on `ClientConfig.secretHash`,
227
- * then return the final `ClientConfig` along with the secret in the
228
- * `RegisterClientResponse` so the RP can record it.
268
+ * Return the config you actually persisted. Adjusting it first is fine
269
+ * (narrowing `scopes`, substituting your own `id`); if you replace `id`
270
+ * or `secretHash`, return the matching plaintext as `secret` so the RP
271
+ * receives something that works.
229
272
  */
230
273
  export type RegisterClient = (input: {
231
274
  tenant: TenantContext
232
275
  request: RegisterClientRequest
276
+ /**
277
+ * Framework-minted `ClientConfig`, ready to persist as-is. Public
278
+ * clients carry `pkceRequired: true`; confidential clients carry
279
+ * `secretHash` for `secret` below.
280
+ */
281
+ client: ClientConfig
282
+ /**
283
+ * Plaintext secret matching `client.secretHash`. Present only for
284
+ * confidential clients. Return it in the result so the RP can record
285
+ * it — this is the only time it exists.
286
+ */
287
+ secret?: string
233
288
  }) => Promise<Result<{ client: ClientConfig; secret?: string }, AuthError>>
234
289
 
235
290
  /**
@@ -328,12 +383,6 @@ export type IdPOptions = {
328
383
 
329
384
  theme?: ThemeConfig
330
385
 
331
- hooks?: {
332
- /** Observation only — does NOT influence the subject. */
333
- onSuccess?: (event: SuccessEvent) => Promise<void>
334
- onFailure?: (event: FailureEvent) => Promise<void>
335
- }
336
-
337
386
  /**
338
387
  * Optional hook fired when an upstream provider signals that a
339
388
  * federated session ended — SAML front-channel Single Logout today.
@@ -342,9 +391,9 @@ export type IdPOptions = {
342
391
  * OIDC subject (if any) whose library-issued tokens to revoke. See
343
392
  * the `LogoutEventInput` / `LogoutHookResult` type docs.
344
393
  *
345
- * Unlike `hooks.onSuccess`/`onFailure` (observation only) this hook
346
- * **influences** library behaviour — its return drives token
347
- * revocation — so it sits at the top level alongside `success`.
394
+ * Unlike `AuditLog` (observation only) this hook **influences**
395
+ * library behaviour — its return drives token revocation — so it sits
396
+ * at the top level alongside `success`.
348
397
  *
349
398
  * Absent ⇒ the library still verifies the logout, emits a
350
399
  * `session_logout` audit event, and returns the protocol
@@ -364,6 +413,23 @@ export type IdPOptions = {
364
413
  */
365
414
  persistUpstreamTokens?: PersistUpstreamTokens
366
415
 
416
+ /**
417
+ * Optional hook fired as tokens are minted, carrying the derived
418
+ * `subjectId` alongside the claim it came from.
419
+ *
420
+ * Supply this if you need to revoke a user's tokens later — on
421
+ * offboarding, deactivation, or a membership change. `subjectId` is
422
+ * computed inside the library and appears nowhere else, so without
423
+ * this hook `TokenStore.revokeBySubject` / `revokeAllForSubject` are
424
+ * uncallable and `onLogout`'s `revokeSubject` has nothing to resolve
425
+ * against. See {@link OnTokenIssued} — in particular, one principal
426
+ * has many subject ids and they must be accumulated, not overwritten.
427
+ *
428
+ * Throwing aborts the grant; it runs before the refresh token is
429
+ * persisted so no unrecorded chain can survive a failure.
430
+ */
431
+ onTokenIssued?: OnTokenIssued
432
+
367
433
  /**
368
434
  * Optional RFC 8693 token-exchange hook. If absent, exchange
369
435
  * requests at `/token` return `unsupported_grant_type`. See the