@ilha/router 0.9.1 → 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/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
1
  import { t as runWithIslandRequest } from "./request-scope-C4reU4v0.js";
2
- import { FrameError, getFrameGuard, getFrameLoaderRunner, renderServerIsland } from "./server-island-registry.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 origin = request.headers.get("origin");
46
- const host = request.headers.get("host");
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 getFrameGuard()?.(request);
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) > MAX_BODY) return json(413, { error: "frame failed" });
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,36 +129,51 @@ 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 getFrameGuard()?.(request);
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.text();
117
- if (text.length > MAX_BODY) return json(413, { error: "frame failed" });
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" && body.path.startsWith("/") && !body.path.includes("//") && body.path.length <= 2048) path = body.path;
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, `http://${host ?? "localhost"}`), {
176
+ const scoped = new Request(new URL(path, origin), {
136
177
  method: "POST",
137
178
  headers
138
179
  });
@@ -157,4 +198,4 @@ async function ssr(request) {
157
198
  ssr.imports = ["ilha:pages/server", "ilha:loaders"];
158
199
 
159
200
  //#endregion
160
- 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
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-DogkskcY.js";
1
+ import { t as ilhaPages } from "./plugin-BHuojFhQ.js";
2
2
 
3
3
  //#region src/vite.ts
4
4
  /** Vite plugin — use via `@ilha/router/vite`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.9.1",
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.0",
79
+ "ilha": "0.11.1",
80
80
  "vite": "^8.2.2"
81
81
  },
82
82
  "peerDependencies": {
83
- "ilha": ">=0.11.0"
83
+ "ilha": ">=0.11.1"
84
84
  }
85
85
  }