@ambushsoftworks/nestjs-auth-graphql 0.11.0 → 0.14.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 (105) hide show
  1. package/CHANGELOG.md +300 -0
  2. package/README.md +411 -3
  3. package/dist/auth.module.d.ts +15 -1
  4. package/dist/auth.module.d.ts.map +1 -1
  5. package/dist/auth.module.js +114 -16
  6. package/dist/auth.module.js.map +1 -1
  7. package/dist/constants.d.ts +2 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +3 -1
  10. package/dist/constants.js.map +1 -1
  11. package/dist/decorators/require-scopes.decorator.d.ts +2 -0
  12. package/dist/decorators/require-scopes.decorator.d.ts.map +1 -0
  13. package/dist/decorators/require-scopes.decorator.js +8 -0
  14. package/dist/decorators/require-scopes.decorator.js.map +1 -0
  15. package/dist/guards/csrf.guard.d.ts +2 -0
  16. package/dist/guards/csrf.guard.d.ts.map +1 -1
  17. package/dist/guards/csrf.guard.js +9 -2
  18. package/dist/guards/csrf.guard.js.map +1 -1
  19. package/dist/guards/scope.guard.d.ts +8 -0
  20. package/dist/guards/scope.guard.d.ts.map +1 -0
  21. package/dist/guards/scope.guard.js +53 -0
  22. package/dist/guards/scope.guard.js.map +1 -0
  23. package/dist/index.d.ts +6 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +6 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/interfaces/api-key-repository.interface.d.ts +3 -0
  28. package/dist/interfaces/api-key-repository.interface.d.ts.map +1 -1
  29. package/dist/interfaces/biometric-repository.interface.d.ts +29 -17
  30. package/dist/interfaces/biometric-repository.interface.d.ts.map +1 -1
  31. package/dist/interfaces/biometric-verifier.interface.d.ts +10 -0
  32. package/dist/interfaces/biometric-verifier.interface.d.ts.map +1 -0
  33. package/dist/interfaces/{magic-link-repository.interface.js → biometric-verifier.interface.js} +1 -1
  34. package/dist/interfaces/biometric-verifier.interface.js.map +1 -0
  35. package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
  36. package/dist/resolvers/base-auth.resolver.js +0 -15
  37. package/dist/resolvers/base-auth.resolver.js.map +1 -1
  38. package/dist/services/auth.service.d.ts.map +1 -1
  39. package/dist/services/auth.service.js +10 -3
  40. package/dist/services/auth.service.js.map +1 -1
  41. package/dist/services/biometric-auth.service.d.ts +47 -17
  42. package/dist/services/biometric-auth.service.d.ts.map +1 -1
  43. package/dist/services/biometric-auth.service.js +109 -68
  44. package/dist/services/biometric-auth.service.js.map +1 -1
  45. package/dist/services/biometric-challenge.service.d.ts +27 -0
  46. package/dist/services/biometric-challenge.service.d.ts.map +1 -0
  47. package/dist/services/biometric-challenge.service.js +116 -0
  48. package/dist/services/biometric-challenge.service.js.map +1 -0
  49. package/dist/services/biometric-verification.service.d.ts.map +1 -1
  50. package/dist/services/biometric-verification.service.js +4 -0
  51. package/dist/services/biometric-verification.service.js.map +1 -1
  52. package/dist/services/verification.service.d.ts +2 -1
  53. package/dist/services/verification.service.d.ts.map +1 -1
  54. package/dist/services/verification.service.js +38 -8
  55. package/dist/services/verification.service.js.map +1 -1
  56. package/dist/strategies/api-key.strategy.d.ts +6 -1
  57. package/dist/strategies/api-key.strategy.d.ts.map +1 -1
  58. package/dist/strategies/api-key.strategy.js +40 -6
  59. package/dist/strategies/api-key.strategy.js.map +1 -1
  60. package/dist/strategies/external-jwt.strategy.d.ts +19 -0
  61. package/dist/strategies/external-jwt.strategy.d.ts.map +1 -0
  62. package/dist/strategies/external-jwt.strategy.js +124 -0
  63. package/dist/strategies/external-jwt.strategy.js.map +1 -0
  64. package/dist/strategies/jwt.strategy.d.ts.map +1 -1
  65. package/dist/strategies/jwt.strategy.js +1 -0
  66. package/dist/strategies/jwt.strategy.js.map +1 -1
  67. package/dist/test-utils/mock-repositories.d.ts.map +1 -1
  68. package/dist/test-utils/mock-repositories.js +8 -7
  69. package/dist/test-utils/mock-repositories.js.map +1 -1
  70. package/dist/utils/provider-helpers.d.ts +6 -0
  71. package/dist/utils/provider-helpers.d.ts.map +1 -1
  72. package/dist/utils/provider-helpers.js +4 -0
  73. package/dist/utils/provider-helpers.js.map +1 -1
  74. package/dist/utils/verification-base-url.d.ts +2 -0
  75. package/dist/utils/verification-base-url.d.ts.map +1 -0
  76. package/dist/utils/verification-base-url.js +19 -0
  77. package/dist/utils/verification-base-url.js.map +1 -0
  78. package/dist/verifiers/es256-device-key.verifier.d.ts +14 -0
  79. package/dist/verifiers/es256-device-key.verifier.d.ts.map +1 -0
  80. package/dist/verifiers/es256-device-key.verifier.js +42 -0
  81. package/dist/verifiers/es256-device-key.verifier.js.map +1 -0
  82. package/package.json +8 -2
  83. package/dist/interfaces/magic-link-repository.interface.d.ts +0 -6
  84. package/dist/interfaces/magic-link-repository.interface.d.ts.map +0 -1
  85. package/dist/interfaces/magic-link-repository.interface.js.map +0 -1
  86. package/dist/interfaces/password-reset-strategy.interface.d.ts +0 -7
  87. package/dist/interfaces/password-reset-strategy.interface.d.ts.map +0 -1
  88. package/dist/interfaces/password-reset-strategy.interface.js +0 -3
  89. package/dist/interfaces/password-reset-strategy.interface.js.map +0 -1
  90. package/dist/repositories/noop-biometric.repository.d.ts +0 -26
  91. package/dist/repositories/noop-biometric.repository.d.ts.map +0 -1
  92. package/dist/repositories/noop-biometric.repository.js +0 -56
  93. package/dist/repositories/noop-biometric.repository.js.map +0 -1
  94. package/dist/repositories/noop-magic-link.repository.d.ts +0 -9
  95. package/dist/repositories/noop-magic-link.repository.d.ts.map +0 -1
  96. package/dist/repositories/noop-magic-link.repository.js +0 -37
  97. package/dist/repositories/noop-magic-link.repository.js.map +0 -1
  98. package/dist/strategies/magic-link.strategy.d.ts +0 -16
  99. package/dist/strategies/magic-link.strategy.d.ts.map +0 -1
  100. package/dist/strategies/magic-link.strategy.js +0 -80
  101. package/dist/strategies/magic-link.strategy.js.map +0 -1
  102. package/dist/strategies/verification-code.strategy.d.ts +0 -11
  103. package/dist/strategies/verification-code.strategy.d.ts.map +0 -1
  104. package/dist/strategies/verification-code.strategy.js +0 -44
  105. package/dist/strategies/verification-code.strategy.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -27,6 +27,306 @@ for buried obligations. **The marker was introduced in 0.9.0 and has not been
