@ambushsoftworks/nestjs-auth-graphql 0.9.0-rc.1 → 0.9.0-rc.3

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/CHANGELOG.md +822 -0
  2. package/README.md +14 -1
  3. package/dist/auth.module.d.ts +2 -0
  4. package/dist/auth.module.d.ts.map +1 -1
  5. package/dist/auth.module.js.map +1 -1
  6. package/dist/exceptions/account-inactive.exception.d.ts +5 -0
  7. package/dist/exceptions/account-inactive.exception.d.ts.map +1 -0
  8. package/dist/exceptions/account-inactive.exception.js +16 -0
  9. package/dist/exceptions/account-inactive.exception.js.map +1 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/interfaces/auth-logger.interface.d.ts +1 -0
  15. package/dist/interfaces/auth-logger.interface.d.ts.map +1 -1
  16. package/dist/interfaces/auth-logger.interface.js +1 -0
  17. package/dist/interfaces/auth-logger.interface.js.map +1 -1
  18. package/dist/interfaces/auth-user.interface.d.ts.map +1 -1
  19. package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
  20. package/dist/services/auth.service.d.ts +2 -0
  21. package/dist/services/auth.service.d.ts.map +1 -1
  22. package/dist/services/auth.service.js +18 -0
  23. package/dist/services/auth.service.js.map +1 -1
  24. package/dist/services/password-validation.service.js +1 -1
  25. package/dist/services/password-validation.service.js.map +1 -1
  26. package/dist/services/refresh-token.service.d.ts.map +1 -1
  27. package/dist/services/refresh-token.service.js +1 -0
  28. package/dist/services/refresh-token.service.js.map +1 -1
  29. package/dist/utils/account-status.d.ts +3 -0
  30. package/dist/utils/account-status.d.ts.map +1 -0
  31. package/dist/utils/account-status.js +19 -0
  32. package/dist/utils/account-status.js.map +1 -0
  33. package/package.json +10 -5
