@ilha/router 0.10.4 → 0.11.1

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.
@@ -1 +1,2 @@
1
+ export declare function sanitizeSnapshotObject(value: unknown): Record<string, unknown> | undefined;
1
2
  export declare function parseSnapshotAttr(raw: string): Record<string, unknown> | undefined;
@@ -1,7 +1,10 @@
1
- import { M as parsePattern, N as safeDecode, b as resolveRedirectTarget, j as matchSegments } from "./src-PCXNZfql.js";
2
- import "ilha";
3
- import { bindServerAction, setServerManifestSerializer } from "ilha/internal";
1
+ import { t as REQUEST_ALS_KEY } from "./als-key-CVxbuM3z.js";
2
+ import { n as sanitizeSnapshotObject } from "./snapshot-C0E2OGwL.js";
3
+ import { renderToString } from "ilha";
4
+ import * as Result from "effect/Result";
4
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
+ import * as Data from "effect/Data";
7
+ import * as Effect from "effect/Effect";
5
8
 
6
9
  //#region src/request-scope.ts
7
10
  /**
@@ -19,7 +22,6 @@ import { AsyncLocalStorage } from "node:async_hooks";
19
22
  * storage without importing `node:async_hooks`; this node-only module is the
20
23
  * sole place that constructs it.
21
24
  */
22
- const REQUEST_ALS_KEY = Symbol.for("ilha.requestAls");
23
25
  /** Installed by oxidejs when its module loads. Lets `useRequest()` resolve
24
26
  * inside island renders and frames, not just `/__oxide/action`. */
25
27
  const OXIDE_RUN_WITH_REQUEST = Symbol.for("oxidejs.runWithRequest");