27
27
  retro-applied**, so it is reliable from 0.9.0 onward only. Convention adopted
28
28
  from `@ambushsoftworks/nestjs-payments-graphql`.
29
29
 
30
+ ## [0.14.0] - 2026-09-29
31
+
32
+ Biometric authentication works. It has been exported since v0.1.3 and never did.
33
+
34
+ ### ⚠ Security
35
+
36
+ - **The previous `authenticateWithBiometric` verified nothing.** It took
37
+ `(userId, deviceId)`, checked only that an enrolled credential's id equalled the
38
+ supplied `deviceId`, and then issued an access and refresh token. No signature,
39
+ no challenge, no nonce; the public key `enableBiometric` stored was never read
40
+ by anything. Neither input is a secret, so anyone who knew a user id and an
41
+ enrolled device id could obtain a session. It also minted its own tokens, so it
42
+ bypassed the account-status check every other login goes through — a suspended
43
+ user could sign in.
44
+
45
+ **If you wired biometric authentication on 0.13.0 or earlier, treat any session
46
+ it issued as unauthenticated**, and revoke refresh tokens for affected users.
47
+ We know of no consumer that did: `IBiometricRepository` had no implementations,
48
+ and LiftIQ, the only app with biometrics, built its own rather than use this.
49
+
50
+ ### Added
51
+
52
+ - **A working challenge-response flow.**
53
+ ```
54
+ enrolCredential({ userId, publicKey, deviceName }) -> credentialId
55
+ requestChallenge(credentialId) -> { challengeId, challenge, expiresAt }
56
+ authenticateWithBiometric({ challengeId, credentialId, signature }) -> session
57
+ ```
58
+ 32-byte single-use challenges, 60-second default expiry, ES256 signature
59
+ verification, and a session issued through `AuthService.issueAuthSession` — the
60
+ same sink as every other login, so `assertUserActive`, cookie auth, realm claims
61
+ and `SESSION_ISSUED` all apply without being reimplemented.
62
+ - **`IBiometricVerifier`**, with `Es256DeviceKeyVerifier` as the default. The
63
+ verifier is selected by each credential's stored `algorithm`, so a credential
64
+ enrolled as ES256 is only ever verified as ES256 — the same pinning `JwtStrategy`
65
+ gained in 0.12.0. WebAuthn can be added later against this interface without
66
+ reshaping the port.
67
+ - **`biometric` module options**: `verifiers`, `challengeExpirySeconds`,
68
+ `challengeRateLimit`. Boot refuses an empty verifier list, two verifiers claiming
69
+ one algorithm, a non-positive expiry, and a rate limit without
70
+ `rateLimiterInstance`.
71
+ - **A per-IP rate limit** on challenge issuance alongside the per-credential one.
72
+ The per-credential limit cannot see a caller varying the credential id.
73
+
74
+ ### Changed — breaking
75
+
76
+ - **`IBiometricRepository` is reshaped**, composed from a new
77
+ `IBiometricChallengeStore`. It had no implementations, so this breaks nobody in
78
+ practice. Credentials now carry `algorithm`, `isActive`, `lastUsedAt` and
79
+ optional `metadata`; challenges are keyed on `credentialId`.
80
+
81
+ Its one hard contract: **`consumeChallenge` must be atomic.** A read, a check and
82
+ then a write lets two concurrent requests with one challenge both succeed. Use a
83
+ single conditional update and check the affected count — the README has the
84
+ Prisma form.
85
+
86
+ Two schema notes: `credentialId` on a challenge is **not** a foreign key (a
87
+ challenge is stored for ids that do not exist, so the signed-out request cannot
88
+ reveal which credentials are enrolled), and there is **no uniqueness** on any
89
+ device string.
90
+ - **`authenticateWithBiometric`'s signature changed** to
91
+ `({ challengeId, credentialId, signature }, opts)`.
92
+ - **`enableBiometric`, `disableBiometric` and `updateLastBiometricLogin` are gone
93
+ from `IUserRepositoryBiometric`.** Having credentials in two ports is why
94
+ enrolment and authentication used different stores and never met.
95
+ - **`NoOpBiometricRepository` is removed.** A no-op that accepts enrolments and
96
+ returns no credentials turns a missing implementation into a login that silently
97
+ never works. Without `biometricRepositoryInstance` the services are not
98
+ registered.
99
+ - **`credentialId` is generated by the package**, not by the client or the adapter.
100
+ It travels on an unauthenticated request, so it must be unguessable.
101
+
102
+ ### Security properties worth knowing
103
+
104
+ - **What it proves.** Possession of the enrolled private key — **not** that a
105
+ biometric check happened. Nothing in an ECDSA signature attests to that, and the
106
+ server cannot tell a hardware-backed key from a software one. Most mobile
107
+ biometric APIs accept a device PIN by default. Weigh that before making it a
108
+ sole factor.
109
+ - **Every biometric failure returns one 401.** Unknown credential, deactivated
110
+ credential, replayed or mismatched challenge, bad signature, missing verifier.
111
+ The route is reachable signed-out, so anything distinguishable reveals which
112
+ credentials exist. An inactive *account* is deliberately not flattened into it.
113
+ - **A failed attempt still burns the challenge**, so one challenge cannot be used
114
+ to try many signatures.
115
+
116
+ ### Documentation
117
+
118
+ - README: **Biometric Authentication**, leading with what a successful
119
+ authentication does and does not prove, the `IBiometricRepository` contract with
120
+ the atomic-consume example, and a `biometric` options table.
121
+ - CLAUDE.md: replaced a flow description that named three methods which never
122
+ existed and described verification the code never performed.
123
+
124
+ ### Testing
125
+
126
+ - 24 unit tests on the flow with real P-256 keys, real signatures and a real
127
+ `AuthService` — so the suspended-user case exercises `assertUserActive` rather
128
+ than a double. The spec they replace mocked both sides of every seam and
129
+ asserted the method returned tokens, which was true and hid that it returned
130
+ them without verifying anything.
131
+ - 9 end-to-end tests on both NestJS majors, including that the issued token works
132
+ on a guarded route, which is what proves the session came from the shared sink.
133
+ - 13 on the challenge service, including twenty concurrent claims on one challenge
134
+ yielding exactly one winner.
135
+ - Mutation checks: dropping the challenge/credential binding, forcing the verifier
136
+ true, ignoring `isActive`, minting tokens locally instead of via
137
+ `issueAuthSession`, and making the test store non-atomic. Each compiles; each
138
+ fails the suite.
139
+ ## [0.13.0] - 2026-09-29
140
+
141
+ **Never published separately — these changes ship inside `0.14.0`.** Both landed
142
+ on master before either was released, and `npm version` writes one version per
143
+ release, so `0.13.0` does not exist on npm. The entry stays because the changes
144
+ are real and worth reading on their own; if you are upgrading, everything here is
145
+ in `0.14.0`.
146
+
147
+ A documentation release, with one removal. Nothing in the runtime behaviour of
148
+ any supported path changes.
149
+
150
+ ### Removed
151
+
152
+ - **`MagicLinkStrategy`, `VerificationCodeStrategy`, `IPasswordResetStrategy`,
153
+ `IMagicLinkRepository` and `NoOpMagicLinkRepository`** are no longer exported,
154
+ and their files are gone. Nothing in the package consumed
155
+ `IPasswordResetStrategy` — `AuthService` calls `VerificationService` directly —
156
+ and `MagicLinkStrategy` could not be constructed through Nest DI at all,
157
+ because it injects a `'MAGIC_LINK_REPOSITORY'` token that `AuthModule` never
158
+ registered. The `NoOpMagicLinkRepository` docblock even showed a
159
+ `magicLinkRepositoryInstance` option that does not exist in
160
+ `AuthModuleOptions`.
161
+
162
+ This is a public API removal, hence the minor bump, but it cannot break a
163
+ working consumer: there was no supported way to reach any of it. If you
164
+ constructed `MagicLinkStrategy` yourself by registering that token, the file is
165
+ small and self-contained — copy it into your application. Password reset
166
+ through the package remains the 6-digit code flow, unchanged.
167
+
168
+ ### Documentation
169
+
170
+ - **README: new `Password Reset` section.** The feature had one passing mention,
171
+ despite the properties a consumer cannot infer and must not discover in
172
+ production: 3 attempts before the code is destroyed, the 15-minute expiry, the
173
+ 60-second per-user cooldown, the per-IP limit of 5/hour checked *before* the
174
+ user lookup for enumeration safety, and that a successful reset **revokes every
175
+ refresh token for that user**. It also states plainly that the response is
176
+ identical whether the address was sent a code, skipped or does not exist, and
177
+ that the distinguishing signal is `PASSWORD_RESET_REQUESTED` with
178
+ `result: 'SKIPPED_SOCIAL_ONLY'` on your `IAuthLogger`.
179
+ - **README: new `Phone & SMS Verification` section.** Five `perform*` methods
180
+ that previously appeared only as rows in a table. Records that SMS is
181
+ code-only — `features.verificationMode: 'token'` does not apply to it — and
182
+ that omitting `smsServiceInstance` substitutes a no-op, so codes are stored
183
+ but never delivered.
184
+ - **README: `sendgrid` and `twilio` option tables.** The only two of 42
185
+ `AuthModuleOptions` fields documented nowhere. Both are read solely by the
186
+ bundled services; a custom `IEmailService` or `ISmsService` ignores them.
187
+ - **`verification.baseUrl`** now names its exported type,
188
+ `VerificationBaseUrlResolver`.
189
+ - **`package.json` gains `homepage` and `bugs`.** npmjs.com renders both in the
190
+ sidebar, so until now the package page offered no route to the repository or
191
+ to filing an issue.
192
+
193
+ ## [0.12.0] - 2026-09-20
194
+
195
+ Ambush Desk asked for two things: verifying JWTs that other systems sign, and API
196
+ keys with scopes, expiry and prefixes. They also reported duplicate security
197
+ events, and checking that turned up a false one. Jobsites then filed two more:
198
+ a verification link that cannot follow a realm, and a password reset that
199
+ silently skips accounts it should not. See **Fixed**.
200
+
201
+ ### Fixed
202
+ - **Every signup, login and logout was logged twice.** `BaseAuthResolver` logged
203
+ `SIGNUP_SUCCESS`, `LOGIN_SUCCESS` and `LOGOUT_SUCCESS` after `AuthService` had
204
+ already logged each, so an `IAuthLogger` writing to an audit table stored two
205
+ rows per operation. `AuthService` keeps its copies: they also cover flows that
206
+ never reach the resolver. Reported by Ambush Desk, and reproduced end to end
207
+ before the fix.
208
+ - **A signup refused by `features.preventEnumerationOnSignup` logged
209
+ `SIGNUP_SUCCESS`** for an account that was never created, carrying the
210
+ synthetic response's random user ID. The resolver logged it without being able
211
+ to tell the signup apart from a real one; `AuthService`, which can, does not.
212
+ - **A password reset for an account with no password sent nothing, silently.**
213
+ Both `requestPasswordReset` and `resetPassword` tested `passwordHash == null`
214
+ and read it as "this user signed in with Google". It is also how an
215
+ operator-provisioned account starts, so an account created by an admin CLI
216
+ got no email — and the request returned the same generic success as a real
217
+ send, so the operator saw success for an account nobody could ever sign into.
218
+ Both guards now test for an actual social identity via the exported
219
+ `hasSocialIdentity(user)` (`googleId`, `facebookId` or `appleId`). A
220
+ social-only account behaves exactly as before. **The two guards have to agree**
221
+ — correcting only the first would email a credential the second refuses, which
222
+ is worse than the silence. A skip is now reported through `IAuthLogger` as
223
+ `PASSWORD_RESET_REQUESTED` with `result: 'SKIPPED_SOCIAL_ONLY'`; the public
224
+ response is deliberately unchanged, since `performRequestPasswordReset` is
225
+ reachable unauthenticated and a distinguishable return value would be an
226
+ enumeration oracle. Reported by jobsites.
227
+ - Consequence worth deciding on rather than discovering: a half-finished
228
+ signup row with no password and no social identity can now be claimed by
229
+ whoever controls the address. Previously it was unreachable, which is not
230
+ the same as safe.
231
+
232
+ ### Added
233
+ - **`createExternalJwtStrategy(name, options | { inject, useFactory })`**: a
234
+ Passport strategy for JWTs another system signs, with
235
+ `secretProvider(kid, request)`, required `algorithms`, `issuer`, `audience`,
236
+ `clockToleranceSec`, `maxTokenBytes` (default 8192), `mapClaims` and
237
+ `jwtFromRequest`. Separate from the package's `jwt` strategy: no realm check
238
+ and no user lookup. Every rejection fails softly, so it composes in
239
+ `createAuthGuard`. Requested by Ambush Desk.
240
+ - Unsafe configuration fails boot: missing or empty `algorithms`, `none`, an
241
+ algorithm jsonwebtoken does not know, HMAC mixed with asymmetric families
242
+ (which lets a public key be used as an HMAC secret), or one of the package's
243
+ own strategy names.
244
+ - The `{ inject, useFactory }` form wires constructor injection, so
245
+ `secretProvider` can use application services without a subclass.
246
+ - **API key scopes**: `@RequireScopes(...)` and `ScopeGuard`, with
247
+ `IApiKeyAccount.scopes?: string[]`. A key needs every listed scope, and
248
+ otherwise gets 403 `INSUFFICIENT_SCOPE` naming what the operation requires. A
249
+ key with no `scopes` has none. Signed-in people pass, since their access is
250
+ `PermissionGuard`'s job. Requested by Ambush Desk.
251
+ - **API key expiry**: `IApiKeyAccount.expiresAt?: Date | null`. A key past it is
252
+ refused with `401 API key has expired`. Requested by Ambush Desk.
253
+ - **`apiKey.headerName`**, such as `'X-API-Key'`, read before
254
+ `Authorization: Bearer`, which keeps working. `CsrfGuard` treats a request
255
+ carrying it as credentialed, as it already does for `Authorization`. Requested
256
+ by Ambush Desk.
257
+ - **`apiKey.prefix`**, such as `'ait_'`. A token without it is not an API key and
258
+ goes to the next strategy with no lookup; one with it that matches no key is
259
+ refused with `401 Invalid API key`, since it cannot be a JWT. That replaces the
260
+ requested `onUnknownKey: 'fail' | 'error'` switch. Requested by Ambush Desk.
261
+ - **`IApiKeyRepository.touchLastUsed?(id)`**, called once per successful match
262
+ and never for a refused key. It is not awaited, and a failure is logged rather
263
+ than failing the request. Requested by Ambush Desk.
264
+ - **`verification.baseUrl` accepts a resolver**: `string | ((realm?: string) =>
265
+ string)`. One deployment serving several brands needs a link that opens on the
266
+ site the user came from; the realm already reached the credential and was
267
+ dropped one line before the URL. It applies to both verification modes, since
268
+ the token link and the code page link are built from the same resolved base.
269
+ Purely additive — a string behaves exactly as before. Requested by jobsites.
270
+ - Called once per link and synchronous: resolve from a map, not a query.
271
+ - Must return a URL for `realm === undefined` too — a request exempted by
272
+ `realm.skip`, or a single-realm deployment, has no realm.
273
+ - Boot validation is necessarily weaker for a resolver: it is probed once with
274
+ `undefined`, because there is no request at startup to supply a realm. A
275
+ per-realm value is checked when the link is built, where token mode throws
276
+ (the link carries the only copy of the credential) and code mode warns and
277
+ sends without a link (the code is in the body).
278
+
279
+ ### Changed
280
+ - **The `jwt` strategy pins `algorithms: ['HS256']`**, the algorithm the module's
281
+ `JwtModule` signs with. Unpinned, jsonwebtoken 9 also accepted HS384 and HS512
282
+ tokens signed with `jwtSecret`. Tokens this package issues are unaffected.
283
+ - **`ApiKeyStrategy`'s constructor takes the `apiKey` options** as an optional
284
+ second argument. This matters only if you construct the strategy yourself.
285
+
286
+ ### Declined
287
+ - **Publishable, origin-restricted API keys.** A key an app embeds is public by
288
+ construction: it identifies an app rather than authenticating anyone, and
289
+ `Origin` is set freely by any non-browser client, so restricting it is abuse
290
+ control rather than authentication. Keeping them out means every credential
291
+ this package accepts is a secret one. Requested by Ambush Desk, which keeps
292
+ ingest keys as its own concept.
293
+
294
+ ### Documentation
295
+ - README: **External JWTs**; **API Key Authentication** extended with the prefix,
296
+ header, expiry, scopes and last-used tracking; `ScopeGuard` and
297
+ `@RequireScopes` in the guard and decorator tables; the new options; and
298
+ **Migrating to v0.12.0**.
299
+ - CLAUDE.md: **API Keys, Scopes and External JWTs**, and the rule that each
300
+ security event is logged once, by the layer that performs the action.
301
+ - README: **One API, several brands: `baseUrl` per realm**, the widened
302
+ `verification.baseUrl` row, and both new **Migrating to v0.12.0** notes.
303
+ - CLAUDE.md: base URL resolution is realm-aware through one seam, with the
304
+ warning not to answer it by handing the raw token to the email renderer; and
305
+ the rule that password reset tests for a social identity, never for a missing
306
+ password hash.
307
+
308
+ ### Testing
309
+ - **End-to-end** (`test/e2e/tokens-and-keys.e2e-spec.ts`), on both NestJS majors:
310
+ a customer token through `createAuthGuard(['customer-jwt'])` on an HTTP route
311
+ and in a resolver, both guard orders with staff tokens, and refusals for
312
+ another algorithm, an unknown key ID, another audience and an expired token.
313
+ For API keys: a staff JWT accepted with a prefix set, a key in `X-API-Key` with
314
+ its use recorded, an unknown prefixed key, an expired key, `@RequireScopes`
315
+ against keys and people, and the CSRF skip.
316
+ - **Signup, login and logout now run end to end through `BaseAuthResolver`**,
317
+ counting the events each produces, including a signup refused by
318
+ `preventEnumerationOnSignup`.
319
+ - **Per-realm links**, in `auth.service.verification-mode.spec.ts` against a real
320
+ `VerificationService`, and end to end in `auth-module.e2e-spec.ts` — the realm
321
+ is attached by `RealmMiddleware`, so only an assembled app proves it survives
322
+ the whole path from header to emailed link. Each test parses the rendered URL
323
+ and feeds the credential back through `resetPassword`, rather than asserting
324
+ that a send happened.
325
+ - **43 mutation checks**, each reintroducing one defect: every one failed a test.
326
+ The five added here include reverting each password-reset guard separately —
327
+ reverting only `resetPassword` is caught by the close-the-loop test, which is
328
+ the half-fix worth guarding against.
329
+
30
330
  ## [0.11.0] - 2026-09-15
31
331
 
32
332
  Ambush Desk asked for two things: refresh retries that survive a second API