@lenne.tech/nest-server 11.35.0 → 11.36.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 (42) hide show
  1. package/.claude/rules/configurable-features.md +2 -1
  2. package/FRAMEWORK-API.md +1 -1
  3. package/dist/core/common/helpers/file.helper.js +3 -2
  4. package/dist/core/common/helpers/file.helper.js.map +1 -1
  5. package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
  6. package/dist/core/common/services/brevo.service.d.ts +1 -0
  7. package/dist/core/common/services/brevo.service.js +10 -2
  8. package/dist/core/common/services/brevo.service.js.map +1 -1
  9. package/dist/core/modules/better-auth/better-auth.config.js +1 -0
  10. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  11. package/dist/core/modules/better-auth/core-better-auth.controller.js +19 -0
  12. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
  13. package/dist/core/modules/file/core-file-access-audit.initializer.d.ts +13 -0
  14. package/dist/core/modules/file/core-file-access-audit.initializer.js +109 -0
  15. package/dist/core/modules/file/core-file-access-audit.initializer.js.map +1 -0
  16. package/dist/core/modules/file/file-roles.config.d.ts +21 -0
  17. package/dist/core/modules/file/file-roles.config.js +51 -2
  18. package/dist/core/modules/file/file-roles.config.js.map +1 -1
  19. package/dist/core/modules/file/file-roles.helper.d.ts +2 -2
  20. package/dist/core/modules/file/file-roles.helper.js +10 -16
  21. package/dist/core/modules/file/file-roles.helper.js.map +1 -1
  22. package/dist/core.module.js +3 -1
  23. package/dist/core.module.js.map +1 -1
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +4 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/tsconfig.build.tsbuildinfo +1 -1
  28. package/migration-guides/11.34.x-to-11.35.x.md +17 -3
  29. package/migration-guides/11.35.0-to-11.35.1.md +152 -0
  30. package/migration-guides/11.35.1-to-11.36.0.md +168 -0
  31. package/package.json +21 -21
  32. package/src/core/common/helpers/file.helper.ts +11 -5
  33. package/src/core/common/interfaces/server-options.interface.ts +24 -0
  34. package/src/core/common/services/brevo.service.ts +34 -6
  35. package/src/core/modules/better-auth/better-auth.config.ts +4 -0
  36. package/src/core/modules/better-auth/core-better-auth.controller.ts +38 -0
  37. package/src/core/modules/file/README.md +10 -0
  38. package/src/core/modules/file/core-file-access-audit.initializer.ts +164 -0
  39. package/src/core/modules/file/file-roles.config.ts +156 -1
  40. package/src/core/modules/file/file-roles.helper.ts +15 -22
  41. package/src/core.module.ts +10 -2
  42. package/src/index.ts +4 -0
@@ -49,11 +49,12 @@ Then run the one-time audit in §3.
49
49
  them is half-wired if you take one side and not the other, and a half-wired enforcement layer is worse
50
50
  than none because it reads as protection.
51
51
 
52
- **Seven new files:**
52
+ **Nine new files** (eight rows — the two file-module files ship as one pair):
53
53
 
54
54
  | File | Belongs with |
55
55
  |------|--------------|
56
56
  | `common/plugins/mongoose-system-role.plugin.ts` | `common/enums/role.enum.ts` — the plugin is the unconditional layer, the enum holds the predicates (§1) |
57
+ | `modules/tenant/core-role-scope.registry.ts` | `modules/tenant/core-tenant.helpers.ts` + `modules/tenant/core-tenant.module.ts` + `common/enums/role.enum.ts` — the registry holds the global-vs-tenant split the guards resolve against, `core-tenant.helpers.ts` feeds it from configuration, and `core-tenant.module.ts` is where `forRoot()` registers that source and runs `assertRoleVocabularyIsCoherent()`. Without the module wiring the registry stays empty, every role resolves to `RoleScope.UNKNOWN`, and the boot check that refuses an incoherent vocabulary never runs (§5) |
57
58
  | `common/decorators/nested-type.registry.ts` | `decorators/restricted.decorator.ts` + `decorators/unified-field.decorator.ts` — the registry is written by one and read by the other (§8) |
58
59
  | `common/decorators/restrictions-checked.marker.ts` | `decorators/restricted.decorator.ts` + both interceptors — all four read the marker; an old truthy check anywhere reopens the bypass (§13) |
59
60
  | `common/helpers/graphql-ws-context.helper.ts` | `common/services/core-tenant-context.registry.ts` + `core.module.ts` + `modules/tenant/core-tenant.guard.ts` — the context is useless without the resolver, and unreached without the `core.module.ts` wiring (§7) |
