nestjs-boost 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +6 -0
  4. package/README.md +168 -2
  5. package/bin/nestjs-boost.mjs +148 -0
  6. package/guidelines/core.md +64 -0
  7. package/lib/agents.mjs +137 -0
  8. package/lib/installer.mjs +450 -0
  9. package/lib/prompt.mjs +131 -0
  10. package/lib/setup.mjs +60 -0
  11. package/manifest.json +676 -0
  12. package/package.json +67 -6
  13. package/scripts/postinstall.mjs +49 -0
  14. package/skills/nestjs-ai/SKILL.md +145 -0
  15. package/skills/nestjs-ai/references/ai-reference.md +143 -0
  16. package/skills/nestjs-architecture/SKILL.md +83 -0
  17. package/skills/nestjs-architecture/references/dynamic-modules.md +92 -0
  18. package/skills/nestjs-architecture/references/scopes-lifecycle.md +98 -0
  19. package/skills/nestjs-architecture/references/troubleshooting.md +77 -0
  20. package/skills/nestjs-authentication/SKILL.md +115 -0
  21. package/skills/nestjs-authentication/references/flows.md +180 -0
  22. package/skills/nestjs-authorization/SKILL.md +104 -0
  23. package/skills/nestjs-caching/SKILL.md +109 -0
  24. package/skills/nestjs-cli-tooling/SKILL.md +94 -0
  25. package/skills/nestjs-cli-tooling/references/nest-cli-json.md +91 -0
  26. package/skills/nestjs-configuration/SKILL.md +109 -0
  27. package/skills/nestjs-cqrs/SKILL.md +124 -0
  28. package/skills/nestjs-database-drizzle/SKILL.md +117 -0
  29. package/skills/nestjs-database-mikro-orm/SKILL.md +109 -0
  30. package/skills/nestjs-database-mongoose/SKILL.md +124 -0
  31. package/skills/nestjs-database-prisma/SKILL.md +126 -0
  32. package/skills/nestjs-database-sequelize/SKILL.md +88 -0
  33. package/skills/nestjs-database-typeorm/SKILL.md +110 -0
  34. package/skills/nestjs-deployment/SKILL.md +133 -0
  35. package/skills/nestjs-deployment/references/serverless-and-systemd.md +133 -0
  36. package/skills/nestjs-events/SKILL.md +78 -0
  37. package/skills/nestjs-file-storage/SKILL.md +111 -0
  38. package/skills/nestjs-file-storage/references/storage-reference.md +82 -0
  39. package/skills/nestjs-graphql/SKILL.md +119 -0
  40. package/skills/nestjs-graphql/references/federation.md +110 -0
  41. package/skills/nestjs-graphql/references/schema-features.md +133 -0
  42. package/skills/nestjs-health-checks/SKILL.md +114 -0
  43. package/skills/nestjs-http-client/SKILL.md +118 -0
  44. package/skills/nestjs-http-client/references/http-client-reference.md +116 -0
  45. package/skills/nestjs-http-platform/SKILL.md +104 -0
  46. package/skills/nestjs-http-platform/references/adapters.md +88 -0
  47. package/skills/nestjs-http-web-features/SKILL.md +113 -0
  48. package/skills/nestjs-http-web-features/references/uploads-streaming.md +108 -0
  49. package/skills/nestjs-i18n/SKILL.md +113 -0
  50. package/skills/nestjs-idempotency/SKILL.md +112 -0
  51. package/skills/nestjs-locks/SKILL.md +108 -0
  52. package/skills/nestjs-logging/SKILL.md +123 -0
  53. package/skills/nestjs-mail/SKILL.md +125 -0
  54. package/skills/nestjs-mail/references/mail-reference.md +90 -0
  55. package/skills/nestjs-mcp-server/SKILL.md +152 -0
  56. package/skills/nestjs-mcp-server/references/mcp-reference.md +146 -0
  57. package/skills/nestjs-microservices/SKILL.md +108 -0
  58. package/skills/nestjs-microservices/references/transports.md +117 -0
  59. package/skills/nestjs-observability/SKILL.md +108 -0
  60. package/skills/nestjs-observability/references/apm-and-otel.md +106 -0
  61. package/skills/nestjs-openapi/SKILL.md +90 -0
  62. package/skills/nestjs-openapi/references/swagger-reference.md +180 -0
  63. package/skills/nestjs-outbox/SKILL.md +119 -0
  64. package/skills/nestjs-outbox/references/outbox-reference.md +74 -0
  65. package/skills/nestjs-passport-auth/SKILL.md +125 -0
  66. package/skills/nestjs-queues/SKILL.md +118 -0
  67. package/skills/nestjs-queues/references/bull-legacy.md +75 -0
  68. package/skills/nestjs-request-pipeline/SKILL.md +146 -0
  69. package/skills/nestjs-request-pipeline/references/filters-interceptors.md +114 -0
  70. package/skills/nestjs-request-pipeline/references/pipes-decorators.md +80 -0
  71. package/skills/nestjs-resilience/SKILL.md +116 -0
  72. package/skills/nestjs-resilience/references/resilience-reference.md +74 -0
  73. package/skills/nestjs-scheduling/SKILL.md +100 -0
  74. package/skills/nestjs-security-hardening/SKILL.md +99 -0
  75. package/skills/nestjs-security-hardening/references/throttler-advanced.md +78 -0
  76. package/skills/nestjs-standalone-apps/SKILL.md +78 -0
  77. package/skills/nestjs-testing/SKILL.md +118 -0
  78. package/skills/nestjs-testing/references/advanced-testing.md +62 -0
  79. package/skills/nestjs-upgrading/SKILL.md +82 -0
  80. package/skills/nestjs-upgrading/references/esm-switch.md +114 -0
  81. package/skills/nestjs-validation-serialization/SKILL.md +124 -0
  82. package/skills/nestjs-validation-serialization/references/validation-options.md +84 -0
  83. package/skills/nestjs-webhooks/SKILL.md +116 -0
  84. package/skills/nestjs-webhooks/references/webhooks-reference.md +132 -0
  85. package/skills/nestjs-websockets/SKILL.md +108 -0
  86. package/skills/nestjs-workflows/SKILL.md +123 -0
  87. package/skills/nestjs-workflows/references/workflows-reference.md +85 -0
