/** * Shared server-frame helpers used by the Vite/Rsbuild pages plugin and by * `@ilha/router/ssr`. Kept free of the optional `oxidejs` peer so * `@ilha/router/vite` can load without oxide installed. * * Server-frame state (renderers registry, frame/loader guards, auth policy) * lives on `globalThis` so every module copy (plugin bundle, SSR graph, frame * entry) shares one instance — same pattern as `request-scope.ts`. */ import * as Effect from "effect/Effect"; import type * as Result from "effect/Result"; import type { SnapshotObject } from "./snapshot"; /** JSON payload for frame envelopes. */ export type FrameJsonValue = string | number | boolean | null | FrameJsonValue[] | FrameJsonObject; export interface FrameJsonObject { readonly [key: string]: FrameJsonValue | undefined; } /** A frame render: optionally preceded by running the page's `load`. */ export interface ServerIslandEntry { /** Returns the renderState fn (`Symbol.for("ilha.renderState")` getter). */ render: () => ServerIslandRenderFn; } export type ServerIslandRenderFn = (props?: SnapshotObject) => string | Promise | object; export type FrameGuard = (request: Request) => Response | undefined | Promise; /** * Install a guard consulted by every `/__ilha/frame` request (dev middleware * and the production `@ilha/router/ssr` handler share this slot — both read * it from `globalThis`). Return a `Response` to reject; return nothing to * allow. Island state is world-readable through frames unless you gate them, * so apps serving private data should install a session check here. */ export declare const setFrameGuard: (guard: FrameGuard) => void; export declare const getFrameGuard: () => FrameGuard | undefined; /** Frame-authorization policy, installed via {@link setFrameAuth}. */ export interface FrameAuthPolicy { /** * Action taken when no frame guard is registered. `"deny"` (default in the * production handler) rejects every `/__ilha/frame` request with 403; * `"open"` preserves the legacy unauthenticated behavior. The dev * middleware stays permissive unless a guard is registered. */ defaultAction?: "open" | "deny"; /** * Explicit trusted origins (e.g. `"https://app.example.com"`). When set, * origin checks accept only these; otherwise the check compares the `Origin` * header against `https://{host}` / `http://{host}`. */ trustedOrigins?: string[]; /** * Optional CSRF verifier for the state-changing frame POST. Receives the * original `Request`; returning falsy rejects the request. Use this for * server-to-server frame callers that have no browser `Origin`. */ csrf?: (request: Request) => boolean | Promise; } /** * Install the frame-authorization policy consumed by the production * `@ilha/router/ssr` handler. `trustedOrigins` and `csrf` are also applied by * the dev middleware (via `IlhaPagesOptions`). */ export declare const setFrameAuth: (policy: FrameAuthPolicy) => void; export declare const getFrameAuth: () => FrameAuthPolicy | undefined; /** * Same-origin check for frame/loader requests. Browsers always send `Origin` * on cross-origin and same-origin `POST`; its absence implies a non-browser * caller (allowed — gate those via a guard or `csrf`). When `Origin` is * present it must match the configured trusted origins, else the request's * own `Host`. */ export declare const isTrustedOrigin: (request: Request, policy: FrameAuthPolicy | undefined) => boolean; /** * Path-only route context for frame/loader scoped requests. Leading slash, * no `//` or backslash (WHATWG URLs treat `\` as `/` for http(s), so a * `\evil.com` prefix would smuggle a foreign authority past a plain `//` * check), bounded length. `false` for anything else. */ export declare const isSafeFramePath: (framePath: string) => boolean; /** * Build the scoped frame render URL from the incoming request URL and frame * path. Absolute `incomingUrl` values supply the origin. Relative values (for * example Vite's `req.url`) require an explicit trusted `serverOrigin` — never * a client `Host` header. */ export declare const frameScopedUrl: (incomingUrl: string, framePath: string, serverOrigin?: string) => string; type HeaderSource = Headers | Readonly>; /** * Copy identity headers (cookie, authorization, user-agent) onto a fresh * `Headers`. Accepts a `Headers` or a Node `IncomingHttpHeaders`-style plain * object. Client-supplied `x-forwarded-for` is deliberately NOT forwarded — * it is spoofable and must not be trusted by loaders for IP checks. */ export declare const forwardIdentityHeaders: (source: HeaderSource) => Headers; /** No-store JSON envelope shared by dev and production frame handlers. */ export interface FrameEnvelope { status: number; headers: Record; body: string; } export declare const frameEnvelope: (status: number, body: FrameJsonObject) => FrameEnvelope; export declare const registerServerIsland: (id: string, render: () => ServerIslandRenderFn) => void; export declare const getServerIslandEntry: (id: string) => ServerIslandEntry | undefined; declare const FrameError_base: new = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & { readonly _tag: "FrameError"; } & Readonly; /** Client-facing frame failure. `redirect` carries a same-origin redirect target. */ export declare class FrameError extends FrameError_base<{ status: number; message?: string; redirect?: string; }> { } /** * Render a registered server island. Typed error channel: the only failure is * `FrameError`; everything else is a defect surfaced as a 400 to the client. */ export declare const renderServerIsland: (id: string, request: Request, runWithScope: (request: Request, fn: () => T) => T | Promise, incomingProps?: SnapshotObject) => Effect.Effect; /** Convenience for non-Effect callers: run the render and resolve to a Result. */ export declare const renderServerIslandResult: (id: string, request: Request, runWithScope: (request: Request, fn: () => T) => T | Promise, incomingProps?: SnapshotObject) => Promise>; export declare const FRAME_ENDPOINT = "/__ilha/frame"; /** Max request body size — matches the dev middleware cap. */ export declare const MAX_BODY: number; /** Parent-island props on a frame POST. Missing is fine; anything else is 400. */ export declare const parseFrameProps: (value?: T) => SnapshotObject | undefined; export declare const json: (status: number, body: FrameJsonObject) => Response; /** * Read a request body as UTF-8, streaming it with a hard byte cap. Returns * `null` when the body exceeds `maxBytes` (the reader is cancelled before the * cap is far exceeded) or when decoding fails. */ export declare const readBodyBounded: (request: Request, maxBytes: number) => Promise; /** * Shared frame-request authorization used by both the production handler * below and the Vite/Rsbuild dev middleware: same-origin check against the * frame-auth policy, the registered frame guard, and the optional CSRF * verifier. `defaultAction` selects the deny-by-default production posture or * the permissive development one. * * Returns the forwarded identity headers on success so callers render frames * with cookie/auth/UA context, or the HTTP status to reject with. */ export declare const authorizeFrameRequest: (request: Request, options: { defaultAction: "open" | "deny"; onGuardError?: (error: E) => void; }) => Promise<{ ok: true; identityHeaders: Headers; } | { ok: false; status: number; }>; export {};