dinou 4.0.10 → 4.0.12
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/CHANGELOG.md +15 -0
- package/dinou/core/server-function-proxy-webpack.js +62 -63
- package/dinou/core/server-function-proxy.js +62 -63
- package/dinou/core/server.js +28 -22
- package/dinou/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [4.0.12] - 2026-06-28
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- **Fix DOM XSS / Script Injection Vulnerability:** Replaced inline HTML/Script injection (`<script>` tags executed via `createContextualFragment` in client-side proxies) with a secure, data-driven line-based protocol for active streams and custom HTTP headers/JSON payload signaling for redirects.
|
|
12
|
+
- **CSP (Content Security Policy) Compatibility:** Removed all `'unsafe-inline'` script injections, ensuring full compatibility with strict security policies.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Stream Redirection & Cookie Handling:** Improved robustness and predictability of stream reading in server-function proxies by switching to a line-by-line (`\n` delimited) streaming command parser, eliminating regex-based tag extraction.
|
|
16
|
+
|
|
17
|
+
## [4.0.11]
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Security: Fix server functions endpoint vulnerability and add improvements.
|
|
22
|
+
|
|
8
23
|
## [4.0.10]
|
|
9
24
|
|
|
10
25
|
### Fixed
|
|
@@ -30,15 +30,30 @@ function createServerFunctionProxy(id) {
|
|
|
30
30
|
|
|
31
31
|
if (!res.ok) throw new Error("Server function failed");
|
|
32
32
|
|
|
33
|
+
// Check header first
|
|
34
|
+
const redirectUrl = res.headers.get("X-Dinou-Redirect");
|
|
35
|
+
if (redirectUrl) {
|
|
36
|
+
window.location.href = redirectUrl;
|
|
37
|
+
return new Promise(() => { });
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
const contentType = res.headers.get("content-type") || "";
|
|
34
41
|
|
|
35
|
-
//
|
|
42
|
+
// Reject unexpected HTML responses entirely to prevent security bugs
|
|
36
43
|
if (contentType.includes("text/html")) {
|
|
37
44
|
const html = await res.text();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
console.warn("[Dinou] Received unexpected HTML response from server function:", html);
|
|
46
|
+
throw new Error("Unexpected HTML response from server function");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if it's application/json (Scenario A clean redirect or json response)
|
|
50
|
+
if (contentType.includes("application/json")) {
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
if (data && data.redirect) {
|
|
53
|
+
window.location.href = data.redirect;
|
|
54
|
+
return new Promise(() => { });
|
|
55
|
+
}
|
|
56
|
+
return data;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
// Case 2: RSC or Hybrid Stream (Buffered)
|
|
@@ -46,7 +61,6 @@ function createServerFunctionProxy(id) {
|
|
|
46
61
|
const reader = res.body.getReader();
|
|
47
62
|
const decoder = new TextDecoder();
|
|
48
63
|
const encoder = new TextEncoder();
|
|
49
|
-
const scriptRegex = /<script>(.*?)<\/script>/gs;
|
|
50
64
|
|
|
51
65
|
const readableStream = new ReadableStream({
|
|
52
66
|
async start(controller) {
|
|
@@ -58,16 +72,20 @@ function createServerFunctionProxy(id) {
|
|
|
58
72
|
const { done, value } = await reader.read();
|
|
59
73
|
|
|
60
74
|
if (done) {
|
|
61
|
-
// If something remains in the buffer upon finishing, we send it (as long as it's not a broken script)
|
|
62
75
|
if (buffer.length > 0) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
if (buffer.startsWith("D:")) {
|
|
77
|
+
try {
|
|
78
|
+
const payload = JSON.parse(buffer.slice(2));
|
|
79
|
+
if (payload.type === "redirect") {
|
|
80
|
+
isRedirecting = true;
|
|
81
|
+
window.location.href = payload.url;
|
|
82
|
+
} else if (payload.type === "cookie") {
|
|
83
|
+
document.cookie = payload.cookie;
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error("[Dinou] Failed to parse stream command line at end:", e);
|
|
87
|
+
}
|
|
69
88
|
} else {
|
|
70
|
-
// It is safe content (e.g., "a < b" or a cut JSON that React will handle)
|
|
71
89
|
controller.enqueue(encoder.encode(buffer));
|
|
72
90
|
}
|
|
73
91
|
}
|
|
@@ -77,65 +95,46 @@ function createServerFunctionProxy(id) {
|
|
|
77
95
|
// 1. ACCUMULATE
|
|
78
96
|
buffer += decoder.decode(value, { stream: true });
|
|
79
97
|
|
|
80
|
-
// 2. PROCESS COMPLETE
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
while ((match = scriptRegex.exec(buffer)) !== null) {
|
|
86
|
-
const fullMatch = match[0];
|
|
87
|
-
const scriptContent = match[1];
|
|
98
|
+
// 2. PROCESS COMPLETE LINES
|
|
99
|
+
const lastNewlineIndex = buffer.lastIndexOf("\n");
|
|
100
|
+
if (lastNewlineIndex !== -1) {
|
|
101
|
+
const completeChunk = buffer.slice(0, lastNewlineIndex + 1);
|
|
102
|
+
buffer = buffer.slice(lastNewlineIndex + 1);
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
isRedirecting = true;
|
|
92
|
-
}
|
|
104
|
+
const lines = completeChunk.split("\n");
|
|
105
|
+
let cleanChunk = "";
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
const line = lines[i];
|
|
109
|
+
if (!line && i === lines.length - 1) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
} else {
|
|
116
|
-
// B) If no open script, check if the end looks like a cut tag
|
|
117
|
-
// Regex: Looks for '<' optionally followed by s, c, r, i, p, t AT THE END of the string ($)
|
|
118
|
-
const partialTagMatch = buffer.match(/<s?c?r?i?p?t?$/);
|
|
119
|
-
|
|
120
|
-
if (partialTagMatch) {
|
|
121
|
-
// We save from where the suspicion starts
|
|
122
|
-
cutoffIndex = partialTagMatch.index;
|
|
113
|
+
if (line.startsWith("D:")) {
|
|
114
|
+
try {
|
|
115
|
+
const payload = JSON.parse(line.slice(2));
|
|
116
|
+
if (payload.type === "redirect") {
|
|
117
|
+
isRedirecting = true;
|
|
118
|
+
window.location.href = payload.url;
|
|
119
|
+
} else if (payload.type === "cookie") {
|
|
120
|
+
document.cookie = payload.cookie;
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error("[Dinou] Failed to parse stream command line:", e);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
cleanChunk += line + "\n";
|
|
127
|
+
}
|
|
123
128
|
}
|
|
124
|
-
}
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
buffer = buffer.slice(cutoffIndex);
|
|
130
|
-
|
|
131
|
-
if (safeChunk) {
|
|
132
|
-
controller.enqueue(encoder.encode(safeChunk));
|
|
130
|
+
if (cleanChunk) {
|
|
131
|
+
controller.enqueue(encoder.encode(cleanChunk));
|
|
132
|
+
}
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
} catch (err) {
|
|
136
136
|
controller.error(err);
|
|
137
137
|
} finally {
|
|
138
|
-
// If it is NOT a redirect, we close. If it is a redirect, we leave hanging (your master trick).
|
|
139
138
|
if (!isRedirecting) {
|
|
140
139
|
controller.close();
|
|
141
140
|
}
|
|
@@ -31,15 +31,30 @@ export function createServerFunctionProxy(id) {
|
|
|
31
31
|
|
|
32
32
|
if (!res.ok) throw new Error("Server function failed");
|
|
33
33
|
|
|
34
|
+
// Check header first
|
|
35
|
+
const redirectUrl = res.headers.get("X-Dinou-Redirect");
|
|
36
|
+
if (redirectUrl) {
|
|
37
|
+
window.location.href = redirectUrl;
|
|
38
|
+
return new Promise(() => {});
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
const contentType = res.headers.get("content-type") || "";
|
|
35
42
|
|
|
36
|
-
//
|
|
43
|
+
// Reject unexpected HTML responses entirely to prevent security bugs
|
|
37
44
|
if (contentType.includes("text/html")) {
|
|
38
45
|
const html = await res.text();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
console.warn("[Dinou] Received unexpected HTML response from server function:", html);
|
|
47
|
+
throw new Error("Unexpected HTML response from server function");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if it's application/json (Scenario A clean redirect or json response)
|
|
51
|
+
if (contentType.includes("application/json")) {
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
if (data && data.redirect) {
|
|
54
|
+
window.location.href = data.redirect;
|
|
55
|
+
return new Promise(() => {});
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
// Case 2: RSC or Hybrid Stream (Buffered)
|
|
@@ -47,7 +62,6 @@ export function createServerFunctionProxy(id) {
|
|
|
47
62
|
const reader = res.body.getReader();
|
|
48
63
|
const decoder = new TextDecoder();
|
|
49
64
|
const encoder = new TextEncoder();
|
|
50
|
-
const scriptRegex = /<script>(.*?)<\/script>/gs;
|
|
51
65
|
|
|
52
66
|
const readableStream = new ReadableStream({
|
|
53
67
|
async start(controller) {
|
|
@@ -59,16 +73,20 @@ export function createServerFunctionProxy(id) {
|
|
|
59
73
|
const { done, value } = await reader.read();
|
|
60
74
|
|
|
61
75
|
if (done) {
|
|
62
|
-
// If something remains in the buffer upon finishing, we send it (as long as it's not a broken script)
|
|
63
76
|
if (buffer.length > 0) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
if (buffer.startsWith("D:")) {
|
|
78
|
+
try {
|
|
79
|
+
const payload = JSON.parse(buffer.slice(2));
|
|
80
|
+
if (payload.type === "redirect") {
|
|
81
|
+
isRedirecting = true;
|
|
82
|
+
window.location.href = payload.url;
|
|
83
|
+
} else if (payload.type === "cookie") {
|
|
84
|
+
document.cookie = payload.cookie;
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error("[Dinou] Failed to parse stream command line at end:", e);
|
|
88
|
+
}
|
|
70
89
|
} else {
|
|
71
|
-
// It is safe content (e.g., "a < b" or a cut JSON that React will handle)
|
|
72
90
|
controller.enqueue(encoder.encode(buffer));
|
|
73
91
|
}
|
|
74
92
|
}
|
|
@@ -78,65 +96,46 @@ export function createServerFunctionProxy(id) {
|
|
|
78
96
|
// 1. ACCUMULATE
|
|
79
97
|
buffer += decoder.decode(value, { stream: true });
|
|
80
98
|
|
|
81
|
-
// 2. PROCESS COMPLETE
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
while ((match = scriptRegex.exec(buffer)) !== null) {
|
|
87
|
-
const fullMatch = match[0];
|
|
88
|
-
const scriptContent = match[1];
|
|
99
|
+
// 2. PROCESS COMPLETE LINES
|
|
100
|
+
const lastNewlineIndex = buffer.lastIndexOf("\n");
|
|
101
|
+
if (lastNewlineIndex !== -1) {
|
|
102
|
+
const completeChunk = buffer.slice(0, lastNewlineIndex + 1);
|
|
103
|
+
buffer = buffer.slice(lastNewlineIndex + 1);
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
isRedirecting = true;
|
|
93
|
-
}
|
|
105
|
+
const lines = completeChunk.split("\n");
|
|
106
|
+
let cleanChunk = "";
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
if (!line && i === lines.length - 1) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
100
113
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else {
|
|
117
|
-
// B) If no open script, check if the end looks like a cut tag
|
|
118
|
-
// Regex: Looks for '<' optionally followed by s, c, r, i, p, t AT THE END of the string ($)
|
|
119
|
-
const partialTagMatch = buffer.match(/<s?c?r?i?p?t?$/);
|
|
120
|
-
|
|
121
|
-
if (partialTagMatch) {
|
|
122
|
-
// We save from where the suspicion starts
|
|
123
|
-
cutoffIndex = partialTagMatch.index;
|
|
114
|
+
if (line.startsWith("D:")) {
|
|
115
|
+
try {
|
|
116
|
+
const payload = JSON.parse(line.slice(2));
|
|
117
|
+
if (payload.type === "redirect") {
|
|
118
|
+
isRedirecting = true;
|
|
119
|
+
window.location.href = payload.url;
|
|
120
|
+
} else if (payload.type === "cookie") {
|
|
121
|
+
document.cookie = payload.cookie;
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error("[Dinou] Failed to parse stream command line:", e);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
cleanChunk += line + "\n";
|
|
128
|
+
}
|
|
124
129
|
}
|
|
125
|
-
}
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
buffer = buffer.slice(cutoffIndex);
|
|
131
|
-
|
|
132
|
-
if (safeChunk) {
|
|
133
|
-
controller.enqueue(encoder.encode(safeChunk));
|
|
131
|
+
if (cleanChunk) {
|
|
132
|
+
controller.enqueue(encoder.encode(cleanChunk));
|
|
133
|
+
}
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
} catch (err) {
|
|
137
137
|
controller.error(err);
|
|
138
138
|
} finally {
|
|
139
|
-
// If it is NOT a redirect, we close. If it is a redirect, we leave hanging (your master trick).
|
|
140
139
|
if (!isRedirecting) {
|
|
141
140
|
controller.close();
|
|
142
141
|
}
|
package/dinou/core/server.js
CHANGED
|
@@ -380,7 +380,7 @@ function getContextForServerFunctionEndpoint(req, res) {
|
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
// SCENARIO B: Streaming active (Headers Sent)
|
|
383
|
-
//
|
|
383
|
+
// Send safe stream command.
|
|
384
384
|
|
|
385
385
|
// 🛑 Security: JS cannot write HttpOnly cookies
|
|
386
386
|
if (options && options.httpOnly) {
|
|
@@ -404,11 +404,11 @@ function getContextForServerFunctionEndpoint(req, res) {
|
|
|
404
404
|
if (options.sameSite) cookieStr += `; samesite=${options.sameSite}`;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
// Safe packaging for
|
|
407
|
+
// Safe packaging for stream command
|
|
408
408
|
const safeCookieStr = JSON.stringify(cookieStr);
|
|
409
409
|
|
|
410
410
|
// Write to stream
|
|
411
|
-
res.write(
|
|
411
|
+
res.write(`D:{"type":"cookie","cookie":${safeCookieStr}}\n`);
|
|
412
412
|
},
|
|
413
413
|
|
|
414
414
|
clearCookie: (name, options) => {
|
|
@@ -421,13 +421,10 @@ function getContextForServerFunctionEndpoint(req, res) {
|
|
|
421
421
|
return;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
// SCENARIO B:
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
res.write(
|
|
429
|
-
`<script>document.cookie = ${safeName} + "=; Max-Age=0; path=" + ${safePath} + ";";</script>`,
|
|
430
|
-
);
|
|
424
|
+
// SCENARIO B: Custom stream command
|
|
425
|
+
const cookieStr = `${name}=; Max-Age=0; path=${path};`;
|
|
426
|
+
const safeCookieStr = JSON.stringify(cookieStr);
|
|
427
|
+
res.write(`D:{"type":"cookie","cookie":${safeCookieStr}}\n`);
|
|
431
428
|
},
|
|
432
429
|
},
|
|
433
430
|
};
|
|
@@ -752,7 +749,7 @@ function isOriginAllowed(req) {
|
|
|
752
749
|
try {
|
|
753
750
|
// Parse to ignore protocol (http/https) and port if they differ subtly
|
|
754
751
|
const originHost = new URL(origin).host;
|
|
755
|
-
const serverHost = req.headers.host;
|
|
752
|
+
const serverHost = req.headers["x-forwarded-host"] || req.headers.host;
|
|
756
753
|
|
|
757
754
|
// Compare host (domain:port)
|
|
758
755
|
return originHost === serverHost;
|
|
@@ -782,7 +779,7 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
782
779
|
|
|
783
780
|
// 1. Check Origin (Prevent calls from other domains)
|
|
784
781
|
const origin = req.headers.origin;
|
|
785
|
-
const host = req.headers.host;
|
|
782
|
+
const host = req.headers["x-forwarded-host"] || req.headers.host;
|
|
786
783
|
|
|
787
784
|
// Note: Locally sometimes origin is undefined or null, allow in dev if necessary
|
|
788
785
|
if (!isDevelopment && origin && !origin.includes(host)) {
|
|
@@ -851,10 +848,15 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
851
848
|
|
|
852
849
|
// Extract relativePath and normalize (remove 'file://' and potential '/')
|
|
853
850
|
let relativePath = fileUrl.replace(/^file:\/\/\/?/, "").trim();
|
|
854
|
-
|
|
851
|
+
const normalizedRelative = relativePath.replace(/\\/g, "/");
|
|
852
|
+
if (
|
|
853
|
+
normalizedRelative.startsWith("/") ||
|
|
854
|
+
normalizedRelative.includes("..") ||
|
|
855
|
+
normalizedRelative.includes(":")
|
|
856
|
+
) {
|
|
855
857
|
return res
|
|
856
858
|
.status(400)
|
|
857
|
-
.json({ error: "Invalid path: no absolute or
|
|
859
|
+
.json({ error: "Invalid path: no absolute, traversal, or drive letter allowed" });
|
|
858
860
|
}
|
|
859
861
|
// console.log("relPath", relativePath);
|
|
860
862
|
// Restrict to 'src/' folder: prepend 'src/' if missing, and resolve absolutePath
|
|
@@ -881,6 +883,11 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
881
883
|
// Prod: use manifest (relativePath is already normalized)
|
|
882
884
|
allowedExports = serverFunctionsManifest[relativePath];
|
|
883
885
|
} else {
|
|
886
|
+
if (!isDevelopment) {
|
|
887
|
+
return res
|
|
888
|
+
.status(403)
|
|
889
|
+
.json({ error: "Access denied: Manifest missing in production" });
|
|
890
|
+
}
|
|
884
891
|
const fileContent = readFileSync(absolutePath, "utf8"); // Reads only once
|
|
885
892
|
if (!useServerRegex.test(fileContent)) {
|
|
886
893
|
return res
|
|
@@ -925,18 +932,17 @@ app.post("/____server_function____", async (req, res) => {
|
|
|
925
932
|
} catch (err) {
|
|
926
933
|
// 💡 WE INTERCEPT THE REDIRECT
|
|
927
934
|
if (err && err.$$type === "dinou-internal-redirect") {
|
|
928
|
-
// 1. Always sanitize the URL
|
|
929
935
|
const safeUrl = JSON.stringify(err.url);
|
|
930
|
-
const script = `<script>window.location.href = ${safeUrl};</script>`;
|
|
931
936
|
|
|
932
937
|
if (!res.headersSent) {
|
|
933
|
-
// SCENARIO A: Clean (Content-Type
|
|
934
|
-
res.setHeader("Content-Type", "
|
|
935
|
-
|
|
938
|
+
// SCENARIO A: Clean (Content-Type application/json)
|
|
939
|
+
res.setHeader("Content-Type", "application/json");
|
|
940
|
+
res.setHeader("X-Dinou-Redirect", err.url);
|
|
941
|
+
return res.status(200).json({ redirect: err.url });
|
|
936
942
|
} else {
|
|
937
|
-
// SCENARIO B: Dirty/Active Stream
|
|
938
|
-
// Write
|
|
939
|
-
res.write(
|
|
943
|
+
// SCENARIO B: Dirty/Active Stream
|
|
944
|
+
// Write a custom line-based stream command
|
|
945
|
+
res.write(`D:{"type":"redirect","url":${safeUrl}}\n`);
|
|
940
946
|
|
|
941
947
|
// ⚠️ IMPORTANT:
|
|
942
948
|
// 1. We close the response, since we redirected and there will be no RSC payload.
|
package/dinou/package.json
CHANGED