package/CHANGELOG.md ADDED
@@ -0,0 +1,822 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## Reading this file
9
+
10
+ Alongside the standard Keep-a-Changelog sections, releases may carry:
11
+
12
+ ### ⚠ External configuration required
13
+
14
+ Steps that must be performed **outside your codebase** — a frontend route, an
15
+ OAuth provider console, a DNS record, an environment variable in your
16
+ deployment platform — for the release to work.
17
+
18
+ These get their own heading because they are a different species from API
19
+ changes. A removed export, a new required field, a changed signature: your
20
+ compiler, your tests, or a code review will catch every one of them. A reset
21
+ link pointing at a route your frontend does not serve is invisible to all
22
+ three, and surfaces as a user who cannot complete a flow.
23
+
24
+ Grep for `⚠ External configuration required` across the versions you are
25
+ skipping to build an upgrade checklist mechanically, rather than reading prose
26
+ for buried obligations. **The marker was introduced in 0.9.0 and has not been
27
+ retro-applied**, so it is reliable from 0.9.0 onward only. Convention adopted
28
+ from `@ambushsoftworks/nestjs-payments-graphql`.
29
+
30
+ ## [0.9.0] - Unreleased
31
+
32
+ Three independent workstreams land here: a trim of the email interface surface
33
+ down to what the package actually fires, a fix making
34
+ `verificationMode: 'token'` functional for the first time, and enforcement of
35
+ account status — which had never been checked at all. See **Security** first.
36
+
37
+ Alongside them, `bcrypt` moves from 5 to 6, taking `tar@6.2.1` and its
38
+ advisories out of consumers' production dependency trees. No hash migration.
39
+
40
+ > Published as a release candidate under the `next` dist-tag first, so the
41
+ > verification and password-reset emails could be confirmed through a real
42
+ > mail client before reaching `latest`. That last hop — a mail client
43
+ > rendering the link — is exactly where the original defect lived and the one
44
+ > thing no test suite in this repo can exercise. Install the candidate with
45
+ > `npm install @ambushsoftworks/nestjs-auth-graphql@next`.
46
+ >
47
+ > **Test against `0.9.0-rc.3`.** It is `rc.2` plus the move to bcrypt 6 in
48
+ > **Security** below and its documentation — nothing else in the published
49
+ > package changed. `rc.1` predates the account-status fix in the same section
50
+ > and still has the suspension window open.
51
+
52
+ ### ⚠ External configuration required
53
+ - **`verificationMode: 'token'` requires your frontend to serve two routes.**
54
+ Token-mode links point at `<verification.baseUrl>/verify-email` and
55
+ `<verification.baseUrl>/reset-password`, each carrying `token` and `email`
56
+ query parameters. Your app must serve both paths, read both parameters, and
57
+ pass them to `verifyEmail` / `resetPassword`. Nothing in this package can
58
+ check that — a link to a route you do not serve compiles, tests green, sends
59
+ successfully, and 404s in the recipient's browser. `verification.baseUrl`
60
+ itself IS validated at boot; the routes behind it cannot be.
61
+ **Not applicable in `'code'` mode**, which is the default and ships no link.
62
+
63
+ ### Security
64
+ - **`status` and `deletedAt` were never checked — suspending a user revoked nothing.**
65
+ `IAuthUser.status` was documented as *"Checked during login to prevent
66
+ suspended/deleted users from authenticating"*. Nothing checked it, anywhere:
67
+ outside the `UserStatus` enum declaration and that docblock, `SUSPENDED` and
68
+ `DELETED` did not appear in `src/`. `login()` gated on rate limit, lockout,
69
+ user existence, password presence and password validity — never status.
70
+ `validateUser()`, the JWT path on every authenticated request, verified only
71
+ that the user still existed.
72
+
73
+ What provided revocation in practice was an **undocumented contract**: because
74
+ `refreshToken()` calls `findById` and rejects on `null`, revocation worked only
75
+ for consumers whose repository happened to filter revoked users. A consumer
76
+ who read the docblock and reasonably concluded the package handled it had an
77
+ **unbounded** exposure window — a suspended user kept refreshing indefinitely,
78
+ minting a fresh access token each time. Reported by the
79
+ nestjs-account-management team, who found it while planning a `suspendAccount`
80
+ operation; two consumers were affected in production.
81
+
82
+ Status is now enforced in four places: `login()` (after password verification,
83
+ so it cannot become an enumeration oracle), `validateUser()`, both branches of
84
+ `refreshToken()` including the grace-period cache, and as a backstop in the
85
+ private token-issuance sink every flow funnels through — so no path, including
86
+ `issueAuthSession`, can mint tokens for an inactive account. Rejection is
87
+ `AccountInactiveException` (code `ACCOUNT_INACTIVE`, HTTP 403) and logs the new
88
+ `ACCOUNT_INACTIVE_BLOCKED` security event with the phase.
89
+
90
+ **The default is a deny-list, and that is deliberate.** An account is inactive
91
+ when `deletedAt` is set, or `status` is `SUSPENDED`/`DELETED` compared
92
+ case-insensitively. It is *not* "anything that is not `ACTIVE`": `status` is
93
+ typed `string` so consumers can map their own model, and an allow-list would
94
+ have locked out every user of a consumer storing `'active'`, `'ENABLED'` or
95
+ `''` the moment they upgraded. A deny-list cannot produce a false rejection.
96
+ **If your vocabulary differs from `UserStatus`, the default cannot recognize
97
+ it** — supply the new `isUserActive` option.
98
+
99
+ **`jwtValidation: 'payload-only'` bounds revocation by token TTL rather than
100
+ making it immediate.** That mode skips the per-request lookup by design, so
101
+ there is nothing to check; a suspended user keeps working until their access
102
+ token expires. Refresh is still blocked, so the window is exactly one
103
+ access-token lifetime (15 minutes by default). This was previously
104
+ undocumented — the option's docblock said only that it "does not check if user
105
+ still exists" — and now states the consequence. No behavioral change to that
106
+ mode in this release.
107
+
108
+ - **`bcrypt` 5.1.1 → 6.0.0: `tar@6.2.1` is no longer in your production dependencies.**
109
+ `bcrypt@5` installed its native binary through `@mapbox/node-pre-gyp@1.0.11`,
110
+ which depends on `tar@6.2.1` — 1 critical and 8 high advisories (path
111
+ traversal, file overwrite, denial of service). All three were the last release
112
+ of their major, so nothing on the bcrypt 5 line fixes it, and consumers could
113
+ not fix it either: this package declared `bcrypt ^5.1.1`, so upgrading your
114
+ own `bcrypt` only nested a second copy. `bcrypt@6` ships prebuilt binaries
115
+ loaded by `node-gyp-build`; its dependency tree is two packages and audits
116
+ clean. Reported by Lift, whose production-dependency audit gate flagged this
117
+ package as high.
118
+
119
+ **The exposure was at install time, not on the request path.** `tar` ran
120
+ while installing bcrypt, to unpack the downloaded binary; at runtime
121
+ `node-pre-gyp` loads and `tar` does not. The advisories still failed
122
+ `npm audit --omit=dev`, which is how consumers gate a release.
123
+
124
+ **No hash migration, in either direction.** Hashes written by bcrypt 5 verify
125
+ under 6 and the reverse, so existing users sign in unchanged and rolling back
126
+ to 0.8.0 does not lock out anyone who set a password on 0.9.0. Checked across
127
+ `$2a$` and `$2b$`, costs 10 and 12, unicode, and passwords past bcrypt's
128
+ 72-byte limit, on Node 22 (glibc) and `node:20-alpine` (musl, no compiler
129
+ toolchain). `auth.service.password-hash-compat.spec.ts` pins it: it signs in
130
+ and changes passwords against hashes generated by bcrypt 5.1.1, which the
131
+ suite never regenerates.
132
+
133
+ **If your app also depends on `bcrypt` directly, move it to `^6.0.0`** in the
134
+ same deploy. Otherwise your own bcrypt 5 keeps `tar` in the tree.
135
+
136
+ ### Added
137
+ - **`isUserActive?: (user: IAuthUser) => boolean` module option.** Overrides the
138
+ default account-active predicate for consumers whose status vocabulary differs
139
+ from `UserStatus`. Called on login, on refresh, on every request in
140
+ `jwtValidation: 'full'` mode, and before any token pair is minted. Must be pure
141
+ and synchronous — it runs on the authenticated request path and receives the
142
+ already-loaded user. Also exported: `isUserActiveByDefault` (the default
143
+ predicate) and `AccountInactiveException`.
144
+
145
+ ### Fixed
146
+ - **`verificationMode: 'token'` produced unusable password-reset emails.** `AuthService.requestPasswordReset` built the reset URL by string concatenation (`${process.env.FRONTEND_URL}/reset-password`) with no query parameters, then passed the freshly minted credential separately — which `ConfigurableEmailService` discards in token mode. The recipient received a "Reset Password" button pointing at a bare page URL, with no path by which they could ever learn the token. Reported by the Ariadne team; reproduced end to end. No consumer configuration could work around it: both defects sat between `requestPasswordReset` and the renderer.
147
+ - **`verificationMode: 'token'` produced unusable verification emails.** `ConfigurableEmailService.sendVerificationEmail` assigned the raw `code` to the renderer's `verificationUrl` parameter, so the "Verify Email" button's href was the 6-digit code itself (e.g. `href="483920"`), which mail clients resolve as a relative path. Both parameters are typed `string`, so the compiler could not catch it.
148
+ - **`VerificationService.buildVerificationUrl` was never called by any send path.** The package shipped a correct URL builder (setting both `token` and `email`) alongside a second, incorrect one; the send paths used the one that loses the token. There is now exactly one place that knows the URL shape.
149
+ - **Token mode was never wired into the auth flows at all.** `AuthService` called `generateCode`/`validateCode` unconditionally; `generateToken`, `verifyToken`, and `buildVerificationUrl` existed and were unit tested but unreachable from signup, resend, verify, request-reset, or reset. `ConfigurableEmailService` was the only consumer of the `verificationMode` flag, and read it at a layer that has a credential but no URL — which is why it fabricated one.
150
+ - **`expiresInMinutes` was accepted and discarded** (`void expiresInMinutes`) by `ConfigurableEmailService.sendVerificationEmail`, so email copy could not state an accurate expiry. It is now plumbed to the renderer and reflects the active mode's real expiry (15 min for codes, `verification.tokenExpiresInMinutes` for tokens).
151
+
152
+ ### Added
153
+ - **`IAuthLifecycleHooks.onAccountLocked(userId, email, ipAddress, lockDurationSeconds, realm?)`** — fires from `BruteForceProtectionService.recordFailedAttempt` on the not-locked → locked transition. Subsequent failed attempts while already locked do NOT re-fire; use `onAuthFailure` for per-attempt signal. Fire-and-forget (`void`, not `Promise`) — the lock is already recorded by the time the hook runs, and hook failures are caught and logged rather than rolling it back. This is the wiring point for account-locked notification email now that the package no longer sends one itself.
154
+ - **`AuthService.readRefreshTokenFromCookie(req)`** — public symmetric counterpart to `setAuthCookies` / `clearAuthCookies`. Reads `refresh_token` / `__Host-refresh_token` from the request. Useful when wrapping the resolver methods yourself.
155
+ - **`VerificationService.issueCredential(identifier, type, realm?)`** — mode-agnostic issuance returning `IssuedVerificationCredential` (`{ credential, url?, expiresInMinutes, mode }`). Mints the credential and builds its matching URL under a single mode decision, so the two cannot describe different verification schemes.
156
+ - **`VerificationService.verifyCredential(identifier, type, submitted, realm?)`** — the counterpart; branches on the same mode, making it structurally impossible to issue under one scheme and validate under the other.
157
+ - **`IssuedVerificationCredential`** — new exported interface.
158
+ - **`VerificationService.codeExpiresInMinutes` / `tokenExpiresInMinutes` / `credentialExpiresInMinutes`** getters — single source for expiry values previously duplicated as literals across services.
159
+ - **Boot-time validation: `verificationMode: 'token'` requires an absolute http(s) `verification.baseUrl`**, enforced by `validateAuthModuleOptions` → `InvalidAuthConfigException`. Token-mode emails carry no readable credential, so a missing base URL yields mail that delivers successfully and cannot be acted on; this makes it a deploy-time failure rather than a support ticket. Follows the existing `bruteForce.ipRateLimit` ↔ `rateLimiterInstance` precedent. **Code mode is unaffected — `baseUrl` remains optional there.**
160
+ - **`ConfigurableEmailService` refuses to send a token-mode email with no link**, throwing `InternalServerErrorException` rather than delivering an unusable message.
161
+ - Expiry line in the default renderer (HTML + plain text), sourced from the caller and omitted entirely when unknown rather than guessing.
162
+ - **`NoOpEmailSender` now logs every link in the message it would have sent** (HTML entities decoded, duplicates collapsed, and an explicit "contains no links" line for code-mode mail). Previously it logged only recipient, sender, and subject, so locally "an email was sent" was the entirety of what a developer could observe and a dead href was undetectable without a real mailbox — the exact blind spot that let unusable links reach production. Walking an auth flow locally is now: trigger it, read the link from the log, open it. Suggested by the Ariadne team, who had independently built the same thing on their side.
163
+
164
+ - **`RefreshTokenService` held the Node event loop open.** Its constructor
165
+ starts a 60-second sweep over the in-memory refresh-token cache, and the
166
+ timer was never `unref()`d. `onModuleDestroy` clears it, so a normal NestJS
167
+ shutdown was unaffected — but any process that finished its work and expected
168
+ to exit (a script, a CLI, a short-lived job importing the package) would hang
169
+ until something killed it, and Jest workers had to be force-exited. The timer
170
+ is now unref'd: it still fires for as long as the process is alive for its own
171
+ reasons, but no longer keeps it alive. `onModuleDestroy` still clears it.
172
+
173
+ ### Fixed (release tooling)
174
+ - **`npm run lint` had never worked.** Five ESLint packages were in
175
+ `devDependencies` and the script had been present for months, but no config
176
+ file existed anywhere, so it failed on "couldn't find a configuration file" —
177
+ and CI never invoked it, so nothing surfaced. The glob also referenced a
178
+ `tests/` directory that does not exist, which fails ESLint on its own. Adds
179
+ `.eslintrc.js`, splits `lint` (checks) from `lint:fix` (mutates) so CI can
180
+ run the check form, and wires `npm run lint` into the pipeline. The existing
181
+ codebase produced exactly one violation.
182
+ - **Test job now runs on the default branch.** It ran only on merge requests
183
+ and tags, so a semantic conflict introduced by a merge — green on both sides
184
+ in isolation — would not surface until someone cut a release.
185
+ - **`--forceExit` removed from the CI test command**, now that nothing holds
186
+ the event loop open. Force-exiting masks precisely the leak fixed above.
187
+ - **`CHANGELOG.md` is now published to npm** (added to `files`), so consumers
188
+ can read release notes without leaving the registry.
189
+ - **Prerelease tags no longer publish as `latest`.** `.gitlab-ci.yml` now detects a prerelease tag (`v1.2.3-rc.1`, `-alpha`, `-beta`) and publishes under the `next` dist-tag. Previously `npm publish` defaulted to `latest` for every tag, so cutting an RC would have served it to anyone running a bare `npm install`.
190
+
191
+ ### Changed
192
+ - **`BaseAuthResolver.performRefreshToken` and `performLogout` are now cookie/input symmetric.** When `features.cookieAuth` is enabled and `input.refreshToken` is empty, both read the refresh cookie instead. Browser SPAs cannot read HttpOnly cookies, so they send an empty input and rely on the credentialed request carrying the cookie. Explicit `input.refreshToken` still takes precedence for mobile/CLI callers. Both empty → `UnauthorizedException('Refresh token required')`. **Consumer DTOs must mark `refreshToken` `@IsOptional()`** (or `@IsString()` without `@IsNotEmpty()`) when targeting browser cookie auth, otherwise validation rejects the empty value before the resolver ever runs.
193
+ - **`IEmailService.sendVerificationEmail`** gained an optional 4th parameter `verificationUrl?: string`; **`sendPasswordResetEmail`** gained an optional 4th parameter `expiresInMinutes?: number`. Both are optional, so existing implementations continue to satisfy the interface unchanged.
194
+ - **`IEmailTemplateRenderer.renderVerificationEmail` / `renderPasswordResetEmail`** params gained optional `expiresInMinutes?: number`.
195
+ - **Base URL resolution consolidated on `verification.baseUrl`.** `AuthService` no longer reads `process.env.FRONTEND_URL` directly. In code mode the legacy env var is still honored as a fallback (deprecated, logged once); in token mode `verification.baseUrl` is required and the env fallback is deliberately not consulted.
196
+ - Default renderer copy is now mode-aware — a token-mode verification email no longer instructs the reader to "enter the code below" when the email contains no code.
197
+ - `PASSWORD_RESET_COMPLETED` security-event metadata now reports the real `method` (`'code'` or `'token'`) instead of a hardcoded `'code'`.
198
+ - **`engines.node` is now declared as `>=18`**, which bcrypt 6 requires. This declares the floor rather than raising it: `google-auth-library@10`, a dependency since 0.1.3, already required Node 18, so no install that worked before is excluded. npm warns on an older Node rather than refusing.
199
+
200
+ ### Breaking
201
+ - **`IEmailService` — four notification methods removed.** The package never fired these on its own; they were interface surface consumers had to implement for no benefit. Notification beyond verification, password-reset, and password-changed is now consumer-owned via lifecycle hooks. Only affects consumers who implement `IEmailService` **directly** — the bundled `ConfigurableEmailService`, `SendGridEmailService`, and `NoOpEmailService` are updated, so `emailServiceInstance` / `email` users need no action. **If you did implement them, those emails were never sent** — the package had no call site for any of the four, so an implementation that looked wired up was dead code, and moving it onto the hooks below is the first time it will run. Raised by Lift.
202
+ - `sendAccountLockedEmail` → wire from the new `onAccountLocked` hook
203
+ - `sendWelcomeEmail` → wire from `onSignup` (or `onEmailVerified`)
204
+ - `sendAccountLinkedEmail` → wire from `onOAuthAccountLinked`
205
+ - `sendAccountUnlinkedEmail` → wire from your unlink resolver directly
206
+ - **`IEmailTemplateRenderer.renderLoginAlertEmail` removed.** The package never fired a login-from-new-device event, so nothing rendered it. Device-fingerprint storage and the send belong on the consumer side.
207
+ - **The corresponding renderers were deliberately kept.** `renderWelcomeEmail`, `renderAccountLockedEmail`, and `renderEmailChangedEmail` remain on `IEmailTemplateRenderer` and on `DefaultEmailTemplateRenderer`. Migration path for each removed send method is therefore: call the renderer you already have from your hook, then hand the result to your own `IEmailSender` — you keep the branded templates without the package pretending to own the trigger.
208
+
209
+ ### Documentation
210
+ - **`IBruteForceRepository.deleteOlderThan` contract clarified.** Documented explicitly that it is a retention operation ("prune whatever attempt history you keep, if any"), not a guarantee the caller can rely on, and that **a no-op returning `0` is a valid implementation** for schemas keeping no attempt history — e.g. `failedLoginAttempts` / `lockedUntil` as columns on the user row, overwritten in place. Raised as a question by the Ariadne team, whose schema is exactly that shape.
211
+ - README: the `deleteOlderThan` Prisma example no longer uses `createdAt`, and says to substitute whichever column records when an attempt happened. The name is schema-specific, and a copied name that happens to exist in your schema with a different meaning compiles and prunes the wrong rows. Raised by Lift, whose column is `attemptedAt`.
212
+ - README: **Installation** states the Node.js 18 floor and the platforms bcrypt ships prebuilt binaries for; **Migrating to v0.9.0** covers the bcrypt move.
213
+ - README: the two verification modes differ in **where the secret lives**, not merely in formatting — token mode puts the credential in the link, code mode keeps it in the body so it never reaches browser history, referrer headers, or proxy logs.
214
+ - README: new "Migrating to v0.9.0" section covering both workstreams — the removed interface methods with their hook-based replacements, and the action-required items for token-mode consumers (`verification.baseUrl` now required at boot; `resetPassword`/`verifyEmail` now receive a 128-character token, so input DTO constraints sized for 6-digit codes must be relaxed).
215
+ - README: "Verification Modes" section rewritten with a code-vs-token comparison table, the exact link format the package emits, base-URL resolution order, and guidance for custom `IEmailService` implementations.
216
+ - CLAUDE.md: documents the single-place-for-the-mode-branch rule, the prohibition on calling `generateCode`/`generateToken` directly from `AuthService`, and the testing rule derived from how these bugs escaped.
217
+ - LOCAL_DEVELOPMENT.md: added guidance for pnpm/yarn consumers (`link:` protocol rather than `npm link`), `npm pack` as the recommended way to verify a fix pre-release, and a rewritten "Nest can't resolve dependencies" entry covering duplicate peer-dependency resolution under symlinks.
218
+
219
+ ### Notes
220
+ - **Backward compatibility of the verification fix:** code mode — the default, and what every existing consumer runs unless they opted into `'token'` — behaves identically, including the `FRONTEND_URL` fallback chain. Token mode was non-functional before this release, so there is no working behavior to preserve. The breaking changes in this release come from the email interface trim above, not from the verification fix.
221
+ - **Why not a one-line patch:** wiring `buildVerificationUrl` into the existing send path would have produced a working link carrying a **6-digit code** as its `token` parameter — functional, but shipping code-mode entropy in a URL that lands in inboxes, referrer headers, and proxy logs. Token mode now mints real tokens end to end.
222
+ - **Test-design change.** Both bugs survived because each side of the seam was unit tested against arguments the real call site never produced: `configurable-email.service.spec.ts` passed a ready-made URL in as the `code` parameter, so the assertion passed while production sent a bare digit string. New tests parse the rendered URL and feed the credential back through `verifyEmail`/`resetPassword` — see `auth.service.verification-mode.spec.ts` and `verification-credential.spec.ts`. Both fixes were verified by reintroducing each bug and confirming the new tests fail.
223
+
224
+ ## [0.8.0] - 2026-05-14
225
+
226
+ ### Added
227
+ - **Async `IRealmExtractor` and `ITenantExtractor` returns** — both interfaces now accept `string | null | Promise<string | null>` from their extractor method. `RealmMiddleware` and `TenantGuard` await the result when a Promise is returned, with no microtask overhead for sync returns (`instanceof Promise` gate). Enables consumers to resolve realm/tenant from a database lookup without bootstrap-cache workarounds. Async extractor throws (rejected Promise or sync `throw`) are treated as `400 Bad Request`, identical to the existing sync-`null` path; the internal cause is logged server-side via Nest's `Logger` but is NOT leaked to the client. Backward-compatible — existing sync extractors compile unchanged.
228
+ - **`BruteForceProtectionService.cleanupOldAttempts(olderThan: Date): Promise<number>`** — implemented. Calls through to the new `IBruteForceRepository.deleteOlderThan(beforeDate)` method and returns the deletion count. The package does not register a scheduler — wire your own cron with your own retention policy. See README "Brute Force Protection" section for an `@nestjs/schedule` example.
229
+ - **`BruteForceProtectionService.checkIpRateLimit(ipAddress, realm?)`** — opt-in implementation behind the new `bruteForce.ipRateLimit` config slot. Reuses the existing `IRateLimiter` infrastructure (the same one used for password-reset throttling) — NO new interface or injection token. Behavior is unchanged when `bruteForce.ipRateLimit` is omitted: returns `false`. The previous per-request "not implemented" WARN log is replaced with a one-time WARN on first call. When `bruteForce.ipRateLimit` IS configured, consumers MUST also explicitly provide `rateLimiterInstance` — `AuthModule.forRootAsync` throws `InvalidAuthConfigException` at boot otherwise (the default `InMemoryRateLimiterService` is silently broken in multi-replica deployments and is not auto-applied for IP rate limiting). Realm-aware: when `realm` is provided, rate-limit keys are prefixed with the realm to prevent cross-realm collisions on a shared store.
230
+ - **`AuthService.issueAuthSession(userId, res?, realm?)`** — new public method that mints an access + refresh pair, persists the refresh token, and sets cookies when `cookieAuth` is enabled AND `res` is supplied. Use after `changePassword()` to keep the current device logged in (other devices stay logged out — `changePassword` already revokes all refresh tokens). Also useful for admin-driven account creation, post-MFA-enrollment session bootstrap, and any flow that needs a session without going through `login()`. The caller is responsible for verifying the user's identity — this method takes a `userId` and trusts it.
231
+ - **`SecurityEvent.IP_RATE_LIMIT_EXCEEDED`** — emitted by `checkIpRateLimit` when an IP exceeds the configured `bruteForce.ipRateLimit` threshold. Metadata: `ipHash`, `realm`, `remainingAttempts`, `resetAt`, `maxAttempts`, `windowMs`. Raw IP is never logged.
232
+ - **`SecurityEvent.SESSION_ISSUED`** — emitted by `issueAuthSession`. Distinct from `LOGIN_SUCCESS` because no credential was verified at this layer. Metadata: `userId`, `realm`, `method: 'issueAuthSession'`.
233
+ - **`hashIp(ipAddress, salt)` utility** — exported HMAC-SHA256-based IP hashing helper used by `checkIpRateLimit`. Pure function: the salt is passed in by the caller (resolved inside `BruteForceProtectionService` from `AUTH_IP_HASH_SALT` env var, falling back to `options.jwtSecret`, with a one-time INFO log on the fallback path when `bruteForce.ipRateLimit` is configured). Threading the salt through the options object (rather than reading `process.env.JWT_SECRET` directly) keeps the package compatible with consumers wiring their JWT secret via `@nestjs/config` / a secrets manager. Output is a 16-character hex prefix.
234
+ - **`InvalidAuthConfigException`** — new exported exception thrown at boot from `AuthModule.forRootAsync` when configuration is internally inconsistent (currently: `bruteForce.ipRateLimit` set without explicit `rateLimiterInstance`). Extends native `Error`, NOT `HttpException` — this is a startup wiring error, not a runtime HTTP error.
235
+ - **`validateAuthModuleOptions(opts)`** — exported (for unit testing) helper invoked inside the `forRootAsync` factory. Currently checks the `bruteForce.ipRateLimit` ↔ `rateLimiterInstance` invariant.
236
+ - **`AUTH_IP_HASH_SALT` env var** — recommended dedicated salt for `hashIp`. Falls back to `JWT_SECRET` with a one-time INFO log when unset.
237
+
238
+ ### Changed
239
+ - **`BruteForceProtectionService` constructor** — now requires `AUTH_MODULE_OPTIONS` and `RATE_LIMITER` injections in addition to the existing args. Internal change; Nest DI handles it. Consumer test code that constructs the service directly with `new BruteForceProtectionService(...)` must update its argument list.
240
+ - **`RealmMiddleware.use` is now async** (`Promise<void>`). Express middleware accepts both shapes — no impact on consumers.
241
+
242
+ ### Breaking
243
+ - **`BruteForceProtectionService.cleanupOldAttempts(olderThan: Date)`** — signature changed from no-arg `Promise<void>` to 1-arg `Promise<number>`. The previous no-arg version was a documented stub that did nothing, so callers wired into a cron were no-op'ing. Migration: `await bruteForce.cleanupOldAttempts(new Date(Date.now() - 90 * 24 * 60 * 60 * 1000));` (90-day retention).
244
+ - **`IBruteForceRepository`** — new required method `deleteOlderThan(beforeDate: Date): Promise<number>`. Consumer implementations must add this method. Reference: `NoOpBruteForceRepository.deleteOlderThan` returns `0`. Prisma example: `return (await this.prisma.bruteForceAttempt.deleteMany({ where: { createdAt: { lt: beforeDate } } })).count;`.
245
+
246
+ ### Documentation
247
+ - Tightened JSDoc on every `BaseAuthResolver.perform*` method to include a copy-paste-ready `@Mutation`/`@Query` snippet — actionable guidance instead of "consumer MUST override and add @Mutation."
248
+ - New reference file `examples/full-resolver-template.ts` shipped in the published package. Consumers can copy this file wholesale as a starting point for their own AuthResolver. Listed in `package.json` `files` array (alongside `dist`, `README.md`, `LICENSE`).
249
+ - README updated: new "IP Rate Limiting" and "Brute-Force Cleanup" subsections under Brute Force Protection; new "Stay Logged In After Password Change" subsection covering the `changePassword` + `issueAuthSession` two-call pattern; async-extractor note added to the Realm-Based Identity Isolation and Multi-Tenancy sections; configuration table extended with `bruteForce.ipRateLimit` and `AUTH_IP_HASH_SALT`; security-events list extended with `IP_RATE_LIMIT_EXCEEDED` and `SESSION_ISSUED`; `BaseAuthResolver` section now points consumers at `examples/full-resolver-template.ts`.
250
+
251
+ ### Notes
252
+ - IP rate limit threshold is inclusive: `maxAttempts` is the first BLOCKED attempt, not the first allowed-over. With `maxAttempts: 5`, the user is blocked starting on the 5th attempt within the window.
253
+ - Auto-cron registration (`@nestjs/schedule`) was considered for `cleanupOldAttempts` and explicitly rejected: pinning consumers to a scheduler is the wrong layer. The package exposes the working primitive; consumers wire their own cron.
254
+ - A separate `IRateLimitStore` interface was considered for IP rate limiting and rejected: the existing `IRateLimiter` already covers the use case (its JSDoc explicitly lists "login attempt throttling"). Adding a parallel store would force consumers to wire two equivalent interfaces.
255
+ - A `BaseAuthResolver` mutation-exposure feature was requested and rejected. The package's compiled output cannot register `@Mutation` decorators that consumers' GraphQL schemas pick up — TypeScript decorator metadata is not preserved across module boundaries (see `src/resolvers/base-auth.resolver.ts:38-40`). The two consolation deliverables above replace the rejected feature.
256
+ - A `pwv` (password-version) JWT claim with immediate access-token revocation on `changePassword` is out of scope for this release; tracked for a future minor.
257
+
258
+ ## [0.7.2] - 2026-05-12
259
+
260
+ ### Fixed
261
+ - **CI publish pipeline** - Switched the publish job to `node:24` (ships with npm 11+) and removed the brittle `npm install -g npm@latest` self-upgrade step that failed for v0.7.0 and v0.7.1 with `MODULE_NOT_FOUND: promise-retry`. The test job remains on `node:22` to match the realistic consumer runtime.
262
+ - **Release plumbing** - Re-cuts the v0.7.0 release. No code changes from v0.7.0; CHANGELOG is brought up to date with the 0.4.0–0.7.0 release line.
263
+
264
+ ## [0.7.1] - 2026-05-12
265
+
266
+ Skipped — tag pushed but publish failed in CI before reaching npm. Superseded by 0.7.2.
267
+
268
+ ## [0.7.0] - 2026-05-12
269
+
270
+ ### Added
271
+ - **Realm-Based Identity Isolation** - Realms partition the user identity space so users in one realm are invisible to another. The same email address can exist independently in different realms, each with its own password, tokens, and verification state. Designed for white-label platforms, multi-brand businesses, and franchise systems.
272
+ - `IRealmExtractor` interface — extract a realm identifier from each request (subdomain, header, JWT claim, etc.)
273
+ - `RealmMiddleware` — registered automatically by `AuthModule` when an extractor is provided; runs before guards and rejects requests with `400 Bad Request` when the extractor returns `null` (strict mode)
274
+ - `@CurrentRealm()` decorator — pulls the resolved realm out of the GraphQL context
275
+ - `realm` claim embedded in JWT access tokens; `JwtStrategy` validates the claim against the request realm (mismatch → `401 Unauthorized`)
276
+ - Identity-resolution repository methods (`findByEmail`, `findByPhoneNumber`, `findByOAuthProvider`, `create`) accept an optional `realm` parameter so consumer implementations can scope queries
277
+ - `BruteForceProtectionService` and `VerificationService` accept an optional `realm` parameter; in-memory rate limiter keys are prefixed with the realm to prevent cross-realm interference
278
+ - `CreateUserData.realm` field on user creation payloads
279
+ - Opt-in: provide `realmExtractorInstance` in `AuthModuleOptions`. No separate feature flag. Existing single-realm consumers are unaffected.
280
+ - Files: `src/interfaces/realm-extractor.interface.ts`, `src/middleware/realm.middleware.ts`, `src/decorators/current-realm.decorator.ts`, plus updates across `auth.service.ts`, `brute-force-protection.service.ts`, `verification.service.ts`, `jwt.strategy.ts`, `base-auth.resolver.ts`
281
+
282
+ ### Fixed
283
+ - Documentation accuracy pass — corrected several inaccuracies in the README and CLAUDE.md around realm wiring and middleware registration
284
+
285
+ ### Notes
286
+ - The legacy OAuth controller callback methods (deprecated) are intentionally **not** realm-aware. Use the GraphQL mutations and token-based OAuth endpoints for realm-scoped flows.
287
+
288
+ ## [0.6.7] - 2026-05-01
289
+
290
+ ### Added
291
+ - **Code Quality Audit (Phases 3–7)** - Comprehensive deduplication, type-safety, dead-code, and guard-cleanup pass across the codebase
292
+ - Extracted `generateTokenPairAndStore()` to deduplicate token generation logic
293
+ - Extracted `mapToRepositoryType()` and `resolveUser()` helpers in `VerificationService`
294
+ - Removed dead code paths and tightened types in several services
295
+
296
+ ### Fixed
297
+ - **`ApiKeyStrategy`** - Returns `null` instead of throwing when the auth header is missing, allowing strategy chains (JWT → API key → anonymous) to fall through cleanly
298
+
299
+ ### Changed
300
+ - **README rewrite** - Documentation overhauled for accuracy against the current feature set (cookie auth, CSRF, API keys, multi-tenancy, magic links, biometrics)
301
+ - **Publishing checklist** - Added a required "review and update README before tagging" step to `CLAUDE.md`
302
+
303
+ ## [0.6.5] - 2026-04 (CI tracking releases)
304
+
305
+ ### Fixed
306
+ - CI publishing pipeline: corrected OIDC audience, upgraded npm to 11.5.1+, added `SIGSTORE_ID_TOKEN` for provenance, switched to `NPM_ID_TOKEN` for trusted publishing
307
+ - Multiple iterative CI fixes published as 0.6.1 – 0.6.5 while npm Trusted Publishing was being wired up
308
+
309
+ ## [0.6.0]
310
+
311
+ ### Added
312
+ - **Email Template Architecture (P4)** - Branded email templates with token verification support, customizable per-consumer
313
+ - **Security Hardening (P5)** - `__Host-` cookie prefix, `Referrer-Policy` header, additional cookie security defaults, and supporting documentation
314
+ - GitLab CI with npm Trusted Publishing (OIDC) — eliminates need for long-lived npm tokens
315
+
316
+ ### Fixed
317
+ - `JwtModule.expiresIn` type compatibility issue
318
+
319
+ ## [0.5.0]
320
+
321
+ ### Added
322
+ - **Cookie-Based Authentication (P2)** - HTTP-only access/refresh token cookies as an alternative to bearer tokens
323
+ - **CSRF Guard** - Double-submit cookie CSRF protection that pairs with cookie auth
324
+ - **JWT Payload Factory (P3)** - `IJwtPayloadFactory` interface for customizing JWT claims; default factory provided; wired through `AuthService` and `JwtStrategy`
325
+ - **API Key Strategy** - `ApiKeyStrategy` + `IApiKeyRepository` for service-account / non-interactive authentication
326
+ - **Tenancy Layer (P1b)** - `TenantGuard`, `PermissionGuard`, `createAuthGuard()` factory, `HeaderTenantExtractor`, NoOp tenant implementations, `ITenantRepository`, `IResourcePermissionRepository`
327
+ - **Auth Decorators** - `@Public()`, `@SkipTenant()`, `@RequirePermissions()`, `@ResourceScope()`, plus supporting metadata keys
328
+ - Module options expansion to wire all of the above through `AuthModule.forRootAsync()`
329
+
330
+ ## [0.4.0]
331
+
332
+ ### Added
333
+ - **Unified OAuth Methods on `IUserRepository`** - Consolidated `findByGoogleId`/`findByFacebookId` and friends into provider-agnostic `findByOAuthProvider(provider, providerId, realm?)` and related methods
334
+ - **Modular Interface Split** - `IUserRepository` decomposed into `IUserRepositoryCore`, `IUserRepositoryOAuth`, `IUserRepositoryBiometric`, `IUserRepositoryPhone`. `IAuthUser` similarly split into sub-interfaces. Consumers can implement only the sub-interfaces relevant to the features they enable.
335
+ - Widened peer dependency range for NestJS 11 compatibility; added `passport-custom` to peer deps
336
+
337
+ ## [0.3.4] - 2025-11-16
338
+
339
+ ### Fixed
340
+ - **Phone Verification for OAuth Users** - Fixed "User not found" error when OAuth users (Google/Facebook) attempt to add phone verification
341
+ - Added `generateCodeForUserId()`, `validateCodeForUserId()`, `hasActiveCodeForUserId()` to `VerificationService`
342
+ - Updated `sendPhoneVerification()`, `verifyPhone()`, and `resendPhoneVerification()` to use user ID-based methods
343
+ - Previous implementation required phone number to exist in database before verification could be initiated
344
+ - New implementation uses user ID for code generation/validation, allowing OAuth users to add phone numbers
345
+ - Files: `src/services/verification.service.ts`, `src/services/auth.service.ts`
346
+
347
+ ## [0.3.0] - 2025-11-16
348
+
349
+ ### Added
350
+
351
+ #### Enhancement #1: Configurable Password Policy
352
+ - **PasswordPolicyConfig Interface** - Customize password strength requirements for signup and password reset
353
+ - Configurable: `minLength`, `maxLength`, `requireUppercase`, `requireLowercase`, `requireNumber`, `requireSpecialChar`
354
+ - Custom validator function support for advanced requirements (e.g., leaked password detection, dictionary checks)
355
+ - Default policy: min 8 chars, uppercase, lowercase, number (backward compatible with v0.2.x)
356
+ - Files: `src/interfaces/password-policy-config.interface.ts`
357
+
358
+ - **PasswordValidationService** - Centralized password validation with configurable rules
359
+ - Fail-fast validation (returns all errors at once for better UX)
360
+ - Support for async custom validators
361
+ - Immutable policy configuration via `getPolicy()`
362
+ - 26 comprehensive unit tests
363
+ - Files: `src/services/password-validation.service.ts`, `src/services/password-validation.service.spec.ts`
364
+
365
+ #### Enhancement #2: IP-Based Rate Limiting
366
+ - **IRateLimiter Interface** - IP-based rate limiting to prevent abuse and email enumeration
367
+ - Methods: `checkRateLimit()`, `recordAttempt()`
368
+ - Sliding window algorithm for accurate rate limiting
369
+ - Files: `src/interfaces/rate-limiter.interface.ts`
370
+
371
+ - **InMemoryRateLimiterService** - Production-ready rate limiter for single-instance deployments
372
+ - Sliding window algorithm
373
+ - Automatic cleanup of expired entries (prevents memory leaks)
374
+ - Default: 5 password reset requests per hour per IP
375
+ - 23 comprehensive unit tests (including edge cases, cleanup, concurrency)
376
+ - Files: `src/services/in-memory-rate-limiter.service.ts`, `src/services/in-memory-rate-limiter.service.spec.ts`
377
+
378
+ - **NoOpRateLimiter** - Fallback implementation for development/testing
379
+ - Logs rate limit checks without enforcing limits
380
+ - Useful for testing environments
381
+ - Files: `src/repositories/noop-rate-limiter.ts`
382
+
383
+ - **IP Rate Limiting in Password Reset** - Enhanced security for `requestPasswordReset()`
384
+ - Checks IP rate limit BEFORE user lookup (prevents enumeration)
385
+ - Throws `PasswordResetRateLimitException` with `retryAfterSeconds` field
386
+ - Configurable limits via `AuthModuleOptions.rateLimiterInstance`
387
+ - Security logging for rate limit violations
388
+
389
+ #### Enhancement #3: Configurable Reset Strategy (Partial Implementation)
390
+ - **IPasswordResetStrategy Interface** - Strategy pattern for password reset methods
391
+ - Methods: `generateResetToken()`, `validateResetToken()`, `getExpiryDuration()`, `deleteResetToken()`
392
+ - Enables flexible password reset implementations (codes, magic links, TOTP, etc.)
393
+ - Files: `src/interfaces/password-reset-strategy.interface.ts`
394
+
395
+ - **VerificationCodeStrategy** - Current 6-digit code implementation (default, backward compatible)
396
+ - Wraps existing `VerificationService`
397
+ - 15-minute expiry, 3 attempts max, HMAC-SHA256 hashing
398
+ - Mobile-friendly (easy to copy-paste)
399
+ - Files: `src/strategies/verification-code.strategy.ts`
400
+
401
+ - **MagicLinkStrategy** - JWT-based magic link implementation (ready for future use)
402
+ - JWT-signed tokens with 1-hour expiry
403
+ - HMAC-SHA256 storage in repository
404
+ - Web-friendly (click link, auto-fill form)
405
+ - Requires `IMagicLinkRepository` implementation by consumers
406
+ - Files: `src/strategies/magic-link.strategy.ts`
407
+
408
+ - **IMagicLinkRepository Interface** - Storage contract for magic link tokens
409
+ - Methods: `storeMagicLink()`, `validateMagicLink()`, `deleteMagicLink()`
410
+ - Consumers implement with Prisma/TypeORM/etc.
411
+ - Files: `src/interfaces/magic-link-repository.interface.ts`
412
+
413
+ - **NoOpMagicLinkRepository** - Fallback implementation
414
+ - Logs operations without storage (magic links won't work)
415
+ - Files: `src/repositories/noop-magic-link.repository.ts`
416
+
417
+ ### Changed
418
+
419
+ - **AuthModuleOptions Extended** - Added optional configuration fields:
420
+ - `passwordPolicy?: PasswordPolicyConfig` - Customize password strength requirements
421
+ - `rateLimiterInstance?: IRateLimiter` - Provide custom rate limiter (default: `InMemoryRateLimiterService`)
422
+
423
+ - **AuthService Updated** - Integrated new services:
424
+ - Now uses `PasswordValidationService` for password strength validation (was hardcoded)
425
+ - Now uses `IRateLimiter` for IP-based rate limiting on password reset
426
+ - Injects `AUTH_MODULE_OPTIONS` for password policy configuration
427
+ - Injects `RATE_LIMITER` for IP rate limiting
428
+
429
+ - **Injection Tokens** - Added new constants:
430
+ - `RATE_LIMITER` - Rate limiter service token
431
+ - Exported from `src/constants.ts` and `src/auth.module.ts`
432
+
433
+ ### Tests
434
+
435
+ - **Total Tests: 467 → 516 (+49 new tests, +10.5% coverage)**
436
+ - Password Validation: 26 tests (default policy, custom policies, custom validators, edge cases)
437
+ - IP Rate Limiting: 23 tests (sliding window, cleanup, concurrency, edge cases)
438
+ - All existing tests pass (backward compatibility verified)
439
+
440
+ ### Backward Compatibility
441
+
442
+ - **100% Backward Compatible** - No breaking changes
443
+ - Default password policy matches v0.2.x hardcoded behavior
444
+ - Rate limiter defaults to `InMemoryRateLimiterService` (automatically registered)
445
+ - Password reset still uses verification codes by default
446
+ - Existing consumers require no code changes to upgrade
447
+
448
+ ### Security Improvements
449
+
450
+ - **IP-based rate limiting** prevents email enumeration attacks on password reset
451
+ - **Configurable password policies** enable stricter security requirements
452
+ - **Custom validators** support leaked password detection (HaveIBeenPwned, etc.)
453
+ - **Strategy pattern** enables future password reset methods with consistent security
454
+
455
+ ### Documentation
456
+
457
+ - CLAUDE.md: Added "Optional Password Reset Enhancements" section (pending)
458
+ - README.md: Added "Advanced Password Reset Configuration" section (pending)
459
+ - All new interfaces/classes have comprehensive JSDoc documentation
460
+ - Examples provided for all three enhancements
461
+
462
+ ### Migration Guide
463
+
464
+ No migration required! All enhancements are optional and backward compatible.
465
+
466
+ To use new features:
467
+
468
+ ```typescript
469
+ import {
470
+ InMemoryRateLimiterService,
471
+ PasswordPolicyConfig
472
+ } from '@ambushsoftworks/nestjs-auth-graphql';
473
+
474
+ AuthModule.forRootAsync({
475
+ useFactory: () => ({
476
+ // Enhancement #1: Custom password policy
477
+ passwordPolicy: {
478
+ minLength: 12,
479
+ requireSpecialChar: true,
480
+ customValidator: async (password) => {
481
+ // Check against leaked passwords
482
+ const isLeaked = await checkLeakedPasswords(password);
483
+ return {
484
+ isValid: !isLeaked,
485
+ errors: isLeaked ? ['Password found in data breaches'] : []
486
+ };
487
+ }
488
+ },
489
+
490
+ // Enhancement #2: Rate limiting (default InMemoryRateLimiterService)
491
+ rateLimiterInstance: new InMemoryRateLimiterService(),
492
+
493
+ // ... other options
494
+ }),
495
+ })
496
+ ```
497
+
498
+ ### Known Limitations
499
+
500
+ - **Enhancement #3 (Reset Strategy)** - Partially implemented:
501
+ - Interfaces and strategies created but not integrated into AuthService
502
+ - Current implementation still uses VerificationService directly
503
+ - Full strategy pattern integration deferred to future release
504
+ - Magic link strategy requires consumer-implemented repository
505
+
506
+ - **InMemoryRateLimiterService** - Not suitable for multi-instance deployments:
507
+ - Rate limit state not shared between instances
508
+ - For production load-balanced deployments, implement Redis-backed IRateLimiter
509
+ - Single-instance deployments work perfectly
510
+
511
+ ## [0.2.2] - 2025-11-16
512
+
513
+ ### Fixed
514
+
515
+ - **Critical: Password Reset Email Template** - Fixed broken email template that was showing URL links instead of 6-digit verification codes
516
+ - Email service now properly displays 6-digit codes in mobile-responsive format
517
+ - Removed unused `_resetToken` parameter prefix from `sendPasswordResetEmail()` signature
518
+ - Updated HTML and plain text templates to match email/SMS verification pattern
519
+ - Users can now successfully complete password reset flow
520
+ - Template design: Mobile-responsive, centered code display, 15-minute expiry notice, security warning
521
+ - Files affected: `src/services/sendgrid-email.service.ts`
522
+
523
+ ### Added
524
+
525
+ - **Comprehensive Password Reset Test Suite** - Added 17 unit tests covering all password reset scenarios
526
+ - `requestPasswordReset()` tests: rate limiting, enumeration protection, OAuth rejection, account lock detection
527
+ - `resetPassword()` tests: code validation, password strength, token revocation, lifecycle hooks
528
+ - Edge case tests: concurrent requests, email failures, invalid codes
529
+ - Achieved 95%+ line coverage for password reset code paths
530
+ - Total package test count: 246 → 263 tests
531
+ - Files affected: `src/services/auth.service.spec.ts`
532
+
533
+ ### Changed
534
+
535
+ - **Documentation Improvements**
536
+ - Updated CLAUDE.md to clarify password reset implementation status and remove misleading "not yet supported" example
537
+ - Added comprehensive "Password Reset Flow" section to CLAUDE.md with security features documentation
538
+ - Added complete password reset section to README.md with step-by-step consumer setup guide
539
+ - Added consumer examples for GraphQL DTOs, resolver mutations, and error handling
540
+ - Documented 6-digit code approach (vs. magic link strategy) and security considerations
541
+ - Added lifecycle hooks example for `onPasswordReset()` event
542
+
543
+ ### Technical Details
544
+
545
+ **Email Template Fix:**
546
+ - Changed: `renderPasswordResetHtmlTemplate()` now accepts `code: string` parameter (was `_resetToken`)
547
+ - Changed: `renderPasswordResetTextTemplate()` now accepts `code: string` parameter (was `_resetToken`)
548
+ - Both templates now display 6-digit code prominently instead of URL links
549
+ - Template design: Mobile-responsive, centered code display, 15-minute expiry notice, security warning
550
+
551
+ **Test Suite:**
552
+ - Added: 7 tests for `requestPasswordReset()` method
553
+ - Added: 10 tests for `resetPassword()` method
554
+ - Coverage: 95%+ lines, 100% branches, 100% functions for password reset code
555
+
556
+ **Migration Notes:**
557
+ - No breaking changes
558
+ - No consumer action required (backward compatible)
559
+ - Consumers implementing password reset for the first time should follow README.md setup guide
560
+ - Existing implementations (if any) will automatically benefit from email template fix
561
+
562
+ ### Security
563
+
564
+ - Email enumeration protection maintained (generic success messages)
565
+ - Rate limiting enforced (60-second cooldown per user via `passwordResetSentAt` field)
566
+ - HMAC-SHA256 code hashing with constant-time comparison
567
+ - Token revocation on password change (all refresh tokens invalidated)
568
+ - OAuth user protection (users without passwords cannot reset)
569
+ - 15-minute code expiry with max 3 validation attempts
570
+ - Account lock integration (brute force protection applies to reset flow)
571
+
572
+ ---
573
+
574
+ ## [0.2.1] - 2025-01-16
575
+
576
+ ### Fixed
577
+
578
+ - **Facebook OAuth Email Fallback GraphQL Schema**: Added missing `performCompleteFacebookSignUp()` method to `BaseAuthResolver` for consumers using inheritance pattern
579
+ - Consumers extending `BaseAuthResolver` can now expose `completeFacebookSignUp` mutation in their GraphQL schema
580
+ - Fixes production issue where Facebook sign-in fails for users without public email (business accounts, privacy settings)
581
+ - Backend service logic already existed in `AuthService.completeFacebookSignUp()`, but was not accessible via `BaseAuthResolver`
582
+ - No breaking changes - additive only (new protected method + interface export)
583
+
584
+ ### Added
585
+
586
+ - `IAuthCompleteFacebookSignUpInput` interface export for type safety in consumer resolvers
587
+ - `BaseAuthResolver.performCompleteFacebookSignUp()` protected method following established pattern
588
+
589
+ ### Technical Details
590
+
591
+ **Root Cause**: Facebook OAuth email fallback logic was implemented in `AuthService` (v0.2.0) but the corresponding `performCompleteFacebookSignUp()` protected method was never added to `BaseAuthResolver`. This broke the inheritance pattern for consumers.
592
+
593
+ **Impact**: Consumers using recommended `BaseAuthResolver` pattern (e.g., Lift app) could not complete Facebook sign-in for users without public email (business accounts, privacy settings). This affected ~30% of Facebook OAuth users.
594
+
595
+ **Solution**: Added protected `performCompleteFacebookSignUp()` method to `BaseAuthResolver`, following the same pattern as all other auth operations (signup, login, verifyEmail, verifyPhone, etc.).
596
+
597
+ **Migration**: None required for existing consumers not using Facebook OAuth. Consumers needing Facebook email fallback should:
598
+ 1. Upgrade to v0.2.1
599
+ 2. Add `completeFacebookSignUp` mutation to their custom resolver (see example below)
600
+ 3. No configuration changes needed
601
+
602
+ ### Example Consumer Implementation
603
+
604
+ ```typescript
605
+ import { Mutation, Args } from '@nestjs/graphql';
606
+ import { Throttle } from '@nestjs/throttler';
607
+ import { AuthCompleteFacebookSignUpInput } from './dto/complete-facebook-signup.input';
608
+ import { AuthResponse } from './dto/auth-response.dto';
609
+
610
+ @Mutation(() => AuthResponse, {
611
+ name: 'completeFacebookSignUp',
612
+ description: 'Complete Facebook sign-up when email not provided by Facebook',
613
+ })
614
+ @Throttle({ default: { limit: 5, ttl: 60000 } })
615
+ async completeFacebookSignUp(
616
+ @Args('input') input: AuthCompleteFacebookSignUpInput,
617
+ ): Promise<AuthResponse> {
618
+ return this.performCompleteFacebookSignUp(input) as Promise<AuthResponse>;
619
+ }
620
+ ```
621
+
622
+ ---
623
+
624
+ ## [0.2.0] - 2025-01-15
625
+
626
+ ### BREAKING CHANGES
627
+
628
+ This version introduces a complete architectural refactor for maximum portability across diverse NestJS projects. The package now uses an interface-based architecture with abstract base classes instead of concrete decorated resolvers.
629
+
630
+ **Migration Required**: See [MIGRATION.md](./MIGRATION.md) for step-by-step upgrade instructions.
631
+
632
+ #### Removed
633
+
634
+ - **Concrete `AuthResolver` class** - Package no longer exports a decorated GraphQL resolver
635
+ - Consumers must create their own resolver extending `BaseAuthResolver<T>`
636
+ - GraphQL decorators (@Mutation, @Query, etc.) must be added by consumer
637
+
638
+ - **GraphQL decorators on package entities and DTOs** - Package no longer provides GraphQL types
639
+ - All package entities converted to interfaces (IAuthUser, IAuthSignupInput, etc.)
640
+ - Consumers define their own @ObjectType and @InputType classes implementing package interfaces
641
+ - This eliminates GraphQL schema conflicts when integrating into existing projects
642
+
643
+ - **Direct Prisma/TypeORM coupling** - Package no longer assumes specific ORM
644
+ - Repository interfaces require manual implementation by consumer
645
+ - Full control over database schema and queries
646
+
647
+ #### Added
648
+
649
+ - **`BaseAuthResolver<T extends IAuthUser>` abstract class**
650
+ - Provides protected helper methods for all auth operations
651
+ - `performSignup()`, `performLogin()`, `performRefreshToken()`, etc.
652
+ - Type-safe generic parameter allows using any User model implementing IAuthUser
653
+
654
+ - **Interface-based architecture for complete portability**
655
+ - `IAuthUser` - User model contract (replaces concrete User entity)
656
+ - `IAuthSignupInput`, `IAuthLoginInput`, etc. - Input DTOs as interfaces
657
+ - `IAuthResponse`, `ILogoutResponse`, etc. - Response DTOs as interfaces
658
+ - Works with any database, ORM, or GraphQL schema structure
659
+
660
+ - **Comprehensive documentation**
661
+ - [QUICK-START.md](./QUICK-START.md) - Step-by-step setup guide
662
+ - [MIGRATION.md](./MIGRATION.md) - Upgrade guide from v0.1.x
663
+ - [docs/IMPLEMENTATION-ISSUES.md](./docs/IMPLEMENTATION-ISSUES.md) - Common issues and solutions
664
+ - [docs/IMPLEMENTATION-PLAN-V2.md](./docs/IMPLEMENTATION-PLAN-V2.md) - Architecture deep dive
665
+
666
+ - **Canonical enum exports**
667
+ - Package owns `AuthProvider` enum (EMAIL, GOOGLE, FACEBOOK, APPLE)
668
+ - Package owns `UserStatus` enum (ACTIVE, SUSPENDED, DELETED)
669
+ - Single source of truth prevents enum compatibility issues
670
+
671
+ #### Changed
672
+
673
+ - **User entity**: `User` class → `IAuthUser` interface
674
+ - Consumers implement interface with their own GraphQL @ObjectType class
675
+ - Full control over additional custom fields and relationships
676
+
677
+ - **All DTOs converted to interfaces with I* prefix**
678
+ - Example: `AuthSignupInput` → `IAuthSignupInput`
679
+ - Consumers create @InputType classes implementing these interfaces
680
+
681
+ - **Repository interfaces use generic type constraints**
682
+ - More flexible type system allows adaptation to various ORMs
683
+ - Type assertions at repository boundary for Prisma/TypeORM compatibility
684
+
685
+ - **Package architecture follows Layer 0 principles**
686
+ - Complete abstraction over database, ORM, and GraphQL implementation
687
+ - Zero coupling to Prisma, TypeORM, or specific GraphQL libraries
688
+ - Dependency injection via instance-based pattern (not class references)
689
+
690
+ ### Features Preserved
691
+
692
+ All authentication features from v0.1.x remain fully functional:
693
+
694
+ - JWT authentication with automatic refresh token rotation
695
+ - OAuth 2.0 (Google, Facebook, Apple)
696
+ - Email verification with 6-digit PIN codes
697
+ - SMS verification via Twilio
698
+ - Brute force protection with account lockout
699
+ - Biometric authentication support
700
+ - Account linking (multiple OAuth providers per account)
701
+ - Security: HMAC-SHA256 token hashing, AES-256-GCM encryption, constant-time comparison
702
+
703
+ ### Migration Summary
704
+
705
+ **Before (v0.1.x)**:
706
+ ```typescript
707
+ import { AuthModule, AuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
708
+
709
+ @Module({
710
+ imports: [AuthModule.forRoot(...)],
711
+ providers: [AuthResolver] // Use package resolver directly
712
+ })
713
+ ```
714
+
715
+ **After (v0.2.0)**:
716
+ ```typescript
717
+ import { AuthModule, BaseAuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
718
+
719
+ // 1. Create your resolver
720
+ @Resolver()
721
+ export class MyAuthResolver extends BaseAuthResolver<User> {
722
+ @Mutation(() => AuthResponse)
723
+ async signup(@Args('input') input: SignupInput): Promise<AuthResponse> {
724
+ return this.performSignup(input) as Promise<AuthResponse>;
725
+ }
726
+ // ... override all 11 mutations + 3 queries
727
+ }
728
+
729
+ @Module({
730
+ imports: [AuthModule.forRoot(...)],
731
+ providers: [MyAuthResolver] // Use your custom resolver
732
+ })
733
+ ```
734
+
735
+ **Benefits**:
736
+ - Zero GraphQL schema conflicts with existing projects
737
+ - Full control over GraphQL types, fields, and descriptions
738
+ - Works with any database schema (not just Prisma)
739
+ - Type-safe integration through interfaces
740
+ - Easier to customize and extend
741
+
742
+ ### Testing
743
+
744
+ - **553+ tests passing** in Lift integration (production validation)
745
+ - **0 TypeScript errors** in package build
746
+ - **0 runtime regressions** from architectural changes
747
+ - **100% backward compatibility** of authentication features
748
+
749
+ ### Known Issues
750
+
751
+ - **Pre-existing test failures**: BruteForceProtectionService and MuscleGroupRecoveryService tests have DI mock issues (not caused by v0.2.0 refactor, existed in v0.1.x)
752
+ - **Migration effort**: Upgrading from v0.1.x requires 2-4 hours to create resolver and GraphQL types
753
+
754
+ ---
755
+
756
+ ## [0.1.10] - 2025-01-14
757
+
758
+ ### Fixed
759
+ - Fixed instance-based DI pattern (removed ModuleRef complexity)
760
+ - Cleaned up repository injection in AuthModule.forRootAsync()
761
+
762
+ ### Changed
763
+ - Simplified dependency injection to use direct instance passing
764
+ - Improved documentation for repository configuration
765
+
766
+ ---
767
+
768
+ ## [0.1.9] - 2025-01-13
769
+
770
+ ### Added
771
+ - Published package to npm registry as `@ambushsoftworks/nestjs-auth-graphql`
772
+ - Standalone repository on GitLab
773
+
774
+ ### Changed
775
+ - Migrated Lift backend to use published npm package
776
+ - Removed local package directory from monorepo
777
+
778
+ ---
779
+
780
+ ## [0.1.8] - 2025-01-12
781
+
782
+ ### Changed
783
+ - Backend package cleanup and optimization
784
+ - Updated authentication service interfaces
785
+
786
+ ---
787
+
788
+ ## [0.1.7] - 2025-01-11
789
+
790
+ ### Added
791
+ - SMS verification via Twilio integration
792
+ - Phone number validation using libphonenumber-js
793
+ - `verifyPhone`, `resendPhoneVerification`, `removePhoneNumber` mutations
794
+
795
+ ### Changed
796
+ - Enhanced verification service with SMS support
797
+ - Router fixes for phone verification flow
798
+
799
+ ---
800
+
801
+ ## [0.1.0] - 2024-11-01
802
+
803
+ ### Added
804
+ - Initial package extraction from Lift app
805
+ - JWT authentication with refresh tokens
806
+ - Email verification via SendGrid
807
+ - Brute force protection
808
+ - Google and Facebook OAuth
809
+ - Biometric authentication support
810
+ - 246+ tests from production codebase
811
+
812
+ ---
813
+
814
+ [0.8.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.7.2...v0.8.0
815
+ [0.2.2]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.2.1...v0.2.2
816
+ [0.2.1]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.2.0...v0.2.1
817
+ [0.2.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.10...v0.2.0
818
+ [0.1.10]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.9...v0.1.10
819
+ [0.1.9]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.8...v0.1.9
820
+ [0.1.8]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.7...v0.1.8
821
+ [0.1.7]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.0...v0.1.7
822
+ [0.1.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/releases/v0.1.0