@lenne.tech/nest-server 11.41.6 → 11.41.8

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 (55) hide show
  1. package/.claude/rules/better-auth.md +26 -22
  2. package/.claude/rules/configurable-features.md +1 -1
  3. package/.claude/rules/package-management.md +8 -0
  4. package/.claude/rules/testing.md +13 -10
  5. package/CLAUDE.md +6 -4
  6. package/FRAMEWORK-API.md +1 -1
  7. package/dist/core/common/helpers/file.helper.js.map +1 -1
  8. package/dist/core/common/services/core-s3.service.d.ts +8 -0
  9. package/dist/core/common/services/core-s3.service.js +44 -3
  10. package/dist/core/common/services/core-s3.service.js.map +1 -1
  11. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +1 -0
  12. package/dist/core/modules/ai/core-ai-mcp.controller.js +10 -0
  13. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  14. package/dist/core/modules/better-auth/core-better-auth-account-issuer.helper.d.ts +2 -0
  15. package/dist/core/modules/better-auth/core-better-auth-account-issuer.helper.js +13 -0
  16. package/dist/core/modules/better-auth/core-better-auth-account-issuer.helper.js.map +1 -0
  17. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +3 -2
  18. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  19. package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +1 -0
  20. package/dist/core/modules/better-auth/core-better-auth.constants.js +2 -1
  21. package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -1
  22. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -0
  23. package/dist/core/modules/better-auth/core-better-auth.service.js +46 -3
  24. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  25. package/dist/core/modules/file/core-file.service.js +3 -2
  26. package/dist/core/modules/file/core-file.service.js.map +1 -1
  27. package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +1 -0
  28. package/dist/core/modules/file/s3-file.helper.d.ts +1 -0
  29. package/dist/core/modules/file/s3-file.helper.js +10 -4
  30. package/dist/core/modules/file/s3-file.helper.js.map +1 -1
  31. package/dist/core/modules/system-setup/core-system-setup.service.js +3 -2
  32. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  33. package/dist/tsconfig.build.tsbuildinfo +1 -1
  34. package/docs/REQUEST-LIFECYCLE.md +1 -1
  35. package/docs/security-overrides.md +1 -1
  36. package/migration-guides/11.41.6-to-11.41.7.md +192 -0
  37. package/migration-guides/11.41.7-to-11.41.8.md +135 -0
  38. package/package.json +24 -22
  39. package/src/core/common/helpers/file.helper.ts +4 -0
  40. package/src/core/common/helpers/gridfs.helper.ts +1 -1
  41. package/src/core/common/services/core-s3.service.ts +125 -7
  42. package/src/core/modules/ai/README.md +8 -0
  43. package/src/core/modules/ai/core-ai-mcp.controller.ts +33 -0
  44. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +2 -2
  45. package/src/core/modules/better-auth/README.md +20 -6
  46. package/src/core/modules/better-auth/core-better-auth-account-issuer.helper.ts +25 -0
  47. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -7
  48. package/src/core/modules/better-auth/core-better-auth.constants.ts +20 -11
  49. package/src/core/modules/better-auth/core-better-auth.service.ts +87 -3
  50. package/src/core/modules/file/INTEGRATION-CHECKLIST.md +39 -11
  51. package/src/core/modules/file/README.md +30 -5
  52. package/src/core/modules/file/core-file.service.ts +15 -6
  53. package/src/core/modules/file/interfaces/file-upload.interface.ts +18 -0
  54. package/src/core/modules/file/s3-file.helper.ts +28 -9
  55. package/src/core/modules/system-setup/core-system-setup.service.ts +12 -8