@@ -66,8 +67,21 @@ than none because it reads as protection.
66
67
  `common/plugins/mongoose-tenant.plugin.ts`, `common/services/core-s3.service.ts`,
67
68
  `modules/file/{core-file.service,file-metadata.helper,file-roles.helper}.ts`,
68
69
  `modules/tus/{core-tus.service,tus.constants,index}.ts`, `modules/user/inputs/core-user.input.ts`,
69
- `modules/user/core-user.service.ts`, `modules/tenant/core-tenant.helpers.ts`, and the three module
70
- READMEs (`file`, `tenant`, `tus`) plus `modules/file/INTEGRATION-CHECKLIST.md`.
70
+ `modules/user/core-user.service.ts`,
71
+ `modules/tenant/{core-tenant.helpers,core-tenant.module,core-tenant.service}.ts`, and the three
72
+ module READMEs (`file`, `tenant`, `tus`) plus `modules/file/INTEGRATION-CHECKLIST.md`.
73
+
74
+ `core-tenant.module.ts` and `core-tenant.service.ts` are easy to miss and both carry enforcement:
75
+ `forRoot()` registers the role-scope source and asserts the vocabulary is coherent, and
76
+ `core-tenant.service.ts` gains `assertAssignableMembershipRole()`, which refuses a membership role
77
+ named after a system (`s_*`) or global-only role at assignment time (§5).
78
+
79
+ The remaining changed files — `common/enums/role.enum.ts`,
80
+ `common/decorators/{restricted,unified-field}.decorator.ts`, both interceptors
81
+ (`check-security`, `response-model`), and all three guards
82
+ (`modules/auth/guards/roles.guard.ts`, `modules/better-auth/better-auth-roles.guard.ts`,
83
+ `modules/tenant/core-tenant.guard.ts`) — are the counterparts already named in the "Belongs with"
84
+ column above. Take them with the new file they pair with.
71
85
 