@@ -35,6 +37,21 @@ function runWithIslandRequest(request, fn) {
35
37
 
36
38
  //#endregion
37
39
  //#region src/ssr.ts
40
+ /**
41
+ * Production SSR endpoint for server-owned islands.
42
+ *
43
+ * Default export is an oxidejs-style fetch middleware:
44
+ * `(request) => Response | undefined`. Returns `undefined` for any request it
45
+ * does not own, so hosts can chain it ahead of their own handler:
46
+ *
47
+ * ```ts
48
+ * oxide({ middleware: ["@ilha/router/ssr"] });
49
+ * ```
50
+ *
51
+ * Serves `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }`
52
+ * in, `{ html }` out). Renderers come from the process-global registry
53
+ * populated by self-registration code appended to `.server` modules.
54
+ */
38
55
  const REGISTRY_KEY = Symbol.for("ilha.serverIslandRenderers");
39
56
  function registry() {
40
57
  const g = globalThis;
@@ -60,20 +77,6 @@ function setFrameGuard(guard) {
60
77
  function getFrameGuard() {
61
78
  return globalThis[GUARD_KEY];
62
79
  }
63
- const LOADER_GUARD_KEY = Symbol.for("ilha.loaderGuard");
64
- /**
65
- * Install a guard consulted only by `GET /__ilha/loader`. When absent, the
66
- * loader endpoint falls back to `getFrameGuard()` for backwards compatibility.
67
- * Prefer a dedicated loader guard so gating the loader endpoint is independent
68
- * of frame rendering.
69
- */
70
- function setLoaderGuard(guard) {
71
- const g = globalThis;
72
- g[LOADER_GUARD_KEY] = guard;
73
- }
74
- function getLoaderGuard() {
75
- return globalThis[LOADER_GUARD_KEY];
76
- }
77
80
  const AUTH_KEY = Symbol.for("ilha.frameAuth");
78
81
  /**
79
82
  * Install the frame-authorization policy consumed by the production
@@ -121,6 +124,30 @@ function isTrustedOrigin(request, policy) {
121
124
  function isSafeFramePath(path) {
122
125
  return path.startsWith("/") && !path.includes("//") && !path.includes("\\") && path.length <= 2048;
123
126
  }
127
+ /**
128
+ * Build the scoped frame render URL from the incoming request URL and frame
129
+ * path. Absolute `incomingUrl` values supply the origin. Relative values (for
130
+ * example Vite's `req.url`) require an explicit trusted `serverOrigin` — never
131
+ * a client `Host` header.
132
+ */
133
+ function frameScopedUrl(incomingUrl, framePath, serverOrigin) {
134
+ let origin;
135
+ try {
136
+ const incoming = new URL(incomingUrl);
137
+ if (incoming.protocol !== "http:" && incoming.protocol !== "https:") throw new TypeError("unsupported protocol");
138
+ origin = incoming.origin;
139
+ } catch {
140
+ if (serverOrigin == null) throw new Error("frameScopedUrl: relative incomingUrl requires serverOrigin");
141
+ origin = parseServerOrigin(serverOrigin);
142
+ }
143
+ return new URL(framePath, origin).href;
144
+ }
145
+ function parseServerOrigin(value) {
146
+ if (!value.includes("://")) throw new Error("frameScopedUrl: serverOrigin must be an absolute URL");
147
+ const parsed = new URL(value);
148
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("frameScopedUrl: serverOrigin must be http(s)");
149
+ return parsed.origin;
150
+ }
124
151
  /** Identity headers forwarded onto scoped render/loader requests. */
125
152
  const FORWARD_IDENTITY_HEADERS = [
126
153
  "cookie",
@@ -157,118 +184,83 @@ function frameEnvelope(status, body) {
157
184
  body: JSON.stringify(body)
158
185
  };
159
186
  }
160
- const LOADER_RUNNER_KEY = Symbol.for("ilha.frameLoaderRunner");
161
- /**
162
- * Install the handler backing `GET /__ilha/loader` in production. The
163
- * generated `pages.server.ts` wires this to `pageRouter.runLoader`, so
164
- * regular-page server loads get full route matching, layout chains, and
165
- * redirect/error semantics. Dev and prod handlers share the slot.
166
- */
167
- function setFrameLoaderRunner(runner) {
168
- const g = globalThis;
169
- g[LOADER_RUNNER_KEY] = runner;
170
- }
171
- function getFrameLoaderRunner() {
172
- return globalThis[LOADER_RUNNER_KEY];
173
- }
174
- setServerManifestSerializer({ template(manifest) {
175
- return `<template data-ilha-actions='${JSON.stringify(Object.fromEntries(manifest)).replace(/&/g, "&amp;").replace(/'/g, "&#39;").replace(/</g, "&lt;")}'></template>`;
176
- } });
187
+ const ACTION_CALL = Symbol.for("ilha.actionCall");
177
188
  /** @internal Brand an exported server action with its generated RPC transport key. */
178
189
  function __ilhaServerAction(key, fn) {
179
- return bindServerAction(fn, key);
190
+ const wrapped = ((...args) => fn(...args));
191
+ wrapped.with = (...args) => {
192
+ const handler = ((..._ev) => wrapped(...args));
193
+ handler[ACTION_CALL] = {
194
+ k: key,
195
+ a: args
196
+ };
197
+ return handler;
198
+ };
199
+ return wrapped;
180
200
  }
181
- function registerServerIsland(id, render, options) {
182
- registry().set(id, {
183
- render,
184
- load: options?.load,
185
- pattern: options?.pattern
186
- });
201
+ function registerServerIsland(id, render) {
202
+ registry().set(id, { render });
187
203
  }
188
204
  function getServerIslandEntry(id) {
189
205
  return registry().get(id);
190
206
  }
191
- /** Client-facing frame failure. `redirect` carries a loader redirect target. */
192
- var FrameError = class extends Error {
193
- status;
194
- redirect;
195
- constructor(status, message, redirect) {
196
- super(message);
197
- this.status = status;
198
- this.redirect = redirect;
199
- }
200
- };
201
- /** Match a route pattern (`/user/:id`, `/docs/**:slug`) against a pathname.
202
- * Returns decoded params, or null when the path doesn't match. Shares the
203
- * router's matcher semantics via `route-match.ts`. */
204
- function matchPatternParams(pattern, pathname) {
205
- const raw = matchSegments(parsePattern(pattern).segments, pathname);
206
- if (!raw) return null;
207
- const params = {};
208
- for (const [k, v] of Object.entries(raw)) params[k] = safeDecode(v);
209
- return params;
210
- }
207
+ /** Client-facing frame failure. `redirect` carries a same-origin redirect target. */
208
+ var FrameError = class extends Data.TaggedError("FrameError") {};
211
209
  /**
212
210
  * Shared tail of every frame request: run the page's `load` when registered
213
211
  * (params matched from the frame path), then invoke the renderer inside the
214
212
  * caller's scope. Throws `FrameError` with an HTTP status for client-facing
215
213
  * failures; loader redirects surface via `FrameError.redirect`.
216
214
  */
217
- async function renderServerIsland(id, request, runWithScope, onHead, incomingProps) {
218
- const entry = registry().get(id);
219
- if (!entry) throw new FrameError(400, "unknown island");
220
- let props = incomingProps;
221
- if (entry.load) {
222
- let url;
223
- try {
224
- url = new URL(request.url);
225
- } catch {
226
- throw new FrameError(400, "frame failed");
227
- }
228
- const params = entry.pattern ? matchPatternParams(entry.pattern, url.pathname) : {};
229
- if (!params) throw new FrameError(400, "frame failed");
230
- try {
231
- const headEntries = [];
232
- const result = await entry.load({
233
- params,
234
- request,
235
- url,
236
- signal: request.signal,
237
- head: (input) => headEntries.push(input)
238
- });
239
- if (headEntries.length > 0) onHead?.(headEntries);
240
- props = { load: {
241
- loading: false,
242
- value: result ?? {},
243
- error: void 0
244
- } };
245
- } catch (error) {
246
- const marker = error;
247
- if (marker.__ilhaRedirect === true) {
248
- const r = error;
249
- const safe = resolveRedirectTarget(r.to, url, false);
250
- if (!safe.ok) throw new FrameError(500, "unsafe redirect target");
251
- throw new FrameError(r.status || 302, "frame failed", safe.to);
252
- }
253
- if (marker.__ilhaLoaderError === true) throw new FrameError(error.status || 500, "frame failed");
254
- throw error;
255
- }
256
- }
257
- const render = entry.render();
258
- if (typeof render !== "function") throw new FrameError(400, "unknown island");
259
- const html = await runWithScope(request, () => render(props));
260
- return String(html);
215
+ const frameFail = (status, message) => new FrameError({
216
+ status,
217
+ message: message ?? "frame failed"
218
+ });
219
+ /**
220
+ * Render a registered server island. Typed error channel: the only failure is
221
+ * `FrameError`; everything else is a defect surfaced as a 400 to the client.
222
+ */
223
+ function renderServerIsland(id, request, runWithScope, incomingProps) {
224
+ return Effect.gen(function* () {
225
+ const entry = registry().get(id);
226
+ if (!entry) return yield* Effect.fail(frameFail(400, "unknown island"));
227
+ const render = entry.render();
228
+ if (typeof render !== "function") return yield* Effect.fail(frameFail(400, "unknown island"));
229
+ return yield* Effect.tryPromise({
230
+ try: async () => {
231
+ return await runWithScope(request, () => renderToStringOf(render, incomingProps));
232
+ },
233
+ catch: (error) => error instanceof FrameError ? error : frameFail(400, error instanceof Error ? error.message : void 0)
234
+ });
235
+ });
236
+ }
237
+ /** Convenience for non-Effect callers: run the render and resolve to a Result. */
238
+ function renderServerIslandResult(id, request, runWithScope, incomingProps) {
239
+ return Effect.runPromise(Effect.result(renderServerIsland(id, request, runWithScope, incomingProps)));
240
+ }
241
+ function renderToStringOf(render, props) {
242
+ const out = render(props);
243
+ return Promise.resolve(out).then((view) => typeof view === "string" ? view : renderToString(() => view, {
244
+ captureActions: true,
245
+ markers: false
246
+ }));
261
247
  }
262
248
  const FRAME_ENDPOINT = "/__ilha/frame";
263
- /** Regular-page server loads: served through the loader-runner slot. */
264
- const LOADER_ENDPOINT = "/__ilha/loader";
265
249
  /** Max request body size — matches the dev middleware cap. */
266
250
  const MAX_BODY = 16384;
267
251
  /** Parent-island props on a frame POST. Missing is fine; anything else is 400. */
268
252
  function parseFrameProps(value) {
269
253
  if (value == null) return void 0;
270
- if (typeof value !== "object" || Array.isArray(value)) throw new FrameError(400, "frame failed");
271
- return value;
254
+ if (typeof value !== "object" || Array.isArray(value)) throw new FrameError({
255
+ status: 400,
256
+ message: "frame failed"
257
+ });
258
+ const sanitized = sanitizeSnapshotObject(value);
259
+ if (!sanitized) throw new FrameError({
260
+ status: 400,
261
+ message: "frame failed"
262
+ });
263
+ return sanitized;
272
264
  }
273
265
  function json(status, body) {
274
266
  const env = frameEnvelope(status, body);
@@ -353,107 +345,50 @@ async function authorizeFrameRequest(request, options) {
353
345
  };
354
346
  }
355
347
  async function ssr(request) {
356
- let pathname;
348
+ let url;
357
349
  try {
358
- pathname = new URL(request.url).pathname;
350
+ url = new URL(request.url);
359
351
  } catch {
360
352
  return json(400, { error: "frame failed" });
361
353
  }
362
- if (pathname !== "/__ilha/frame" && pathname !== "/__ilha/loader") return;
363
- const auth = getFrameAuth();
364
- if (!isTrustedOrigin(request, auth)) return json(403, { error: "frame failed" });
365
- if (pathname === "/__ilha/loader") {
366
- if (request.method !== "GET") return json(405, { error: "method not allowed" });
367
- const guard = getLoaderGuard() ?? getFrameGuard();
368
- if (!guard && (auth?.defaultAction ?? "deny") === "deny") return json(403, { error: "loader failed" });
369
- try {
370
- const denied = await guard?.(request);
371
- if (denied) return denied;
372
- } catch {
373
- return json(403, { error: "loader failed" });
374
- }
375
- const runner = getFrameLoaderRunner();
376
- if (!runner) return json(404, {
377
- kind: "error",
378
- status: 404,
379
- message: "not found"
380
- });
381
- const cl = request.headers.get("content-length");
382
- if (cl && Number(cl) > 16384) return json(413, { error: "frame failed" });
383
- let target = "/";
384
- try {
385
- target = new URL(request.url).searchParams.get("path") ?? "/";
386
- } catch {
387
- return json(400, {
388
- kind: "error",
389
- status: 400,
390
- message: "bad request"
391
- });
392
- }
393
- if (!isSafeFramePath(target)) return json(400, {
394
- kind: "error",
395
- status: 400,
396
- message: "bad request"
354
+ if (url.pathname !== "/__ilha/frame") return void 0;
355
+ const program = Effect.gen(function* () {
356
+ const auth = getFrameAuth();
357
+ if (!isTrustedOrigin(request, auth)) return yield* Effect.fail(frameFail(403));
358
+ if (request.method !== "POST") return yield* Effect.fail(frameFail(405));
359
+ if (!(request.headers.get("content-type") ?? "").startsWith("application/json")) return yield* Effect.fail(frameFail(415));
360
+ const authorized = yield* Effect.tryPromise({
361
+ try: () => authorizeFrameRequest(request, {
362
+ defaultAction: auth?.defaultAction ?? "deny",
363
+ onGuardError: (error) => console.error("[ilha-router] frame guard failed:", error)
364
+ }),
365
+ catch: () => frameFail(403)
397
366
  });
398
- try {
399
- const result = await runWithIslandRequest(request, () => runner(target, request));
400
- if (result.kind === "redirect") return json(result.status || 302, {
401
- kind: "redirect",
402
- to: result.to,
403
- status: result.status
367
+ if (!authorized.ok) return yield* Effect.fail(frameFail(authorized.status));
368
+ let id;
369
+ let path = "/";
370
+ let incomingProps;
371
+ {
372
+ const text = yield* Effect.tryPromise({
373
+ try: () => readBodyBounded(request, MAX_BODY),
374
+ catch: () => frameFail(400)
404
375
  });
405
- if (result.kind !== "data") {
406
- const status = result.status || 500;
407
- return json(status, {
408
- kind: result.kind,
409
- status,
410
- message: result.message
411
- });
376
+ if (text === null) return yield* Effect.fail(frameFail(413));
377
+ try {
378
+ const body = JSON.parse(text);
379
+ id = String(body.id ?? "");
380
+ incomingProps = parseFrameProps(body.props);
381
+ if (typeof body.path === "string") {
382
+ if (!isSafeFramePath(body.path)) return yield* Effect.fail(frameFail(400));
383
+ path = body.path;
384
+ }
385
+ } catch {
386
+ return yield* Effect.fail(frameFail(400));
412
387
  }
413
- return json(200, result);
414
- } catch (error) {
415
- console.error("[ilha-router] loader endpoint failed:", error);
416
- return json(500, {
417
- kind: "error",
418
- status: 500,
419
- message: "loader failed"
420
- });
421
388
  }
422
- }
423
- if (request.method !== "POST") return json(405, { error: "frame failed" });
424
- if (!(request.headers.get("content-type") ?? "").startsWith("application/json")) return json(415, { error: "frame failed" });
425
- const authorized = await authorizeFrameRequest(request, {
426
- defaultAction: auth?.defaultAction ?? "deny",
427
- onGuardError: (error) => console.error("[ilha-router] frame guard failed:", error)
428
- });
429
- if (!authorized.ok) return json(authorized.status, { error: "frame failed" });
430
- let id;
431
- let path = "/";
432
- let incomingProps;
433
- try {
434
- const text = await readBodyBounded(request, MAX_BODY);
435
- if (text === null) return json(413, { error: "frame failed" });
436
- const body = JSON.parse(text);
437
- id = String(body.id ?? "");
438
- incomingProps = parseFrameProps(body.props);
439
- if (typeof body.path === "string") {
440
- if (!isSafeFramePath(body.path)) return json(400, { error: "frame failed" });
441
- path = body.path;
442
- }
443
- } catch {
444
- return json(400, { error: "frame failed" });
445
- }
446
- try {
447
- let origin;
448
- try {
449
- origin = new URL(request.url).origin;
450
- } catch {
451
- return json(400, { error: "frame failed" });
452
- }
453
- const headers = forwardIdentityHeaders(request.headers);
454
- const scoped = new Request(new URL(path, origin), {
389
+ const scoped = new Request(frameScopedUrl(url.href, path), {
455
390
  method: "POST",
456
- headers
391
+ headers: forwardIdentityHeaders(request.headers)
457
392
  });
458
393
  for (const sym of Object.getOwnPropertySymbols(request)) {
459
394
  if (Symbol.keyFor(sym) === void 0) continue;
@@ -461,23 +396,19 @@ async function ssr(request) {
461
396
  scoped[sym] = request[sym];
462
397
  } catch {}
463
398
  }
464
- let head;
465
- return json(200, {
466
- html: await renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn)), (entries) => head = entries, incomingProps),
467
- head
468
- });
469
- } catch (error) {
470
- if (error instanceof FrameError) {
399
+ return json(200, { html: yield* renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn)), incomingProps) });
400
+ });
401
+ return Effect.runPromise(Effect.map(Effect.result(program), Result.match({
402
+ onFailure: (error) => {
471
403
  if (error.redirect) return json(error.status, { redirect: error.redirect });
472
404
  if (error.status >= 500) console.error("[ilha-router] frame render failed:", error);
473
405
  return json(error.status, { error: "frame failed" });
474
- }
475
- console.error("[ilha-router] frame render failed:", error);
476
- return json(400, { error: "frame failed" });
477
- }
406
+ },
407
+ onSuccess: (response) => response
408
+ })));
478
409
  }
479
410
  /** Side-effect imports required alongside this handler. */
480
- ssr.imports = ["ilha:pages/server", "ilha:loaders"];
411
+ ssr.imports = ["ilha:pages/server"];
481
412
 
482
413
  //#endregion
483
- export { setFrameLoaderRunner as C, runWithIslandRequest as E, setFrameGuard as S, ssr as T, parseFrameProps as _, __ilhaServerAction as a, renderServerIsland as b, frameEnvelope as c, getFrameLoaderRunner as d, getLoaderGuard as f, json as g, isTrustedOrigin as h, MAX_BODY as i, getFrameAuth as l, isSafeFramePath as m, FrameError as n, authorizeFrameRequest as o, getServerIslandEntry as p, LOADER_ENDPOINT as r, forwardIdentityHeaders as s, FRAME_ENDPOINT as t, getFrameGuard as u, readBodyBounded as v, setLoaderGuard as w, setFrameAuth as x, registerServerIsland as y };
414
+ export { runWithIslandRequest as C, ssr as S, registerServerIsland as _, authorizeFrameRequest as a, setFrameAuth as b, frameScopedUrl as c, getServerIslandEntry as d, isSafeFramePath as f, readBodyBounded as g, parseFrameProps as h, __ilhaServerAction as i, getFrameAuth as l, json as m, FrameError as n, forwardIdentityHeaders as o, isTrustedOrigin as p, MAX_BODY as r, frameEnvelope as s, FRAME_ENDPOINT as t, getFrameGuard as u, renderServerIsland as v, setFrameGuard as x, renderServerIslandResult as y };
package/dist/ssr.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Production SSR endpoints for server-owned islands and regular-page loads.
2
+ * Production SSR endpoint for server-owned islands.
3
3
  *
4
4
  * Default export is an oxidejs-style fetch middleware:
5
5
  * `(request) => Response | undefined`. Returns `undefined` for any request it
@@ -9,16 +9,12 @@
9
9
  * oxide({ middleware: ["@ilha/router/ssr"] });
10
10
  * ```
11
11
  *
12
- * Serves:
13
- * - `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }` in,
14
- * `{ html }` out). Renderers come from the process-global registry
15
- * populated by self-registration code appended to `.server` modules.
16
- * - `GET /__ilha/loader?path=…` — regular-page server loads via the loader
17
- * runner (`setFrameLoaderRunner`, wired by the generated server module).
12
+ * Serves `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }`
13
+ * in, `{ html }` out). Renderers come from the process-global registry
14
+ * populated by self-registration code appended to `.server` modules.
18
15
  */
19
- import "ilha";
20
- import { type ServerAction } from "ilha/internal";
21
- import type { HeadInput } from "./head";
16
+ import * as Effect from "effect/Effect";
17
+ import * as Result from "effect/Result";
22
18
  /**
23
19
  * Server-frame state shared by the dev middleware and the production
24
20
  * `@ilha/router/ssr` handler: the renderers registry (keyed by the public
@@ -33,25 +29,10 @@ import type { HeadInput } from "./head";
33
29
  * additionally register their `load` and route pattern so frame handlers can
34
30
  * run the loader with matched params.
35
31
  */
36
- /** Loader context for server-page `load` — mirrors the router's shape. */
37
- export interface FrameLoaderContext {
38
- params: Record<string, string>;
39
- request: Request;
40
- url: URL;
41
- signal: AbortSignal;
42
- /** Contribute `<head>` data for this route. Safe to call multiple times. */
43
- head: (input: HeadInput) => void;
44
- }
45
- export type ServerPageLoader = (ctx: FrameLoaderContext) => unknown;
46
32
  /** A frame render: optionally preceded by running the page's `load`. */
47
33
  export interface ServerIslandEntry {
48
34
  /** Returns the renderState fn (`Symbol.for("ilha.renderState")` getter). */
49
35
  render: () => unknown;
50
- /** The module's `load` export — runs at frame time; its return value
51
- * becomes the island's render props. */
52
- load?: ServerPageLoader;
53
- /** Route pattern for the page (`/user/:id`) — matches params for `load`. */
54
- pattern?: string;
55
36
  }
56
37
  export type FrameGuard = (request: Request) => Response | void | Promise<Response | void>;
57
38
  /**
@@ -63,14 +44,6 @@ export type FrameGuard = (request: Request) => Response | void | Promise<Respons
63
44
  */
64
45
  export declare function setFrameGuard(guard: FrameGuard): void;
65
46
  export declare function getFrameGuard(): FrameGuard | undefined;
66
- /**
67
- * Install a guard consulted only by `GET /__ilha/loader`. When absent, the
68
- * loader endpoint falls back to `getFrameGuard()` for backwards compatibility.
69
- * Prefer a dedicated loader guard so gating the loader endpoint is independent
70
- * of frame rendering.
71
- */
72
- export declare function setLoaderGuard(guard: FrameGuard): void;
73
- export declare function getLoaderGuard(): FrameGuard | undefined;
74
47
  /** Frame-authorization policy, installed via {@link setFrameAuth}. */
75
48
  export interface FrameAuthPolicy {
76
49
  /**
@@ -115,6 +88,13 @@ export declare function isTrustedOrigin(request: Request, policy: FrameAuthPolic
115
88
  * check), bounded length. `false` for anything else.
116
89
  */
117
90
  export declare function isSafeFramePath(path: string): boolean;
91
+ /**
92
+ * Build the scoped frame render URL from the incoming request URL and frame
93
+ * path. Absolute `incomingUrl` values supply the origin. Relative values (for
94
+ * example Vite's `req.url`) require an explicit trusted `serverOrigin` — never
95
+ * a client `Host` header.
96
+ */
97
+ export declare function frameScopedUrl(incomingUrl: string, framePath: string, serverOrigin?: string): string;
118
98
  /**
119
99
  * Copy identity headers (cookie, authorization, user-agent) onto a fresh
120
100
  * `Headers`. Accepts a `Headers` or a Node `IncomingHttpHeaders`-style plain
@@ -129,45 +109,30 @@ export interface FrameEnvelope {
129
109
  body: string;
130
110
  }
131
111
  export declare function frameEnvelope(status: number, body: Record<string, unknown>): FrameEnvelope;
132
- export type FrameLoaderRunner = (path: string, request?: Request) => Promise<{
133
- kind: string;
134
- data?: unknown;
135
- headEntries?: unknown;
136
- status?: number;
137
- to?: string;
138
- message?: string;
139
- }>;
140
- /**
141
- * Install the handler backing `GET /__ilha/loader` in production. The
142
- * generated `pages.server.ts` wires this to `pageRouter.runLoader`, so
143
- * regular-page server loads get full route matching, layout chains, and
144
- * redirect/error semantics. Dev and prod handlers share the slot.
145
- */
146
- export declare function setFrameLoaderRunner(runner: FrameLoaderRunner): void;
147
- export declare function getFrameLoaderRunner(): FrameLoaderRunner | undefined;
148
112
  /** @internal Brand an exported server action with its generated RPC transport key. */
149
- export declare function __ilhaServerAction<A extends unknown[], R>(key: string, fn: (...args: A) => R): ServerAction<A, R>;
150
- export declare function registerServerIsland(id: string, render: () => unknown, options?: {
151
- load?: ServerPageLoader;
152
- pattern?: string;
153
- }): void;
113
+ export declare function __ilhaServerAction<A extends unknown[], R>(key: string, fn: (...args: A) => R): ((...args: A) => R) & {
114
+ with: (...args: A) => (...ev: unknown[]) => R;
115
+ };
116
+ export declare function registerServerIsland(id: string, render: () => unknown): void;
154
117
  export declare function getServerIslandEntry(id: string): ServerIslandEntry | undefined;
155
- /** Client-facing frame failure. `redirect` carries a loader redirect target. */
156
- export declare class FrameError extends Error {
118
+ declare const FrameError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
119
+ readonly _tag: "FrameError";
120
+ } & Readonly<A>;
121
+ /** Client-facing frame failure. `redirect` carries a same-origin redirect target. */
122
+ export declare class FrameError extends FrameError_base<{
157
123
  status: number;
124
+ message?: string;
158
125
  redirect?: string;
159
- constructor(status: number, message: string, redirect?: string);
126
+ }> {
160
127
  }
161
128
  /**
162
- * Shared tail of every frame request: run the page's `load` when registered
163
- * (params matched from the frame path), then invoke the renderer inside the
164
- * caller's scope. Throws `FrameError` with an HTTP status for client-facing
165
- * failures; loader redirects surface via `FrameError.redirect`.
129
+ * Render a registered server island. Typed error channel: the only failure is
130
+ * `FrameError`; everything else is a defect surfaced as a 400 to the client.
166
131
  */
167
- export declare function renderServerIsland(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>, onHead?: (entries: HeadInput[]) => void, incomingProps?: Record<string, unknown>): Promise<string>;
132
+ export declare function renderServerIsland(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>, incomingProps?: Record<string, unknown>): Effect.Effect<string, FrameError>;
133
+ /** Convenience for non-Effect callers: run the render and resolve to a Result. */
134
+ export declare function renderServerIslandResult(id: string, request: Request, runWithScope: <T>(request: Request, fn: () => T) => T | Promise<T>, incomingProps?: Record<string, unknown>): Promise<Result.Result<string, FrameError>>;
168
135
  export declare const FRAME_ENDPOINT = "/__ilha/frame";
169
- /** Regular-page server loads: served through the loader-runner slot. */
170
- export declare const LOADER_ENDPOINT = "/__ilha/loader";
171
136
  /** Max request body size — matches the dev middleware cap. */
172
137
  export declare const MAX_BODY: number;
173
138
  /** Parent-island props on a frame POST. Missing is fine; anything else is 400. */
package/dist/ssr.js CHANGED
@@ -1,3 +1,3 @@
1
- import { C as setFrameLoaderRunner, S as setFrameGuard, T as ssr, _ as parseFrameProps, a as __ilhaServerAction, b as renderServerIsland, 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 readBodyBounded, w as setLoaderGuard, x as setFrameAuth, y as registerServerIsland } from "./ssr-BTmqfAp9.js";
1
+ import { S as ssr, _ as registerServerIsland, a as authorizeFrameRequest, b as setFrameAuth, c as frameScopedUrl, d as getServerIslandEntry, f as isSafeFramePath, g as readBodyBounded, h as parseFrameProps, i as __ilhaServerAction, l as getFrameAuth, m as json, n as FrameError, o as forwardIdentityHeaders, p as isTrustedOrigin, r as MAX_BODY, s as frameEnvelope, t as FRAME_ENDPOINT, u as getFrameGuard, v as renderServerIsland, x as setFrameGuard, y as renderServerIslandResult } from "./ssr-WA3bSsCo.js";
2
2
 
3
- export { FRAME_ENDPOINT, FrameError, LOADER_ENDPOINT, MAX_BODY, __ilhaServerAction, authorizeFrameRequest, ssr as default, forwardIdentityHeaders, frameEnvelope, getFrameAuth, getFrameGuard, getFrameLoaderRunner, getLoaderGuard, getServerIslandEntry, isSafeFramePath, isTrustedOrigin, json, parseFrameProps, readBodyBounded, registerServerIsland, renderServerIsland, setFrameAuth, setFrameGuard, setFrameLoaderRunner, setLoaderGuard };
3
+ export { FRAME_ENDPOINT, FrameError, MAX_BODY, __ilhaServerAction, authorizeFrameRequest, ssr as default, forwardIdentityHeaders, frameEnvelope, frameScopedUrl, getFrameAuth, getFrameGuard, getServerIslandEntry, isSafeFramePath, isTrustedOrigin, json, parseFrameProps, readBodyBounded, registerServerIsland, renderServerIsland, renderServerIslandResult, setFrameAuth, setFrameGuard };
package/dist/vite.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ilhaPages } from "./plugin-BQil-7B_.js";
1
+ import { t as ilhaPages } from "./plugin-C3AAI3e_.js";
2
2
 
3
3
  //#region src/vite.ts
4
4
  /** Vite plugin — use via `@ilha/router/vite`. */