@lenne.tech/nest-server 11.42.0 → 11.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ # Migration Guide: 11.42.0 → 11.42.1
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | --------------------------------------------------------------------------------------------- |
7
+ | **Bugfix** | Browser uploads through TUS work with cookie authentication and separate app/API origins (§1) |
8
+ | **New option** | `tus.allowedOrigins` overrides the derived origins (§1) |
9
+ | **Migration Effort** | None — `pnpm update` |
10
+
11
+ ## Quick Migration
12
+
13
+ ```bash
14
+ pnpm update @lenne.tech/nest-server
15
+ ```
16
+
17
+ Vendor-mode projects pick up the core update; nothing else to change.
18
+
19
+ ## 1. TUS answered a credentialed request with `Access-Control-Allow-Origin: *`
20
+
21
+ The tus handler writes its responses itself, past the API's CORS layer, and `@tus/server` answers
22
+ `Access-Control-Allow-Origin: *` unless it is given `allowedOrigins`. The core gave it none. With cookie
23
+ authentication and separate app and API origins (`lt dev up`, or `app.example.com` /
24
+ `api.example.com`), the request is credentialed and the API adds
25
+ `Access-Control-Allow-Credentials: true` — a combination every browser refuses:
26
+
27
+ > The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'
28
+ > when the request's credentials mode is 'include'.
29
+
30
+ So every browser upload failed, while the server answered `201`. Preflights and unauthenticated
31
+ requests were fine, which is why no test noticed — only an **authenticated** request with an `Origin`
32
+ reaches the header.
33
+
34
+ Since 11.42.1 the tus server takes its origins from `buildCorsConfig()`, the decision REST, GraphQL and
35
+ Better-Auth already share (`appUrl`, `baseUrl`, `cors.allowedOrigins`, `cors.allowAll`), and mirrors an
36
+ allowed `Origin`. A foreign origin gets no `Access-Control-Allow-Origin` at all. To use different
37
+ origins for uploads:
38
+
39
+ ```typescript
40
+ tus: {
41
+ allowedOrigins: ['https://uploads.example.com'];
42
+ }
43
+ ```
44
+
45
+ Without credentialed CORS configured — cookies off, or no origin resolvable — nothing changes; the tus
46
+ default `*` stays.
47
+
48
+ ## 2. Dependencies
49
+
50
+ Updated within their majors: `@modelcontextprotocol/sdk` 1.32.0, `mongoose` 9.10.4, `supertest` 7.3.1, and
51
+ the `@aws-sdk/*` S3 packages 3.1146.0 (dev). NestJS stays on 11.
52
+
53
+ `@modelcontextprotocol/sdk` 1.32.0 lets its CLIENT transports follow redirects only within the same
54
+ origin. The framework uses the server side only, so this matters only to a project that builds its own MCP
55
+ client transport and relied on a cross-origin redirect.
56
+
57
+ ## Troubleshooting
58
+
59
+ | Symptom | Cause | Fix |
60
+ | -------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------ |
61
+ | Browser: "must not be the wildcard '*' when the request's credentials mode is 'include'" on `/tus` | 11.42.0 or older | Update to 11.42.1 |
62
+ | Upload from an origin that is not the app refused by CORS | Origin not in `appUrl` / `cors.allowedOrigins` | Add it to `cors.allowedOrigins`, or set `tus.allowedOrigins` |
63
+
64
+ ## Module Documentation
65
+
66
+ - [TUS README](../src/core/modules/tus/README.md) — CORS Headers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.42.0",
3
+ "version": "11.42.1",
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",
@@ -92,7 +92,7 @@
92
92
  "@apollo/server": "5.5.1",
93
93
  "@as-integrations/express5": "1.1.2",
94
94
  "@getbrevo/brevo": "6.0.3",
95
- "@modelcontextprotocol/sdk": "1.31.0",
95
+ "@modelcontextprotocol/sdk": "1.32.0",
96
96
  "@nestjs/apollo": "13.4.5",
97
97
  "@nestjs/common": "11.2.7",
98
98
  "@nestjs/core": "11.2.7",
@@ -126,7 +126,7 @@
126
126
  "json-to-graphql-query": "2.3.0",
127
127
  "lodash": "4.18.1",