@@ -0,0 +1,98 @@
1
+ # Injection scopes, lifecycle hooks, ModuleRef and lazy loading
2
+
3
+ Sources: [Injection scopes](https://docs.nestjs.com/fundamentals/injection-scopes) · [Lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events) · [Module reference](https://docs.nestjs.com/fundamentals/module-ref) · [Lazy-loading modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules) · [Discovery service](https://docs.nestjs.com/fundamentals/discovery-service)
4
+
5
+ ## Contents
6
+ - Choosing a scope
7
+ - How REQUEST scope spreads
8
+ - Accessing the request; INQUIRER
9
+ - Durable providers (multi-tenant)
10
+ - Lifecycle hooks and shutdown
11
+ - ModuleRef, lazy loading, discovery
12
+
13
+ ## Choosing a scope
14
+
15
+ | Scope | Instance | Use when |
16
+ | --- | --- | --- |
17
+ | `DEFAULT` (singleton) | One per app, created at bootstrap | Almost always. Recommended by the docs. |
18
+ | `REQUEST` | One per request, garbage-collected after | The provider itself must hold per-request state |
19
+ | `TRANSIENT` | One per consumer | Stateful helpers each consumer needs its own copy of (e.g. a logger with context) |
20
+
21
+ ```typescript
22
+ @Injectable({ scope: Scope.REQUEST })
23
+ export class TenantService {}
24
+
25
+ { provide: 'CACHE_MANAGER', useClass: CacheManager, scope: Scope.TRANSIENT } // long-hand form
26
+ @Controller({ path: 'cats', scope: Scope.REQUEST }) // controller scope
27
+ ```
28
+
29
+ **Prefer singletons plus `AsyncLocalStorage`** when a provider only needs to *read* per-request values (current user, tenant, locale). Every provider stays a singleton, and it works in HTTP, microservices and gateways (see the nestjs-logging skill).
30
+
31
+ ## How REQUEST scope spreads
32
+
33
+ - REQUEST scope **bubbles up**: anything that injects a request-scoped provider becomes request-scoped, including the controller. Making a widely used provider (DB connection, logger) request-scoped makes most of the app request-scoped.
34
+ - TRANSIENT does **not** bubble up: a singleton injecting a transient provider stays a singleton.
35
+ - Request-scoped providers cost a new instance tree per request. The docs expect ≤ ~5% latency in a well-designed app.
36
+ - Must stay singletons: Passport strategies, cron jobs.
37
+ - Since v12, WebSocket gateways support request-scoped providers, scoped to the socket connection.
38
+
39
+ ## Accessing the request; INQUIRER
40
+
41
+ ```typescript
42
+ import { REQUEST, INQUIRER } from '@nestjs/core';
43
+
44
+ @Injectable({ scope: Scope.REQUEST })
45
+ export class CatsService {
46
+ constructor(@Inject(REQUEST) private readonly request: Request) {} // GraphQL: @Inject(CONTEXT) from @nestjs/graphql
47
+ }
48
+
49
+ @Injectable({ scope: Scope.TRANSIENT })
50
+ export class ScopedLogger {
51
+ constructor(@Inject(INQUIRER) private readonly parent: object) {} // the class that injected this provider
52
+ }
53
+ ```
54
+
55
+ Injecting `REQUEST` makes the provider request-scoped automatically.
56
+
57
+ ## Durable providers (multi-tenant)
58
+
59
+ With a few tenants, avoid rebuilding the tree per request: register a `ContextIdStrategy` that maps requests to one DI sub-tree per tenant, and mark providers `durable: true`. Durability bubbles up like request scope. Not suitable for very many tenants. See the docs section "Durable providers" for the strategy code.
60
+
61
+ ## Lifecycle hooks and shutdown
62
+
63
+ | Hook | When |
64
+ | --- | --- |
65
+ | `onModuleInit()` | Host module's dependencies resolved |
66
+ | `onApplicationBootstrap()` | All modules initialized, before listening |
67
+ | `onModuleDestroy()` | Termination started (signal or `app.close()`) |
68
+ | `beforeApplicationShutdown(signal?)` | After all `onModuleDestroy()` complete |
69
+ | `onApplicationShutdown(signal?)` | After connections close; last step of `app.close()` |
70
+
71
+ - `onModuleInit`/`onApplicationBootstrap` may be `async`; bootstrap waits for them.
72
+ - Init hooks run module by module: the most deeply imported modules (and global modules) first, the root module last. In v12, hooks are ordered by component hierarchy level, so don't depend on the order between siblings.
73
+ - Shutdown hooks need `app.enableShutdownHooks()` in `main.ts` to react to `SIGTERM`/`SIGINT`. They are off by default because the listeners use memory; avoid enabling them in many parallel test apps. On Windows, `SIGTERM` never fires.
74
+ - Hooks are **not** called for request-scoped classes or in lazy-loaded modules.
75
+
76
+ ## ModuleRef, lazy loading, discovery
77
+
78
+ ```typescript
79
+ constructor(private readonly moduleRef: ModuleRef) {}
80
+
81
+ this.moduleRef.get(Service); // singleton, current module
82
+ this.moduleRef.get(Service, { strict: false }); // look in the whole app
83
+ await this.moduleRef.resolve(ScopedService); // transient/request-scoped: new instance per call
84
+ const id = ContextIdFactory.create();
85
+ await this.moduleRef.resolve(ScopedService, id); // same instance for the same contextId
86
+ await this.moduleRef.create(SomeClass); // instantiate a class not registered as provider
87
+ ```
88
+
89
+ Lazy loading (no lifecycle hooks run in the loaded module):
90
+
91
+ ```typescript
92
+ constructor(private readonly lazyModuleLoader: LazyModuleLoader) {}
93
+ const { ReportsModule } = await import('./reports/reports.module.js');
94
+ const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule);
95
+ const reports = moduleRef.get(ReportsService);
96
+ ```
97
+
98
+ `DiscoveryService` (from `@nestjs/core`) lists providers and controllers at runtime, e.g. to find every provider carrying a custom decorator's metadata. See the docs chapter for its API.
@@ -0,0 +1,77 @@
1
+ # Troubleshooting DI and module errors
2
+
3
+ Sources: [Common errors](https://docs.nestjs.com/faq/common-errors) · [Circular dependency](https://docs.nestjs.com/fundamentals/circular-dependency) · [SWC](https://docs.nestjs.com/recipes/swc)
4
+
5
+ ## Contents
6
+ - Procedure for any startup DI error
7
+ - "Cannot resolve dependency" with a token name
8
+ - "... at index [n] is available in the current module" (no token name)
9
+ - Circular dependency between providers or modules
10
+ - Monorepo: ModuleRef cannot be resolved
11
+ - "File change detected" loops endlessly (Windows)
12
+
13
+ ## Procedure for any startup DI error
14
+
15
+ 1. Read the error: which **provider** is being built, which **token** at which **index** is missing, and in which **module** Nest looked.
16
+ 2. Open that module. For the missing token, it must be either in the module's `providers`, or exported by a module listed in its `imports`.
17
+ 3. If the token belongs to another feature module: add it to that module's `exports` and import that module. Do not copy the provider into the failing module's `providers`.
18
+ 4. If it still fails, run once with `NEST_DEBUG=1` to see each resolution step and the module searched.
19
+ 5. Rebuild and **start the app** to confirm. Compilation alone doesn't prove the fix.
20
+
21
+ ## "Cannot resolve dependency" with a token name
22
+
23
+ ```
24
+ Nest can't resolve dependencies of the OrdersService (?). Please make sure that the argument UsersService at index [0] is available in the OrdersModule module.
25
+ ```
26
+
27
+ Causes, most common first:
28
+ - The provider isn't exported by its module, or that module isn't imported. Fix: `exports: [UsersService]` in `UsersModule`, and `imports: [UsersModule]` in `OrdersModule`.
29
+ - An `@Injectable()` class was put in `imports` instead of `providers`. Nest reports this explicitly; move it.
30
+ - The same provider is listed in both a feature module and the root module, so Nest instantiates it twice, the second time without its dependencies. Keep it in one module and export it.
31
+ - Self-injection: the missing token equals the provider being built.
32
+
33
+ ## "... at index [n] is available in the current module" (no token name)
34
+
35
+ The dependency is `undefined` or `Object` at runtime. Causes:
36
+ 1. **Circular file import**: two files import each other (e.g. a module file declares a token and imports a provider that imports the token back). Move shared constants to their own file.
37
+ 2. **Injection by interface or type alias**: use a class, an abstract class, or `@Inject(TOKEN)`.
38
+ 3. **`import type { X }`** for an injectable class: use a value import.
39
+
40
+ ## Circular dependency between providers or modules
41
+
42
+ ```
43
+ Nest cannot create the <module> instance.
44
+ The module at index [<n>] of the <module> "imports" array is undefined.
45
+ ```
46
+
47
+ First try to break the cycle: extract the shared logic into a third provider or module that both depend on. If the cycle is genuine:
48
+
49
+ ```typescript
50
+ // Providers: forwardRef on BOTH sides
51
+ @Injectable()
52
+ export class CatsService {
53
+ constructor(@Inject(forwardRef(() => CommonService)) private readonly common: WrapperType<CommonService>) {}
54
+ }
55
+ @Injectable()
56
+ export class CommonService {
57
+ constructor(@Inject(forwardRef(() => CatsService)) private readonly cats: WrapperType<CatsService>) {}
58
+ }
59
+
60
+ // Modules: forwardRef on BOTH sides
61
+ @Module({ imports: [forwardRef(() => CatsModule)] })
62
+ export class CommonModule {}
63
+ ```
64
+
65
+ - `WrapperType<T> = T` (define it once, e.g. `src/common/wrapper-type.ts`) stops the type being stored in decorator metadata. It is required with SWC and ESM, where emitted types otherwise trigger circular-import errors. ORMs provide an equivalent, e.g. TypeORM's `Relation<T>`.
66
+ - Instantiation order is indeterminate: constructors must not call into the other side.
67
+ - Request-scoped providers in a cycle can end up `undefined`.
68
+ - Alternatively, inject `ModuleRef` and get the other provider lazily at call time.
69
+ - Barrel files (`index.ts`) importing modules and providers are a frequent hidden cause.
70
+
71
+ ## Monorepo: ModuleRef cannot be resolved
72
+
73
+ `... the argument ModuleRef at index [n] is available ...` usually means two copies of `@nestjs/core` are installed (e.g. one nested under a workspace package). For pnpm, declare `@nestjs/core` as a peer dependency of the internal package and use `dependenciesMeta.<pkg>.injected: true`. For Yarn workspaces, use `nohoist`.
74
+
75
+ ## "File change detected" loops endlessly (Windows)
76
+
77
+ Add to `tsconfig.json`: `"watchOptions": { "watchFile": "fixedPollingInterval" }`.
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: nestjs-authentication
3
+ description: Adds sign-in to NestJS 12 with the first-party @nestjs/authentication package — global AuthenticationGuard with @Public(), credential providers (SessionCookieProvider, JwtBearerProvider, ApiKeyProvider) registered on AuthenticationRegistry, scrypt PasswordHasher, SignInService sessions in __Host- cookies, TokenService access and rotating refresh tokens, @CurrentUser/@Authenticate/AuthenticationContext, email verification, password reset, TOTP two-factor, magic links, OpenID Connect (Google, GitHub, Microsoft), session management, API keys, database stores and the production guard. Use when building sign-up/sign-in, sessions or JWTs, protecting routes, reading the current user, adding 2FA, social login, passwordless login or API keys, in a project that has or can add @nestjs/authentication. Not for role/permission checks (nestjs-authorization), the Passport recipe (nestjs-passport-auth) or CSRF/rate-limit setup in general (nestjs-security-hardening).
4
+ compatibility: NestJS 12.x (12.1+ for enableCsrfProtection); @nestjs/authentication; optional @nestjs/throttler, @nestjs/mail, @nestjs/drizzle or TypeORM for stores
5
+ metadata:
6
+ version: "0.1.0"
7
+ nestjs-major: "12"
8
+ docs-crawled: "2026-11-05"
9
+ ---
10
+
11
+ # NestJS authentication (@nestjs/authentication)
12
+
13
+ Detect first: if the project already uses `@nestjs/passport`, keep it and use nestjs-passport-auth unless the user asks to migrate. For new auth, prefer `@nestjs/authentication`: it ships sessions, hashing, tokens, 2FA, OIDC and API keys built on Node built-ins. Don't hand-roll bcrypt + `@nestjs/jwt` guards.
14
+
15
+ ```bash
16
+ npm i @nestjs/authentication
17
+ ```
18
+
19
+ ## Core setup
20
+
21
+ ```typescript
22
+ // auth/session-auth.provider.ts — a credential provider: request → user or null
23
+ @Injectable()
24
+ export class SessionAuth extends SessionCookieProvider<User> {
25
+ constructor(private readonly users: UsersRepository, registry: AuthenticationRegistry) {
26
+ super();
27
+ registry.registerProvider(this); // order 0; JwtAuth { order: 1 }, ApiKeyAuth { order: 2 }
28
+ }
29
+ validate(session: SessionRecord) { return this.users.findById(session.userId); } // null ends the session
30
+ }
31
+
32
+ // app.module.ts — options are plain values; classes register themselves
33
+ AuthenticationModule.forRoot({ session: { absoluteTtl: '14d', idleTtl: '3d' } }),
34
+ ```
35
+
36
+ - The module registers a **global guard: every route needs a user** (401 `missing_credentials`). Mark open routes/controllers with `@Public()` (health checks, sign-in, catalog).
37
+ - Lifetimes are ms or strings (`'15m'`, `'14d'`). Unlike `@nestjs/jwt`, numbers are **milliseconds**: `3600` is 3.6 s.
38
+ - Your user store stays yours: providers and handlers call your repository. Never return password hashes from it.
39
+ - Type the user once: `declare module '@nestjs/authentication' { interface AuthenticationTypes { user: User } }`.
40
+
41
+ ## Sign-up and sign-in (password → session cookie)
42
+
43
+ ```typescript
44
+ // credentials.service.ts
45
+ async verify(email: string, password: string) {
46
+ const found = await this.users.findCredentials(email);
47
+ const valid = await this.passwordHasher.verify(password, found?.passwordHash); // dummy hash when unknown: no timing leak
48
+ if (!valid || !found?.passwordHash) return null;
49
+ if (this.passwordHasher.needsRehash(found.passwordHash))
50
+ await this.users.updatePasswordHash(found.user.id, await this.passwordHasher.hash(password));
51
+ return found.user;
52
+ }
53
+
54
+ // auth.controller.ts
55
+ @Public() @Controller('auth')
56
+ export class AuthController {
57
+ @Post('sign-in') @HttpCode(200)
58
+ async signIn(@Body() body: SignInDto) {
59
+ const user = await this.credentials.verify(body.email, body.password);
60
+ if (!user) throw new UnauthorizedException('Invalid email or password');
61
+ const { session } = await this.signInService.signIn(user.id, { method: 'password' });
62
+ return { mfaRequired: session.mfa === 'pending' };
63
+ }
64
+ }
65
+ ```
66
+
67
+ `signIn()` sets the `__Host-sid` cookie (HttpOnly, Secure, SameSite=Lax; hash stored), replaces the previous session (fixation defence) and rejects cross-origin posts (login CSRF). `http://localhost` accepts Secure cookies in development.
68
+
69
+ ## Reading the user and route options
70
+
71
+ - `@CurrentUser()` / `@CurrentUser('id')`, `@CurrentSession()` in handlers; `AuthenticationContext.requireUser()` in services (AsyncLocalStorage-backed singleton).
72
+ - `@Authenticate({ optional: true })` lets guests through with `null`; `{ verifiedEmail: true }` → 403 `email_unverified`; `{ mfa: true }` requires a verified second factor; `{ providers: [ApiKeyAuth] }` restricts providers.
73
+ - `globalGuard: false` makes routes open until `@UseGuards(AuthenticationGuard)`.
74
+
75
+ ## Bearer tokens for mobile/API clients
76
+
77
+ `accessToken: { key: JWT_SECRET (≥32 bytes, or an ES256/RS256/EdDSA private key), issuer, audience, ttl: '15m' }`, a `JwtBearerProvider` subclass whose `validate({ sub })` loads the user, and `TokenService.issue(user.id, { method: 'password', claims: { amr: ['pwd'] } })` / `refresh(refreshToken)` behind `@Public()` routes. Refresh tokens rotate and reuse revokes the family. `refreshToken: false` issues access tokens only (declare `refreshTokens: false` in `AuthenticationTypes` too).
78
+
79
+ ## More flows
80
+
81
+ Each needs its options **and** a registered handler of the same name (`registry.registerHandler('emailVerification', this)`), or startup fails:
82
+ - email verification and password reset (`EmailVerificationHandler`, `PasswordResetHandler` + `@nestjs/mail`)
83
+ - TOTP 2FA (`MfaService`, `SignInService.confirmMfa/completeMfa`, `mfa.encryption.keys`)
84
+ - OIDC (`OidcAccountResolver`, `OidcService.start/finish`, `google()`/`github()`/`microsoft()`)
85
+ - magic links (`MagicLinkHandler`, `MagicLinkService`)
86
+ - sessions list/sign-out everywhere (`SessionService`, `signOut()`, `signOutEverywhere()`)
87
+ - API keys (`ApiKeyProvider` with `findKey(id)`)
88
+
89
+ Read `references/flows.md` for each.
90
+
91
+ ## Production rules
92
+
93
+ - Register a database store implementing the six contracts (`sessions`, `refreshTokens`, `mfa`, `magicLinks`, `oidcStates`, `emailTokens`) via `AuthenticationStorage.registerSource()`. With `NODE_ENV=production` the module refuses in-memory storage for configured features.
94
+ - HTTPS; list other web origins in `session.trustedOrigins`; call `app.enableCsrfProtection()` (12.1+) with the same origins.
95
+ - Rate-limit sign-in with `@nestjs/throttler` using a per-IP and a per-email+IP definition (shared Redis storage in clusters).
96
+ - Rotate on privilege change (`SignInService.rotateSession()`); after password/2FA changes revoke other sessions and tokens.
97
+ - Import `AuthenticationModule` **before** `AuthorizationModule`.
98
+ - Never use `LogMailTransport` in production: it logs sign-in links.
99
+ - Reset/verify pages must POST the token (mail scanners follow GET links).
100
+ - scrypt uses 128 MiB per hash on the libuv pool: size servers and in tests use `new PasswordHasher({ logN: 10 })`.
101
+
102
+ ## Workflow
103
+
104
+ - [ ] 1. Install; define the `User` type and repository; add `SessionAuth` (and `JwtAuth`/`ApiKeyAuth` if needed).
105
+ - [ ] 2. Register `AuthenticationModule.forRoot[Async]()` with config from `ConfigService`; add `@Public()` to routes that must stay open.
106
+ - [ ] 3. Add sign-up/sign-in controllers with validated DTOs or schemas; add each extra flow with its handler.
107
+ - [ ] 4. Add the database store and migrations; throttler; `enableCsrfProtection()`.
108
+ - [ ] 5. E2E tests: real `AppModule`, PGlite for the DB, `InMemoryMailTransport`, cheap `PasswordHasher`; assert cookies, 401s, `mfa_required`. Existing e2e tests now need credentials or `@Public()`.
109
+ - [ ] 6. Build, lint, run unit and e2e tests, start the app.
110
+
111
+ ## Reference files
112
+
113
+ - `references/flows.md`: email verification, password reset, 2FA, OIDC, magic link, session management, API keys, GraphQL/WebSocket use, store contract, testing, module options, events and errors. Read when adding any flow beyond password sign-in or writing the store.
114
+
115
+ Sources: [Authentication](https://docs.nestjs.com/security/authentication)
@@ -0,0 +1,180 @@
1
+ # @nestjs/authentication flows and reference
2
+
3
+ ## Contents
4
+ - Handler pattern
5
+ - Email verification
6
+ - Password reset
7
+ - Two-factor authentication (TOTP)
8
+ - OpenID Connect (Google, GitHub, Microsoft)
9
+ - Magic links
10
+ - Session management
11
+ - API keys
12
+ - GraphQL and WebSockets
13
+ - Database store contract
14
+ - Testing
15
+ - Module options
16
+ - Events and errors
17
+
18
+ ## Handler pattern
19
+
20
+ Features with side effects (sending mail, finding accounts) need a handler registered under the feature's name. Handlers are your `@Injectable()` classes:
21
+
22
+ ```typescript
23
+ @Injectable()
24
+ export class EmailVerificationMailer extends EmailVerificationHandler {
25
+ constructor(private readonly users: UsersRepository, private readonly mailer: Mailer, registry: AuthenticationRegistry) {
26
+ super();
27
+ registry.registerHandler('emailVerification', this);
28
+ }
29
+ async send(link: EmailVerificationLink) { await this.mailer.send(VerifyEmailMail, { to: link.email, data: link }); }
30
+ markVerified(userId: string, email: string) { return this.users.markEmailVerified(userId, email); } // conditional: only if email unchanged
31
+ }
32
+ ```
33
+
34
+ Names: `emailVerification`, `passwordReset`, `magicLink`, `oidc`. Option without handler (or vice versa) fails startup. Mail through `@nestjs/mail` (nestjs-mail).
35
+
36
+ ## Email verification
37
+
38
+ - Options: `emailVerification: { url: `${APP_URL}/verify-email`, ttl? ('24h') }`.
39
+ - `EmailVerificationService.send(user)` after sign-up (or a resend route); `verify(token)` from a POST route the page calls.
40
+ - `markVerified()` must update only while the stored email still matches (`UPDATE … WHERE id = $1 AND email = $2`).
41
+ - `@Authenticate({ verifiedEmail: true })` gates routes on it.
42
+
43
+ ## Password reset
44
+
45
+ - Options: `passwordReset: { url: `${APP_URL}/reset-password`, ttl? ('1h') }`.
46
+ - Handler (`PasswordResetHandler`): `findUser(email)` → `{ id, email (stored), passwordHash } | null`, `send(link)`, `updatePassword(userId, passwordHash)`.
47
+ - `forgot` route: call `passwordResetService.request(email)` **without awaiting** and answer 202 every time (no account enumeration).
48
+ - `reset` route: `passwordResetService.reset(token, password, { signIn: true })` → `null` for invalid links; result has `signedIn?.session.mfa`.
49
+ - A reset revokes the user's sessions and refresh tokens. Links stop working after any password or email change.
50
+
51
+ ## Two-factor authentication (TOTP)
52
+
53
+ ```typescript
54
+ mfa: { issuer: 'example.com', encryption: { keys: [process.env.TOTP_ENCRYPTION_KEY!] } }, // ≥32 random chars; newest key first
55
+ ```
56
+
57
+ - Enroll: `mfaService.enroll(user.id, user.email)` (QR/otpauth data); `{ replace: true }` to swap authenticators (else `MfaAlreadyEnrolledError` 409).
58
+ - Confirm: `signInService.confirmMfa(userId, code)`, then show `mfaService.generateRecoveryCodes(userId)` once.
59
+ - Sign-in with an enrolled user creates a **pending** session: protected routes answer 401 `mfa_required`. A `@Public()` `verify` route calls `signInService.completeMfa({ code, recoveryCode })`, which rotates to `mfa: 'verified'`. Pending sessions expire after `mfa.pendingTtl` (10m).
60
+ - Disable: verify a code with `mfaService.verifyTotp(userId, code)`, `mfaService.disable(userId)`, then `sessionService.revokeAll(userId, { except: current.id })` and `tokenService.revokeAll(userId)`.
61
+ - Tokens: `tokenService.issue()` answers `mfa_required` for enrolled users until a code is given; put `amr` claims in tokens so `@Authenticate({ mfa: true })` works for bearer clients.
62
+ - Every app sharing the user database must configure `mfa`, or users bypass it via another app.
63
+ - Lockout: `maxAttempts` (5) per `lockoutWindow` (15m).
64
+
65
+ ## OpenID Connect (Google, GitHub, Microsoft)
66
+
67
+ ```typescript
68
+ oidc: {
69
+ callbackUrl: `${process.env.APP_URL}/auth/oidc/:provider/callback`,
70
+ providers: { google: google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! }) },
71
+ },
72
+
73
+ @Public() @Controller('auth/oidc')
74
+ export class OidcController {
75
+ @Get(':provider/login') @Redirect()
76
+ login(@Param('provider') p: string, @Query('redirectTo') redirectTo?: string, @Query('link') link?: string) {
77
+ return this.oidcService.start(p, { redirectTo, link: link === 'true' });
78
+ }
79
+ @Get(':provider/callback') @Redirect()
80
+ callback(@Param('provider') p: string, @Query() query: Record<string, string>) { return this.oidcService.finish(p, query); }
81
+ }
82
+ ```
83
+
84
+ Resolver (`OidcAccountResolver`, registered as `'oidc'`): `resolveUser(profile, tokens, { linkTo })`.
85
+ - With `linkTo` (a "Connect Google" link flow): link to that user unless the identity belongs to someone else.
86
+ - Known identity → its user.
87
+ - New identity: only trust `profile.email` if `profile.emailVerified`; link to an existing account only if **that account's** email is verified (prevents pre-hijacking); otherwise return `null` (403) or throw `ForbiddenException`.
88
+ - `redirectTo` must be relative; `microsoft()` takes a `tenant`. Errors: 400 forged callback, 502 provider unreachable.
89
+
90
+ ## Magic links
91
+
92
+ - Options: `magicLink: { url: `${APP_URL}/sign-in/magic`, ttl? ('15m'), bindToBrowser? (true) }`.
93
+ - Handler (`MagicLinkHandler`): `send(link)`, `resolveUser(email)` (create the account on first use; refuse unverified password accounts with 403).
94
+ - `magicLinkService.create(email, { redirectTo })` → always answer 202. `consume(token)` → `null` or `{ session, redirectTo }`.
95
+ - A link works only in the browser that requested it (`__Host-magic_link_tx`); otherwise 401 `not_this_browser`.
96
+
97
+ ## Session management
98
+
99
+ - `session.metadata: (req) => ({ userAgent: req.headers['user-agent'] })` stores data with each session.
100
+ - `sessionService.list(userId)`, `sessionService.revoke(id, { userId })`, `signInService.signOut()`, `signInService.signOutEverywhere(userId)`, `tokenService.revoke(refreshToken)`.
101
+
102
+ ## API keys
103
+
104
+ ```typescript
105
+ @Injectable()
106
+ export class ApiKeyAuth extends ApiKeyProvider<User> {
107
+ constructor(@InjectDrizzle() private readonly db: NodePgDatabase, private readonly users: UsersRepository, registry: AuthenticationRegistry) {
108
+ super({ prefix: 'cat', realm: 'store' }); // keys: cat_<16-hex id>_<secret>
109
+ registry.registerProvider(this, { order: 2 });
110
+ }
111
+ async findKey(id: string) {
112
+ const [key] = await this.db.select().from(apiKeys).where(eq(apiKeys.id, id));
113
+ const user = key && (await this.users.findById(key.userId));
114
+ return user ? { hash: key.hash, user, expiresAt: key.expiresAt } : null;
115
+ }
116
+ }
117
+ ```
118
+
119
+ - Your table stores `id`, `userId`, `hash` (SHA-256), `createdAt`, `expiresAt`. `generate()` returns `{ key, id, hash }`: show `key` once.
120
+ - Keys never satisfy `@Authenticate({ mfa: true })`; requests with keys emit no events (log `keyId` from `@CurrentSession()`).
121
+ - Password reset / sign-out everywhere leave keys alone; revoke them yourself if needed. Add the prefix to secret scanning.
122
+
123
+ ## GraphQL and WebSockets
124
+
125
+ - GraphQL: `context: ({ req, res, extra }) => ({ req: req ?? extra?.request, res })` (must be a function), `fieldResolverEnhancers: ['guards', 'interceptors']` if field resolvers need it.
126
+ - Gateways: no guards on `handleConnection()`; call `wsAuthenticator.authenticateConnection(client, request)` (closes with 1008; pass `{ required: false }` as third arg to allow guests).
127
+
128
+ ## Database store contract
129
+
130
+ One `@Injectable()` class (or several) calling `storage.registerSource({ sessions: this, refreshTokens: this, mfa: this, magicLinks: this, oidcStates: this, emailTokens: this })` with `AuthenticationStorage` injected. Contracts used by configured features are mandatory in production.
131
+
132
+ Atomicity rules (one conditional statement, never read-then-write):
133
+ - `touchSession()`: conditional UPDATE moving `lastActiveAt` forward only.
134
+ - `deleteSession()`: DELETE resolving whether a row was deleted.
135
+ - `markRefreshTokenUsed()`: `UPDATE … WHERE used_at IS NULL`, count rows.
136
+ - `saveTotp()`: upsert keeping the larger `lastUsedStep` (`GREATEST`, raw SQL on Prisma).
137
+ - `claimTotpStep()`, `consumeRecoveryCode()`: conditional UPDATE/DELETE.
138
+ - `recordMfaFailure()`: insert, then count, each committed separately.
139
+ - `saveRecoveryCodes()`: one transaction (delete + insert).
140
+ - `consumeMagicLink()`, `consumeOidcState()`, `consumeEmailToken()`: `DELETE … RETURNING`.
141
+
142
+ `getSession()` may return the user in `extra` (one query per request). Test with `authenticationStoreContract()` from `@nestjs/authentication/testing`, `{ concurrent: true }`, against PostgreSQL with a pool.
143
+
144
+ ## Testing
145
+
146
+ ```typescript
147
+ const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
148
+ .overrideProvider(getDrizzleToken()).useValue(drizzle({ client: new PGlite() }))
149
+ .overrideProvider(MailTransport).useValue(mailbox) // InMemoryMailTransport
150
+ .overrideProvider(PasswordHasher).useValue(new PasswordHasher({ logN: 10 }))
151
+ .compile();
152
+ ```
153
+
154
+ Read the session cookie from `res.headers['set-cookie'][0].split(';')[0]`; read links with `mailbox.assertSent({ to, mail: VerifyEmailMail }).link('/verify-email').searchParams.get('token')`. Set test env vars (JWT secret ≥32 bytes, TOTP key) in the test runner config.
155
+
156
+ ## Module options
157
+
158
+ `forRootAsync()` factory returns everything except `isGlobal` and `globalGuard`. Lifetimes up to 100 years; every clocked group takes `now` for tests.
159
+
160
+ | Option | Default |
161
+ | --- | --- |
162
+ | `globalGuard` / `isGlobal` / `allowInMemoryStorage` | `true` / `true` / `false` |
163
+ | `session.cookieName` | `__Host-sid` (`sid` without the prefix rules) |
164
+ | `session.absoluteTtl` / `idleTtl` / `touchInterval` | `'7d'` / `'1d'` / `'1m'` (shorter than `idleTtl`) |
165
+ | `session.cookie` | `secure: true, sameSite: 'lax', path: '/'`, always HttpOnly |
166
+ | `session.trustedOrigins`, `session.metadata` | none |
167
+ | `password.logN` (`r`, `p`, `keyLength`, `saltLength`) | 17 (8, 1, 32, 16) |
168
+ | `accessToken.key` / `alg` / `issuer` / `audience` / `ttl` / `kid` / `type` | required / from key / – / – / `'15m'` / – / `JWT` |
169
+ | `refreshToken` (`ttl`, `absoluteTtl`) / `false` | on (`'30d'`, `'90d'`) |
170
+ | `mfa.encryption` / `issuer` / `window` / `maxAttempts` / `lockoutWindow` / `recoveryCodes` / `pendingTtl` | required / – / 1 / 5 / `'15m'` / 10 / `'10m'` |
171
+ | `emailVerification.url` / `passwordReset.url` / `magicLink.url` | required when enabled |
172
+ | `oidc.callbackUrl`, `providers`, `redirectAfterLogin` (`/`), `transactionTtl` (`'10m'`), `discoveryTtl` (`'1h'`), `clockTolerance` (`'30s'`) | |
173
+
174
+ A value outside its range (including `NaN` from an unset env var) fails startup.
175
+
176
+ ## Events and errors
177
+
178
+ `AuthenticationEvents.events$` and diagnostics channels `nestjs:authentication:<type>`: `sign-in`, `sign-out`, `mfa-verified`, `mfa-failed` (`locked`), `mfa-enabled`, `mfa-disabled`, `recovery-codes-generated`, `refresh-token-reused`, `password-reset-requested`, `password-reset`, `email-verified`, `magic-link-refused`, `session-touch-failed`. Keep them as an audit trail and alert on locked MFA failures and touch failures.
179
+
180
+ Errors: `AuthenticationError` (base; `status`, `code` → `errorCode`, `details`, `challenge`), `JwtError` 401, `RefreshTokenError` 401 (`reason`), `MagicLinkError` 401 `not_this_browser`, `MfaAlreadyEnrolledError` 409. Guard codes: `missing_credentials` 401, `mfa_required` 401, `email_unverified` 403. Errors are converted for GraphQL (`UNAUTHENTICATED`), WebSockets (`WsException`) and microservices (`RpcException`).
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: nestjs-authorization
3
+ description: Adds permissions to NestJS 12 with the first-party @nestjs/authorization package — @Policy() classes whose type-checked ability methods decide what a user may do, a deny-by-default global AuthorizationGuard with @Can(Policy, 'ability', argsFn) and @Can.Anyone(), record-level checks in services with AuthorizationService.authorize()/can(), before() admin bypass, policies that inject services, 401 vs 403 mapping, permission flags for the frontend, denial events, and testing policies. Use when the user asks for roles, permissions, ownership checks ("users can only see their own orders"), admin overrides, RBAC/ABAC, or when a route returns 403 unexpectedly after adding @nestjs/authorization. Not for signing users in (nestjs-authentication, nestjs-passport-auth).
4
+ compatibility: NestJS 12.x; @nestjs/authorization; works with @nestjs/authentication or any guard that sets request.user
5
+ metadata:
6
+ version: "0.1.0"
7
+ nestjs-major: "12"
8
+ docs-crawled: "2026-11-05"
9
+ ---
10
+
11
+ # NestJS authorization (@nestjs/authorization)
12
+
13
+ Prefer policies from `@nestjs/authorization` over hand-written `RolesGuard` + `@Roles()` metadata when the package is installed or the project uses `@nestjs/authentication`. For a tiny role check in a project without it, the guard + `Reflector` pattern in nestjs-request-pipeline is fine.
14
+
15
+ ```bash
16
+ npm i @nestjs/authorization
17
+ ```
18
+
19
+ ## Register (after authentication)
20
+
21
+ ```typescript
22
+ @Module({
23
+ imports: [
24
+ AuthenticationModule.forRootAsync({ ... }), // FIRST: its guard sets request.user
25
+ AuthorizationModule.forRoot(), // global, deny-by-default guard
26
+ ProductsModule,
27
+ ],
28
+ })
29
+ export class AppModule {}
30
+ ```
31
+
32
+ - Global guards run in module scan order. With `@nestjs/authentication` the wrong order fails at startup; with other auth guards (Passport `AuthGuard`) it only logs an error and **every user looks like a guest** — register those with `APP_GUARD` before the module and cover it with an e2e test.
33
+ - **Deny by default:** a handler with no `@Can()`, `@Can.Anyone()` or `@Public()` gets 403 (user) / 401 (guest). The startup log lists every such handler: start migrations from that list.
34
+ - Custom user source: `AuthorizationModule.forRoot({ getUser: (ctx) => ... })`.
35
+
36
+ ## Write a policy
37
+
38
+ ```typescript
39
+ @Policy()
40
+ export class OrderPolicy implements PolicyBefore<User> {
41
+ constructor(private readonly limits: RefundLimitsService) {} // policies are providers
42
+
43
+ before(user: User | null, ability: string) { // true allow, false deny, undefined → ability decides
44
+ if (user?.roles.includes('admin') && ability !== 'refund') return true;
45
+ }
46
+ viewAll(user: User) { return user.roles.includes('staff'); }
47
+ view(user: User, order: Order) { return order.userId === user.id || user.roles.includes('staff'); }
48
+ viewOrdersOf(user: User | null, customerId: string) { return !!user && (user.id === customerId || user.roles.includes('staff')); }
49
+ async refund(user: User, order: Order) {
50
+ return user.roles.includes('staff') && order.status === 'paid' && order.total <= (await this.limits.maxRefundFor(user));
51
+ }
52
+ }
53
+ ```
54
+
55
+ - An ability's first parameter is the user; `User | null` means it also runs for guests. Only a literal `true` allows.
56
+ - Register the policy as a provider of its feature module (discovered automatically), or in `forRoot({ policies, imports })`.
57
+ - Keep policies stateless (singletons).
58
+ - `before()` skips every ability of the policy — don't use it where a rule must bind admins too (e.g. "never refund shipped orders"); check the ability name.
59
+
60
+ ## Where each check goes
61
+
62
+ | Check depends on | Put it | How |
63
+ | --- | --- | --- |
64
+ | Only the user (create product, admin page) | Route | `@Can(ProductPolicy, 'create')` |
65
+ | A route param, no record load | Route | `@Can(OrderPolicy, 'viewOrdersOf', (ctx) => [ctx.switchToHttp().getRequest().params.customerId] as const)` |
66
+ | A loaded record | Service, after loading | `await this.authz.authorize(OrderPolicy, 'view', user, order)` + `@Can.Anyone()` on the route |
67
+ | A list | Query | `can(OrderPolicy, 'viewAll', user)` then filter in the DB, never per row |
68
+ | A UI flag | Response | `canRefund: await this.authz.can(OrderPolicy, 'refund', user, order)` (never throws) |
69
+
70
+ - `@Can()` is type-checked: misspelled abilities, missing args or wrong arg types don't compile. Route abilities must accept `null`.
71
+ - The args function runs in the guard **before pipes**: values are raw strings and unvalidated. Pass ids, not records.
72
+ - Other transports: `GqlExecutionContext.create(ctx).getArgs()`, `ctx.switchToWs().getData()`, `ctx.switchToRpc().getData()`.
73
+ - Guests on public routes: combine `@Authenticate({ optional: true })` with `@Can.Anyone()` and pass `@CurrentUser() user: User | null` to the service.
74
+
75
+ ## 401 vs 403
76
+
77
+ - Guest on a route requiring sign-in → 401 from authentication. Guest denied by a policy on an optional route → 401. Signed-in user denied → 403.
78
+ - `authorize()` throws `AuthorizationError` (`reason: 'unauthenticated' | 'forbidden'`, `status`, `policy`, `ability`), mapped to HTTP/GraphQL/WS/RPC errors when it leaves a handler. Thrown in your own guard or middleware it becomes a **500**: use `can()` there.
79
+
80
+ ## Gotchas
81
+
82
+ - Hybrid apps: `connectMicroservice(opts, { inheritAppConfig: true })` or `@Can()` isn't enforced on message handlers.
83
+ - GraphQL field resolvers aren't guarded unless `fieldResolverEnhancers: ['guards']`; then each `@ResolveField()` needs a declaration (class-level covers it).
84
+ - Don't put `CacheInterceptor` (URL cache) on routes with record checks; it answers before the policy.
85
+ - Race: re-check state in the write (`UPDATE … WHERE status = 'paid'`).
86
+ - Choose 404 over 403 where record existence is sensitive.
87
+ - Each `can()` flag runs the ability per row, async work included; cache slow lookups.
88
+
89
+ ## Testing
90
+
91
+ - Unit-test policies as plain classes: `new OrderPolicy(fakeLimits).view(alice, order)`. Direct calls skip `before()`.
92
+ - Service tests: `Test.createTestingModule({ imports: [AuthorizationModule.forRoot()], providers: [OrdersService, OrderPolicy, RefundLimitsService] })` and expect `rejects.toThrow(AuthorizationError)` / `toMatchObject({ reason: 'forbidden', ability: 'refund' })`.
93
+ - E2E: sign in through the real endpoint (cheap `PasswordHasher({ logN: 11 })`), assert 401/403/200. Replace a policy with `.overrideProvider(OrderPolicy).useValue(...)`.
94
+
95
+ ## Workflow
96
+
97
+ - [ ] 1. Install; import `AuthorizationModule` after the authentication module.
98
+ - [ ] 2. Write one policy per resource; register it in the feature module.
99
+ - [ ] 3. Add `@Can()` to user-only routes, `@Can.Anyone()` + `authorize()` in services for record checks; fix every handler in the startup "denied" log.
100
+ - [ ] 4. Subscribe to `AuthorizationEvents.events$` (`denied`: policy, ability, reason, user id) for auditing.
101
+ - [ ] 5. Add policy unit tests and e2e 401/403 tests.
102
+ - [ ] 6. Build, lint, run unit and e2e tests, start the app.
103
+
104
+ Sources: [Authorization](https://docs.nestjs.com/security/authorization)