@lenne.tech/nest-server 11.42.12 → 11.42.14

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 (37) hide show
  1. package/.claude/rules/testing.md +4 -4
  2. package/FRAMEWORK-API.md +1 -1
  3. package/dist/core/modules/auth/guards/auth.guard.js +14 -0
  4. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  5. package/dist/core/modules/error-code/error-codes.d.ts +45 -0
  6. package/dist/core/modules/error-code/error-codes.js +40 -0
  7. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  8. package/dist/core/modules/file/core-file.service.d.ts +1 -0
  9. package/dist/core/modules/file/core-file.service.js +27 -0
  10. package/dist/core/modules/file/core-file.service.js.map +1 -1
  11. package/dist/core/modules/tenant/core-tenant.guard.js +7 -7
  12. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  13. package/dist/core/modules/tus/core-tus.service.d.ts +1 -0
  14. package/dist/core/modules/tus/core-tus.service.js +10 -4
  15. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  16. package/dist/core/modules/tus/tus.constants.d.ts +1 -0
  17. package/dist/core/modules/tus/tus.constants.js +2 -1
  18. package/dist/core/modules/tus/tus.constants.js.map +1 -1
  19. package/dist/server/modules/error-code/error-codes.d.ts +5 -0
  20. package/dist/tsconfig.build.tsbuildinfo +1 -1
  21. package/docs/REQUEST-LIFECYCLE.md +9 -1
  22. package/docs/error-codes.md +70 -0
  23. package/migration-guides/11.42.12-to-11.42.13.md +154 -0
  24. package/migration-guides/11.42.13-to-11.42.14.md +74 -0
  25. package/package.json +3 -3
  26. package/src/core/modules/api-token/README.md +5 -0
  27. package/src/core/modules/auth/guards/auth.guard.ts +31 -1
  28. package/src/core/modules/error-code/error-codes.ts +49 -0
  29. package/src/core/modules/file/README.md +1 -0
  30. package/src/core/modules/file/core-file.service.ts +39 -0
  31. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +2 -2
  32. package/src/core/modules/tenant/README.md +17 -1
  33. package/src/core/modules/tenant/core-tenant.guard.ts +7 -7
  34. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +2 -1
  35. package/src/core/modules/tus/README.md +42 -3
  36. package/src/core/modules/tus/core-tus.service.ts +32 -6
  37. package/src/core/modules/tus/tus.constants.ts +11 -0
@@ -470,7 +470,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
470
470
  }
471
471
  const isVerified = !!(user.verified || user.verifiedAt || user.emailVerified);
472
472
  if (!isVerified) {
473
- throw new ForbiddenException('Verification required');
473
+ throw new ForbiddenException(ErrorCode.VERIFICATION_REQUIRED);
474
474
  }
475
475
  if (headerTenantId && !hasSkipDecorator) {
476
476
  return this.handleSystemRoleWithTenantHeader(user, headerTenantId, request, isAdmin);
@@ -536,7 +536,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
536
536
  const membership = await this.findMembershipCached(user.id, headerTenantId);
537
537
 
538
538
  if (!membership) {
539
- throw new ForbiddenException('Not a member of this tenant');
539
+ throw new ForbiddenException(ErrorCode.NOT_TENANT_MEMBER);
540
540
  }
541
541
  await this.assertTenantActive(headerTenantId);
542
542
 
@@ -567,7 +567,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
567
567
  tenantRoles.length > 0 && memberRole !== null && checkRoleAccess(tenantRoles, undefined, memberRole);
568
568
 
569
569
  if (!satisfiedGlobally && !satisfiedByTenant) {
570
- throw new ForbiddenException('Insufficient tenant role');
570
+ throw new ForbiddenException(ErrorCode.INSUFFICIENT_TENANT_ROLE);
571
571
  }
572
572
  }
573
573
 
@@ -595,7 +595,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
595
595
 
596
596
  // Check role access against user.roles (hierarchy: level comparison, normal: exact match)
597
597
  if (!checkRoleAccess(checkableRoles, user.roles, undefined)) {
598
- throw new ForbiddenException('Insufficient role');
598
+ throw new ForbiddenException(ErrorCode.INSUFFICIENT_ROLE);
599
599
  }
600
600
 
601
601
  // Resolve tenant IDs filtered by minimum required hierarchy level
@@ -904,7 +904,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
904
904
  /** 403 for a deactivated tenant. */
905
905
  private async assertTenantActive(tenantId: string): Promise<void> {
906
906
  if (!(await this.isTenantActive(tenantId))) {
907
- throw new ForbiddenException('Tenant is inactive');
907
+ throw new ForbiddenException(ErrorCode.TENANT_INACTIVE);
908
908
  }
909
909
  }
910
910
 
@@ -946,7 +946,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
946
946
 
947
947
  const membership = await this.findMembershipCached(user.id, headerTenantId);