@@ -45,11 +45,11 @@ file: {
45
45
 
46
46
  Three equivalent options. They differ only in where the bytes end up:
47
47
 
48
- | Driver | Bytes | Survives a restart | Shared between replicas | Needs |
49
- | -------------- | ------------------------- | ------------------------ | ----------------------- | ---------------------------------- |
50
- | `'s3'` | S3-compatible bucket | yes | **yes** | `s3` config + `@aws-sdk/client-s3` |
51
- | `'gridfs'` | MongoDB GridFS | yes | yes | nothing beyond the database |
52
- | `'filesystem'` | local disk (`storageDir`) | only on a mounted volume | **no** | nothing |
48
+ | Driver | Bytes | Survives a restart | Shared between replicas | Needs |
49
+ | -------------- | ------------------------- | ------------------------ | ----------------------- | ------------------------------------------------------------------------ |
50
+ | `'s3'` | S3-compatible bucket | yes | **yes** | `s3` config + `@aws-sdk/client-s3` (+ `@aws-sdk/lib-storage`, see below) |
51
+ | `'gridfs'` | MongoDB GridFS | yes | yes | nothing beyond the database |
52
+ | `'filesystem'` | local disk (`storageDir`) | only on a mounted volume | **no** | nothing |
53
53
 
54
54
  **Metadata always lives in the database**, whichever driver holds the bytes. Filename, content type,
55
55
  length and the custom `metadata` a per-file rule reads have to be queryable — `findFileInfo()` filters
@@ -57,6 +57,31 @@ and pages over them, `checkRights()` reads them per request. A directory listing
57
57
  that, and sidecar files would reinvent an index the database already is. So `'filesystem'` moves the
58
58
  bytes off the database, not the bookkeeping.
59
59
 
60
+ ### How an upload reaches S3
61
+
62
+ No upload is read into memory on its way to S3. Which request carries it depends on whether its
63
+ length is known:
64
+
65
+ | Length | Request | Memory |
66
+ | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------ |
67
+ | known, up to 5 GiB — `size` on the upload, a TUS upload, a buffer | one streamed `PutObject` | constant |
68
+ | known, above 5 GiB | multipart upload via `@aws-sdk/lib-storage` (AWS S3 refuses larger single requests) | at most four parts |
69
+ | unknown — every GraphQL and streamed REST upload | multipart upload via `@aws-sdk/lib-storage` | at most four 5 MiB parts |
70
+ | unknown, and `@aws-sdk/lib-storage` not installed | one `PutObject` of the whole file, read into memory first | **the file's size** |
71
+
72
+ Install the optional peer next to `@aws-sdk/client-s3` (`pnpm add @aws-sdk/lib-storage`, same
73
+ version). Without it uploads still work, but a large one of unknown length costs its own size in
74
+ memory — a multi-GB file then gets the container OOM-killed — and a known length above 5 GiB goes up
75
+ as one request, which AWS S3 refuses (MinIO and RustFS accept it). The boot log says so once when
76
+ the package is missing.
77
+
78
+ Pass `size` on a `FileUploadSource` only when it is the file's EXACT length — a `stat()` of a file on
79
+ disk. Never pass the `Content-Length` of a multipart/form-data request: it counts the whole envelope
80
+ and is always larger than the file, and a wrong length breaks the upload. Leaving `size` out costs
81
+ nothing but the multipart round trips, and for data already in memory it is the better choice: the
82
+ SDK retries the Buffer parts of a multipart upload after a transient S3 error, but not one streamed
83
+ request.
84
+
60
85
  ### Choosing the driver
61
86
 
62
87
  **Set it explicitly and it is enforced.** If the chosen store is not available, the boot **fails**:
@@ -31,7 +31,7 @@ import {
31
31
  FilesystemFileHelper,
32
32
  FilesystemFileInfo,
33
33
  } from './filesystem-file.helper';
34
- import { S3_FILES_COLLECTION, S3FileHelper, S3FileInfo, streamToBuffer } from './s3-file.helper';
34
+ import { S3_FILES_COLLECTION, S3FileHelper, S3FileInfo } from './s3-file.helper';
35
35
 
36
36
  /**
37
37
  * Type for checking input
@@ -160,7 +160,7 @@ export abstract class CoreFileService {
160
160
  if (!(await this.checkRights(file, { ...serviceOptions, checkInputType: 'file' }))) {
161
161
  return null;
162
162
  }
163
- const { createReadStream, filename, mimetype } = await file;
163
+ const { createReadStream, filename, mimetype, size } = await file;
164
164
  const readStream = createReadStream();
165
165
  // Resolved ONCE for all three driver branches: under an `'owner'` / `'tenant'` preset this adds
166
166
  // the very fields the preset decides on, so an upload through this service is authorizable
@@ -176,11 +176,20 @@ export abstract class CoreFileService {
176
176
  return this.prepareOutput(fsFileInfo as unknown as CoreFileInfo, serviceOptions);
177
177
  }
178
178
  if (this.storageDriver === 's3') {
179
- // Only buffer when the size is unknown: a GraphQL upload stream carries no length, and the
180
- // S3 SDK needs one. Callers that know it (TUS) pass `body` + `contentLength` instead and
181
- // stream straight through — see S3FileHelper.writeFile.
179
+ // The stream is handed to S3 as it is, never read into memory here. With a `size` the object
180
+ // goes up as one streamed PutObject; without one, `CoreS3Service.putObject()` uses a multipart
181
+ // upload, which needs no total up front (and only buffers when its optional peer dependency
182
+ // is missing). Reading the stream into a Buffer at this point — what this branch used to do
183
+ // for every upload — materialised the whole file in one process: a multi-GB upload then
184
+ // threw "Array buffer allocation failed" or got the container OOM-killed at 100 % progress,
185
+ // taking every other in-flight request with it.
186
+ //
187
+ // A GraphQL or streamed REST upload carries no trustworthy length (a multipart request's
188
+ // `Content-Length` counts the whole envelope), so the unknown-length case is the common one,
189
+ // not a fallback.
182
190
  const s3FileInfo = await S3FileHelper.writeFile(this.options.s3Service, this.s3Files, {
183
- buffer: await streamToBuffer(readStream),
191
+ body: readStream,
192
+ contentLength: size, // validated in writeFile(): anything but a positive integer counts as unknown
184
193
  contentType: mimetype,
185
194
  filename,
186
195
  ...(metadata ? { metadata } : {}),
@@ -15,6 +15,24 @@ export interface FileUploadSource {
15
15
  encoding?: string;
16
16
  filename: string;
17
17
  mimetype: string;
18
+
19
+ /**
20
+ * EXACT size of the file in bytes, when the caller knows it — otherwise leave it out.
21
+ *
22
+ * Only the S3 driver reads it: with a size the object goes up as one streamed request (as a
23
+ * multipart upload above 5 GiB, which AWS S3 refuses in one request), without one it goes up as a
24
+ * multipart upload (`@aws-sdk/lib-storage`, an optional peer — without it the
25
+ * file is read into memory). Omitting it is not a fallback: a GraphQL or streamed REST upload never
26
+ * knows its length, and that is the common case.
27
+ *
28
+ * A WRONG size breaks the upload instead of slowing it down. So never pass the `Content-Length` of a
29
+ * multipart/form-data request: it counts the whole envelope (boundaries, part headers, other
30
+ * fields) and is always larger than the file. Pass it only when it is the file's own length — a
31
+ * TUS upload's `upload.size`, a `stat()` of a file on disk. For data already in memory leave it
32
+ * out: the SDK retries the Buffer parts of a multipart upload after a transient S3 error, but not
33
+ * one streamed request. Anything but a positive integer is treated as unknown.
34
+ */
35
+ size?: number;
18
36
  }