72
86
  **Also worth taking:** `src/test/test.helper.ts` (subscription `connectionParams`, and a refused
73
87
  subscription now rejects instead of delivering an indistinguishable `null` — without it you cannot test
@@ -0,0 +1,152 @@
1
+ # Migration Guide: 11.35.0 → 11.35.1
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ |----------|---------|
7
+ | **Breaking Changes** | None in signatures. One **client-visible** change: a refused upload now answers **400** instead of **500**, with Nest's standard error envelope instead of an internal-error one (§1) |
8
+ | **Bugfixes** | `multerFileFilter` rejections finally map to a 4xx (§1). Seven documented helper names are reachable from the package root for the first time (§2). The file-access boot warning can no longer be fooled by a subclassed controller (§3) |
9
+ | **Migration Effort** | Nothing to configure. Read §1 if a frontend, a test, or an alerting rule branches on the status or body of a rejected upload. **Read §3 if you subclass `CoreFileController` / `CoreFileResolver` and re-declare a member** — you may see a new warning at startup, and it is probably telling you something true |
10
+
11
+ ---
12
+
13
+ ## Quick Migration
14
+
15
+ ```bash
16
+ pnpm update @lenne.tech/nest-server@11.35.1
17
+ pnpm run build
18
+ pnpm test
19
+ ```
20
+
21
+ No configuration change is required, and no source change is required in a typical project.
22
+
23
+ **Vendor-mode projects:** §1 and §2 are single-file and independent —
24
+ `src/core/common/helpers/file.helper.ts` and `src/index.ts`. §3 is **not**: it adds
25
+ `src/core/modules/file/core-file-access-audit.initializer.ts` and changes
26
+ `file-roles.config.ts`, `file-roles.helper.ts` and `core.module.ts` together. Taking the initializer
27
+ without the `core.module.ts` provider registration gives you a file that is never constructed — a
28
+ security check that silently does not run, which is worse than not having it. Sync those four
29
+ together.
30
+
31
+ ---
32
+
33
+ ## 1. A refused upload answers 400, not 500
34
+
35
+ 11.32.4 §8 changed `multerFileFilter`'s rejection from a bare string to `new Error(...)`, intending a
36
+ clean 400. It never produced one.
37
+
38
+ `transformException` in `@nestjs/platform-express` returns any **non-`HttpException`** unchanged. Its
39
+ switch matches only multer's own message constants (`LIMIT_FILE_SIZE`, `LIMIT_UNEXPECTED_FILE`, …),
40
+ and a message this helper writes matches none of them — so the rejection fell off the end of the
41
+ switch, reached Nest's exception layer as a plain error, and the client was told the **server** had
42
+ failed for a file the **client** sent wrong.
43
+
44
+ Both rejection paths are affected and both are fixed: the allow-list refusal, and the
45
+ scriptable-type refusal (`text/html`, `image/svg+xml`) added by 11.32.4 §6.
46
+
47
+ | | Before | After |
48
+ |---|---|---|
49
+ | Status | `500 Internal Server Error` | `400 Bad Request` |
50
+ | Body | internal-error envelope | Nest's standard 400 shape |
51
+ | Message text | unchanged | unchanged — same text, still built from the accept list |
52
+
53
+ **What to check:**
54
+
55
+ - **Frontends** that branch on the status of an upload response. A rejected file was previously
56
+ indistinguishable from a storage outage; it now identifies itself, and the message is worth showing
57
+ to the user.
58
+ - **Tests** pinned to the broken output. A test asserting `500` on a refused upload fails and should
59
+ be updated to `400`.
60
+ - **Alerting.** Refused uploads no longer raise 5xx. If a dashboard counted them, its numbers drop —
61
+ that is the fix, not a regression. Conversely, an alert that was tuned to tolerate that noise can
62
+ be tightened again.
63
+
64
+ A consumer catching the rejection in code is unaffected: `BadRequestException extends HttpException
65
+ extends Error`, so an `instanceof Error` check keeps working.
66
+
67
+ ---
68
+
69
+ ## 2. Seven documented helpers are now exported from the package root
70
+
71
+ The 11.34.x → 11.35.x guide's *New Exports* block lists names that `src/index.ts` never exported, so
72
+ importing them from `@lenne.tech/nest-server` failed to compile:
73
+
74
+ | Name | Module |
75
+ |---|---|
76
+ | `resolveGuardRequest` | `common/helpers/execution-context-request.helper` |
77
+ | `buildRequestContextAwareExecute` | `common/helpers/graphql-ws-context.helper` |
78
+ | `buildRequestContextAwareSubscribe` | same |
79
+ | `createRequestContextAwareExecute` | same |
80
+ | `createRequestContextAwareSubscribe` | same |
81
+ | `getTenantContextResolver` | `common/services/core-tenant-context.registry` |
82
+ | `setTenantContextResolver` | same |
83
+
84
+ Purely additive — three `export * from` lines. Nothing was renamed or removed, and no existing import
85
+ changes meaning.
86
+
87
+ **Who this affected:** npm-mode consumers only, and only those who followed the guide. In-tree and
88
+ vendor-mode consumers import over relative paths and never touched the barrel, which is also why
89
+ nothing in this repo noticed: no framework module, test or e2e spec imports from `src/index.ts`, so a
90
+ helper could be written, wired, documented and shipped with the full suite green.
91
+ `tests/unit/public-api-barrel.spec.ts` now reads the expected names out of the migration guides
92
+ themselves and walks the re-export graph, so a documented-but-unexported name fails at test time
93
+ rather than at a consumer's `import`.
94
+
95
+ ---
96
+
97
+ ## 3. The file-access warning now sees a subclassed controller
98
+
99
+ The boot warning that reports "the file gate is open and nothing declares a per-file policy" read
100
+ **configuration** — `file.downloadRoles` and friends. That is the correct source for a member you
101
+ inherit, and the wrong one for a member you re-declare.
102
+
103
+ ```typescript
104
+ @Controller('files')
105
+ export class FileController extends CoreFileController {
106
+ @Get('id/:id')
107
+ @Roles(RoleEnum.S_EVERYONE) // anonymous downloads
108
+ override async getFileById(...) { return super.getFileById(...); }
109
+ }
110
+ ```
111
+
112
+ `applyFileRoles()` writes the configured roles onto `CoreFileController.prototype.getFileById`, but
113
+ decorator metadata lives on the **function object** — your override is a different function, it keeps
114
+ its own `@Roles()`, and that is the function Nest registers. The configuration never reaches the
115
+ route. The old warning asked `resolveRoles('downloadRoles', config)`, got `[ADMIN]`, and stayed quiet
116
+ while the route served everyone. Two consumer projects shipped exactly that.
117
+
118
+ `CoreFileAccessAuditInitializer` now reads the roles off the class you actually registered, at
119
+ `onApplicationBootstrap` (the earliest point at which the route table exists), and names the member:
120
+
121
+ ```
122
+ A registered file endpoint is open beyond platform admins through roles declared in your own class,
123
+ not through configuration (FileController.getFileById → ["s_everyone"]), and no per-file policy is
124
+ declared: file.access is unset and CoreFileService.checkRights() is not overridden. …
125
+ ```
126
+
127
+ **Nothing changes about who may reach what.** Your `@Roles()` still wins; the audit reports and does
128
+ not correct. That is deliberate: rewriting an override's metadata would silently relax a route a
129
+ project pinned on purpose — which is why the TUS module's approach, where configuration wins over the
130
+ override, was not adopted here.
131
+
132
+ **What to do if the warning appears:**
133
+
134
+ | If the open access is | Then |
135
+ |---|---|
136
+ | intended, with a per-file rule | declare it: `file.access: 'owner' \| 'tenant' \| …`, or override `CoreFileService.checkRights()`. Either silences the warning, for both halves of the check |
137
+ | intended, genuinely public files | nothing is broken — but the warning is the reminder that ids are enumerable and the routes are not rate-limited |
138
+ | **not** intended | inherit the member instead of re-declaring it, so `file.downloadRoles` governs it again |
139
+
140
+ **No new warning if you do not subclass**, and none if you subclass without re-declaring a governed
141
+ member — the audit only reports roles the configuration cannot account for, so it never duplicates
142
+ the existing configuration-side warning.
143
+
144
+ Governed members: `getFileById`, `getFile` (controller); `getFileInfo`, `uploadFile`, `uploadFiles`,
145
+ `deleteFile` (resolver).
146
+
147
+ ---
148
+
149
+ ## Module Documentation
150
+
151
+ - File module, upload filters and access rules: [`src/core/modules/file/README.md`](../src/core/modules/file/README.md)
152
+ - Request lifecycle and exception handling: [`docs/REQUEST-LIFECYCLE.md`](../docs/REQUEST-LIFECYCLE.md)
@@ -0,0 +1,168 @@
1
+ # Migration Guide: 11.35.1 → 11.36.0
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ |----------|---------|
7
+ | **Breaking Changes** | Sign-in and sign-up now **validate their input**. An address that does not satisfy `IsEmail` is refused with a 400 before Better-Auth sees it — which can lock out an existing account created outside `CoreUserInput` (§2) |
8
+ | **New Features** | `betterAuth.emailAndPassword.revokeSessionsOnPasswordReset` — opt-in, ends the sessions that already existed when the user resets their password (§1) |
9
+ | **Bugfixes** | `brevo.exclude` stopped excluding every second recipient (§3). Malformed or absent sign-in input answers 400 instead of 500 (§2) |
10
+ | **Migration Effort** | **Read §2 before upgrading** and run the account audit there. §1 is opt-in and off by default; §3 needs nothing |
11
+
12
+ ---
13
+
14
+ ## Quick Migration
15
+
16
+ ```bash
17
+ pnpm update @lenne.tech/nest-server@11.36.0
18
+ pnpm run build
19
+ pnpm test
20
+ ```
21
+
22
+ Then run the audit in §2 — it is the only step that can surprise you.
23
+
24
+ **Vendor-mode projects:** all three changes are independent and single-file —
25
+ `modules/better-auth/better-auth.config.ts` (§1),
26
+ `modules/better-auth/core-better-auth.controller.ts` (§2),
27
+ `common/services/brevo.service.ts` (§3). §1 also adds the option to
28
+ `common/interfaces/server-options.interface.ts`; taking the config field without the
29
+ `better-auth.config.ts` half gives you a setting that type-checks and does nothing, so sync those two
30
+ together.
31
+
32
+ ---
33
+
34
+ ## 1. End existing sessions on a password reset (opt-in)
35
+
36
+ A password reset is what somebody reaches for when they suspect their account was taken over.
37
+ Leaving the older sessions alive defeats the point: the attacker keeps theirs, and the new password
38
+ changes nothing for them.
39
+
40
+ ```typescript
41
+ // config.env.ts
42
+ betterAuth: {
43
+ emailAndPassword: {
44
+ revokeSessionsOnPasswordReset: true,
45
+ },
46
+ },
47
+ ```
48
+
49
+ Forwarded to Better-Auth's native flag, which honours it on **all three** reset routes — password
50
+ reset, email-OTP and phone-number.
51
+
52
+ **Off by default, and staying that way.** It is a behaviour change for an existing deployment: after
53
+ a reset the user is signed out everywhere, including on the devices they still hold. That is worth
54
+ choosing, not worth inflicting on upgrade.
55
+
56
+ The value is matched with `=== true`, so a truthy-but-not-`true` value arriving as JSON through
57
+ `NSC__*` or `NEST_SERVER_CONFIG` (a hand-written `"true"`, say) does **not** silently enable a
58
+ sign-out-everywhere behaviour.
59
+
60
+ > **Do not route this through `betterAuth.options` instead.** That object is spread *shallowly* over
61
+ > the resolved config — only `advanced` is deep-merged — so an `options.emailAndPassword` does not
62
+ > add a key, it **replaces the whole block**, including the scrypt `password.hash` / `password.verify`
63
+ > pair the framework installs. Every credential in your database would stop verifying, at runtime,
64
+ > with no boot error. This is why the flag is a first-class config field.
65
+
66
+ ---
67
+
68
+ ## 2. Sign-in and sign-up validate their input — read this before upgrading
69
+
70
+ ### What changed
71
+
72
+ `CoreBetterAuthSignInInput` and `CoreBetterAuthSignUpInput` carried `@ApiProperty()` only:
73
+ documentation, no validation. Every line of the handler then reads `input.email`, so a body that was
74
+ malformed or absent produced a `TypeError` and reached the client as a **500**.
75
+
76
+ | Request | Before | After |
77
+ |---|---|---|
78
+ | `{"email": "not-an-email", "password": "x"}` | `500` | `400`, naming the field |
79
+ | `{}` | `500` | `400`, naming both fields |
80
+ | no body at all | `500` | `400` `Missing input` |
81
+ | valid credentials | unchanged | unchanged |
82
+
83
+ On the most-probed endpoint any deployment has, a 500 tells the caller to retry something that can
84
+ never succeed and files their mistake in the same bucket as a real outage. The legacy sign-in
85
+ answered these cases with a 400; that contract is restored.
86
+
87
+ ### The part that can lock someone out
88
+
89
+ `@IsEmail()` is now enforced on **sign-in**, not just on sign-up. An account whose stored address
90
+ does not satisfy it can no longer authenticate — the request is refused before Better-Auth is
91
+ reached.
92
+
93
+ This is unlikely but not impossible. `CoreUserInput.email` has always carried the same `IsEmail()`
94
+ validator, so an account created through the normal API cannot be affected. An account can still
95
+ have been created another way:
96
+
97
+ - a seed script or migration writing directly through Mongoose,
98
+ - Better-Auth's own native sign-up route,
99
+ - an import from another system.
100
+
101
+ The addresses at risk are the ones that look fine to a human and fail `validator.js`: no TLD
102
+ (`admin@localhost`), a dotless domain (`test@test`), or trailing whitespace.
103
+
104
+ **Audit before upgrading.** Use the real validator rather than a hand-written pattern — `IsEmail`
105
+ delegates to `validator.js`, whose rules are far more involved than any regex you would write for
106
+ this, and a check that disagrees with the one actually enforced is worse than none:
107
+
108
+ ```js
109
+ // node — run from your project root, where class-validator's `validator` resolves
110
+ const isEmail = require('validator/lib/isEmail');
111
+ const { MongoClient } = require('mongodb');
112
+
113
+ const client = await MongoClient.connect(process.env.MONGODB_URI);
114
+ const users = await client.db().collection('users').find({}, { projection: { email: 1 } }).toArray();
115
+ const locked = users.filter(u => !u.email || !isEmail(u.email));
116
+
117
+ console.log(`${locked.length} account(s) can no longer sign in:`);
118
+ locked.forEach(u => console.log(' ', JSON.stringify(u.email), u._id.toString()));
119
+ await client.close();
120
+ ```
121
+
122
+ Anything this lists needs its address corrected before the upgrade, or those users are refused with a
123
+ validation error rather than an authentication error — and the message will not point at the cause.
124
+
125
+ ### If you subclass the controller
126
+
127
+ The validation lives on the DTOs, so an override that keeps the parameter types inherits it. A
128
+ subclass that declares its **own** input class is responsible for its own validators —
129
+ `MapAndValidatePipe` walks the prototype chain child-first and skips a property once a child class
130
+ has validated it.
131
+
132
+ `termsAndPrivacyAccepted` is deliberately the one sign-up field without a presence validator: whether
133
+ consent is required is a policy question answered by `betterAuth.signUpValidation` through
134
+ `CoreBetterAuthSignUpValidatorService`, not by the DTO. It does now carry `@IsBoolean()`.
135
+
136
+ ---
137
+
138
+ ## 3. `brevo.exclude` excluded only every second recipient
139
+
140
+ `BrevoService` called `.test()` on the configured `RegExp`. Projects declare that pattern with the
141
+ `g` flag:
142
+
143
+ ```typescript
144
+ brevo: { exclude: /@(testuser\.com|test\.de)/gi }
145
+ ```
146
+
147
+ `RegExp.prototype.test` advances `lastIndex` on a `g` or `y` pattern, and the config holds a single
148
+ shared instance — so the same address answered `true`, `false`, `true`, … across calls. **Every
149
+ second excluded recipient was sent a real, billable transactional mail.**
150
+
151
+ Found in production: a CI end-to-end run delivered five mails to a `@testuser.com` address that the
152
+ project's own exclude pattern lists, and the provider flagged the runner's IP before anyone noticed
153
+ the guard was only half working.
154
+
155
+ The check now matches against a flagless copy, so each call is independent and the configured
156
+ pattern's `lastIndex` is left untouched — it is shared process-wide, and mutating it leaked into
157
+ every later reader. Both send paths were affected; both are fixed.
158
+
159
+ **Nothing to do.** If you kept the `g` flag off as a workaround, you can stop — it no longer matters
160
+ either way.
161
+
162
+ ---
163
+
164
+ ## Module Documentation
165
+
166
+ - BetterAuth integration: [`src/core/modules/better-auth/INTEGRATION-CHECKLIST.md`](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
167
+ - BetterAuth customization: [`src/core/modules/better-auth/CUSTOMIZATION.md`](../src/core/modules/better-auth/CUSTOMIZATION.md)
168
+ - Configuration patterns: [`.claude/rules/configurable-features.md`](../.claude/rules/configurable-features.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.35.0",
3
+ "version": "11.36.0",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -91,21 +91,21 @@
91
91
  "@apollo/server": "5.5.1",
92
92
  "@as-integrations/express5": "1.1.2",
93
93
  "@better-auth/passkey": "1.6.26",
94
- "@getbrevo/brevo": "6.0.2",
94
+ "@getbrevo/brevo": "6.0.3",
95
95
  "@modelcontextprotocol/sdk": "1.30.0",
96
- "@nestjs/apollo": "13.4.2",
97
- "@nestjs/common": "11.1.29",
98
- "@nestjs/core": "11.1.29",
99
- "@nestjs/graphql": "13.4.2",
96
+ "@nestjs/apollo": "13.4.5",
97
+ "@nestjs/common": "11.2.1",
98
+ "@nestjs/core": "11.2.1",
99
+ "@nestjs/graphql": "13.4.5",
100
100
  "@nestjs/jwt": "11.0.2",
101
101
  "@nestjs/mongoose": "11.0.4",
102
102
  "@nestjs/passport": "11.0.5",
103
- "@nestjs/platform-express": "11.1.29",
103
+ "@nestjs/platform-express": "11.2.1",
104
104
  "@nestjs/schedule": "6.1.3",
105
- "@nestjs/swagger": "11.4.6",
105
+ "@nestjs/swagger": "11.4.7",
106
106
  "@nestjs/terminus": "11.1.1",
107
107
  "@tus/file-store": "2.1.1",
108
- "@tus/server": "2.4.3",
108
+ "@tus/server": "2.4.4",
109
109
  "@types/supertest": "7.2.1",
110
110
  "bcrypt": "6.0.0",
111
111
  "better-auth": "1.6.26",
@@ -122,12 +122,12 @@
122
122
  "graphql-subscriptions": "3.0.0",
123
123
  "graphql-upload": "15.0.2",
124
124
  "graphql-ws": "6.2.1",
125
- "jose": "6.2.8",
125
+ "jose": "6.2.9",
126
126
  "js-sha256": "1.0.0",
127
127
  "json-to-graphql-query": "2.3.0",
128
128
  "lodash": "4.18.1",
129
129
  "mongodb": "7.5.0",
130
- "mongoose": "9.9.1",
130
+ "mongoose": "9.9.3",
131
131
  "multer": "2.2.0",
132
132
  "node-mailjet": "6.0.11",
133
133
  "nodemailer": "9.0.5",
@@ -166,15 +166,15 @@
166
166
  }
167
167
  },
168
168
  "devDependencies": {
169
- "@aws-sdk/client-s3": "3.1106.0",
170
- "@aws-sdk/s3-request-presigner": "3.1106.0",
169
+ "@aws-sdk/client-s3": "3.1112.0",
170
+ "@aws-sdk/s3-request-presigner": "3.1112.0",
171
171
  "@compodoc/compodoc": "2.0.0",
172
172
  "@nestjs/cli": "11.0.24",
173
173
  "@nestjs/schematics": "11.1.0",
174
- "@nestjs/testing": "11.1.28",
174
+ "@nestjs/testing": "11.2.1",
175
175
  "@swc/cli": "0.8.1",
176
- "@swc/core": "1.15.47",
177
- "@tus/s3-store": "2.0.5",
176
+ "@swc/core": "1.16.0",
177
+ "@tus/s3-store": "2.0.6",
178
178
  "@types/compression": "1.8.1",
179
179
  "@types/cookie-parser": "1.4.10",
180
180
  "@types/ejs": "3.1.5",
@@ -187,22 +187,22 @@
187
187
  "@vitest/coverage-v8": "4.1.10",
188
188
  "@vitest/ui": "4.1.10",
189
189
  "ansi-colors": "4.1.3",
190
- "bullmq": "6.0.9",
190
+ "bullmq": "6.1.2",
191
191
  "find-file-up": "2.0.1",
192
192
  "husky": "9.1.7",
193
193
  "ioredis": "6.0.0",
194
194
  "nodemon": "3.1.14",
195
195
  "npm-watch": "0.13.0",
196
196
  "otpauth": "9.5.1",
197
- "oxfmt": "0.62.0",
198
- "oxlint": "1.77.0",
197
+ "oxfmt": "0.63.0",
198
+ "oxlint": "1.78.0",
199
199
  "rimraf": "6.1.3",
200
200
  "ts-node": "10.9.2",
201
201
  "tsconfig-paths": "4.2.0",
202
- "tsx": "4.23.11",
202
+ "tsx": "4.23.12",
203
203
  "tus-js-client": "4.3.1",
204
204
  "typescript": "5.9.3",
205
- "unplugin-swc": "1.5.10",
205
+ "unplugin-swc": "1.5.11",
206
206
  "vite": "8.2.1",
207
207
  "vite-plugin-node": "8.0.0",
208
208
  "vitest": "4.1.10"
@@ -1,3 +1,4 @@
1
+ import { BadRequestException } from '@nestjs/common';
1
2
  import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
2
3
  import { randomBytes } from 'crypto';
3
4
  import { diskStorage, memoryStorage } from 'multer';
@@ -147,9 +148,14 @@ function normalizeMimeType(value: string): string {
147
148
  * `options.allowScriptableTypes` is set — that is what closes the hole for
148
149
  * expressions that already exist in consumer projects.
149
150
  *
150
- * Rejections are reported as a real `Error`. Passing a bare string (as this
151
- * helper did before) leaves multer with an "error" that has no `message`, which
152
- * NestJS's `transformException` cannot map to a 4xx.
151
+ * Rejections are reported as a `BadRequestException`, so the caller gets a 400
152
+ * with a readable reason. An `HttpException` is required, not merely tidier:
153
+ * NestJS's `transformException` returns any NON-`HttpException` unchanged — its
154
+ * switch matches only multer's own message constants (`LIMIT_FILE_SIZE`,
155
+ * `LIMIT_UNEXPECTED_FILE`, …), and a message written here matches none of them.
156
+ * A bare `new Error()` therefore still surfaced to the client as a 500, which
157
+ * reads as "the server broke" for what is in fact a refused file. See
158
+ * `tests/unit/file-upload-rejection-status.spec.ts`.
153
159
  */
154
160
  export function multerFileFilter(
155
161
  accept: RegExp | UploadAllowList = IMAGE_UPLOAD_ALLOW_LIST,
@@ -163,7 +169,7 @@ export function multerFileFilter(
163
169
  !options?.allowScriptableTypes &&
164
170
  (SCRIPTABLE_UPLOAD_MIME_TYPES.includes(mimeType) || SCRIPTABLE_UPLOAD_EXTENSIONS.includes(extension))
165
171
  ) {
166
- return cb(new Error(`File upload rejected: ${mimeType || 'unknown type'} may execute as script`));
172
+ return cb(new BadRequestException(`File upload rejected: ${mimeType || 'unknown type'} may execute as script`));
167
173
  }
168
174
 
169
175
  const accepted =
@@ -174,7 +180,7 @@ export function multerFileFilter(
174
180
  if (accepted) {
175
181
  return cb(null, true);
176
182
  }
177
- cb(new Error(`File upload only supports the following filetypes - ${describeAccept(accept)}`));
183
+ cb(new BadRequestException(`File upload only supports the following filetypes - ${describeAccept(accept)}`));
178
184
  };
179
185
  }
180
186
 
@@ -3414,6 +3414,30 @@ interface IBetterAuthBase {
3414
3414
  * @default true
3415
3415
  */
3416
3416
  enabled?: boolean;
3417
+
3418
+ /**
3419
+ * End every existing session when the user completes a password reset.
3420
+ *
3421
+ * A reset is what somebody reaches for when they suspect their account was
3422
+ * taken over, so leaving the older sessions alive defeats the point: the
3423
+ * attacker keeps theirs and the new password changes nothing for them.
3424
+ *
3425
+ * Left off by default because it is a behaviour change for existing
3426
+ * deployments — a reset then signs the user out everywhere, including on
3427
+ * the devices they still hold.
3428
+ *
3429
+ * Passed through to better-auth's native
3430
+ * `emailAndPassword.revokeSessionsOnPasswordReset`. It cannot be set via
3431
+ * `options` instead: that object is spread SHALLOWLY over the resolved
3432
+ * config, so an `options.emailAndPassword` would replace the whole block
3433
+ * — including the scrypt `password.hash` / `password.verify` pair this
3434
+ * framework installs — and every credential in the database would stop
3435
+ * verifying.
3436
+ *
3437
+ * @default false
3438
+ * @since 11.36.0
3439
+ */
3440
+ revokeSessionsOnPasswordReset?: boolean;
3417
3441
  };
3418
3442
 
3419
3443
  /**
@@ -53,9 +53,8 @@ export class BrevoService {
53
53
  return false;
54
54
  }
55
55
 
56
- // Exclude (test) users, must be done via config and not via configFastButReadOnly,
57
- // otherwise the error TypeError: Cannot assign to read only property 'lastIndex' of object '[object RegExp]' occurs
58
- if (this.configService.config?.brevo?.exclude?.test?.(to)) {
56
+ // Exclude (test) users
57
+ if (this.isExcluded(to)) {
59
58
  return 'TEST_USER!';
60
59
  }
61
60
 
@@ -98,9 +97,8 @@ export class BrevoService {
98
97
  return false;
99
98
  }
100
99
 
101
- // Exclude (test) users, must be done via config and not via configFastButReadOnly,
102
- // otherwise the error TypeError: Cannot assign to read only property 'lastIndex' of object '[object RegExp]' occurs
103
- if (this.configService.config?.brevo?.exclude?.test?.(to)) {
100
+ // Exclude (test) users
101
+ if (this.isExcluded(to)) {
104
102
  return 'TEST_USER!';
105
103
  }
106
104
 
@@ -135,6 +133,36 @@ export class BrevoService {
135
133
  return { 'Idempotency-Key': randomUUID() };
136
134
  }
137
135
 
136
+ /**
137
+ * Checks a recipient against `brevo.exclude` without inheriting the pattern's match state.
138
+ *
139
+ * Two traps live in this one line, and both have bitten:
140
+ *
141
+ * 1. `RegExp.prototype.test` ADVANCES `lastIndex` on a pattern carrying `g` or `y`. The config
142
+ * holds a single shared instance and projects declare it as `/…/gi`, so calling `.test()` on
143
+ * it directly answers true, false, true, … for the very same address — every second excluded
144
+ * recipient receives a real mail. Matching against a flagless copy keeps each call
145
+ * independent and leaves the configured pattern untouched.
146
+ * 2. It must be read from `config`, never from `configFastButReadOnly`: assigning `lastIndex` on
147
+ * the frozen copy throws `TypeError: Cannot assign to read only property 'lastIndex'`.
148
+ * Point 1 removes the assignment, but the frozen object may still be a `deepFreeze`d clone
149
+ * whose flags differ, so the mutable side stays the source of truth.
150
+ *
151
+ * @param to - Recipient email address
152
+ * @returns `true` when the recipient matches the configured exclude pattern
153
+ */
154
+ protected isExcluded(to: string): boolean {
155
+ const exclude = this.configService.config?.brevo?.exclude;
156
+ if (typeof exclude?.test !== 'function') {
157
+ return false;
158
+ }
159
+
160
+ const stateless =
161
+ exclude.global || exclude.sticky ? new RegExp(exclude.source, exclude.flags.replace(/[gy]/g, '')) : exclude;
162
+
163
+ return stateless.test(to);
164
+ }
165
+
138
166
  /**
139
167
  * Lazily constructs (and memoises) the Brevo SDK client.
140
168
  *
@@ -444,6 +444,10 @@ export function createBetterAuthInstance(options: CreateBetterAuthOptions): Crea
444
444
  hash: nativeScryptHash,
445
445
  verify: nativeScryptVerify,
446
446
  },
447
+ // Opt-in: a reset then also ends the sessions that already existed, which
448
+ // is the point of resetting after a suspected takeover. Off by default so
449
+ // the behaviour of existing deployments does not change under them.
450
+ revokeSessionsOnPasswordReset: config.emailAndPassword?.revokeSessionsOnPasswordReset === true,
447
451
  },
448
452
  plugins,
449
453
  secret: validation.resolvedSecret || config.secret,