948
948
  if (!membership) {
949
- throw new ForbiddenException('Not a member of this tenant');
949
+ throw new ForbiddenException(ErrorCode.NOT_TENANT_MEMBER);
950
950
  }
951
951
  await this.assertTenantActive(headerTenantId);
952
952
  request.tenantId = headerTenantId;
@@ -965,7 +965,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
965
965
  throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
966
966
  }
967
967
  if (!isAdmin && !checkRoleAccess(checkableRoles, user.roles, undefined)) {
968
- throw new ForbiddenException('Insufficient role');
968
+ throw new ForbiddenException(ErrorCode.INSUFFICIENT_ROLE);
969
969
  }
970
970
  }
971
971
  return true;
@@ -151,7 +151,7 @@ export class TusService extends CoreTusService {
151
151
 
152
152
  // After the last byte.
153
153
  protected override async onUploadComplete(upload: Upload): Promise<void> {
154
- await super.onUploadComplete(upload); // migrates the file into the configured storage
154
+ await super.onUploadComplete(upload); // stores the file and records its ID for X-File-Id (11.42.13+)
155
155
  // custom logic
156
156
  }
157
157
  }
@@ -243,6 +243,7 @@ TusModule.forRoot({ config: false });
243
243
  | Forgot to register custom service | `onUploadCreate` / `onUploadComplete` overrides never run | Add `service: TusService` to `forRoot()` (11.41.9+) — Step 2 |
244
244
  | Constructor dependencies on the service subclass | `undefined` dependencies, or a crash at boot | Resolve them at call time via `this.options?.moduleRef` — Step 2 |
245
245
  | Override of `onUploadCreate` without `super` | Uploads have no owner or tenant; `allowedTypes` ignored | `return super.onUploadCreate(req, upload)` — Step 2 |
246
+ | Override of `onUploadComplete` without `super` | No `X-File-Id`; links built from the TUS URL answer `404` | Call `super.onUploadComplete(upload)` with the upload received (11.42.13+) — Step 2 |
246
247
  | Upload succeeds but the file cannot be downloaded | `403` on `GET /files/id/:id` | `file.downloadRoles` defaults to `[ADMIN]` — widen it, or authorize per file in `checkRights()` |
247
248
  | Using wrong endpoint path | 404 on upload | Ensure client uses same path as config |
248
249
 
@@ -111,6 +111,7 @@ The TUS server automatically handles CORS headers for browser-based clients:
111
111
  - `Upload-Length`
112
112
  - `Upload-Offset`
113
113
  - `Upload-Metadata`
114
+ - `X-File-Id` (11.42.13+) — the ID of the stored file, see [The ID of the Stored File](#the-id-of-the-stored-file-114213)
114
115
 
115
116
  **`Access-Control-Allow-Origin` follows the API's CORS configuration (11.42.1+).** The tus handler
116
117
  writes its responses itself, and without `allowedOrigins` `@tus/server` answers `*`. With cookie
@@ -265,11 +266,19 @@ const upload = new Upload(file, {
265
266
  const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
266
267
  console.log(`${percentage}%`);
267
268
  },
269
+ // The response that completes the upload names the stored file (11.42.13+).
270
+ onAfterResponse: (_req, res) => {
271
+ const id = res.getHeader('X-File-Id');
272
+ if (id) {
273
+ fileId = id;
274
+ }
275
+ },
268
276
  onSuccess: () => {
269
- console.log('Upload complete!');
270
- console.log('File URL:', upload.url);
277
+ // `upload.url` is the TUS upload URL — its last segment is NOT the file id.
278
+ console.log('Upload complete, file:', `/files/id/${fileId}`);
271
279
  },
272
280
  });
281
+ let fileId: string | undefined;
273
282
 
274
283
  // Start or resume upload
275
284
  upload.start();
@@ -423,6 +432,10 @@ export class TusService extends CoreTusService {
423
432
  }
