@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as runWithIslandRequest } from "./request-scope-
|
|
2
|
-
import { FrameError, getFrameGuard, renderServerIsland, setFrameGuard } from "./server-island-registry.js";
|
|
1
|
+
import { t as runWithIslandRequest } from "./request-scope-C4reU4v0.js";
|
|
2
|
+
import { FrameError, getFrameAuth, getFrameGuard, isTrustedOrigin, renderServerIsland, setFrameAuth, setFrameGuard, setLoaderGuard } from "./server-island-registry.js";
|
|
3
3
|
import { existsSync, readFileSync, statSync, watch } from "node:fs";
|
|
4
4
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { createUnplugin } from "unplugin";
|
|
@@ -472,7 +472,7 @@ function buildServerFile(entries, serverFile) {
|
|
|
472
472
|
`// Import via: import { pageRouter, registry } from "ilha:pages/server";`,
|
|
473
473
|
``,
|
|
474
474
|
...imports,
|
|
475
|
-
...entries.some((e) => e.hasLoader || e.loaderLayouts.length > 0) ? [`import { setFrameLoaderRunner } from "@ilha/router/server-island-registry";`, `setFrameLoaderRunner((path) => pageRouter.runLoader(path));`] : [],
|
|
475
|
+
...entries.some((e) => e.hasLoader || e.loaderLayouts.length > 0) ? [`import { setFrameLoaderRunner } from "@ilha/router/server-island-registry";`, `setFrameLoaderRunner((path, request) => pageRouter.runLoader(path, request));`] : [],
|
|
476
476
|
``,
|
|
477
477
|
...wrappedIslandLines,
|
|
478
478
|
``,
|
|
@@ -989,6 +989,12 @@ const pagesFactory = (options = {}) => {
|
|
|
989
989
|
configureServer(server) {
|
|
990
990
|
server.watcher.add(state.pagesDir);
|
|
991
991
|
if (options.frameGuard) setFrameGuard(options.frameGuard);
|
|
992
|
+
if (options.loaderGuard) setLoaderGuard(options.loaderGuard);
|
|
993
|
+
if (options.trustedOrigins || options.csrf) setFrameAuth({
|
|
994
|
+
trustedOrigins: options.trustedOrigins,
|
|
995
|
+
csrf: options.csrf,
|
|
996
|
+
defaultAction: "open"
|
|
997
|
+
});
|
|
992
998
|
server.middlewares.use(async (req, res, next) => {
|
|
993
999
|
if ((req.url ?? "").split("?")[0] !== "/__ilha/frame") return next();
|
|
994
1000
|
if (req.method !== "POST") {
|
|
@@ -1001,19 +1007,24 @@ const pagesFactory = (options = {}) => {
|
|
|
1001
1007
|
res.end();
|
|
1002
1008
|
return;
|
|
1003
1009
|
}
|
|
1010
|
+
const guardHeaders = new Headers();
|
|
1011
|
+
if (req.headers.origin) guardHeaders.set("origin", String(req.headers.origin));
|
|
1012
|
+
if (req.headers.host) guardHeaders.set("host", String(req.headers.host));
|
|
1013
|
+
if (!isTrustedOrigin(new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, { headers: guardHeaders }), getFrameAuth())) {
|
|
1014
|
+
console.warn(`[ilha-router] dev frame request rejected: Origin ${String(req.headers.origin)} is not trusted (host: ${String(req.headers.host ?? "localhost")}). Configure trustedOrigins via IlhaPagesOptions if this origin is expected.`);
|
|
1015
|
+
res.statusCode = 403;
|
|
1016
|
+
res.end();
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const identityHeaders = new Headers();
|
|
1020
|
+
for (const name of ["cookie", "authorization"]) {
|
|
1021
|
+
const v = req.headers[name];
|
|
1022
|
+
if (typeof v === "string") identityHeaders.set(name, v);
|
|
1023
|
+
}
|
|
1004
1024
|
try {
|
|
1005
|
-
const guardHeaders = new Headers();
|
|
1006
|
-
for (const name of [
|
|
1007
|
-
"cookie",
|
|
1008
|
-
"authorization",
|
|
1009
|
-
"x-forwarded-for"
|
|
1010
|
-
]) {
|
|
1011
|
-
const v = req.headers[name];
|
|
1012
|
-
if (typeof v === "string") guardHeaders.set(name, v);
|
|
1013
|
-
}
|
|
1014
1025
|
const denied = await getFrameGuard()?.(new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, {
|
|
1015
1026
|
method: req.method,
|
|
1016
|
-
headers:
|
|
1027
|
+
headers: identityHeaders
|
|
1017
1028
|
}));
|
|
1018
1029
|
if (denied) {
|
|
1019
1030
|
res.statusCode = denied.status;
|
|
@@ -1026,9 +1037,17 @@ const pagesFactory = (options = {}) => {
|
|
|
1026
1037
|
res.end();
|
|
1027
1038
|
return;
|
|
1028
1039
|
}
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1040
|
+
const csrf = getFrameAuth()?.csrf;
|
|
1041
|
+
if (csrf) try {
|
|
1042
|
+
if (!await csrf(new Request(`http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, {
|
|
1043
|
+
method: req.method,
|
|
1044
|
+
headers: identityHeaders
|
|
1045
|
+
}))) {
|
|
1046
|
+
res.statusCode = 403;
|
|
1047
|
+
res.end();
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
} catch {
|
|
1032
1051
|
res.statusCode = 403;
|
|
1033
1052
|
res.end();
|
|
1034
1053
|
return;
|
|
@@ -1049,15 +1068,14 @@ const pagesFactory = (options = {}) => {
|
|
|
1049
1068
|
const target = serverIslands.get(body.id ?? "");
|
|
1050
1069
|
if (!target) throw new Error("unknown island");
|
|
1051
1070
|
let framePath = "/__ilha/frame";
|
|
1052
|
-
if (typeof body.path === "string" && body.path.startsWith("/") && !body.path.includes("//") && body.path.length <= 2048) framePath = body.path;
|
|
1071
|
+
if (typeof body.path === "string" && body.path.startsWith("/") && !body.path.includes("//") && !body.path.includes("\\") && body.path.length <= 2048) framePath = body.path;
|
|
1053
1072
|
await server.ssrLoadModule(VIRTUAL_PAGES_SERVER);
|
|
1054
1073
|
if (typeof (await server.ssrLoadModule(target.file))[target.name]?.[Symbol.for("ilha.renderState")] !== "function") throw new Error("unknown island");
|
|
1055
1074
|
const headers = new Headers();
|
|
1056
1075
|
for (const name of [
|
|
1057
1076
|
"cookie",
|
|
1058
1077
|
"authorization",
|
|
1059
|
-
"user-agent"
|
|
1060
|
-
"x-forwarded-for"
|
|
1078
|
+
"user-agent"
|
|
1061
1079
|
]) {
|
|
1062
1080
|
const value = req.headers[name];
|
|
1063
1081
|
if (typeof value === "string") headers.set(name, value);
|
package/dist/plugin.d.ts
CHANGED
|
@@ -36,6 +36,23 @@ export interface IlhaPagesOptions {
|
|
|
36
36
|
* `setFrameGuard()` from `@ilha/router/server-island-registry`.
|
|
37
37
|
*/
|
|
38
38
|
frameGuard?: (request: Request) => Response | void | Promise<Response | void>;
|
|
39
|
+
/**
|
|
40
|
+
* Guard consulted only by `GET /__ilha/loader` in production. When absent,
|
|
41
|
+
* the loader endpoint falls back to the frame guard for backwards
|
|
42
|
+
* compatibility. Mirrors `setLoaderGuard()`.
|
|
43
|
+
*/
|
|
44
|
+
loaderGuard?: (request: Request) => Response | void | Promise<Response | void>;
|
|
45
|
+
/**
|
|
46
|
+
* Explicit trusted origins for frame/loader requests (e.g. a `.vercel.app`
|
|
47
|
+
* or custom domain). When unset, origin checks compare the `Origin` header
|
|
48
|
+
* against the request's own `Host`. Mirrors `setFrameAuth({ trustedOrigins })`.
|
|
49
|
+
*/
|
|
50
|
+
trustedOrigins?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Optional CSRF verifier for the state-changing `/__ilha/frame` POST.
|
|
53
|
+
* Mirrors `setFrameAuth({ csrf })`.
|
|
54
|
+
*/
|
|
55
|
+
csrf?: (request: Request) => boolean | Promise<boolean>;
|
|
39
56
|
/**
|
|
40
57
|
* Fail codegen on duplicate route patterns / registry name collisions
|
|
41
58
|
* instead of warning. Recommended for CI/production builds. Default: `false`.
|
|
@@ -0,0 +1,7 @@
|
|
|
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;
|
|
@@ -8,7 +8,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
8
8
|
* page SSR through the router, or streamed frames through the plugin's
|
|
9
9
|
* `/__ilha/frame` endpoint. Both seed this scope with the originating
|
|
10
10
|
* `Request`, so render functions can read request data (URL, headers,
|
|
11
|
-
* cookies)
|
|
11
|
+
* cookies) through `useContext().request` or a host integration such as Oxide's `useRequest()`.
|
|
12
12
|
*
|
|
13
13
|
* The storage lives on `globalThis` under `ilha.requestAls` so every module
|
|
14
14
|
* copy (plugin bundle, SSR graph) shares one instance. The public accessor
|
|
@@ -17,10 +17,17 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
17
17
|
* sole place that constructs it.
|
|
18
18
|
*/
|
|
19
19
|
const REQUEST_ALS_KEY = Symbol.for("ilha.requestAls");
|
|
20
|
-
/**
|
|
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. */
|
|
21
26
|
function runWithIslandRequest(request, fn) {
|
|
22
27
|
const g = globalThis;
|
|
23
|
-
|
|
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());
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
//#endregion
|
package/dist/request-scope.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* page SSR through the router, or streamed frames through the plugin's
|
|
6
6
|
* `/__ilha/frame` endpoint. Both seed this scope with the originating
|
|
7
7
|
* `Request`, so render functions can read request data (URL, headers,
|
|
8
|
-
* cookies)
|
|
8
|
+
* cookies) through `useContext().request` or a host integration such as Oxide's `useRequest()`.
|
|
9
9
|
*
|
|
10
10
|
* The storage lives on `globalThis` under `ilha.requestAls` so every module
|
|
11
11
|
* copy (plugin bundle, SSR graph) shares one instance. The public accessor
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { IslandContext } from "./index";
|
|
17
17
|
export declare const REQUEST_ALS_KEY: unique symbol;
|
|
18
|
-
/** Run `fn` with `request` available to `useContext().request`.
|
|
18
|
+
/** Run `fn` with `request` available to `useContext().request`. When oxidejs
|
|
19
|
+
* is loaded, its action scope is entered too, so `useRequest()` works in
|
|
20
|
+
* island renders and streamed frames. */
|
|
19
21
|
export declare function runWithIslandRequest<T>(request: Request, fn: () => T): T;
|
|
20
22
|
export type { IslandContext };
|
package/dist/rolldown.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export type { LayoutHandler, ErrorHandler, RouteSnapshot, AppError } from "./ind
|
|
|
2
2
|
export { ilhaPages, type IlhaPagesOptions } from "./plugin";
|
|
3
3
|
import { type IlhaPagesOptions } from "./plugin";
|
|
4
4
|
/** Rolldown plugin — use via `@ilha/router/rolldown`. */
|
|
5
|
-
export declare function pages(options?: IlhaPagesOptions): import("
|
|
5
|
+
export declare function pages(options?: IlhaPagesOptions): import("rolldown").Plugin<any> | import("rolldown").Plugin<any>[];
|
|
6
6
|
export default pages;
|
package/dist/rolldown.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, decoded-aware route pattern matching used by both the router
|
|
3
|
+
* (`index.ts`) and the server-frame path (`server-island-registry.ts`) so the
|
|
4
|
+
* two never drift on segment semantics or parameter decoding.
|
|
5
|
+
*
|
|
6
|
+
* Patterns support `:name` segments, a bare mid-pattern `*` (one segment), and
|
|
7
|
+
* a trailing `/**:name` catch-all. Static segments take priority over params,
|
|
8
|
+
* which take priority over catch-alls — callers sort by that before matching.
|
|
9
|
+
*/
|
|
10
|
+
export interface ParsedPattern {
|
|
11
|
+
segments: string[];
|
|
12
|
+
kinds: number[];
|
|
13
|
+
}
|
|
14
|
+
export declare function parsePattern(pattern: string): ParsedPattern;
|
|
15
|
+
/**
|
|
16
|
+
* Match `segments` against a pathname. Returns raw (still-encoded) captured
|
|
17
|
+
* params, or `null` when the path doesn't match — the caller decides whether to
|
|
18
|
+
* decode via {@link safeDecode}.
|
|
19
|
+
*/
|
|
20
|
+
export declare function matchSegments(segments: string[], pathname: string): Record<string, string> | null;
|
|
21
|
+
/** Decode a route-param value, tolerating malformed percent-encoding. */
|
|
22
|
+
export declare function safeDecode(value: string): string;
|
package/dist/rspack.js
CHANGED
|
@@ -38,7 +38,52 @@ export type FrameGuard = (request: Request) => Response | void | Promise<Respons
|
|
|
38
38
|
*/
|
|
39
39
|
export declare function setFrameGuard(guard: FrameGuard): void;
|
|
40
40
|
export declare function getFrameGuard(): FrameGuard | undefined;
|
|
41
|
-
|
|
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<{
|
|
42
87
|
kind: string;
|
|
43
88
|
data?: unknown;
|
|
44
89
|
headEntries?: unknown;
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
+
import { F as parsePattern, I as safeDecode, P as matchSegments, S as resolveRedirectTarget } from "./src-BBsbD5vU.js";
|
|
2
|
+
|
|
1
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
|
+
*/
|
|
2
16
|
const REGISTRY_KEY = Symbol.for("ilha.serverIslandRenderers");
|
|
3
17
|
function registry() {
|
|
4
18
|
const g = globalThis;
|
|
@@ -24,6 +38,58 @@ function setFrameGuard(guard) {
|
|
|
24
38
|
function getFrameGuard() {
|
|
25
39
|
return globalThis[GUARD_KEY];
|
|
26
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
|
+
}
|
|
27
93
|
const LOADER_RUNNER_KEY = Symbol.for("ilha.frameLoaderRunner");
|
|
28
94
|
/**
|
|
29
95
|
* Install the handler backing `GET /__ilha/loader` in production. The
|
|
@@ -62,27 +128,14 @@ var FrameError = class extends Error {
|
|
|
62
128
|
}
|
|
63
129
|
};
|
|
64
130
|
/** Match a route pattern (`/user/:id`, `/docs/**:slug`) against a pathname.
|
|
65
|
-
* Returns
|
|
66
|
-
*
|
|
131
|
+
* Returns decoded params, or null when the path doesn't match. Shares the
|
|
132
|
+
* router's matcher semantics via `route-match.ts`. */
|
|
67
133
|
function matchPatternParams(pattern, pathname) {
|
|
68
|
-
const
|
|
69
|
-
|
|
134
|
+
const raw = matchSegments(parsePattern(pattern).segments, pathname);
|
|
135
|
+
if (!raw) return null;
|
|
70
136
|
const params = {};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (segment.startsWith("*")) {
|
|
74
|
-
const name = segment.slice(2).replace(/^:/, "");
|
|
75
|
-
if (name) params[name] = pathSegments.slice(cursor).join("/");
|
|
76
|
-
cursor = pathSegments.length;
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
const value = pathSegments[cursor];
|
|
80
|
-
if (value === void 0) return null;
|
|
81
|
-
if (segment.startsWith(":")) params[segment.slice(1)] = value;
|
|
82
|
-
else if (value !== segment) return null;
|
|
83
|
-
cursor++;
|
|
84
|
-
}
|
|
85
|
-
return cursor === pathSegments.length ? params : null;
|
|
137
|
+
for (const [k, v] of Object.entries(raw)) params[k] = safeDecode(v);
|
|
138
|
+
return params;
|
|
86
139
|
}
|
|
87
140
|
/**
|
|
88
141
|
* Shared tail of every frame request: run the page's `load` when registered
|
|
@@ -104,17 +157,23 @@ async function renderServerIsland(id, request, runWithScope) {
|
|
|
104
157
|
const params = entry.pattern ? matchPatternParams(entry.pattern, url.pathname) : {};
|
|
105
158
|
if (!params) throw new FrameError(400, "frame failed");
|
|
106
159
|
try {
|
|
107
|
-
props =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
} };
|
|
113
170
|
} catch (error) {
|
|
114
171
|
const marker = error;
|
|
115
172
|
if (marker.__ilhaRedirect === true) {
|
|
116
173
|
const r = error;
|
|
117
|
-
|
|
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);
|
|
118
177
|
}
|
|
119
178
|
if (marker.__ilhaLoaderError === true) throw new FrameError(error.status || 500, "frame failed");
|
|
120
179
|
throw error;
|
|
@@ -127,4 +186,4 @@ async function renderServerIsland(id, request, runWithScope) {
|
|
|
127
186
|
}
|
|
128
187
|
|
|
129
188
|
//#endregion
|
|
130
|
-
export { FrameError, getFrameGuard, getFrameLoaderRunner, getServerIslandEntry, getServerIslandRenderer, registerServerIsland, renderServerIsland, setFrameGuard, setFrameLoaderRunner };
|
|
189
|
+
export { FrameError, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, getServerIslandEntry, getServerIslandRenderer, isTrustedOrigin, registerServerIsland, renderServerIsland, setFrameAuth, setFrameGuard, setFrameLoaderRunner, setLoaderGuard };
|