@ambushsoftworks/nestjs-auth-graphql 0.9.0 → 0.9.1

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 (3) hide show
  1. package/CHANGELOG.md +22 -6
  2. package/README.md +50 -5
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -27,6 +27,19 @@ for buried obligations. **The marker was introduced in 0.9.0 and has not been
27
27
  retro-applied**, so it is reliable from 0.9.0 onward only. Convention adopted
28
28
  from `@ambushsoftworks/nestjs-payments-graphql`.
29
29
 
30
+ ## [0.9.1] - 2026-09-14
31
+
32
+ Documentation only — no code changes. npm shows the README of the latest
33
+ published version, so these corrections needed a release to reach the package
34
+ page.
35
+
36
+ ### Documentation
37
+ - **README: account-status enforcement was undocumented.** 0.9.0 made suspension and deletion revoke access — `ACCOUNT_INACTIVE` (HTTP 403) at login, on refresh and on every authenticated request — and added the `isUserActive` option, but the README mentioned none of it, including in **Migrating to v0.9.0**. Adds an **Account Status** section, an `isUserActive` row under **Common Options**, a **Security Features** entry, and a migration item: clients need to handle `ACCOUNT_INACTIVE`, and repositories must stop hiding inactive users.
38
+ - **README: JWT Validation Modes** now says `'full'` rejects inactive accounts on the next request, and that in `'payload-only'` a suspended user keeps working until their access token expires while refresh stays blocked.
39
+ - **README: Utilities** lists the helpers 0.9.0 exported: `isUserActiveByDefault`, `emailForLog`, `ipForLog`, `maskPhoneDigits`, `redactSecurityEventMetadata` and `hashIp`.
40
+ - **README: Migrating to v0.9.0** no longer says code mode is entirely unchanged. With no base URL at all, code-mode emails now carry no page link.
41
+ - **CHANGELOG: the 0.9.0 release note** said the `rc.4` and `rc.5` changes had not been run on an adopter's staging. Lift had deployed `rc.5` to staging; the note is corrected in place.
42
+
30
43
  ## [0.9.0] - 2026-09-14
31
44
 
32
45
  Three independent workstreams land here: a trim of the email interface surface
@@ -43,12 +56,15 @@ phone numbers — see **Security**.
43
56
  > Shipped through five release candidates on the `next` dist-tag before
44
57
  > reaching `latest`; this release is the content of `0.9.0-rc.5` under its
45
58
  > final version. Ariadne verified token and code mode end to end before
46
- > `rc.1`, and Lift ran its automated staging checks and its real-inbox, phone
47
- > and Google sign-in checks against `rc.3`. What arrived after that — the
48
- > `features` flag removal and clean build in `rc.4`, and personal data in logs
49
- > and the code-mode base URL in `rc.5` — was verified by this package's test
50
- > suite and against the published candidates, not on an adopter's staging.
51
- > `rc.2` carried eight stale compiled files; see **Fixed (release tooling)**.
59
+ > `rc.1`. Lift ran its automated staging checks and its real-inbox, phone and
60
+ > Google sign-in checks against `rc.3`, then deployed `rc.5` to staging: 18/18
61
+ > automated auth checks, a clean boot, no code-mode base URL warning, and no
62
+ > raw email, IP address or phone number in the package's log output. `rc.2`
63
+ > carried eight stale compiled files; see **Fixed (release tooling)**.
64
+ >
65
+ > *Corrected in 0.9.1. As published in 0.9.0, this note said the `rc.4` and
66
+ > `rc.5` changes had not been run on an adopter's staging; Lift's `rc.5`
67
+ > staging run had been recorded minutes before the note was written.*
52
68
 
53
69
  ### ⚠ External configuration required
54
70
  - **`verificationMode: 'token'` requires your frontend to serve two routes.**