424
433
  ```
425
434
 
435
+ Call `super.onUploadComplete(upload)` with the `upload` you received: it stores the file and records
436
+ its ID for the `X-File-Id` response header (11.42.13+). An override that never calls `super` still
437
+ completes the upload, but the client receives no `X-File-Id`.
438
+
426
439
  ### Tenant Scoping (11.41.9+)
427
440
 
428
441
  **Only with `multiTenancy` configured.** Without it nothing changes: no header is read, no field is
@@ -487,6 +500,31 @@ query {
487
500
 
488
501
  **Recommendation:** Use the ID-based endpoint (`/files/id/:id`) for TUS uploads as filenames may not be unique.
489
502
 
503
+ ### The ID of the Stored File (11.42.13+)
504
+
505
+ The response that completes an upload — the last `PATCH`, or the `POST` of a creation-with-upload —
506
+ carries the ID of the stored file:
507
+
508
+ ```
509
+ HTTP/1.1 204 No Content
510
+ Upload-Offset: 52428800
511
+ X-File-Id: 6720c1f0a2b4c8e9d0f1a2b3
512
+ Access-Control-Expose-Headers: …, X-File-Id
513
+ ```
514
+
515
+ Read it from that response and use it for `/files/id/:id`. **Do not take the last segment of the
516
+ TUS URL** (`upload.url`): that is the TUS UPLOAD id, a key of the staging store, and `/files/id/` answers
517
+ it with 404. Until 11.42.13 the header did not exist and the TUS URL was all a client had.
518
+
519
+ The header is sent on the completing request only — creation and partial `PATCH`es store nothing yet.
520
+ It is listed in `Access-Control-Expose-Headers`, so a browser on a separate app origin can read it. The
521
+ name is exported as `TUS_FILE_ID_HEADER`.
522
+
523
+ A project override of `onUploadComplete()` keeps the header as long as it calls `super` — see
524
+ [Custom Upload Handler](#custom-upload-handler). The hook's signature did not change. A client must
525
+ still cope with a missing header (an older server, or an override that stores the file itself): treat
526
+ it as "no file id known", never as a reason to fall back to the TUS URL.
527
+
490
528
  > **These download routes are gated.** They require `file.downloadRoles`, which defaults to
491
529
  > `[RoleEnum.ADMIN]` — while uploading here only requires `tus.roles` (default `S_USER`). So out of
492
530
  > the box a signed-in user can upload but cannot read their own file back.
@@ -634,7 +672,8 @@ calls `getServer().handle()`. No change to `main.ts` is needed.
634
672
  3. Client: PATCH /tus/:id (send chunks)
635
673
  4. Server: Returns Upload-Offset
636
674
  5. Client: Repeat PATCH until complete
637
- 6. Server: Migrate to GridFS, cleanup temp file
675
+ 6. Server: Migrate to the configured file storage, cleanup temp file
676
+ 7. Server: Answer the completing PATCH with X-File-Id (the stored file's ID)
638
677
  ```
639
678
 
640
679
  ### File Storage
@@ -21,7 +21,7 @@ import {
21
21
  FilesystemFileHelper,
22
22
  } from '../file/filesystem-file.helper';
23
23
  import { S3_FILES_COLLECTION, S3FileHelper } from '../file/s3-file.helper';
24
- import { TUS_OWNER_METADATA_KEY, TUS_TENANT_METADATA_KEY } from './tus.constants';
24
+ import { TUS_FILE_ID_HEADER, TUS_OWNER_METADATA_KEY, TUS_TENANT_METADATA_KEY } from './tus.constants';
25
25
  import { TusRedisLocker } from './tus-redis-locker';
26
26
  import {
27
27
  DEFAULT_TUS_ALLOWED_HEADERS,
@@ -86,6 +86,17 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
86
86
  private files: mongo.GridFSBucket;
87
87
  private cleanupInterval: NodeJS.Timeout | null = null;
88
88
 
89
+ /**
90
+ * The ID of the stored file per finished upload, recorded by {@link CoreTusService.onUploadComplete}
91
+ * and read by `onUploadFinish` for the `X-File-Id` response header.
92
+ *
93
+ * Recorded on the side rather than returned, so the hook keeps its `Promise<void>` signature: a
94
+ * project override in either documented shape — `await super.onUploadComplete(upload)` or
95
+ * `return super.onUploadComplete(upload)` — still compiles AND still yields the header. Keyed by
96
+ * the `Upload` object `onUploadFinish` hands to the hook, so nothing outlives the request.
97
+ */
98
+ private readonly storedFileIds = new WeakMap<Upload, string>();
99
+
89
100
  /** Datastore of the TUS server; set when uploads are staged in S3 */
90
101
  protected s3Store: any = null;
91
102
 
@@ -183,10 +194,15 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
183
194
  }
184
195
 
185
196
  /**
186
- * Handle upload completion - migrate to GridFS
197
+ * Handle upload completion - migrate the finished upload into the configured file storage
187
198
  *
188
199
  * This method can be overridden in extending services to customize
189
200
  * what happens after an upload completes.
201
+ *
202
+ * Records the ID of the stored file, which the completing response sends to the client as
203
+ * `X-File-Id` (`TUS_FILE_ID_HEADER`). An override must call `super.onUploadComplete(upload)` with
204
+ * the `upload` it received; one that does not stores nothing here and sends no header — the client
205
+ * then knows the TUS URL alone, whose last segment is the upload id, not the file id.
190
206
  */
191
207
  protected async onUploadComplete(upload: Upload): Promise<void> {
192
208
  try {
@@ -231,6 +247,7 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
231
247
  `Upload ${upload.id} copied inside S3 to ${fileInfo._id} (filename: ${filename}) — no bytes through this process`,
232
248
  );
233
249
  await this.deleteStagedUpload(upload.id);
250
+ this.storedFileIds.set(upload, String(fileInfo._id));
234
251
  return;
235
252
  }