128
128
  "mongodb": "7.6.0",
129
- "mongoose": "9.10.3",
129
+ "mongoose": "9.10.4",
130
130
  "multer": "2.4.0",
131
131
  "node-mailjet": "6.0.11",
132
132
  "nodemailer": "10.0.13",
@@ -135,7 +135,7 @@
135
135
  "reflect-metadata": "0.2.2",
136
136
  "rfdc": "1.4.1",
137
137
  "rxjs": "7.8.2",
138
- "supertest": "7.3.0",
138
+ "supertest": "7.3.1",
139
139
  "ts-morph": "28.0.0",
140
140
  "undici": "7.30.0",
141
141
  "ws": "8.21.3",
@@ -182,9 +182,9 @@
182
182
  }
183
183
  },
184
184
  "devDependencies": {
185
- "@aws-sdk/client-s3": "3.1145.0",
186
- "@aws-sdk/lib-storage": "3.1145.0",
187
- "@aws-sdk/s3-request-presigner": "3.1145.0",
185
+ "@aws-sdk/client-s3": "3.1146.0",
186
+ "@aws-sdk/lib-storage": "3.1146.0",
187
+ "@aws-sdk/s3-request-presigner": "3.1146.0",
188
188
  "@better-auth/core": "1.7.7",
189
189
  "@better-auth/passkey": "1.7.7",
190
190
  "@compodoc/compodoc": "2.0.0",
@@ -3606,6 +3606,23 @@ export interface ITusConfig {
3606
3606
  */
3607
3607
  allowedHeaders?: string[];
3608
3608
 
3609
+ /**
3610
+ * Origins the tus handler may answer a cross-origin request with (`Access-Control-Allow-Origin`).
3611
+ *
3612
+ * Unset, they come from the API's own CORS configuration — `buildCorsConfig()`, the function REST,
3613
+ * GraphQL and Better-Auth share (`appUrl`, `baseUrl`, `cors.allowedOrigins`, `cors.allowAll`).
3614
+ * Without credentialed CORS configured (cookies off, or no origin resolvable) the tus server keeps
3615
+ * its own default, `*`.
3616
+ *
3617
+ * The wildcard is what broke browser uploads with cookie authentication and separate app/API
3618
+ * origins before 11.42.1: a credentialed response with `Access-Control-Allow-Origin: *` is refused
3619
+ * by every browser, although the server answered 201.
3620
+ *
3621
+ * @default undefined (derived from the API CORS configuration)
3622
+ * @since 11.42.1
3623
+ */
3624
+ allowedOrigins?: string[];
3625
+
3609
3626
  /**
3610
3627
  * Allowed MIME types for uploads.
3611
3628
  * If undefined, all types are allowed.
@@ -107,6 +107,17 @@ The TUS server automatically handles CORS headers for browser-based clients:
107
107
  - `Upload-Offset`
108
108
  - `Upload-Metadata`
109
109
 
110
+ **`Access-Control-Allow-Origin` follows the API's CORS configuration (11.42.1+).** The tus handler
111
+ writes its responses itself, and without `allowedOrigins` `@tus/server` answers `*`. With cookie
112
+ authentication and separate app and API origins the request is credentialed, and every browser
113
+ refuses `*` there — uploads failed in the browser although the server answered 201. The origins now
114
+ come from `buildCorsConfig()` (`appUrl`, `baseUrl`, `cors.allowedOrigins`, `cors.allowAll`), the same
115
+ decision REST, GraphQL and Better-Auth make; `tus.allowedOrigins` overrides. Without credentialed CORS
116
+ (cookies off, or no origin resolvable) the tus default `*` stays.
117
+
118
+ The preflight (`OPTIONS`) is answered by Nest's CORS layer and was never affected — test an
119
+ **authenticated** request with an `Origin` header when checking this.
120
+
110
121
  ---
111
122
 
112
123
  ## Configuration
@@ -9,6 +9,7 @@ import { Readable } from 'stream';
9
9
 
10
10
  import { GridFSHelper } from '../../common/helpers/gridfs.helper';
11
11
  import { ITusConfig } from '../../common/interfaces/server-options.interface';
12
+ import { buildCorsConfig } from '../../common/helpers/cookies.helper';
12
13
  import { ConfigService } from '../../common/services/config.service';
13
14
  import { CoreRedisService } from '../../common/services/core-redis.service';
14
15
  import { CoreS3Service } from '../../common/services/core-s3.service';
@@ -638,6 +639,46 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
638
639
  return { metadata };
639
640
  }
640
641
 
642
+ /**
643
+ * The origins the tus handler may answer a cross-origin request with — or `undefined` to keep the
644
+ * tus server's own default (`*`).
645
+ *
646
+ * The tus handler writes its responses itself, past the API's CORS layer, and without
647
+ * `allowedOrigins` @tus/server sets `Access-Control-Allow-Origin: *`. On a credentialed request —
648
+ * cookie authentication with separate app and API origins — every browser refuses that, so no
649
+ * browser upload could succeed although the server answered 201. Preflights and 401s were fine
650
+ * (Nest answers those), which is why nothing noticed: only an AUTHENTICATED request reaches it.
651
+ *
652
+ * Taken from `tus.allowedOrigins` when set, else from `buildCorsConfig()` — the same decision REST,
653
+ * GraphQL and Better-Auth make. Without credentialed CORS configured the previous default stays.
654
+ */
655
+ protected resolveCorsOrigins(): ((origin: string) => boolean) | undefined {
656
+ const toOrigin = (value: string): string => {
657
+ try {
658
+ return new URL(value).origin;
659
+ } catch {
660
+ return value;
661
+ }
662
+ };
663
+ const fromList = (list: string[]) => {
664
+ const allowed = new Set(list.filter((entry) => typeof entry === 'string').map(toOrigin));
665
+ return (origin: string) => allowed.has(toOrigin(origin));
666
+ };
667
+
668
+ if (Array.isArray(this.config.allowedOrigins) && this.config.allowedOrigins.length) {
669
+ return fromList(this.config.allowedOrigins);
670
+ }
671
+ const options = this.options?.configService?.configFastButReadOnly ?? ConfigService.configFastButReadOnly ?? {};
672
+ const cors = buildCorsConfig(options);
673
+ if (cors.credentials !== true) {
674
+ return undefined;
675
+ }
676
+ if (cors.origin === true) {
677
+ return () => true;
678
+ }
679
+ return Array.isArray(cors.origin) ? fromList(cors.origin as string[]) : undefined;
680
+ }
681
+
641
682
  /**
642
683
  * The VALIDATED tenant a new upload belongs to, or `undefined`.
643
684
  *
@@ -716,8 +757,12 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
716
757
  // correct for the single-replica deployment that configuration describes.
717
758
  const locker = this.createLocker();
718
759
 
760
+ const cors = this.resolveCorsOrigins();
719
761
  const server = new Server({
720
762
  allowedHeaders: this.config.allowedHeaders || DEFAULT_TUS_ALLOWED_HEADERS,
763
+ // Without these, @tus/server answers every request with `Access-Control-Allow-Origin: *`,
764
+ // which a browser refuses on a credentialed (cookie) request — see resolveCorsOrigins().
765
+ ...(cors ? { allowedCredentials: true, allowedOrigins: cors } : {}),
721
766
  datastore,
722
767
  ...(locker ? { locker } : {}),
723
768
  maxSize: this.config.maxSize,
@@ -31,13 +31,16 @@ export const DEFAULT_TUS_ALLOWED_HEADERS: string[] = [];
31
31
  * Default TUS configuration
32
32
  */
33
33
  export const DEFAULT_TUS_CONFIG: Required<
34
- Omit<ITusConfig, 'allowedTypes' | 'creation' | 'expiration'> & {
34
+ Omit<ITusConfig, 'allowedOrigins' | 'allowedTypes' | 'creation' | 'expiration'> & {
35
+ allowedOrigins: undefined;
35
36
  allowedTypes: undefined;
36
37
  creation: boolean;
37
38
  expiration: ITusExpirationConfig;
38
39
  }
39
40
  > = {
40
41
  allowedHeaders: DEFAULT_TUS_ALLOWED_HEADERS,
42
+ // Derived from the API CORS configuration (CoreTusService.resolveCorsOrigins())
43
+ allowedOrigins: undefined,
41
44
  allowedTypes: undefined,
42
45
  checksum: true,
43
46
  concatenation: true,