19
37
 
20
38
  /**
@@ -30,6 +30,20 @@ export const S3_FILES_COLLECTION = 's3-files';
30
30
  */
31
31
  export type S3FileInfo = FileMetadataInfo;
32
32
 
33
+ /**
34
+ * Whether an upload size can be handed to S3 as the object's `Content-Length`.
35
+ *
36
+ * Only a positive safe integer qualifies. Anything else — `undefined`, `0`, a negative number,
37
+ * `NaN`, a fraction — is treated as "length unknown" and goes through the multipart path, which
38
+ * needs no total. The direction matters: a WRONG length is not a slower upload but a broken one,
39
+ * whereas an unknown length only costs the multipart round trips. `0` is included in "unknown" because it is
40
+ * what an unset numeric field defaults to far more often than it is a real empty file — and an
41
+ * empty file stores correctly through the multipart path too.
42
+ */
43
+ export function isKnownUploadSize(size: unknown): size is number {
44
+ return typeof size === 'number' && Number.isSafeInteger(size) && size > 0;
45
+ }
46
+
33
47
  /**
34
48
  * Read a stream completely into a buffer
35
49
  */
@@ -70,21 +84,22 @@ export class S3FileHelper {
70
84
  const _id = new Types.ObjectId();
71
85
  const key = _id.toHexString();
72
86
 
73
- // A stream with a known length streams straight through. Reading it into a Buffer first —
74
- // which every caller used to do — materialises the WHOLE file in one process: a 4 GB
75
- // resumable upload (well inside the 50 GB default cap) then either throws
76
- // "Array buffer allocation failed" or gets the container OOM-killed at 100% progress,
77
- // taking every other in-flight request with it.
87
+ // A stream is never read into a Buffer here. Doing so — which every caller used to do —
88
+ // materialises the WHOLE file in one process: a 4 GB resumable upload (well inside the 50 GB
89
+ // default cap) then either throws "Array buffer allocation failed" or gets the container
90
+ // OOM-killed at 100% progress, taking every other in-flight request with it. With a known
91
+ // length the stream goes up as one PutObject; without one, `putObject()` uploads it in parts.
78
92
  const body = options.body ?? options.buffer;
79
93
  if (!body) {
80
94
  throw new Error('S3FileHelper.writeFile needs either a body stream or a buffer');
81
95
  }
82
- await s3Service.putObject(key, body, options.contentType, options.contentLength ?? options.buffer?.length);
96
+ const contentLength = isKnownUploadSize(options.contentLength) ? options.contentLength : options.buffer?.length;
97
+ await s3Service.putObject(key, body, options.contentType, contentLength);
83
98
 
84
99
  return S3FileHelper.recordFile(s3Service, collection, _id, {
85
100
  contentType: options.contentType,
86
101
  filename: options.filename,
87
- length: options.contentLength ?? options.buffer?.length,
102
+ length: contentLength,
88
103
  metadata: options.metadata,
89
104
  });
90
105
  }
