@ilha/router 0.9.2 → 0.10.0
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 +44 -27
- package/dist/head.d.ts +67 -0
- package/dist/http.d.ts +28 -0
- package/dist/index.d.ts +43 -97
- package/dist/index.js +2 -1
- package/dist/{plugin-BHuojFhQ.js → plugin-CmI3Brr2.js} +102 -115
- package/dist/plugin.d.ts +1 -1
- package/dist/route-match.d.ts +1 -1
- package/dist/{rspack.d.ts → rsbuild.d.ts} +2 -2
- package/dist/rsbuild.js +10 -0
- package/dist/server-island.d.ts +17 -2
- package/dist/server-island.js +13 -25
- package/dist/server-islands.d.ts +16 -2
- package/dist/snapshot-CsEaY6h_.js +337 -0
- package/dist/snapshot.d.ts +1 -0
- package/dist/{src-BBsbD5vU.js → src-B5dHU24f.js} +157 -361
- package/dist/ssr-BxrcUYy5.js +498 -0
- package/dist/ssr.d.ts +173 -0
- package/dist/ssr.js +2 -200
- package/dist/vite.js +1 -1
- package/package.json +7 -14
- package/dist/public-types.d.ts +0 -7
- package/dist/request-scope-C4reU4v0.js +0 -34
- package/dist/rolldown.d.ts +0 -6
- package/dist/rolldown.js +0 -10
- package/dist/rspack.js +0 -10
- package/dist/server-island-registry.d.ts +0 -122
- package/dist/server-island-registry.js +0 -189
package/dist/ssr.js
CHANGED
|
@@ -1,201 +1,3 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { FrameError, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, isTrustedOrigin, renderServerIsland } from "./server-island-registry.js";
|
|
1
|
+
import { C as setLoaderGuard, S as setFrameLoaderRunner, _ as readBodyBounded, a as __ilhaServerAction, b as setFrameAuth, c as frameEnvelope, d as getFrameLoaderRunner, f as getLoaderGuard, g as json, h as isTrustedOrigin, i as MAX_BODY, l as getFrameAuth, m as isSafeFramePath, n as FrameError, o as authorizeFrameRequest, p as getServerIslandEntry, r as LOADER_ENDPOINT, s as forwardIdentityHeaders, t as FRAME_ENDPOINT, u as getFrameGuard, v as registerServerIsland, w as ssr, x as setFrameGuard, y as renderServerIsland } from "./ssr-BxrcUYy5.js";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Production SSR endpoints for server-owned islands and regular-page loads.
|
|
7
|
-
*
|
|
8
|
-
* Default export is an oxidejs-style fetch middleware:
|
|
9
|
-
* `(request) => Response | undefined`. Returns `undefined` for any request it
|
|
10
|
-
* does not own, so hosts can chain it ahead of their own handler:
|
|
11
|
-
*
|
|
12
|
-
* ```ts
|
|
13
|
-
* oxide({ middleware: ["@ilha/router/ssr"] });
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Serves:
|
|
17
|
-
* - `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }` in,
|
|
18
|
-
* `{ html }` out). Renderers come from the process-global registry
|
|
19
|
-
* populated by self-registration code appended to `.server` modules.
|
|
20
|
-
* - `GET /__ilha/loader?path=…` — regular-page server loads via the loader
|
|
21
|
-
* runner (`setFrameLoaderRunner`, wired by the generated server module).
|
|
22
|
-
*/
|
|
23
|
-
const FRAME_ENDPOINT = "/__ilha/frame";
|
|
24
|
-
/** Regular-page server loads: served through the loader-runner slot. */
|
|
25
|
-
const LOADER_ENDPOINT = "/__ilha/loader";
|
|
26
|
-
/** Max request body size — matches the dev middleware cap. */
|
|
27
|
-
const MAX_BODY = 16384;
|
|
28
|
-
function json(status, body) {
|
|
29
|
-
return new Response(JSON.stringify(body), {
|
|
30
|
-
status,
|
|
31
|
-
headers: {
|
|
32
|
-
"cache-control": "no-store",
|
|
33
|
-
"content-type": "application/json;charset=utf-8"
|
|
34
|
-
}
|
|
35
|
-
});
|
|
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
|
-
}
|
|
62
|
-
async function ssr(request) {
|
|
63
|
-
let pathname;
|
|
64
|
-
try {
|
|
65
|
-
pathname = new URL(request.url).pathname;
|
|
66
|
-
} catch {
|
|
67
|
-
return json(400, { error: "frame failed" });
|
|
68
|
-
}
|
|
69
|
-
if (pathname !== "/__ilha/frame" && pathname !== "/__ilha/loader") return;
|
|
70
|
-
const auth = getFrameAuth();
|
|
71
|
-
if (!isTrustedOrigin(request, auth)) return json(403, { error: "frame failed" });
|
|
72
|
-
if (pathname === "/__ilha/loader") {
|
|
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" });
|
|
76
|
-
try {
|
|
77
|
-
const denied = await guard?.(request);
|
|
78
|
-
if (denied) return denied;
|
|
79
|
-
} catch {
|
|
80
|
-
return json(403, { error: "loader failed" });
|
|
81
|
-
}
|
|
82
|
-
const runner = getFrameLoaderRunner();
|
|
83
|
-
if (!runner) return json(404, {
|
|
84
|
-
kind: "error",
|
|
85
|
-
status: 404,
|
|
86
|
-
message: "not found"
|
|
87
|
-
});
|
|
88
|
-
const cl = request.headers.get("content-length");
|
|
89
|
-
if (cl && Number(cl) > 16384) return json(413, { error: "frame failed" });
|
|
90
|
-
let target = "/";
|
|
91
|
-
try {
|
|
92
|
-
target = new URL(request.url).searchParams.get("path") ?? "/";
|
|
93
|
-
} catch {
|
|
94
|
-
return json(400, {
|
|
95
|
-
kind: "error",
|
|
96
|
-
status: 400,
|
|
97
|
-
message: "bad request"
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (!target.startsWith("/") || target.includes("//") || target.includes("\\") || target.length > 2048) return json(400, {
|
|
101
|
-
kind: "error",
|
|
102
|
-
status: 400,
|
|
103
|
-
message: "bad request"
|
|
104
|
-
});
|
|
105
|
-
try {
|
|
106
|
-
const result = await runWithIslandRequest(request, () => runner(target, request));
|
|
107
|
-
if (result.kind === "redirect") return json(result.status || 302, {
|
|
108
|
-
kind: "redirect",
|
|
109
|
-
to: result.to,
|
|
110
|
-
status: result.status
|
|
111
|
-
});
|
|
112
|
-
if (result.kind !== "data") {
|
|
113
|
-
const status = result.status || 500;
|
|
114
|
-
return json(status, {
|
|
115
|
-
kind: result.kind,
|
|
116
|
-
status,
|
|
117
|
-
message: result.message
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
return json(200, result);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error("[ilha-router] loader endpoint failed:", error);
|
|
123
|
-
return json(500, {
|
|
124
|
-
kind: "error",
|
|
125
|
-
status: 500,
|
|
126
|
-
message: "loader failed"
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (request.method !== "POST") return json(405, { error: "frame failed" });
|
|
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" });
|
|
134
|
-
try {
|
|
135
|
-
const denied = await guard?.(request);
|
|
136
|
-
if (denied) return denied;
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.error("[ilha-router] frame guard failed:", error);
|
|
139
|
-
return json(403, { error: "frame failed" });
|
|
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
|
-
}
|
|
146
|
-
let id;
|
|
147
|
-
let path = "/";
|
|
148
|
-
try {
|
|
149
|
-
const text = await readBodyBounded(request, MAX_BODY);
|
|
150
|
-
if (text === null) return json(413, { error: "frame failed" });
|
|
151
|
-
const body = JSON.parse(text);
|
|
152
|
-
id = String(body.id ?? "");
|
|
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
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
return json(400, { error: "frame failed" });
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
let origin;
|
|
162
|
-
try {
|
|
163
|
-
origin = new URL(request.url).origin;
|
|
164
|
-
} catch {
|
|
165
|
-
return json(400, { error: "frame failed" });
|
|
166
|
-
}
|
|
167
|
-
const headers = new Headers();
|
|
168
|
-
for (const name of [
|
|
169
|
-
"cookie",
|
|
170
|
-
"authorization",
|
|
171
|
-
"user-agent"
|
|
172
|
-
]) {
|
|
173
|
-
const value = request.headers.get(name);
|
|
174
|
-
if (value !== null) headers.set(name, value);
|
|
175
|
-
}
|
|
176
|
-
const scoped = new Request(new URL(path, origin), {
|
|
177
|
-
method: "POST",
|
|
178
|
-
headers
|
|
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
|
-
}
|
|
186
|
-
return json(200, { html: await renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn))) });
|
|
187
|
-
} catch (error) {
|
|
188
|
-
if (error instanceof FrameError) {
|
|
189
|
-
if (error.redirect) return json(error.status, { redirect: error.redirect });
|
|
190
|
-
if (error.status >= 500) console.error("[ilha-router] frame render failed:", error);
|
|
191
|
-
return json(error.status, { error: "frame failed" });
|
|
192
|
-
}
|
|
193
|
-
console.error("[ilha-router] frame render failed:", error);
|
|
194
|
-
return json(400, { error: "frame failed" });
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/** Side-effect imports required alongside this handler. */
|
|
198
|
-
ssr.imports = ["ilha:pages/server", "ilha:loaders"];
|
|
199
|
-
|
|
200
|
-
//#endregion
|
|
201
|
-
export { FRAME_ENDPOINT, LOADER_ENDPOINT, MAX_BODY, ssr as default, json, readBodyBounded };
|
|
3
|
+
export { FRAME_ENDPOINT, FrameError, LOADER_ENDPOINT, MAX_BODY, __ilhaServerAction, authorizeFrameRequest, ssr as default, forwardIdentityHeaders, frameEnvelope, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, getServerIslandEntry, isSafeFramePath, isTrustedOrigin, json, readBodyBounded, registerServerIsland, renderServerIsland, setFrameAuth, setFrameGuard, setFrameLoaderRunner, setLoaderGuard };
|
package/dist/vite.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ilha/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A tiny SPA router for Ilha",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontend",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"islands",
|
|
10
10
|
"router",
|
|
11
11
|
"routing",
|
|
12
|
+
"rsbuild",
|
|
12
13
|
"spa",
|
|
13
14
|
"ssr",
|
|
14
15
|
"unplugin",
|
|
@@ -47,21 +48,13 @@
|
|
|
47
48
|
"types": "./dist/ssr.d.ts",
|
|
48
49
|
"import": "./dist/ssr.js"
|
|
49
50
|
},
|
|
50
|
-
"./server-island-registry": {
|
|
51
|
-
"types": "./dist/server-island-registry.d.ts",
|
|
52
|
-
"import": "./dist/server-island-registry.js"
|
|
53
|
-
},
|
|
54
51
|
"./vite": {
|
|
55
52
|
"types": "./dist/vite.d.ts",
|
|
56
53
|
"import": "./dist/vite.js"
|
|
57
54
|
},
|
|
58
|
-
"./
|
|
59
|
-
"types": "./dist/
|
|
60
|
-
"import": "./dist/
|
|
61
|
-
},
|
|
62
|
-
"./rolldown": {
|
|
63
|
-
"types": "./dist/rolldown.d.ts",
|
|
64
|
-
"import": "./dist/rolldown.js"
|
|
55
|
+
"./rsbuild": {
|
|
56
|
+
"types": "./dist/rsbuild.d.ts",
|
|
57
|
+
"import": "./dist/rsbuild.js"
|
|
65
58
|
}
|
|
66
59
|
},
|
|
67
60
|
"publishConfig": {
|
|
@@ -76,10 +69,10 @@
|
|
|
76
69
|
"unplugin": "3.3.0"
|
|
77
70
|
},
|
|
78
71
|
"devDependencies": {
|
|
79
|
-
"ilha": "0.
|
|
72
|
+
"ilha": "0.12.0",
|
|
80
73
|
"vite": "^8.2.2"
|
|
81
74
|
},
|
|
82
75
|
"peerDependencies": {
|
|
83
|
-
"ilha": ">=0.
|
|
76
|
+
"ilha": ">=0.12.0"
|
|
84
77
|
}
|
|
85
78
|
}
|
package/dist/public-types.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compile-time type anchors for the router API. Every imperative call lives
|
|
3
|
-
* inside this never-invoked function so module scope has NO side effects:
|
|
4
|
-
* `router()` resets the module-global route registry, which must not happen
|
|
5
|
-
* at import time (tsconfig.build emits declarations for these anchors only).
|
|
6
|
-
*/
|
|
7
|
-
export declare function typecheckRouterApi(): void;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
|
|
3
|
-
//#region src/request-scope.ts
|
|
4
|
-
/**
|
|
5
|
-
* Request scope for server-owned island rendering.
|
|
6
|
-
*
|
|
7
|
-
* A `.server.tsx` island's render function always executes on the server —
|
|
8
|
-
* page SSR through the router, or streamed frames through the plugin's
|
|
9
|
-
* `/__ilha/frame` endpoint. Both seed this scope with the originating
|
|
10
|
-
* `Request`, so render functions can read request data (URL, headers,
|
|
11
|
-
* cookies) through `useContext().request` or a host integration such as Oxide's `useRequest()`.
|
|
12
|
-
*
|
|
13
|
-
* The storage lives on `globalThis` under `ilha.requestAls` so every module
|
|
14
|
-
* copy (plugin bundle, SSR graph) shares one instance. The public accessor
|
|
15
|
-
* is `useContext()` from the main `@ilha/router` entry, which reads the
|
|
16
|
-
* storage without importing `node:async_hooks`; this node-only module is the
|
|
17
|
-
* sole place that constructs it.
|
|
18
|
-
*/
|
|
19
|
-
const REQUEST_ALS_KEY = Symbol.for("ilha.requestAls");
|
|
20
|
-
/** Installed by oxidejs when its module loads. Lets `useRequest()` resolve
|
|
21
|
-
* inside island renders and frames, not just `/__oxide/action`. */
|
|
22
|
-
const OXIDE_RUN_WITH_REQUEST = Symbol.for("oxidejs.runWithRequest");
|
|
23
|
-
/** Run `fn` with `request` available to `useContext().request`. When oxidejs
|
|
24
|
-
* is loaded, its action scope is entered too, so `useRequest()` works in
|
|
25
|
-
* island renders and streamed frames. */
|
|
26
|
-
function runWithIslandRequest(request, fn) {
|
|
27
|
-
const g = globalThis;
|
|
28
|
-
const als = g[REQUEST_ALS_KEY] ??= new AsyncLocalStorage();
|
|
29
|
-
const oxide = g[OXIDE_RUN_WITH_REQUEST];
|
|
30
|
-
return als.run(request, () => oxide ? oxide(request, fn) : fn());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
//#endregion
|
|
34
|
-
export { runWithIslandRequest as t };
|
package/dist/rolldown.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export type { LayoutHandler, ErrorHandler, RouteSnapshot, AppError } from "./index";
|
|
2
|
-
export { ilhaPages, type IlhaPagesOptions } from "./plugin";
|
|
3
|
-
import { type IlhaPagesOptions } from "./plugin";
|
|
4
|
-
/** Rolldown plugin — use via `@ilha/router/rolldown`. */
|
|
5
|
-
export declare function pages(options?: IlhaPagesOptions): import("rolldown").Plugin<any> | import("rolldown").Plugin<any>[];
|
|
6
|
-
export default pages;
|
package/dist/rolldown.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { t as ilhaPages } from "./plugin-BHuojFhQ.js";
|
|
2
|
-
|
|
3
|
-
//#region src/rolldown.ts
|
|
4
|
-
/** Rolldown plugin — use via `@ilha/router/rolldown`. */
|
|
5
|
-
function pages(options = {}) {
|
|
6
|
-
return ilhaPages.rolldown(options);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
//#endregion
|
|
10
|
-
export { pages as default, pages, ilhaPages };
|
package/dist/rspack.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { t as ilhaPages } from "./plugin-BHuojFhQ.js";
|
|
2
|
-
|
|
3
|
-
//#region src/rspack.ts
|
|
4
|
-
/** Rspack plugin — use via `@ilha/router/rspack`. */
|
|
5
|
-
function pages(options = {}) {
|
|
6
|
-
return ilhaPages.rspack(options);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
//#endregion
|
|
10
|
-
export { pages as default, pages, ilhaPages };
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Process-global registry of server-island renderers, keyed by the public
|
|
3
|
-
* island id (`sha256(file#name)`, see `serverIslandPublicId`). Lives on
|
|
4
|
-
* `globalThis` so every module copy (plugin bundle, SSR graph, frame entry)
|
|
5
|
-
* shares one instance — same pattern as `request-scope.ts`.
|
|
6
|
-
*
|
|
7
|
-
* `.server` modules self-register when the plugin appends registration code
|
|
8
|
-
* to their server-graph copy; the production `/__ilha/frame` handler (see
|
|
9
|
-
* `@ilha/router/frame`) consumes the registry to re-render an island from a
|
|
10
|
-
* client state snapshot. Server pages additionally register their `load` and
|
|
11
|
-
* route pattern so frame handlers can run the loader with matched params.
|
|
12
|
-
*/
|
|
13
|
-
/** Loader context for server-page `load` — mirrors the router's shape. */
|
|
14
|
-
export interface FrameLoaderContext {
|
|
15
|
-
params: Record<string, string>;
|
|
16
|
-
request: Request;
|
|
17
|
-
url: URL;
|
|
18
|
-
signal: AbortSignal;
|
|
19
|
-
}
|
|
20
|
-
export type ServerPageLoader = (ctx: FrameLoaderContext) => unknown;
|
|
21
|
-
/** A frame render: optionally preceded by running the page's `load`. */
|
|
22
|
-
export interface ServerIslandEntry {
|
|
23
|
-
/** Returns the renderState fn (`Symbol.for("ilha.renderState")` getter). */
|
|
24
|
-
render: () => unknown;
|
|
25
|
-
/** The module's `load` export — runs at frame time; its return value
|
|
26
|
-
* becomes the island's render props. */
|
|
27
|
-
load?: ServerPageLoader;
|
|
28
|
-
/** Route pattern for the page (`/user/:id`) — matches params for `load`. */
|
|
29
|
-
pattern?: string;
|
|
30
|
-
}
|
|
31
|
-
export type FrameGuard = (request: Request) => Response | void | Promise<Response | void>;
|
|
32
|
-
/**
|
|
33
|
-
* Install a guard consulted by every `/__ilha/frame` request (dev middleware
|
|
34
|
-
* and the production `@ilha/router/frame` handler share this slot — both read
|
|
35
|
-
* it from `globalThis`). Return a `Response` to reject; return nothing to
|
|
36
|
-
* allow. Island state is world-readable through frames unless you gate them,
|
|
37
|
-
* so apps serving private data should install a session check here.
|
|
38
|
-
*/
|
|
39
|
-
export declare function setFrameGuard(guard: FrameGuard): void;
|
|
40
|
-
export declare function getFrameGuard(): FrameGuard | undefined;
|
|
41
|
-
/**
|
|
42
|
-
* Install a guard consulted only by `GET /__ilha/loader`. When absent, the
|
|
43
|
-
* loader endpoint falls back to `getFrameGuard()` for backwards compatibility.
|
|
44
|
-
* Prefer a dedicated loader guard so gating the loader endpoint is independent
|
|
45
|
-
* of frame rendering.
|
|
46
|
-
*/
|
|
47
|
-
export declare function setLoaderGuard(guard: FrameGuard): void;
|
|
48
|
-
export declare function getLoaderGuard(): FrameGuard | undefined;
|
|
49
|
-
/** Frame-authorization policy, installed via {@link setFrameAuth}. */
|
|
50
|
-
export interface FrameAuthPolicy {
|
|
51
|
-
/**
|
|
52
|
-
* Action taken when no frame guard is registered. `"deny"` (default in the
|
|
53
|
-
* production handler) rejects every `/__ilha/frame` request with 403;
|
|
54
|
-
* `"open"` preserves the legacy unauthenticated behavior. The dev
|
|
55
|
-
* middleware stays permissive unless a guard is registered.
|
|
56
|
-
*/
|
|
57
|
-
defaultAction?: "open" | "deny";
|
|
58
|
-
/**
|
|
59
|
-
* Explicit trusted origins (e.g. `"https://app.example.com"`). When set,
|
|
60
|
-
* origin checks accept only these; otherwise the check compares the `Origin`
|
|
61
|
-
* header against `https://{host}` / `http://{host}`.
|
|
62
|
-
*/
|
|
63
|
-
trustedOrigins?: string[];
|
|
64
|
-
/**
|
|
65
|
-
* Optional CSRF verifier for the state-changing frame POST. Receives the
|
|
66
|
-
* original `Request`; returning falsy rejects the request. Use this for
|
|
67
|
-
* server-to-server frame callers that have no browser `Origin`.
|
|
68
|
-
*/
|
|
69
|
-
csrf?: (request: Request) => boolean | Promise<boolean>;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Install the frame-authorization policy consumed by the production
|
|
73
|
-
* `@ilha/router/ssr` handler. `trustedOrigins` and `csrf` are also applied by
|
|
74
|
-
* the dev middleware (via `IlhaPagesOptions`).
|
|
75
|
-
*/
|
|
76
|
-
export declare function setFrameAuth(policy: FrameAuthPolicy): void;
|
|
77
|
-
export declare function getFrameAuth(): FrameAuthPolicy | undefined;
|
|
78
|
-
/**
|
|
79
|
-
* Same-origin check for frame/loader requests. Browsers always send `Origin`
|
|
80
|
-
* on cross-origin and same-origin `POST`; its absence implies a non-browser
|
|
81
|
-
* caller (allowed — gate those via a guard or `csrf`). When `Origin` is
|
|
82
|
-
* present it must match the configured trusted origins, else the request's
|
|
83
|
-
* own `Host`.
|
|
84
|
-
*/
|
|
85
|
-
export declare function isTrustedOrigin(request: Request, policy: FrameAuthPolicy | undefined): boolean;
|
|
86
|
-
export type FrameLoaderRunner = (path: string, request?: Request) => Promise<{
|
|
87
|
-
kind: string;
|
|
88
|
-
data?: unknown;
|
|
89
|
-
headEntries?: unknown;
|
|
90
|
-
status?: number;
|
|
91
|
-
to?: string;
|
|
92
|
-
message?: string;
|
|
93
|
-
}>;
|
|
94
|
-
/**
|
|
95
|
-
* Install the handler backing `GET /__ilha/loader` in production. The
|
|
96
|
-
* generated `pages.server.ts` wires this to `pageRouter.runLoader`, so
|
|
97
|
-
* regular-page server loads get full route matching, layout chains, and
|
|
98
|
-
* redirect/error semantics. Dev and prod handlers share the slot.
|
|
99
|
-
*/
|
|
100
|
-
export declare function setFrameLoaderRunner(runner: FrameLoaderRunner): void;
|
|
101
|
-
export declare function getFrameLoaderRunner(): FrameLoaderRunner | undefined;
|
|
102
|
-
/** Register `id` → entry. Later registrations win (id encodes file + name). */
|
|
103
|
-
export declare function registerServerIsland(id: string, render: () => unknown, options?: {
|
|
104
|
-
load?: ServerPageLoader;
|
|
105
|
-
pattern?: string;
|
|
106
|
-
}): void;
|
|
107
|
-
export declare function getServerIslandEntry(id: string): ServerIslandEntry | undefined;
|
|
108
|
-
/** Back-compat alias used by tests. */
|
|
109
|
-
export declare const getServerIslandRenderer: typeof getServerIslandEntry;
|
|
110
|
-
/** Client-facing frame failure. `redirect` carries a loader redirect target. */
|
|
111
|
-
export declare class FrameError extends Error {
|
|
112
|
-
status: number;
|
|
113
|
-
redirect?: string;
|
|
114
|
-
constructor(status: number, message: string, redirect?: string);
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Shared tail of every frame request: run the page's `load` when registered
|
|
118
|
-
* (params matched from the frame path), then invoke the renderer inside the
|
|
119
|
-
* caller's scope. Throws `FrameError` with an HTTP status for client-facing
|
|
120
|
-
* failures; loader redirects surface via `FrameError.redirect`.
|
|
121
|
-
*/
|
|
122
|
-
export declare function renderServerIsland(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>): Promise<string>;
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { F as parsePattern, I as safeDecode, P as matchSegments, S as resolveRedirectTarget } from "./src-BBsbD5vU.js";
|
|
2
|
-
|
|
3
|
-
//#region src/server-island-registry.ts
|
|
4
|
-
/**
|
|
5
|
-
* Process-global registry of server-island renderers, keyed by the public
|
|
6
|
-
* island id (`sha256(file#name)`, see `serverIslandPublicId`). Lives on
|
|
7
|
-
* `globalThis` so every module copy (plugin bundle, SSR graph, frame entry)
|
|
8
|
-
* shares one instance — same pattern as `request-scope.ts`.
|
|
9
|
-
*
|
|
10
|
-
* `.server` modules self-register when the plugin appends registration code
|
|
11
|
-
* to their server-graph copy; the production `/__ilha/frame` handler (see
|
|
12
|
-
* `@ilha/router/frame`) consumes the registry to re-render an island from a
|
|
13
|
-
* client state snapshot. Server pages additionally register their `load` and
|
|
14
|
-
* route pattern so frame handlers can run the loader with matched params.
|
|
15
|
-
*/
|
|
16
|
-
const REGISTRY_KEY = Symbol.for("ilha.serverIslandRenderers");
|
|
17
|
-
function registry() {
|
|
18
|
-
const g = globalThis;
|
|
19
|
-
let map = g[REGISTRY_KEY];
|
|
20
|
-
if (!map) {
|
|
21
|
-
map = /* @__PURE__ */ new Map();
|
|
22
|
-
g[REGISTRY_KEY] = map;
|
|
23
|
-
}
|
|
24
|
-
return map;
|
|
25
|
-
}
|
|
26
|
-
const GUARD_KEY = Symbol.for("ilha.frameGuard");
|
|
27
|
-
/**
|
|
28
|
-
* Install a guard consulted by every `/__ilha/frame` request (dev middleware
|
|
29
|
-
* and the production `@ilha/router/frame` handler share this slot — both read
|
|
30
|
-
* it from `globalThis`). Return a `Response` to reject; return nothing to
|
|
31
|
-
* allow. Island state is world-readable through frames unless you gate them,
|
|
32
|
-
* so apps serving private data should install a session check here.
|
|
33
|
-
*/
|
|
34
|
-
function setFrameGuard(guard) {
|
|
35
|
-
const g = globalThis;
|
|
36
|
-
g[GUARD_KEY] = guard;
|
|
37
|
-
}
|
|
38
|
-
function getFrameGuard() {
|
|
39
|
-
return globalThis[GUARD_KEY];
|
|
40
|
-
}
|
|
41
|
-
const LOADER_GUARD_KEY = Symbol.for("ilha.loaderGuard");
|
|
42
|
-
/**
|
|
43
|
-
* Install a guard consulted only by `GET /__ilha/loader`. When absent, the
|
|
44
|
-
* loader endpoint falls back to `getFrameGuard()` for backwards compatibility.
|
|
45
|
-
* Prefer a dedicated loader guard so gating the loader endpoint is independent
|
|
46
|
-
* of frame rendering.
|
|
47
|
-
*/
|
|
48
|
-
function setLoaderGuard(guard) {
|
|
49
|
-
const g = globalThis;
|
|
50
|
-
g[LOADER_GUARD_KEY] = guard;
|
|
51
|
-
}
|
|
52
|
-
function getLoaderGuard() {
|
|
53
|
-
return globalThis[LOADER_GUARD_KEY];
|
|
54
|
-
}
|
|
55
|
-
const AUTH_KEY = Symbol.for("ilha.frameAuth");
|
|
56
|
-
/**
|
|
57
|
-
* Install the frame-authorization policy consumed by the production
|
|
58
|
-
* `@ilha/router/ssr` handler. `trustedOrigins` and `csrf` are also applied by
|
|
59
|
-
* the dev middleware (via `IlhaPagesOptions`).
|
|
60
|
-
*/
|
|
61
|
-
function setFrameAuth(policy) {
|
|
62
|
-
const g = globalThis;
|
|
63
|
-
g[AUTH_KEY] = policy;
|
|
64
|
-
}
|
|
65
|
-
function getFrameAuth() {
|
|
66
|
-
return globalThis[AUTH_KEY];
|
|
67
|
-
}
|
|
68
|
-
function normalizeOrigin(value) {
|
|
69
|
-
try {
|
|
70
|
-
return new URL(value).origin;
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Same-origin check for frame/loader requests. Browsers always send `Origin`
|
|
77
|
-
* on cross-origin and same-origin `POST`; its absence implies a non-browser
|
|
78
|
-
* caller (allowed — gate those via a guard or `csrf`). When `Origin` is
|
|
79
|
-
* present it must match the configured trusted origins, else the request's
|
|
80
|
-
* own `Host`.
|
|
81
|
-
*/
|
|
82
|
-
function isTrustedOrigin(request, policy) {
|
|
83
|
-
const originHeader = request.headers.get("origin");
|
|
84
|
-
if (originHeader === null) return true;
|
|
85
|
-
const origin = normalizeOrigin(originHeader);
|
|
86
|
-
if (origin === null) return false;
|
|
87
|
-
const trusted = policy?.trustedOrigins ?? [];
|
|
88
|
-
if (trusted.length > 0) return trusted.some((o) => normalizeOrigin(o) === origin);
|
|
89
|
-
const host = request.headers.get("host");
|
|
90
|
-
if (!host) return false;
|
|
91
|
-
return origin === `https://${host}` || origin === `http://${host}`;
|
|
92
|
-
}
|
|
93
|
-
const LOADER_RUNNER_KEY = Symbol.for("ilha.frameLoaderRunner");
|
|
94
|
-
/**
|
|
95
|
-
* Install the handler backing `GET /__ilha/loader` in production. The
|
|
96
|
-
* generated `pages.server.ts` wires this to `pageRouter.runLoader`, so
|
|
97
|
-
* regular-page server loads get full route matching, layout chains, and
|
|
98
|
-
* redirect/error semantics. Dev and prod handlers share the slot.
|
|
99
|
-
*/
|
|
100
|
-
function setFrameLoaderRunner(runner) {
|
|
101
|
-
const g = globalThis;
|
|
102
|
-
g[LOADER_RUNNER_KEY] = runner;
|
|
103
|
-
}
|
|
104
|
-
function getFrameLoaderRunner() {
|
|
105
|
-
return globalThis[LOADER_RUNNER_KEY];
|
|
106
|
-
}
|
|
107
|
-
/** Register `id` → entry. Later registrations win (id encodes file + name). */
|
|
108
|
-
function registerServerIsland(id, render, options) {
|
|
109
|
-
registry().set(id, {
|
|
110
|
-
render,
|
|
111
|
-
load: options?.load,
|
|
112
|
-
pattern: options?.pattern
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
function getServerIslandEntry(id) {
|
|
116
|
-
return registry().get(id);
|
|
117
|
-
}
|
|
118
|
-
/** Back-compat alias used by tests. */
|
|
119
|
-
const getServerIslandRenderer = getServerIslandEntry;
|
|
120
|
-
/** Client-facing frame failure. `redirect` carries a loader redirect target. */
|
|
121
|
-
var FrameError = class extends Error {
|
|
122
|
-
status;
|
|
123
|
-
redirect;
|
|
124
|
-
constructor(status, message, redirect) {
|
|
125
|
-
super(message);
|
|
126
|
-
this.status = status;
|
|
127
|
-
this.redirect = redirect;
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
/** Match a route pattern (`/user/:id`, `/docs/**:slug`) against a pathname.
|
|
131
|
-
* Returns decoded params, or null when the path doesn't match. Shares the
|
|
132
|
-
* router's matcher semantics via `route-match.ts`. */
|
|
133
|
-
function matchPatternParams(pattern, pathname) {
|
|
134
|
-
const raw = matchSegments(parsePattern(pattern).segments, pathname);
|
|
135
|
-
if (!raw) return null;
|
|
136
|
-
const params = {};
|
|
137
|
-
for (const [k, v] of Object.entries(raw)) params[k] = safeDecode(v);
|
|
138
|
-
return params;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Shared tail of every frame request: run the page's `load` when registered
|
|
142
|
-
* (params matched from the frame path), then invoke the renderer inside the
|
|
143
|
-
* caller's scope. Throws `FrameError` with an HTTP status for client-facing
|
|
144
|
-
* failures; loader redirects surface via `FrameError.redirect`.
|
|
145
|
-
*/
|
|
146
|
-
async function renderServerIsland(id, request, runWithScope) {
|
|
147
|
-
const entry = registry().get(id);
|
|
148
|
-
if (!entry) throw new FrameError(400, "unknown island");
|
|
149
|
-
let props;
|
|
150
|
-
if (entry.load) {
|
|
151
|
-
let url;
|
|
152
|
-
try {
|
|
153
|
-
url = new URL(request.url);
|
|
154
|
-
} catch {
|
|
155
|
-
throw new FrameError(400, "frame failed");
|
|
156
|
-
}
|
|
157
|
-
const params = entry.pattern ? matchPatternParams(entry.pattern, url.pathname) : {};
|
|
158
|
-
if (!params) throw new FrameError(400, "frame failed");
|
|
159
|
-
try {
|
|
160
|
-
props = { load: {
|
|
161
|
-
loading: false,
|
|
162
|
-
value: await entry.load({
|
|
163
|
-
params,
|
|
164
|
-
request,
|
|
165
|
-
url,
|
|
166
|
-
signal: request.signal
|
|
167
|
-
}) ?? {},
|
|
168
|
-
error: void 0
|
|
169
|
-
} };
|
|
170
|
-
} catch (error) {
|
|
171
|
-
const marker = error;
|
|
172
|
-
if (marker.__ilhaRedirect === true) {
|
|
173
|
-
const r = error;
|
|
174
|
-
const safe = resolveRedirectTarget(r.to, url, false);
|
|
175
|
-
if (!safe.ok) throw new FrameError(500, "unsafe redirect target");
|
|
176
|
-
throw new FrameError(r.status || 302, "frame failed", safe.to);
|
|
177
|
-
}
|
|
178
|
-
if (marker.__ilhaLoaderError === true) throw new FrameError(error.status || 500, "frame failed");
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const render = entry.render();
|
|
183
|
-
if (typeof render !== "function") throw new FrameError(400, "unknown island");
|
|
184
|
-
const html = await runWithScope(request, () => render(props));
|
|
185
|
-
return String(html);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
//#endregion
|
|
189
|
-
export { FrameError, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, getServerIslandEntry, getServerIslandRenderer, isTrustedOrigin, registerServerIsland, renderServerIsland, setFrameAuth, setFrameGuard, setFrameLoaderRunner, setLoaderGuard };
|