package/README.md CHANGED
@@ -19,6 +19,7 @@ Production-grade authentication module for NestJS GraphQL + REST APIs. JWT, cook
19
19
  - [Brute Force Protection](#brute-force-protection)
20
20
  - [Password Policy](#password-policy)
21
21
  - [Lifecycle Hooks](#lifecycle-hooks)
22
+ - [Account Status](#account-status)
22
23
  - [JWT Validation Modes](#jwt-validation-modes)
23
24
  - [JWT Payload Factory](#jwt-payload-factory)
24
25
  - [Configuration Reference](#configuration-reference)
@@ -847,10 +848,33 @@ All hooks are optional. Async hooks are awaited but failures are logged and do n
847
848
 
848
849
  `onAccountLocked` fires from `BruteForceProtectionService.recordFailedAttempt` on the transition from "not locked" → "locked". Subsequent failed attempts while the account remains locked do **not** re-fire (use `onAuthFailure` for per-attempt signal). Wire it to send the account-locked email yourself — the package no longer ships a `sendAccountLockedEmail` method on `IEmailService`; notification surface area beyond verification, password-reset, and password-changed is consumer-owned via this hook.
849
850
 
851
+ ### Account Status
852
+
853
+ Suspending or deleting a user takes effect on their next request. The package checks each user's `status` and `deletedAt` itself:
854
+
855
+ | Where | Effect |
856
+ |-------|--------|
857
+ | `login` | Rejected after the password is verified, so the check cannot reveal to someone without the password that an address is suspended |
858
+ | Every authenticated request (`jwtValidation: 'full'`) | Rejected on the next request, not at token expiry |
859
+ | `refreshToken` | Rejected, including within the refresh grace period |
860
+ | Any token issuance | Rejected, as a backstop for every flow that mints tokens |
861
+
862
+ A rejected request throws `AccountInactiveException`: a GraphQL error with `extensions.code` `ACCOUNT_INACTIVE`, `statusCode` `403`, and the message `This account is no longer active.` It does not say whether the account was suspended or deleted. Unlike `AccountLockedException` it does not clear on its own, so clients should sign the user out rather than offer a retry. Each rejection logs `SecurityEvent.ACCOUNT_INACTIVE_BLOCKED` with a `phase` of `login`, `refresh`, `session` or `issue`.
863
+
864
+ **Which accounts are inactive.** By default (`isUserActiveByDefault`), a user with `deletedAt` set, or whose `status` is `SUSPENDED` or `DELETED`, compared case-insensitively. Any other status — including one the package does not recognise — counts as active, so a status vocabulary that differs from `UserStatus` cannot lock everyone out. If yours differs, supply a predicate:
865
+
866
+ ```typescript
867
+ isUserActive: (user) => user.status !== 'FROZEN' && user.status !== 'CLOSED' && !user.deletedAt,
868
+ ```
869
+
870
+ It must be synchronous and pure: it runs on every authenticated request, against the user already loaded.
871
+
872
+ **Your repository must return inactive users.** `findById` and `findByEmail` should return suspended and soft-deleted users like any other. A repository that hides them turns `ACCOUNT_INACTIVE` into a "not found" failure, and the package can no longer tell the client why.
873
+
850
874
  ### JWT Validation Modes
851
875
 
852
- - **`'full'`** (default) -- Calls `userRepository.findById()` on every request to verify the user still exists.
853
- - **`'payload-only'`** -- Returns `{ id, email }` from the JWT payload without a DB lookup. Faster, but does not detect deleted/disabled users until token expiry.
876
+ - **`'full'`** (default) -- Calls `userRepository.findById()` on every request, rejecting the request if the user no longer exists or is no longer active (see [Account Status](#account-status)).
877
+ - **`'payload-only'`** -- Returns `{ id, email }` from the JWT payload without a DB lookup. Faster, but nothing about the account is re-checked per request: a suspended or deleted user keeps working until their access token expires (`jwtExpiresIn`, 15 minutes by default). Refresh is still blocked, so one access-token lifetime is the whole window.
854
878
 
855
879
  ```typescript
856
880
  jwtValidation: 'payload-only',
@@ -890,6 +914,7 @@ jwtPayloadFactoryInstance: {
890
914
  | `jwtExpiresIn` | `string` | `'15m'` | Access token TTL |
891
915
  | `refreshTokenExpiresIn` | `string` | `'30d'` | Refresh token TTL |
892
916
  | `jwtValidation` | `'full' \| 'payload-only'` | `'full'` | JWT validation strategy |
917
+ | `isUserActive` | `(user) => boolean` | `isUserActiveByDefault` | Decides whether an account may authenticate. See [Account Status](#account-status) |
893
918
  | `bcryptRounds` | `number` | `12` | Password hashing cost |
894
919
  | `nodeEnv` | `string` | `'production'` | Environment identifier |
895
920
  | `passwordPolicy` | `PasswordPolicyConfig` | See [Password Policy](#password-policy) | Password strength rules |
@@ -1034,12 +1059,17 @@ When realm support is enabled, `RealmMiddleware` runs before all guards (as Nest
1034
1059
 
1035
1060
  ## Utilities
1036
1061
 
1037
- Helper functions exported for use in custom guards and middleware:
1062
+ Helper functions exported for use in custom guards, middleware and loggers:
1038
1063
 
1039
1064
  | Function | Description |
1040
1065
  |----------|-------------|
1041
1066
  | `getRequestFromContext(context)` | Extract request from `ExecutionContext` (handles GraphQL + HTTP) |
1042
1067
  | `extractIpAddress(request)` | Extract client IP (checks `clientIp`, `req.ip`, falls back to `'unknown'`) |
1068
+ | `isUserActiveByDefault(user)` | The default account-active predicate (see [Account Status](#account-status)) |
1069
+ | `emailForLog(email)`, `ipForLog(ip)` | An email or IP as it may appear in a log line — `emailHash=…` / `ipHash=…`, salted like the package's own logs |
1070
+ | `maskPhoneDigits(phone)` | Masks every digit but the last two, keeping formatting |
1071
+ | `redactSecurityEventMetadata(metadata)` | Security-event metadata with email, IP and phone fields hashed or masked — what `ConsoleAuthLogger` prints. Call it from a custom `IAuthLogger` for the same treatment |
1072
+ | `hashIp(ip, salt)` | HMAC-SHA256 IP hash (16 hex characters) used for rate-limit keys |
1043
1073
 
1044
1074
  ```typescript
1045
1075
  import { getRequestFromContext, extractIpAddress } from '@ambushsoftworks/nestjs-auth-graphql';
@@ -1059,6 +1089,7 @@ export class CustomGuard implements CanActivate {
1059
1089
 
1060
1090
  - **Refresh token rotation** with HMAC-SHA256 hashing and idempotent 10-second grace period
1061
1091
  - **Bcrypt password hashing** with configurable rounds (default: 12)
1092
+ - **Account status enforcement** -- suspended and deleted users are rejected at login, on refresh and on every authenticated request (`ACCOUNT_INACTIVE`)
1062
1093
  - **AES-256-GCM encryption** for OAuth tokens at rest
1063
1094
  - **Constant-time comparison** for verification codes
1064
1095
  - **Account enumeration prevention** on signup (optional)
@@ -1070,6 +1101,15 @@ export class CustomGuard implements CanActivate {
1070
1101
 
1071
1102
  ## Migrating to v0.9.0
1072
1103
 
1104
+ **Behaviour change — suspended and deleted accounts are rejected.** Before
1105
+ 0.9.0 the package never checked `status` or `deletedAt`, so suspending a user
1106
+ revoked nothing unless your repository happened to hide them. Now those users
1107
+ get `ACCOUNT_INACTIVE` (HTTP 403) at login, on refresh and on their next
1108
+ request. Handle that error in your clients by signing the user out, check that
1109
+ your status values are recognised (supply `isUserActive` if not), and stop
1110
+ filtering inactive users out of `findById` / `findByEmail`. See
1111
+ [Account Status](#account-status).
1112
+
1073
1113
  **Changed — log output no longer contains raw personal data.** If you parse
1074
1114
  `ConsoleAuthLogger` output or the package's log lines, expect `emailHash` /
1075
1115
  `ipHash` in place of `email` / `ipAddress`, masked phone numbers, and user IDs
@@ -1123,8 +1163,13 @@ Three things to check, in the order they are most likely to bite:
1123
1163
  Token TTL also differs from code TTL: 60 minutes by default
1124
1164
  (`verification.tokenExpiresInMinutes`) versus 15 for codes.
1125
1165
 
1126
- **`verificationMode: 'code'` — the default — is unaffected.** Behaviour is
1127
- identical, including the legacy `FRONTEND_URL` fallback for the reset page URL.
1166
+ **`verificationMode: 'code'` — the default — is unaffected by the token-mode
1167
+ fix**, including the legacy `FRONTEND_URL` fallback for the page link. One
1168
+ change applies to code mode separately: with neither `verification.baseUrl` nor
1169
+ `FRONTEND_URL` set, code-mode emails now carry no page link, where they used to
1170
+ carry an `https://app.example.com` placeholder that never resolved. The bundled
1171
+ email service never showed that link in code mode; only a custom `IEmailService`
1172
+ that rendered it notices.
1128
1173
 
1129
1174
  **`bcrypt` 5 → 6.** No code change and no hash migration: existing hashes
1130
1175
  verify unchanged, and hashes written by 0.9.0 still verify if you roll back to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ambushsoftworks/nestjs-auth-graphql",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Production-grade authentication package for NestJS with GraphQL, supporting JWT, OAuth, email/SMS verification, and biometric auth",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",