@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.
- package/.claude/rules/configurable-features.md +1 -1
- package/.claude/rules/testing.md +3 -3
- package/FRAMEWORK-API.md +2 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +1 -0
- package/dist/core/modules/tus/core-tus.service.d.ts +1 -0
- package/dist/core/modules/tus/core-tus.service.js +29 -0
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/interfaces/tus-config.interface.d.ts +2 -1
- package/dist/core/modules/tus/interfaces/tus-config.interface.js +1 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.42.0-to-11.42.1.md +66 -0
- package/package.json +7 -7
- package/src/core/common/interfaces/server-options.interface.ts +17 -0
- package/src/core/modules/tus/README.md +11 -0
- package/src/core/modules/tus/core-tus.service.ts +45 -0
- package/src/core/modules/tus/interfaces/tus-config.interface.ts +4 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
186
|
-
"@aws-sdk/lib-storage": "3.
|
|
187
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
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,
|