@ilha/router 0.9.0 → 0.9.2
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/README.md +26 -6
- package/dist/index.d.ts +52 -3
- package/dist/index.js +2 -2231
- package/dist/{plugin-DPGzrIVj.js → plugin-BHuojFhQ.js} +37 -19
- package/dist/plugin.d.ts +17 -0
- package/dist/public-types.d.ts +7 -0
- package/dist/{request-scope-D6_4rqMb.js → request-scope-C4reU4v0.js} +10 -3
- package/dist/request-scope.d.ts +4 -2
- package/dist/rolldown.d.ts +1 -1
- package/dist/rolldown.js +1 -1
- package/dist/route-match.d.ts +22 -0
- package/dist/rspack.js +1 -1
- package/dist/server-island-registry.d.ts +46 -1
- package/dist/server-island-registry.js +86 -27
- package/dist/src-BBsbD5vU.js +2365 -0
- package/dist/ssr.d.ts +9 -0
- package/dist/ssr.js +64 -17
- package/dist/vite.js +1 -1
- package/package.json +3 -3
package/dist/ssr.d.ts
CHANGED
|
@@ -19,5 +19,14 @@
|
|
|
19
19
|
export declare const FRAME_ENDPOINT = "/__ilha/frame";
|
|
20
20
|
/** Regular-page server loads: served through the loader-runner slot. */
|
|
21
21
|
export declare const LOADER_ENDPOINT = "/__ilha/loader";
|
|
22
|
+
/** Max request body size — matches the dev middleware cap. */
|
|
23
|
+
export declare const MAX_BODY: number;
|
|
24
|
+
export declare function json(status: number, body: Record<string, unknown>): Response;
|
|
25
|
+
/**
|
|
26
|
+
* Read a request body as UTF-8, streaming it with a hard byte cap. Returns
|
|
27
|
+
* `null` when the body exceeds `maxBytes` (the reader is cancelled before the
|
|
28
|
+
* cap is far exceeded) or when decoding fails.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readBodyBounded(request: Request, maxBytes: number): Promise<string | null>;
|
|
22
31
|
declare function ssr(request: Request): Promise<Response | undefined>;
|
|
23
32
|
export default ssr;
|
package/dist/ssr.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as runWithIslandRequest } from "./request-scope-
|
|
2
|
-
import { FrameError, getFrameGuard, getFrameLoaderRunner, renderServerIsland } from "./server-island-registry.js";
|
|
1
|
+
import { t as runWithIslandRequest } from "./request-scope-C4reU4v0.js";
|
|
2
|
+
import { FrameError, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, isTrustedOrigin, renderServerIsland } from "./server-island-registry.js";
|
|
3
3
|
|
|
4
4
|
//#region src/ssr.ts
|
|
5
5
|
/**
|
|
@@ -34,6 +34,31 @@ function json(status, body) {
|
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Read a request body as UTF-8, streaming it with a hard byte cap. Returns
|
|
39
|
+
* `null` when the body exceeds `maxBytes` (the reader is cancelled before the
|
|
40
|
+
* cap is far exceeded) or when decoding fails.
|
|
41
|
+
*/
|
|
42
|
+
async function readBodyBounded(request, maxBytes) {
|
|
43
|
+
const contentLength = request.headers.get("content-length");
|
|
44
|
+
if (contentLength !== null && Number(contentLength) > maxBytes) return null;
|
|
45
|
+
const reader = request.body?.getReader();
|
|
46
|
+
if (!reader) return "";
|
|
47
|
+
const chunks = [];
|
|
48
|
+
let size = 0;
|
|
49
|
+
for (;;) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done) break;
|
|
52
|
+
size += value?.byteLength ?? 0;
|
|
53
|
+
if (size > maxBytes) {
|
|
54
|
+
await reader.cancel().catch(() => {});
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
chunks.push(value);
|
|
58
|
+
}
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
return chunks.map((c) => decoder.decode(c, { stream: true })).join("") + decoder.decode();
|
|
61
|
+
}
|
|
37
62
|
async function ssr(request) {
|
|
38
63
|
let pathname;
|
|
39
64
|
try {
|
|
@@ -42,13 +67,14 @@ async function ssr(request) {
|
|
|
42
67
|
return json(400, { error: "frame failed" });
|
|
43
68
|
}
|
|
44
69
|
if (pathname !== "/__ilha/frame" && pathname !== "/__ilha/loader") return;
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
if (origin && host && origin !== `http://${host}` && origin !== `https://${host}`) return json(403, { error: "frame failed" });
|
|
70
|
+
const auth = getFrameAuth();
|
|
71
|
+
if (!isTrustedOrigin(request, auth)) return json(403, { error: "frame failed" });
|
|
48
72
|
if (pathname === "/__ilha/loader") {
|
|
49
73
|
if (request.method !== "GET") return json(405, { error: "method not allowed" });
|
|
74
|
+
const guard = getLoaderGuard() ?? getFrameGuard();
|
|
75
|
+
if (!guard && (auth?.defaultAction ?? "deny") === "deny") return json(403, { error: "loader failed" });
|
|
50
76
|
try {
|
|
51
|
-
const denied = await
|
|
77
|
+
const denied = await guard?.(request);
|
|
52
78
|
if (denied) return denied;
|
|
53
79
|
} catch {
|
|
54
80
|
return json(403, { error: "loader failed" });
|
|
@@ -60,7 +86,7 @@ async function ssr(request) {
|
|
|
60
86
|
message: "not found"
|
|
61
87
|
});
|
|
62
88
|
const cl = request.headers.get("content-length");
|
|
63
|
-
if (cl && Number(cl) >
|
|
89
|
+
if (cl && Number(cl) > 16384) return json(413, { error: "frame failed" });
|
|
64
90
|
let target = "/";
|
|
65
91
|
try {
|
|
66
92
|
target = new URL(request.url).searchParams.get("path") ?? "/";
|
|
@@ -71,13 +97,13 @@ async function ssr(request) {
|
|
|
71
97
|
message: "bad request"
|
|
72
98
|
});
|
|
73
99
|
}
|
|
74
|
-
if (!target.startsWith("/") || target.includes("//") || target.length > 2048) return json(400, {
|
|
100
|
+
if (!target.startsWith("/") || target.includes("//") || target.includes("\\") || target.length > 2048) return json(400, {
|
|
75
101
|
kind: "error",
|
|
76
102
|
status: 400,
|
|
77
103
|
message: "bad request"
|
|
78
104
|
});
|
|
79
105
|
try {
|
|
80
|
-
const result = await runner(target);
|
|
106
|
+
const result = await runWithIslandRequest(request, () => runner(target, request));
|
|
81
107
|
if (result.kind === "redirect") return json(result.status || 302, {
|
|
82
108
|
kind: "redirect",
|
|
83
109
|
to: result.to,
|
|
@@ -103,39 +129,60 @@ async function ssr(request) {
|
|
|
103
129
|
}
|
|
104
130
|
if (request.method !== "POST") return json(405, { error: "frame failed" });
|
|
105
131
|
if (!(request.headers.get("content-type") ?? "").startsWith("application/json")) return json(415, { error: "frame failed" });
|
|
132
|
+
const guard = getFrameGuard();
|
|
133
|
+
if (!guard && (auth?.defaultAction ?? "deny") === "deny") return json(403, { error: "frame failed" });
|
|
106
134
|
try {
|
|
107
|
-
const denied = await
|
|
135
|
+
const denied = await guard?.(request);
|
|
108
136
|
if (denied) return denied;
|
|
109
137
|
} catch (error) {
|
|
110
138
|
console.error("[ilha-router] frame guard failed:", error);
|
|
111
139
|
return json(403, { error: "frame failed" });
|
|
112
140
|
}
|
|
141
|
+
if (auth?.csrf) try {
|
|
142
|
+
if (!await auth.csrf(request)) return json(403, { error: "frame failed" });
|
|
143
|
+
} catch {
|
|
144
|
+
return json(403, { error: "frame failed" });
|
|
145
|
+
}
|
|
113
146
|
let id;
|
|
114
147
|
let path = "/";
|
|
115
148
|
try {
|
|
116
|
-
const text = await request
|
|
117
|
-
if (text
|
|
149
|
+
const text = await readBodyBounded(request, MAX_BODY);
|
|
150
|
+
if (text === null) return json(413, { error: "frame failed" });
|
|
118
151
|
const body = JSON.parse(text);
|
|
119
152
|
id = String(body.id ?? "");
|
|
120
|
-
if (typeof body.path === "string"
|
|
153
|
+
if (typeof body.path === "string") {
|
|
154
|
+
if (body.path.startsWith("/") && !body.path.includes("//") && !body.path.includes("\\") && body.path.length <= 2048) path = body.path;
|
|
155
|
+
else return json(400, { error: "frame failed" });
|
|
156
|
+
}
|
|
121
157
|
} catch {
|
|
122
158
|
return json(400, { error: "frame failed" });
|
|
123
159
|
}
|
|
124
160
|
try {
|
|
161
|
+
let origin;
|
|
162
|
+
try {
|
|
163
|
+
origin = new URL(request.url).origin;
|
|
164
|
+
} catch {
|
|
165
|
+
return json(400, { error: "frame failed" });
|
|
166
|
+
}
|
|
125
167
|
const headers = new Headers();
|
|
126
168
|
for (const name of [
|
|
127
169
|
"cookie",
|
|
128
170
|
"authorization",
|
|
129
|
-
"user-agent"
|
|
130
|
-
"x-forwarded-for"
|
|
171
|
+
"user-agent"
|
|
131
172
|
]) {
|
|
132
173
|
const value = request.headers.get(name);
|
|
133
174
|
if (value !== null) headers.set(name, value);
|
|
134
175
|
}
|
|
135
|
-
const scoped = new Request(new URL(path,
|
|
176
|
+
const scoped = new Request(new URL(path, origin), {
|
|
136
177
|
method: "POST",
|
|
137
178
|
headers
|
|
138
179
|
});
|
|
180
|
+
for (const sym of Object.getOwnPropertySymbols(request)) {
|
|
181
|
+
if (Symbol.keyFor(sym) === void 0) continue;
|
|
182
|
+
try {
|
|
183
|
+
scoped[sym] = request[sym];
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
139
186
|
return json(200, { html: await renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn))) });
|
|
140
187
|
} catch (error) {
|
|
141
188
|
if (error instanceof FrameError) {
|
|
@@ -151,4 +198,4 @@ async function ssr(request) {
|
|
|
151
198
|
ssr.imports = ["ilha:pages/server", "ilha:loaders"];
|
|
152
199
|
|
|
153
200
|
//#endregion
|
|
154
|
-
export { FRAME_ENDPOINT, LOADER_ENDPOINT, ssr as default };
|
|
201
|
+
export { FRAME_ENDPOINT, LOADER_ENDPOINT, MAX_BODY, ssr as default, json, readBodyBounded };
|
package/dist/vite.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ilha/router",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "A tiny SPA router for Ilha",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontend",
|
|
@@ -76,10 +76,10 @@
|
|
|
76
76
|
"unplugin": "3.3.0"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"ilha": "0.11.
|
|
79
|
+
"ilha": "0.11.1",
|
|
80
80
|
"vite": "^8.2.2"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"ilha": ">=0.11.
|
|
83
|
+
"ilha": ">=0.11.1"
|
|
84
84
|
}
|
|
85
85
|
}
|