236
253
 
@@ -239,15 +256,16 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
239
256
  return;
240
257
  }
241
258
 
259
+ let fileInfo: { _id: unknown };
242
260
  if (this.fileStorageDriver === 'filesystem') {
243
- const fileInfo = await FilesystemFileHelper.writeFile(
261
+ fileInfo = await FilesystemFileHelper.writeFile(
244
262
  this.fileStorageDir,
245
263
  this.connection.db.collection(FILESYSTEM_FILES_COLLECTION),
246
264
  { body: readStream, contentType, filename, metadata: fileMetadata },
247
265
  );
248
266
  this.logger.debug(`Upload ${upload.id} migrated to the filesystem as ${fileInfo._id} (filename: ${filename})`);
249
267
  } else if (this.s3FileStorage) {
250
- const fileInfo = await S3FileHelper.writeFile(
268
+ fileInfo = await S3FileHelper.writeFile(
251
269
  this.options.s3Service,
252
270
  this.connection.db.collection(S3_FILES_COLLECTION),
253
271
  // Streamed with the length TUS already knows, so a multi-GB resumable upload is never
@@ -256,7 +274,7 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
256
274
  );
257
275
  this.logger.debug(`Upload ${upload.id} migrated to S3 as ${fileInfo._id} (filename: ${filename})`);
258
276
  } else {
259
- const fileInfo = await GridFSHelper.writeFileFromStream(this.files, readStream, {
277
+ fileInfo = await GridFSHelper.writeFileFromStream(this.files, readStream, {
260
278
  contentType,
261
279
  filename,
262
280
  metadata: fileMetadata,
@@ -266,6 +284,7 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
266
284
 
267
285
  // Clean up the staged upload
268
286
  await this.deleteStagedUpload(upload.id);
287
+ this.storedFileIds.set(upload, String(fileInfo._id));
269
288
  } catch (error) {
270
289
  this.logger.error(`Failed to migrate upload ${upload.id}: ${error.message}`);
271
290
  throw error;
@@ -837,6 +856,9 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
837
856
  // which a browser refuses on a credentialed (cookie) request — see resolveCorsOrigins().
838
857
  ...(cors ? { allowedCredentials: true, allowedOrigins: cors } : {}),
839
858
  datastore,
859
+ // Appended to the tus protocol headers in `Access-Control-Expose-Headers`: a browser on a
860
+ // separate app origin cannot read a response header that is not listed there.
861
+ exposedHeaders: [TUS_FILE_ID_HEADER],
840
862
  ...(locker ? { locker } : {}),
841
863
  maxSize: this.config.maxSize,
842
864
  // Every request that names an upload — HEAD (offset), PATCH (append bytes), DELETE
@@ -864,10 +886,14 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
864
886
  const relative = this.config.relativeLocation === true || (server as any)?.options?.relativeLocation === true;
865
887
  return relative ? url : `${proto}://${host}${url}`;
866
888
  },
889
+ // The response that completes the upload names the stored file. Without it the client knows
890
+ // only the TUS URL, whose last segment is the upload id — a staging key `/files/id/` answers
891
+ // with 404. An override of onUploadComplete() that never calls super records nothing: no header.
867
892
  onUploadFinish: async (_req, upload) => {
868
893
  try {
869
894
  await this.onUploadComplete(upload);
870
- return {};
895
+ const fileId = this.storedFileIds.get(upload);
896
+ return fileId ? { headers: { [TUS_FILE_ID_HEADER]: fileId } } : {};
871
897
  } catch (error) {
872
898
  // Do NOT swallow this. `onUploadComplete` is what moves the finished upload into its
873
899
  // permanent store; returning `{}` after it failed answers the client with a 204 that
@@ -51,3 +51,14 @@ export const TUS_OWNER_METADATA_KEY = 'ltOwnerId';
51
51
  * resolves to a tenant they are an active member of; otherwise it is `null`.
52
52
  */
53
53
  export const TUS_TENANT_METADATA_KEY = 'ltTenantId';
54
+
55
+ /**
56
+ * Response header naming the ID of the stored file, sent on the request that COMPLETES a tus upload
57
+ * (the last PATCH, or the POST of a creation-with-upload).
58
+ *
59
+ * Without it a client knows only the TUS URL, whose last segment is the tus UPLOAD id — a key of the
60
+ * staging store, which `/files/id/<id>` answers with 404. The header is also listed in
61
+ * `Access-Control-Expose-Headers`, because a browser on a separate app origin cannot read it
62
+ * otherwise.
63
+ */
64
+ export const TUS_FILE_ID_HEADER = 'X-File-Id';