@ambushsoftworks/nestjs-auth-graphql 0.8.0 → 0.9.0-rc.2

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 (90) hide show
  1. package/CHANGELOG.md +787 -0
  2. package/README.md +150 -8
  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 +28 -0
  6. package/dist/auth.module.js.map +1 -1
  7. package/dist/exceptions/account-inactive.exception.d.ts +5 -0
  8. package/dist/exceptions/account-inactive.exception.d.ts.map +1 -0
  9. package/dist/exceptions/account-inactive.exception.js +16 -0
  10. package/dist/exceptions/account-inactive.exception.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts +1 -0
  16. package/dist/interfaces/auth-lifecycle-hooks.interface.d.ts.map +1 -1
  17. package/dist/interfaces/auth-logger.interface.d.ts +1 -0
  18. package/dist/interfaces/auth-logger.interface.d.ts.map +1 -1
  19. package/dist/interfaces/auth-logger.interface.js +1 -0
  20. package/dist/interfaces/auth-logger.interface.js.map +1 -1
  21. package/dist/interfaces/auth-user.interface.d.ts.map +1 -1
  22. package/dist/interfaces/brute-force-repository.interface.d.ts.map +1 -1
  23. package/dist/interfaces/email-service.interface.d.ts +2 -6
  24. package/dist/interfaces/email-service.interface.d.ts.map +1 -1
  25. package/dist/interfaces/email-template-renderer.interface.d.ts +2 -6
  26. package/dist/interfaces/email-template-renderer.interface.d.ts.map +1 -1
  27. package/dist/interfaces/user-repository.interface.d.ts.map +1 -1
  28. package/dist/resolvers/auth.resolver.d.ts +73 -0
  29. package/dist/resolvers/auth.resolver.d.ts.map +1 -0
  30. package/dist/resolvers/auth.resolver.js +472 -0
  31. package/dist/resolvers/auth.resolver.js.map +1 -0
  32. package/dist/resolvers/base-auth.resolver.d.ts +1 -0
  33. package/dist/resolvers/base-auth.resolver.d.ts.map +1 -1
  34. package/dist/resolvers/base-auth.resolver.js +12 -2
  35. package/dist/resolvers/base-auth.resolver.js.map +1 -1
  36. package/dist/services/auth.service.d.ts +5 -0
  37. package/dist/services/auth.service.d.ts.map +1 -1
  38. package/dist/services/auth.service.js +35 -10
  39. package/dist/services/auth.service.js.map +1 -1
  40. package/dist/services/brute-force-protection.service.d.ts +3 -1
  41. package/dist/services/brute-force-protection.service.d.ts.map +1 -1
  42. package/dist/services/brute-force-protection.service.js +12 -2
  43. package/dist/services/brute-force-protection.service.js.map +1 -1
  44. package/dist/services/configurable-email.service.d.ts +3 -7
  45. package/dist/services/configurable-email.service.d.ts.map +1 -1
  46. package/dist/services/configurable-email.service.js +16 -60
  47. package/dist/services/configurable-email.service.js.map +1 -1
  48. package/dist/services/default-email-template-renderer.d.ts +4 -7
  49. package/dist/services/default-email-template-renderer.d.ts.map +1 -1
  50. package/dist/services/default-email-template-renderer.js +40 -55
  51. package/dist/services/default-email-template-renderer.js.map +1 -1
  52. package/dist/services/noop-email-sender.d.ts +2 -0
  53. package/dist/services/noop-email-sender.d.ts.map +1 -1
  54. package/dist/services/noop-email-sender.js +28 -0
  55. package/dist/services/noop-email-sender.js.map +1 -1
  56. package/dist/services/noop-email.service.d.ts +2 -6
  57. package/dist/services/noop-email.service.d.ts.map +1 -1
  58. package/dist/services/noop-email.service.js +9 -17
  59. package/dist/services/noop-email.service.js.map +1 -1
  60. package/dist/services/password-validation.service.js +1 -1
  61. package/dist/services/password-validation.service.js.map +1 -1
  62. package/dist/services/refresh-token.service.d.ts.map +1 -1
  63. package/dist/services/refresh-token.service.js +1 -0
  64. package/dist/services/refresh-token.service.js.map +1 -1
  65. package/dist/services/sendgrid-email.service.d.ts +0 -13
  66. package/dist/services/sendgrid-email.service.d.ts.map +1 -1
  67. package/dist/services/sendgrid-email.service.js +4 -528
  68. package/dist/services/sendgrid-email.service.js.map +1 -1
  69. package/dist/services/verification.service.d.ts +15 -0
  70. package/dist/services/verification.service.d.ts.map +1 -1
  71. package/dist/services/verification.service.js +70 -3
  72. package/dist/services/verification.service.js.map +1 -1
  73. package/dist/test-utils/index.d.ts +1 -1
  74. package/dist/test-utils/index.d.ts.map +1 -1
  75. package/dist/test-utils/index.js +2 -1
  76. package/dist/test-utils/index.js.map +1 -1
  77. package/dist/test-utils/mock-services.d.ts +2 -0
  78. package/dist/test-utils/mock-services.d.ts.map +1 -1
  79. package/dist/test-utils/mock-services.js +10 -4
  80. package/dist/test-utils/mock-services.js.map +1 -1
  81. package/dist/utils/account-status.d.ts +3 -0
  82. package/dist/utils/account-status.d.ts.map +1 -0
  83. package/dist/utils/account-status.js +19 -0
  84. package/dist/utils/account-status.js.map +1 -0
  85. package/dist/utils/passport-inspector.d.ts +11 -0
  86. package/dist/utils/passport-inspector.d.ts.map +1 -0
  87. package/dist/utils/passport-inspector.js +48 -0
  88. package/dist/utils/passport-inspector.js.map +1 -0
  89. package/examples/full-resolver-template.ts +6 -0
  90. package/package.json +5 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,787 @@
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
+ > Published as a release candidate under the `next` dist-tag first, so the
38
+ > verification and password-reset emails could be confirmed through a real
39
+ > mail client before reaching `latest`. That last hop — a mail client
40
+ > rendering the link — is exactly where the original defect lived and the one
41
+ > thing no test suite in this repo can exercise. Install the candidate with
42
+ > `npm install @ambushsoftworks/nestjs-auth-graphql@next`.
43
+ >
44
+ > **`0.9.0-rc.2` supersedes `rc.1`.** `rc.1` predates the account-status fix
45
+ > in **Security** below and still has the suspension window open — test
46
+ > against `rc.2`, not `rc.1`.
47
+
48
+ ### ⚠ External configuration required
49
+ - **`verificationMode: 'token'` requires your frontend to serve two routes.**
50
+ Token-mode links point at `<verification.baseUrl>/verify-email` and
51
+ `<verification.baseUrl>/reset-password`, each carrying `token` and `email`
52
+ query parameters. Your app must serve both paths, read both parameters, and
53
+ pass them to `verifyEmail` / `resetPassword`. Nothing in this package can
54
+ check that — a link to a route you do not serve compiles, tests green, sends
55
+ successfully, and 404s in the recipient's browser. `verification.baseUrl`
56
+ itself IS validated at boot; the routes behind it cannot be.
57
+ **Not applicable in `'code'` mode**, which is the default and ships no link.
58
+
59
+ ### Security
60
+ - **`status` and `deletedAt` were never checked — suspending a user revoked nothing.**
61
+ `IAuthUser.status` was documented as *"Checked during login to prevent
62
+ suspended/deleted users from authenticating"*. Nothing checked it, anywhere:
63
+ outside the `UserStatus` enum declaration and that docblock, `SUSPENDED` and
64
+ `DELETED` did not appear in `src/`. `login()` gated on rate limit, lockout,
65
+ user existence, password presence and password validity — never status.
66
+ `validateUser()`, the JWT path on every authenticated request, verified only
67
+ that the user still existed.
68
+
69
+ What provided revocation in practice was an **undocumented contract**: because
70
+ `refreshToken()` calls `findById` and rejects on `null`, revocation worked only
71
+ for consumers whose repository happened to filter revoked users. A consumer
72
+ who read the docblock and reasonably concluded the package handled it had an
73
+ **unbounded** exposure window — a suspended user kept refreshing indefinitely,
74
+ minting a fresh access token each time. Reported by the
75
+ nestjs-account-management team, who found it while planning a `suspendAccount`
76
+ operation; two consumers were affected in production.
77
+
78
+ Status is now enforced in four places: `login()` (after password verification,
79
+ so it cannot become an enumeration oracle), `validateUser()`, both branches of
80
+ `refreshToken()` including the grace-period cache, and as a backstop in the
81
+ private token-issuance sink every flow funnels through — so no path, including
82
+ `issueAuthSession`, can mint tokens for an inactive account. Rejection is
83
+ `AccountInactiveException` (code `ACCOUNT_INACTIVE`, HTTP 403) and logs the new
84
+ `ACCOUNT_INACTIVE_BLOCKED` security event with the phase.
85
+
86
+ **The default is a deny-list, and that is deliberate.** An account is inactive
87
+ when `deletedAt` is set, or `status` is `SUSPENDED`/`DELETED` compared
88
+ case-insensitively. It is *not* "anything that is not `ACTIVE`": `status` is
89
+ typed `string` so consumers can map their own model, and an allow-list would
90
+ have locked out every user of a consumer storing `'active'`, `'ENABLED'` or
91
+ `''` the moment they upgraded. A deny-list cannot produce a false rejection.
92
+ **If your vocabulary differs from `UserStatus`, the default cannot recognize
93
+ it** — supply the new `isUserActive` option.
94
+
95
+ **`jwtValidation: 'payload-only'` bounds revocation by token TTL rather than
96
+ making it immediate.** That mode skips the per-request lookup by design, so
97
+ there is nothing to check; a suspended user keeps working until their access
98
+ token expires. Refresh is still blocked, so the window is exactly one
99
+ access-token lifetime (15 minutes by default). This was previously
100
+ undocumented — the option's docblock said only that it "does not check if user
101
+ still exists" — and now states the consequence. No behavioral change to that
102
+ mode in this release.
103
+
104
+ ### Added
105
+ - **`isUserActive?: (user: IAuthUser) => boolean` module option.** Overrides the
106
+ default account-active predicate for consumers whose status vocabulary differs
107
+ from `UserStatus`. Called on login, on refresh, on every request in
108
+ `jwtValidation: 'full'` mode, and before any token pair is minted. Must be pure
109
+ and synchronous — it runs on the authenticated request path and receives the
110
+ already-loaded user. Also exported: `isUserActiveByDefault` (the default
111
+ predicate) and `AccountInactiveException`.
112
+
113
+ ### Fixed
114
+ - **`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.
115
+ - **`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.
116
+ - **`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.
117
+ - **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.
118
+ - **`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).
119
+
120
+ ### Added
121
+ - **`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.
122
+ - **`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.
123
+ - **`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.
124
+ - **`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.
125
+ - **`IssuedVerificationCredential`** — new exported interface.
126
+ - **`VerificationService.codeExpiresInMinutes` / `tokenExpiresInMinutes` / `credentialExpiresInMinutes`** getters — single source for expiry values previously duplicated as literals across services.
127
+ - **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.**
128
+ - **`ConfigurableEmailService` refuses to send a token-mode email with no link**, throwing `InternalServerErrorException` rather than delivering an unusable message.
129
+ - Expiry line in the default renderer (HTML + plain text), sourced from the caller and omitted entirely when unknown rather than guessing.
130
+ - **`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.
131
+
132
+ - **`RefreshTokenService` held the Node event loop open.** Its constructor
133
+ starts a 60-second sweep over the in-memory refresh-token cache, and the
134
+ timer was never `unref()`d. `onModuleDestroy` clears it, so a normal NestJS
135
+ shutdown was unaffected — but any process that finished its work and expected
136
+ to exit (a script, a CLI, a short-lived job importing the package) would hang
137
+ until something killed it, and Jest workers had to be force-exited. The timer
138
+ is now unref'd: it still fires for as long as the process is alive for its own
139
+ reasons, but no longer keeps it alive. `onModuleDestroy` still clears it.
140
+
141
+ ### Fixed (release tooling)
142
+ - **`npm run lint` had never worked.** Five ESLint packages were in
143
+ `devDependencies` and the script had been present for months, but no config
144
+ file existed anywhere, so it failed on "couldn't find a configuration file" —
145
+ and CI never invoked it, so nothing surfaced. The glob also referenced a
146
+ `tests/` directory that does not exist, which fails ESLint on its own. Adds
147
+ `.eslintrc.js`, splits `lint` (checks) from `lint:fix` (mutates) so CI can
148
+ run the check form, and wires `npm run lint` into the pipeline. The existing
149
+ codebase produced exactly one violation.
150
+ - **Test job now runs on the default branch.** It ran only on merge requests
151
+ and tags, so a semantic conflict introduced by a merge — green on both sides
152
+ in isolation — would not surface until someone cut a release.
153
+ - **`--forceExit` removed from the CI test command**, now that nothing holds
154
+ the event loop open. Force-exiting masks precisely the leak fixed above.
155
+ - **`CHANGELOG.md` is now published to npm** (added to `files`), so consumers
156
+ can read release notes without leaving the registry.
157
+ - **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`.
158
+
159
+ ### Changed
160
+ - **`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.
161
+ - **`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.
162
+ - **`IEmailTemplateRenderer.renderVerificationEmail` / `renderPasswordResetEmail`** params gained optional `expiresInMinutes?: number`.
163
+ - **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.
164
+ - 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.
165
+ - `PASSWORD_RESET_COMPLETED` security-event metadata now reports the real `method` (`'code'` or `'token'`) instead of a hardcoded `'code'`.
166
+
167
+ ### Breaking
168
+ - **`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.
169
+ - `sendAccountLockedEmail` → wire from the new `onAccountLocked` hook
170
+ - `sendWelcomeEmail` → wire from `onSignup` (or `onEmailVerified`)
171
+ - `sendAccountLinkedEmail` → wire from `onOAuthAccountLinked`
172
+ - `sendAccountUnlinkedEmail` → wire from your unlink resolver directly
173
+ - **`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.
174
+ - **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.
175
+
176
+ ### Documentation
177
+ - **`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.
178
+ - 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.
179
+ - 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).
180
+ - 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.
181
+ - 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.
182
+ - 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.
183
+
184
+ ### Notes
185
+ - **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.
186
+ - **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.
187
+ - **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.
188
+
189
+ ## [0.8.0] - 2026-05-14
190
+
191
+ ### Added
192
+ - **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.
193
+ - **`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.
194
+ - **`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.
195
+ - **`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.
196
+ - **`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.
197
+ - **`SecurityEvent.SESSION_ISSUED`** — emitted by `issueAuthSession`. Distinct from `LOGIN_SUCCESS` because no credential was verified at this layer. Metadata: `userId`, `realm`, `method: 'issueAuthSession'`.
198
+ - **`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.
199
+ - **`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.
200
+ - **`validateAuthModuleOptions(opts)`** — exported (for unit testing) helper invoked inside the `forRootAsync` factory. Currently checks the `bruteForce.ipRateLimit` ↔ `rateLimiterInstance` invariant.
201
+ - **`AUTH_IP_HASH_SALT` env var** — recommended dedicated salt for `hashIp`. Falls back to `JWT_SECRET` with a one-time INFO log when unset.
202
+
203
+ ### Changed
204
+ - **`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.
205
+ - **`RealmMiddleware.use` is now async** (`Promise<void>`). Express middleware accepts both shapes — no impact on consumers.
206
+
207
+ ### Breaking
208
+ - **`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).
209
+ - **`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;`.
210
+
211
+ ### Documentation
212
+ - 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."
213
+ - 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`).
214
+ - 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`.
215
+
216
+ ### Notes
217
+ - 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.
218
+ - 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.
219
+ - 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.
220
+ - 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.
221
+ - 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.
222
+
223
+ ## [0.7.2] - 2026-05-12
224
+
225
+ ### Fixed
226
+ - **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.
227
+ - **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.
228
+
229
+ ## [0.7.1] - 2026-05-12
230
+
231
+ Skipped — tag pushed but publish failed in CI before reaching npm. Superseded by 0.7.2.
232
+
233
+ ## [0.7.0] - 2026-05-12
234
+
235
+ ### Added
236
+ - **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.
237
+ - `IRealmExtractor` interface — extract a realm identifier from each request (subdomain, header, JWT claim, etc.)
238
+ - `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)
239
+ - `@CurrentRealm()` decorator — pulls the resolved realm out of the GraphQL context
240
+ - `realm` claim embedded in JWT access tokens; `JwtStrategy` validates the claim against the request realm (mismatch → `401 Unauthorized`)
241
+ - Identity-resolution repository methods (`findByEmail`, `findByPhoneNumber`, `findByOAuthProvider`, `create`) accept an optional `realm` parameter so consumer implementations can scope queries
242
+ - `BruteForceProtectionService` and `VerificationService` accept an optional `realm` parameter; in-memory rate limiter keys are prefixed with the realm to prevent cross-realm interference
243
+ - `CreateUserData.realm` field on user creation payloads
244
+ - Opt-in: provide `realmExtractorInstance` in `AuthModuleOptions`. No separate feature flag. Existing single-realm consumers are unaffected.
245
+ - 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`
246
+
247
+ ### Fixed
248
+ - Documentation accuracy pass — corrected several inaccuracies in the README and CLAUDE.md around realm wiring and middleware registration
249
+
250
+ ### Notes
251
+ - 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.
252
+
253
+ ## [0.6.7] - 2026-05-01
254
+
255
+ ### Added
256
+ - **Code Quality Audit (Phases 3–7)** - Comprehensive deduplication, type-safety, dead-code, and guard-cleanup pass across the codebase
257
+ - Extracted `generateTokenPairAndStore()` to deduplicate token generation logic
258
+ - Extracted `mapToRepositoryType()` and `resolveUser()` helpers in `VerificationService`
259
+ - Removed dead code paths and tightened types in several services
260
+
261
+ ### Fixed
262
+ - **`ApiKeyStrategy`** - Returns `null` instead of throwing when the auth header is missing, allowing strategy chains (JWT → API key → anonymous) to fall through cleanly
263
+
264
+ ### Changed
265
+ - **README rewrite** - Documentation overhauled for accuracy against the current feature set (cookie auth, CSRF, API keys, multi-tenancy, magic links, biometrics)
266
+ - **Publishing checklist** - Added a required "review and update README before tagging" step to `CLAUDE.md`
267
+
268
+ ## [0.6.5] - 2026-04 (CI tracking releases)
269
+
270
+ ### Fixed
271
+ - 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
272
+ - Multiple iterative CI fixes published as 0.6.1 – 0.6.5 while npm Trusted Publishing was being wired up
273
+
274
+ ## [0.6.0]
275
+
276
+ ### Added
277
+ - **Email Template Architecture (P4)** - Branded email templates with token verification support, customizable per-consumer
278
+ - **Security Hardening (P5)** - `__Host-` cookie prefix, `Referrer-Policy` header, additional cookie security defaults, and supporting documentation
279
+ - GitLab CI with npm Trusted Publishing (OIDC) — eliminates need for long-lived npm tokens
280
+
281
+ ### Fixed
282
+ - `JwtModule.expiresIn` type compatibility issue
283
+
284
+ ## [0.5.0]
285
+
286
+ ### Added
287
+ - **Cookie-Based Authentication (P2)** - HTTP-only access/refresh token cookies as an alternative to bearer tokens
288
+ - **CSRF Guard** - Double-submit cookie CSRF protection that pairs with cookie auth
289
+ - **JWT Payload Factory (P3)** - `IJwtPayloadFactory` interface for customizing JWT claims; default factory provided; wired through `AuthService` and `JwtStrategy`
290
+ - **API Key Strategy** - `ApiKeyStrategy` + `IApiKeyRepository` for service-account / non-interactive authentication
291
+ - **Tenancy Layer (P1b)** - `TenantGuard`, `PermissionGuard`, `createAuthGuard()` factory, `HeaderTenantExtractor`, NoOp tenant implementations, `ITenantRepository`, `IResourcePermissionRepository`
292
+ - **Auth Decorators** - `@Public()`, `@SkipTenant()`, `@RequirePermissions()`, `@ResourceScope()`, plus supporting metadata keys
293
+ - Module options expansion to wire all of the above through `AuthModule.forRootAsync()`
294
+
295
+ ## [0.4.0]
296
+
297
+ ### Added
298
+ - **Unified OAuth Methods on `IUserRepository`** - Consolidated `findByGoogleId`/`findByFacebookId` and friends into provider-agnostic `findByOAuthProvider(provider, providerId, realm?)` and related methods
299
+ - **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.
300
+ - Widened peer dependency range for NestJS 11 compatibility; added `passport-custom` to peer deps
301
+
302
+ ## [0.3.4] - 2025-11-16
303
+
304
+ ### Fixed
305
+ - **Phone Verification for OAuth Users** - Fixed "User not found" error when OAuth users (Google/Facebook) attempt to add phone verification
306
+ - Added `generateCodeForUserId()`, `validateCodeForUserId()`, `hasActiveCodeForUserId()` to `VerificationService`
307
+ - Updated `sendPhoneVerification()`, `verifyPhone()`, and `resendPhoneVerification()` to use user ID-based methods
308
+ - Previous implementation required phone number to exist in database before verification could be initiated
309
+ - New implementation uses user ID for code generation/validation, allowing OAuth users to add phone numbers
310
+ - Files: `src/services/verification.service.ts`, `src/services/auth.service.ts`
311
+
312
+ ## [0.3.0] - 2025-11-16
313
+
314
+ ### Added
315
+
316
+ #### Enhancement #1: Configurable Password Policy
317
+ - **PasswordPolicyConfig Interface** - Customize password strength requirements for signup and password reset
318
+ - Configurable: `minLength`, `maxLength`, `requireUppercase`, `requireLowercase`, `requireNumber`, `requireSpecialChar`
319
+ - Custom validator function support for advanced requirements (e.g., leaked password detection, dictionary checks)
320
+ - Default policy: min 8 chars, uppercase, lowercase, number (backward compatible with v0.2.x)
321
+ - Files: `src/interfaces/password-policy-config.interface.ts`
322
+
323
+ - **PasswordValidationService** - Centralized password validation with configurable rules
324
+ - Fail-fast validation (returns all errors at once for better UX)
325
+ - Support for async custom validators
326
+ - Immutable policy configuration via `getPolicy()`
327
+ - 26 comprehensive unit tests
328
+ - Files: `src/services/password-validation.service.ts`, `src/services/password-validation.service.spec.ts`
329
+
330
+ #### Enhancement #2: IP-Based Rate Limiting
331
+ - **IRateLimiter Interface** - IP-based rate limiting to prevent abuse and email enumeration
332
+ - Methods: `checkRateLimit()`, `recordAttempt()`
333
+ - Sliding window algorithm for accurate rate limiting
334
+ - Files: `src/interfaces/rate-limiter.interface.ts`
335
+
336
+ - **InMemoryRateLimiterService** - Production-ready rate limiter for single-instance deployments
337
+ - Sliding window algorithm
338
+ - Automatic cleanup of expired entries (prevents memory leaks)
339
+ - Default: 5 password reset requests per hour per IP
340
+ - 23 comprehensive unit tests (including edge cases, cleanup, concurrency)
341
+ - Files: `src/services/in-memory-rate-limiter.service.ts`, `src/services/in-memory-rate-limiter.service.spec.ts`
342
+
343
+ - **NoOpRateLimiter** - Fallback implementation for development/testing
344
+ - Logs rate limit checks without enforcing limits
345
+ - Useful for testing environments
346
+ - Files: `src/repositories/noop-rate-limiter.ts`
347
+
348
+ - **IP Rate Limiting in Password Reset** - Enhanced security for `requestPasswordReset()`
349
+ - Checks IP rate limit BEFORE user lookup (prevents enumeration)
350
+ - Throws `PasswordResetRateLimitException` with `retryAfterSeconds` field
351
+ - Configurable limits via `AuthModuleOptions.rateLimiterInstance`
352
+ - Security logging for rate limit violations
353
+
354
+ #### Enhancement #3: Configurable Reset Strategy (Partial Implementation)
355
+ - **IPasswordResetStrategy Interface** - Strategy pattern for password reset methods
356
+ - Methods: `generateResetToken()`, `validateResetToken()`, `getExpiryDuration()`, `deleteResetToken()`
357
+ - Enables flexible password reset implementations (codes, magic links, TOTP, etc.)
358
+ - Files: `src/interfaces/password-reset-strategy.interface.ts`
359
+
360
+ - **VerificationCodeStrategy** - Current 6-digit code implementation (default, backward compatible)
361
+ - Wraps existing `VerificationService`
362
+ - 15-minute expiry, 3 attempts max, HMAC-SHA256 hashing
363
+ - Mobile-friendly (easy to copy-paste)
364
+ - Files: `src/strategies/verification-code.strategy.ts`
365
+
366
+ - **MagicLinkStrategy** - JWT-based magic link implementation (ready for future use)
367
+ - JWT-signed tokens with 1-hour expiry
368
+ - HMAC-SHA256 storage in repository
369
+ - Web-friendly (click link, auto-fill form)
370
+ - Requires `IMagicLinkRepository` implementation by consumers
371
+ - Files: `src/strategies/magic-link.strategy.ts`
372
+
373
+ - **IMagicLinkRepository Interface** - Storage contract for magic link tokens
374
+ - Methods: `storeMagicLink()`, `validateMagicLink()`, `deleteMagicLink()`
375
+ - Consumers implement with Prisma/TypeORM/etc.
376
+ - Files: `src/interfaces/magic-link-repository.interface.ts`
377
+
378
+ - **NoOpMagicLinkRepository** - Fallback implementation
379
+ - Logs operations without storage (magic links won't work)
380
+ - Files: `src/repositories/noop-magic-link.repository.ts`
381
+
382
+ ### Changed
383
+
384
+ - **AuthModuleOptions Extended** - Added optional configuration fields:
385
+ - `passwordPolicy?: PasswordPolicyConfig` - Customize password strength requirements
386
+ - `rateLimiterInstance?: IRateLimiter` - Provide custom rate limiter (default: `InMemoryRateLimiterService`)
387
+
388
+ - **AuthService Updated** - Integrated new services:
389
+ - Now uses `PasswordValidationService` for password strength validation (was hardcoded)
390
+ - Now uses `IRateLimiter` for IP-based rate limiting on password reset
391
+ - Injects `AUTH_MODULE_OPTIONS` for password policy configuration
392
+ - Injects `RATE_LIMITER` for IP rate limiting
393
+
394
+ - **Injection Tokens** - Added new constants:
395
+ - `RATE_LIMITER` - Rate limiter service token
396
+ - Exported from `src/constants.ts` and `src/auth.module.ts`
397
+
398
+ ### Tests
399
+
400
+ - **Total Tests: 467 → 516 (+49 new tests, +10.5% coverage)**
401
+ - Password Validation: 26 tests (default policy, custom policies, custom validators, edge cases)
402
+ - IP Rate Limiting: 23 tests (sliding window, cleanup, concurrency, edge cases)
403
+ - All existing tests pass (backward compatibility verified)
404
+
405
+ ### Backward Compatibility
406
+
407
+ - **100% Backward Compatible** - No breaking changes
408
+ - Default password policy matches v0.2.x hardcoded behavior
409
+ - Rate limiter defaults to `InMemoryRateLimiterService` (automatically registered)
410
+ - Password reset still uses verification codes by default
411
+ - Existing consumers require no code changes to upgrade
412
+
413
+ ### Security Improvements
414
+
415
+ - **IP-based rate limiting** prevents email enumeration attacks on password reset
416
+ - **Configurable password policies** enable stricter security requirements
417
+ - **Custom validators** support leaked password detection (HaveIBeenPwned, etc.)
418
+ - **Strategy pattern** enables future password reset methods with consistent security
419
+
420
+ ### Documentation
421
+
422
+ - CLAUDE.md: Added "Optional Password Reset Enhancements" section (pending)
423
+ - README.md: Added "Advanced Password Reset Configuration" section (pending)
424
+ - All new interfaces/classes have comprehensive JSDoc documentation
425
+ - Examples provided for all three enhancements
426
+
427
+ ### Migration Guide
428
+
429
+ No migration required! All enhancements are optional and backward compatible.
430
+
431
+ To use new features:
432
+
433
+ ```typescript
434
+ import {
435
+ InMemoryRateLimiterService,
436
+ PasswordPolicyConfig
437
+ } from '@ambushsoftworks/nestjs-auth-graphql';
438
+
439
+ AuthModule.forRootAsync({
440
+ useFactory: () => ({
441
+ // Enhancement #1: Custom password policy
442
+ passwordPolicy: {
443
+ minLength: 12,
444
+ requireSpecialChar: true,
445
+ customValidator: async (password) => {
446
+ // Check against leaked passwords
447
+ const isLeaked = await checkLeakedPasswords(password);
448
+ return {
449
+ isValid: !isLeaked,
450
+ errors: isLeaked ? ['Password found in data breaches'] : []
451
+ };
452
+ }
453
+ },
454
+
455
+ // Enhancement #2: Rate limiting (default InMemoryRateLimiterService)
456
+ rateLimiterInstance: new InMemoryRateLimiterService(),
457
+
458
+ // ... other options
459
+ }),
460
+ })
461
+ ```
462
+
463
+ ### Known Limitations
464
+
465
+ - **Enhancement #3 (Reset Strategy)** - Partially implemented:
466
+ - Interfaces and strategies created but not integrated into AuthService
467
+ - Current implementation still uses VerificationService directly
468
+ - Full strategy pattern integration deferred to future release
469
+ - Magic link strategy requires consumer-implemented repository
470
+
471
+ - **InMemoryRateLimiterService** - Not suitable for multi-instance deployments:
472
+ - Rate limit state not shared between instances
473
+ - For production load-balanced deployments, implement Redis-backed IRateLimiter
474
+ - Single-instance deployments work perfectly
475
+
476
+ ## [0.2.2] - 2025-11-16
477
+
478
+ ### Fixed
479
+
480
+ - **Critical: Password Reset Email Template** - Fixed broken email template that was showing URL links instead of 6-digit verification codes
481
+ - Email service now properly displays 6-digit codes in mobile-responsive format
482
+ - Removed unused `_resetToken` parameter prefix from `sendPasswordResetEmail()` signature
483
+ - Updated HTML and plain text templates to match email/SMS verification pattern
484
+ - Users can now successfully complete password reset flow
485
+ - Template design: Mobile-responsive, centered code display, 15-minute expiry notice, security warning
486
+ - Files affected: `src/services/sendgrid-email.service.ts`
487
+
488
+ ### Added
489
+
490
+ - **Comprehensive Password Reset Test Suite** - Added 17 unit tests covering all password reset scenarios
491
+ - `requestPasswordReset()` tests: rate limiting, enumeration protection, OAuth rejection, account lock detection
492
+ - `resetPassword()` tests: code validation, password strength, token revocation, lifecycle hooks
493
+ - Edge case tests: concurrent requests, email failures, invalid codes
494
+ - Achieved 95%+ line coverage for password reset code paths
495
+ - Total package test count: 246 → 263 tests
496
+ - Files affected: `src/services/auth.service.spec.ts`
497
+
498
+ ### Changed
499
+
500
+ - **Documentation Improvements**
501
+ - Updated CLAUDE.md to clarify password reset implementation status and remove misleading "not yet supported" example
502
+ - Added comprehensive "Password Reset Flow" section to CLAUDE.md with security features documentation
503
+ - Added complete password reset section to README.md with step-by-step consumer setup guide
504
+ - Added consumer examples for GraphQL DTOs, resolver mutations, and error handling
505
+ - Documented 6-digit code approach (vs. magic link strategy) and security considerations
506
+ - Added lifecycle hooks example for `onPasswordReset()` event
507
+
508
+ ### Technical Details
509
+
510
+ **Email Template Fix:**
511
+ - Changed: `renderPasswordResetHtmlTemplate()` now accepts `code: string` parameter (was `_resetToken`)
512
+ - Changed: `renderPasswordResetTextTemplate()` now accepts `code: string` parameter (was `_resetToken`)
513
+ - Both templates now display 6-digit code prominently instead of URL links
514
+ - Template design: Mobile-responsive, centered code display, 15-minute expiry notice, security warning
515
+
516
+ **Test Suite:**
517
+ - Added: 7 tests for `requestPasswordReset()` method
518
+ - Added: 10 tests for `resetPassword()` method
519
+ - Coverage: 95%+ lines, 100% branches, 100% functions for password reset code
520
+
521
+ **Migration Notes:**
522
+ - No breaking changes
523
+ - No consumer action required (backward compatible)
524
+ - Consumers implementing password reset for the first time should follow README.md setup guide
525
+ - Existing implementations (if any) will automatically benefit from email template fix
526
+
527
+ ### Security
528
+
529
+ - Email enumeration protection maintained (generic success messages)
530
+ - Rate limiting enforced (60-second cooldown per user via `passwordResetSentAt` field)
531
+ - HMAC-SHA256 code hashing with constant-time comparison
532
+ - Token revocation on password change (all refresh tokens invalidated)
533
+ - OAuth user protection (users without passwords cannot reset)
534
+ - 15-minute code expiry with max 3 validation attempts
535
+ - Account lock integration (brute force protection applies to reset flow)
536
+
537
+ ---
538
+
539
+ ## [0.2.1] - 2025-01-16
540
+
541
+ ### Fixed
542
+
543
+ - **Facebook OAuth Email Fallback GraphQL Schema**: Added missing `performCompleteFacebookSignUp()` method to `BaseAuthResolver` for consumers using inheritance pattern
544
+ - Consumers extending `BaseAuthResolver` can now expose `completeFacebookSignUp` mutation in their GraphQL schema
545
+ - Fixes production issue where Facebook sign-in fails for users without public email (business accounts, privacy settings)
546
+ - Backend service logic already existed in `AuthService.completeFacebookSignUp()`, but was not accessible via `BaseAuthResolver`
547
+ - No breaking changes - additive only (new protected method + interface export)
548
+
549
+ ### Added
550
+
551
+ - `IAuthCompleteFacebookSignUpInput` interface export for type safety in consumer resolvers
552
+ - `BaseAuthResolver.performCompleteFacebookSignUp()` protected method following established pattern
553
+
554
+ ### Technical Details
555
+
556
+ **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.
557
+
558
+ **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.
559
+
560
+ **Solution**: Added protected `performCompleteFacebookSignUp()` method to `BaseAuthResolver`, following the same pattern as all other auth operations (signup, login, verifyEmail, verifyPhone, etc.).
561
+
562
+ **Migration**: None required for existing consumers not using Facebook OAuth. Consumers needing Facebook email fallback should:
563
+ 1. Upgrade to v0.2.1
564
+ 2. Add `completeFacebookSignUp` mutation to their custom resolver (see example below)
565
+ 3. No configuration changes needed
566
+
567
+ ### Example Consumer Implementation
568
+
569
+ ```typescript
570
+ import { Mutation, Args } from '@nestjs/graphql';
571
+ import { Throttle } from '@nestjs/throttler';
572
+ import { AuthCompleteFacebookSignUpInput } from './dto/complete-facebook-signup.input';
573
+ import { AuthResponse } from './dto/auth-response.dto';
574
+
575
+ @Mutation(() => AuthResponse, {
576
+ name: 'completeFacebookSignUp',
577
+ description: 'Complete Facebook sign-up when email not provided by Facebook',
578
+ })
579
+ @Throttle({ default: { limit: 5, ttl: 60000 } })
580
+ async completeFacebookSignUp(
581
+ @Args('input') input: AuthCompleteFacebookSignUpInput,
582
+ ): Promise<AuthResponse> {
583
+ return this.performCompleteFacebookSignUp(input) as Promise<AuthResponse>;
584
+ }
585
+ ```
586
+
587
+ ---
588
+
589
+ ## [0.2.0] - 2025-01-15
590
+
591
+ ### BREAKING CHANGES
592
+
593
+ 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.
594
+
595
+ **Migration Required**: See [MIGRATION.md](./MIGRATION.md) for step-by-step upgrade instructions.
596
+
597
+ #### Removed
598
+
599
+ - **Concrete `AuthResolver` class** - Package no longer exports a decorated GraphQL resolver
600
+ - Consumers must create their own resolver extending `BaseAuthResolver<T>`
601
+ - GraphQL decorators (@Mutation, @Query, etc.) must be added by consumer
602
+
603
+ - **GraphQL decorators on package entities and DTOs** - Package no longer provides GraphQL types
604
+ - All package entities converted to interfaces (IAuthUser, IAuthSignupInput, etc.)
605
+ - Consumers define their own @ObjectType and @InputType classes implementing package interfaces
606
+ - This eliminates GraphQL schema conflicts when integrating into existing projects
607
+
608
+ - **Direct Prisma/TypeORM coupling** - Package no longer assumes specific ORM
609
+ - Repository interfaces require manual implementation by consumer
610
+ - Full control over database schema and queries
611
+
612
+ #### Added
613
+
614
+ - **`BaseAuthResolver<T extends IAuthUser>` abstract class**
615
+ - Provides protected helper methods for all auth operations
616
+ - `performSignup()`, `performLogin()`, `performRefreshToken()`, etc.
617
+ - Type-safe generic parameter allows using any User model implementing IAuthUser
618
+
619
+ - **Interface-based architecture for complete portability**
620
+ - `IAuthUser` - User model contract (replaces concrete User entity)
621
+ - `IAuthSignupInput`, `IAuthLoginInput`, etc. - Input DTOs as interfaces
622
+ - `IAuthResponse`, `ILogoutResponse`, etc. - Response DTOs as interfaces
623
+ - Works with any database, ORM, or GraphQL schema structure
624
+
625
+ - **Comprehensive documentation**
626
+ - [QUICK-START.md](./QUICK-START.md) - Step-by-step setup guide
627
+ - [MIGRATION.md](./MIGRATION.md) - Upgrade guide from v0.1.x
628
+ - [docs/IMPLEMENTATION-ISSUES.md](./docs/IMPLEMENTATION-ISSUES.md) - Common issues and solutions
629
+ - [docs/IMPLEMENTATION-PLAN-V2.md](./docs/IMPLEMENTATION-PLAN-V2.md) - Architecture deep dive
630
+
631
+ - **Canonical enum exports**
632
+ - Package owns `AuthProvider` enum (EMAIL, GOOGLE, FACEBOOK, APPLE)
633
+ - Package owns `UserStatus` enum (ACTIVE, SUSPENDED, DELETED)
634
+ - Single source of truth prevents enum compatibility issues
635
+
636
+ #### Changed
637
+
638
+ - **User entity**: `User` class → `IAuthUser` interface
639
+ - Consumers implement interface with their own GraphQL @ObjectType class
640
+ - Full control over additional custom fields and relationships
641
+
642
+ - **All DTOs converted to interfaces with I* prefix**
643
+ - Example: `AuthSignupInput` → `IAuthSignupInput`
644
+ - Consumers create @InputType classes implementing these interfaces
645
+
646
+ - **Repository interfaces use generic type constraints**
647
+ - More flexible type system allows adaptation to various ORMs
648
+ - Type assertions at repository boundary for Prisma/TypeORM compatibility
649
+
650
+ - **Package architecture follows Layer 0 principles**
651
+ - Complete abstraction over database, ORM, and GraphQL implementation
652
+ - Zero coupling to Prisma, TypeORM, or specific GraphQL libraries
653
+ - Dependency injection via instance-based pattern (not class references)
654
+
655
+ ### Features Preserved
656
+
657
+ All authentication features from v0.1.x remain fully functional:
658
+
659
+ - JWT authentication with automatic refresh token rotation
660
+ - OAuth 2.0 (Google, Facebook, Apple)
661
+ - Email verification with 6-digit PIN codes
662
+ - SMS verification via Twilio
663
+ - Brute force protection with account lockout
664
+ - Biometric authentication support
665
+ - Account linking (multiple OAuth providers per account)
666
+ - Security: HMAC-SHA256 token hashing, AES-256-GCM encryption, constant-time comparison
667
+
668
+ ### Migration Summary
669
+
670
+ **Before (v0.1.x)**:
671
+ ```typescript
672
+ import { AuthModule, AuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
673
+
674
+ @Module({
675
+ imports: [AuthModule.forRoot(...)],
676
+ providers: [AuthResolver] // Use package resolver directly
677
+ })
678
+ ```
679
+
680
+ **After (v0.2.0)**:
681
+ ```typescript
682
+ import { AuthModule, BaseAuthResolver } from '@ambushsoftworks/nestjs-auth-graphql';
683
+
684
+ // 1. Create your resolver
685
+ @Resolver()
686
+ export class MyAuthResolver extends BaseAuthResolver<User> {
687
+ @Mutation(() => AuthResponse)
688
+ async signup(@Args('input') input: SignupInput): Promise<AuthResponse> {
689
+ return this.performSignup(input) as Promise<AuthResponse>;
690
+ }
691
+ // ... override all 11 mutations + 3 queries
692
+ }
693
+
694
+ @Module({
695
+ imports: [AuthModule.forRoot(...)],
696
+ providers: [MyAuthResolver] // Use your custom resolver
697
+ })
698
+ ```
699
+
700
+ **Benefits**:
701
+ - Zero GraphQL schema conflicts with existing projects
702
+ - Full control over GraphQL types, fields, and descriptions
703
+ - Works with any database schema (not just Prisma)
704
+ - Type-safe integration through interfaces
705
+ - Easier to customize and extend
706
+
707
+ ### Testing
708
+
709
+ - **553+ tests passing** in Lift integration (production validation)
710
+ - **0 TypeScript errors** in package build
711
+ - **0 runtime regressions** from architectural changes
712
+ - **100% backward compatibility** of authentication features
713
+
714
+ ### Known Issues
715
+
716
+ - **Pre-existing test failures**: BruteForceProtectionService and MuscleGroupRecoveryService tests have DI mock issues (not caused by v0.2.0 refactor, existed in v0.1.x)
717
+ - **Migration effort**: Upgrading from v0.1.x requires 2-4 hours to create resolver and GraphQL types
718
+
719
+ ---
720
+
721
+ ## [0.1.10] - 2025-01-14
722
+
723
+ ### Fixed
724
+ - Fixed instance-based DI pattern (removed ModuleRef complexity)
725
+ - Cleaned up repository injection in AuthModule.forRootAsync()
726
+
727
+ ### Changed
728
+ - Simplified dependency injection to use direct instance passing
729
+ - Improved documentation for repository configuration
730
+
731
+ ---
732
+
733
+ ## [0.1.9] - 2025-01-13
734
+
735
+ ### Added
736
+ - Published package to npm registry as `@ambushsoftworks/nestjs-auth-graphql`
737
+ - Standalone repository on GitLab
738
+
739
+ ### Changed
740
+ - Migrated Lift backend to use published npm package
741
+ - Removed local package directory from monorepo
742
+
743
+ ---
744
+
745
+ ## [0.1.8] - 2025-01-12
746
+
747
+ ### Changed
748
+ - Backend package cleanup and optimization
749
+ - Updated authentication service interfaces
750
+
751
+ ---
752
+
753
+ ## [0.1.7] - 2025-01-11
754
+
755
+ ### Added
756
+ - SMS verification via Twilio integration
757
+ - Phone number validation using libphonenumber-js
758
+ - `verifyPhone`, `resendPhoneVerification`, `removePhoneNumber` mutations
759
+
760
+ ### Changed
761
+ - Enhanced verification service with SMS support
762
+ - Router fixes for phone verification flow
763
+
764
+ ---
765
+
766
+ ## [0.1.0] - 2024-11-01
767
+
768
+ ### Added
769
+ - Initial package extraction from Lift app
770
+ - JWT authentication with refresh tokens
771
+ - Email verification via SendGrid
772
+ - Brute force protection
773
+ - Google and Facebook OAuth
774
+ - Biometric authentication support
775
+ - 246+ tests from production codebase
776
+
777
+ ---
778
+
779
+ [0.8.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.7.2...v0.8.0
780
+ [0.2.2]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.2.1...v0.2.2
781
+ [0.2.1]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.2.0...v0.2.1
782
+ [0.2.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.10...v0.2.0
783
+ [0.1.10]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.9...v0.1.10
784
+ [0.1.9]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.8...v0.1.9
785
+ [0.1.8]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.7...v0.1.8
786
+ [0.1.7]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/compare/v0.1.0...v0.1.7
787
+ [0.1.0]: https://gitlab.com/ambushworks/nestjs-auth-graphql/-/releases/v0.1.0