@@ -134,7 +149,11 @@ export class S3FileHelper {
134
149
  _id: Types.ObjectId,
135
150
  options: { contentType?: string; filename: string; length?: number; metadata?: Record<string, any> },
136
151
  ): Promise<S3FileInfo> {
137
- if (!(await s3Service.objectExists(_id.toHexString()))) {
152
+ // One HEAD answers both questions: whether the bytes arrived, and — for a stream whose length
153
+ // nobody knew up front — how many there are. The stored object is the authority on its size;
154
+ // a length guessed by the caller (a multipart request's `Content-Length`, say) is not.
155
+ const stat = await s3Service.statObject(_id.toHexString());
156
+ if (!stat) {
138
157
  throw new Error('File uploaded but not found in S3');
139
158
  }
140
159
 
@@ -147,7 +166,7 @@ export class S3FileHelper {
147
166
  _id,
148
167
  contentType: options.contentType,
149
168
  filename: options.filename,
150
- length: options.length,
169
+ length: options.length ?? stat.contentLength,
151
170
  metadata: options.metadata,
152
171
  // Records WHERE the bytes went, so a reader never has to probe all three
153
172
  // stores to find out. Legacy documents lack it and are handled by probing.
@@ -1,10 +1,10 @@
1
- import { createLocalAccountIssuer } from '@better-auth/core/db';
2
1
  import { ForbiddenException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
3
2
  import { InjectConnection } from '@nestjs/mongoose';
4
3
  import { isEmail } from 'class-validator';
5
4
  import { Connection } from 'mongoose';
6
5
 
7
6
  import { ConfigService } from '../../common/services/config.service';
7
+ import { legacyCredentialAccountIssuer } from '../better-auth/core-better-auth-account-issuer.helper';
8
8
  import { CoreBetterAuthUserMapper } from '../better-auth/core-better-auth-user.mapper';
9
9
  import { CoreBetterAuthService } from '../better-auth/core-better-auth.service';
10
10
  import { ErrorCode } from '../error-code/error-codes';
@@ -305,18 +305,22 @@ export class CoreSystemSetupService implements OnApplicationBootstrap {
305
305
 
306
306
  // Hash password and create credential account
307
307
  const hashedPassword = await context.password.hash(normalizedPassword);
308
- // better-auth >= 1.7 keys accounts by (issuer, accountId) and requires the
309
- // issuer. Credential accounts have no real issuer, so better-auth derives a
310
- // synthetic one — always via this helper, never a hand-written literal:
311
- // the format is better-auth's to change, and a copy of it would silently
312
- // stop matching the accounts better-auth writes itself.
308
+ // better-auth 1.7.0–1.7.2 key accounts by (issuer, accountId) and require the issuer;
309
+ // credential accounts carry a synthetic one, always derived via the helper, never a
310
+ // hand-written literal. better-auth 1.7.3+ went back to (providerId, accountId) and knows no
311
+ // issuer field, so the helper answers `undefined` there and nothing is written.
312
+ //
313
+ // Cast to the adapter's OWN parameter type: against better-auth 1.7.0–1.7.2 that type requires
314
+ // `issuer`, and a conditional spread reads as optional — a vendored core compiled against
315
+ // those versions would otherwise not build.
316
+ const credentialIssuer = legacyCredentialAccountIssuer();
313
317
  await context.internalAdapter.linkAccount({
314
318
  accountId: iamUser.id,
315
- issuer: createLocalAccountIssuer('credential'),
319
+ ...(credentialIssuer ? { issuer: credentialIssuer } : {}),
316
320
  password: hashedPassword,
317
321
  providerId: 'credential',
318
322
  userId: iamUser.id,
319
- });
323
+ } as Parameters<typeof context.internalAdapter.linkAccount>[0]);
320
324
 
321
325
  // Sync to nest-server users collection
322
326
  const syncedUser = await this.userMapper.linkOrCreateUser({