@mesofact/runtime 0.8.29

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.
Files changed (77) hide show
  1. package/README.md +19 -0
  2. package/dist/adapters/r2.d.ts +24 -0
  3. package/dist/adapters/r2.d.ts.map +1 -0
  4. package/dist/adapters/r2.js +136 -0
  5. package/dist/adapters/r2.js.map +1 -0
  6. package/dist/adapters/sqlite.d.ts +25 -0
  7. package/dist/adapters/sqlite.d.ts.map +1 -0
  8. package/dist/adapters/sqlite.js +131 -0
  9. package/dist/adapters/sqlite.js.map +1 -0
  10. package/dist/config.d.ts +29 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +139 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/contract.d.ts +40 -0
  15. package/dist/contract.d.ts.map +1 -0
  16. package/dist/contract.js +5 -0
  17. package/dist/contract.js.map +1 -0
  18. package/dist/errors.d.ts +29 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +40 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/head.d.ts +32 -0
  23. package/dist/head.d.ts.map +1 -0
  24. package/dist/head.js +93 -0
  25. package/dist/head.js.map +1 -0
  26. package/dist/health.d.ts +14 -0
  27. package/dist/health.d.ts.map +1 -0
  28. package/dist/health.js +85 -0
  29. package/dist/health.js.map +1 -0
  30. package/dist/hooks.d.ts +28 -0
  31. package/dist/hooks.d.ts.map +1 -0
  32. package/dist/hooks.js +64 -0
  33. package/dist/hooks.js.map +1 -0
  34. package/dist/hydration.d.ts +6 -0
  35. package/dist/hydration.d.ts.map +1 -0
  36. package/dist/hydration.js +68 -0
  37. package/dist/hydration.js.map +1 -0
  38. package/dist/index.d.ts +26 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +16 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/manifest.d.ts +68 -0
  43. package/dist/manifest.d.ts.map +1 -0
  44. package/dist/manifest.js +9 -0
  45. package/dist/manifest.js.map +1 -0
  46. package/dist/routes.d.ts +67 -0
  47. package/dist/routes.d.ts.map +1 -0
  48. package/dist/routes.js +130 -0
  49. package/dist/routes.js.map +1 -0
  50. package/dist/source.d.ts +36 -0
  51. package/dist/source.d.ts.map +1 -0
  52. package/dist/source.js +52 -0
  53. package/dist/source.js.map +1 -0
  54. package/dist/track-ctx.d.ts +13 -0
  55. package/dist/track-ctx.d.ts.map +1 -0
  56. package/dist/track-ctx.js +15 -0
  57. package/dist/track-ctx.js.map +1 -0
  58. package/dist/validate.d.ts +20 -0
  59. package/dist/validate.d.ts.map +1 -0
  60. package/dist/validate.js +333 -0
  61. package/dist/validate.js.map +1 -0
  62. package/package.json +40 -0
  63. package/src/adapters/r2.ts +163 -0
  64. package/src/adapters/sqlite.ts +182 -0
  65. package/src/config.ts +213 -0
  66. package/src/contract.ts +82 -0
  67. package/src/errors.ts +52 -0
  68. package/src/head.ts +130 -0
  69. package/src/health.ts +99 -0
  70. package/src/hooks.ts +72 -0
  71. package/src/hydration.ts +72 -0
  72. package/src/index.ts +113 -0
  73. package/src/manifest.ts +104 -0
  74. package/src/routes.ts +320 -0
  75. package/src/source.ts +91 -0
  76. package/src/track-ctx.ts +29 -0
  77. package/src/validate.ts +388 -0
package/src/health.ts ADDED
@@ -0,0 +1,99 @@
1
+ // `defineReadyz` — the TSX half of mesofact's `/readyz` probe.
2
+ //
3
+ // mesofact serves `/livez` and `/readyz` itself, in Rust
4
+ // (`crates/mesofact/src/health.rs`), gated on the things only the engine can
5
+ // know: is the SSR isolate up, is there a tree to serve, are we draining. An
6
+ // app opts into contributing its own readiness by declaring the path as an
7
+ // ordinary SSR route — there is no config key and no manifest field:
8
+ //
9
+ // // mesofact.routes.ts
10
+ // { route: "/readyz", mode: "ssr", entrypoint: "src/readyz.ts", cache_policy: { ttl: 0 } }
11
+ //
12
+ // The Rust handler notices the app claimed the path, dispatches to it while
13
+ // answering the probe, and folds the response status into its own listing as a
14
+ // check named `app`. Anything outside 2xx takes the pod out of rotation.
15
+ //
16
+ // # The contract, and its one asymmetry
17
+ //
18
+ // An app verdict is **additive**: it can only make the process less ready. A
19
+ // 200 from here cannot un-drain a terminating process or overrule a dead
20
+ // isolate, because those are exactly the states in which app code is the
21
+ // unreliable narrator. Design your checks accordingly — this is the place to
22
+ // say "my database pool is exhausted", not "ignore the engine, I'm fine".
23
+ //
24
+ // # Vanilla, or this helper
25
+ //
26
+ // The route is a plain Fetch handler, so vanilla works and owes nothing to this
27
+ // module:
28
+ //
29
+ // export default async () =>
30
+ // (await db.ping()) ? new Response("ok") : new Response("db", { status: 503 });
31
+ //
32
+ // `defineReadyz` is the opt-in ergonomic layer: it runs named checks and emits
33
+ // the *same* wire format the Rust side emits — the kube-apiserver `[+]name ok`
34
+ // listing under `?verbose`. Matching formats is the point. An operator running
35
+ // `curl localhost:3000/readyz?verbose` should not be able to tell which
36
+ // language answered, and when the Rust side aggregates, the two listings nest
37
+ // instead of clashing.
38
+
39
+ /** One named readiness condition. `check` should be fast and side-effect free —
40
+ * probes run on the kubelet's schedule, per replica. */
41
+ export type ReadyCheck = {
42
+ /** Operator-facing name in `?verbose` output. A noun (`db`, `cache`). */
43
+ name: string;
44
+ check: () => boolean | Promise<boolean>;
45
+ };
46
+
47
+ /** Build the Fetch handler for a `mode:"ssr"` `/readyz` route.
48
+ *
49
+ * Every check runs, even after one fails: reporting only the first failure
50
+ * hides a second broken subsystem behind the first. A check that throws counts
51
+ * as failed — an exception is not an assertion of readiness. */
52
+ export function defineReadyz(
53
+ checks: readonly ReadyCheck[],
54
+ ): (req: Request) => Promise<Response> {
55
+ return async (req: Request): Promise<Response> => {
56
+ const results = await Promise.all(
57
+ checks.map(async (c) => {
58
+ try {
59
+ return { name: c.name, pass: (await c.check()) === true };
60
+ } catch {
61
+ return { name: c.name, pass: false };
62
+ }
63
+ }),
64
+ );
65
+
66
+ const ok = results.every((r) => r.pass);
67
+ const status = ok ? 200 : 503;
68
+ const headers = {
69
+ "content-type": "text/plain; charset=utf-8",
70
+ // A cached 200 outlives the condition it described, which is the exact
71
+ // failure the probe exists to catch.
72
+ "cache-control": "no-cache, no-store, must-revalidate",
73
+ };
74
+
75
+ if (!isVerbose(req)) {
76
+ return new Response(ok ? "ok\n" : "readyz check failed\n", {
77
+ status,
78
+ headers,
79
+ });
80
+ }
81
+
82
+ const listing = results
83
+ .map((r) => (r.pass ? `[+]${r.name} ok\n` : `[-]${r.name} failed\n`))
84
+ .join("");
85
+ const trailer = ok ? "readyz check passed\n" : "readyz check failed\n";
86
+ return new Response(listing + trailer, { status, headers });
87
+ };
88
+ }
89
+
90
+ /** `?verbose` / `?verbose=1`, matching the Rust `is_verbose`. A malformed URL
91
+ * means the non-verbose body, never a crash — a probe that 500s because it
92
+ * could not parse its own URL is worse than one that answers tersely. */
93
+ function isVerbose(req: Request): boolean {
94
+ try {
95
+ return new URL(req.url).searchParams.has("verbose");
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,72 @@
1
+ // Mode 2 hook declaration — W311 §2 "Open Decision 1", answered by R756-F6.
2
+ //
3
+ // # What a hook is
4
+ //
5
+ // mesofact's TSX override surface has two modes (W311 §2). Mode 1 is the
6
+ // general API: `mode:"ssr"` route, `Request -> Response`, the app owns the
7
+ // wire. Mode 2 is the endpoint callback: a small `input -> verdict`, and
8
+ // **Rust** owns the HTTP. R756-F3 shipped Mode 2's wire verb
9
+ // (`__mesofact_ssr.invoke(bundle, hookName, input)`) but hardcoded its single
10
+ // hook name, because `/readyz` — Mode 2's only consumer — could reuse the
11
+ // route-claim opt-in it already had. A hook that is *not* a path (`onRequest`,
12
+ // `onError`, an auth gate) has no route to claim, so it needs a declaration
13
+ // site. This is that site.
14
+ //
15
+ // # Why a top-level `hooks` key, and not a per-route `middleware` field
16
+ //
17
+ // W311 named two candidates and deliberately picked neither. The `hooks` key
18
+ // wins on the doc's own terms:
19
+ //
20
+ // - A per-route `middleware` field is a *chain*, and "a general TS
21
+ // middleware chain" is an explicit W311 non-goal — it has ordering
22
+ // semantics (what runs before what, who may short-circuit, how errors
23
+ // propagate) that Mode 2 does not have and does not want.
24
+ // - Mode 2's defining property is that Rust owns the HTTP. A hook is
25
+ // therefore engine-addressed — the engine decides when to call `readyz`,
26
+ // not a route table — so the declaration belongs beside `routes`, not
27
+ // inside one.
28
+ // - Hook names are a **closed set** for the same reason: only the engine
29
+ // invokes a hook, so a name the engine does not know is dead code and a
30
+ // silent typo. Adding one is three deliberate edits — a name here, an
31
+ // adapter in `crates/mesofact-ssr/js/ssr_harness.js`, and the Rust call
32
+ // site that decides when it runs — and each is a real contract statement.
33
+ //
34
+ // # Adding a hook
35
+ //
36
+ // Add the name to `HOOK_NAMES`, teach `ssr_harness.js` how to call it (the
37
+ // default is plain Mode 2 — `(input) => verdict`, JSON both ways), and write
38
+ // the Rust call site. `hooks` in the manifest carries name → bundled module;
39
+ // `mesofact::ssr::spawn` registers each one on every isolate in the pool and
40
+ // `SsrChild::invoke_hook(name, input)` calls it.
41
+
42
+ /** Every hook name the engine knows how to invoke.
43
+ *
44
+ * `readyz` is the one hook that predates this declaration site, so it has two
45
+ * ways in (see {@link HOOK_ROUTE_CLAIMS}) and its module's contract is a
46
+ * Fetch handler rather than a plain Mode 2 function. Hooks added from here on
47
+ * are `(input) => verdict`. */
48
+ export const HOOK_NAMES = ["readyz"] as const;
49
+
50
+ export type HookName = (typeof HOOK_NAMES)[number];
51
+
52
+ /** The `hooks` block of `defineRoutes` — hook name → entrypoint path, relative
53
+ * to the project root, bundled to `dist/server/hooks/<name>.js`. */
54
+ export type HooksConfig = { readonly [K in HookName]?: string };
55
+
56
+ /** Hooks that may *alternatively* be declared by claiming a route, which is
57
+ * how `/readyz` shipped (R756-F5) before hooks existed.
58
+ *
59
+ * Route-claiming still works and is still the shortest path for an app that
60
+ * wants its readiness handler reachable as an ordinary Fetch handler. The
61
+ * `hooks` declaration is the better one for everything else: the module is
62
+ * not a route, so it never enters `ssr_prefixes`, is never forwarded to the
63
+ * SSR origin by the edge Worker, and is never shadowed by the Rust probe
64
+ * route it would otherwise collide with. Declaring both is rejected — two
65
+ * opt-ins for one verdict is ambiguous, not additive. */
66
+ export const HOOK_ROUTE_CLAIMS: { readonly [K in HookName]?: string } = {
67
+ readyz: "/readyz",
68
+ };
69
+
70
+ export function isHookName(name: string): name is HookName {
71
+ return (HOOK_NAMES as readonly string[]).includes(name);
72
+ }
@@ -0,0 +1,72 @@
1
+ // Hydration handoff — helpers an SSR route's Fetch handler calls to inline
2
+ // server-resolved data into the response body for the client to read on
3
+ // mount. Disposable when RSC streaming lands; until then this is the
4
+ // Universal cell's contract (W173 § "Hydration handoff").
5
+ //
6
+ // API surface is intentionally minimal — two pure functions. The handler
7
+ // composes them into its own HTML response, which means the consumer keeps
8
+ // full control over the document shell. The trade-off is that the consumer
9
+ // has to know its own build_id + hashed client-script name (read from the
10
+ // manifest at boot, or inject via env). Acceptable for the dogfood window;
11
+ // revisit when more consumers exist.
12
+ //
13
+ // Also published standalone as the `@mesofact/runtime/hydration` subpath
14
+ // export (R513-B11): this file must stay free of imports (or only import
15
+ // other import-free, browser-safe modules) so a client bundle can read
16
+ // `SPA_STATE_SCRIPT_ID` without dragging in the `@mesofact/runtime` barrel,
17
+ // which re-exports server-only code (`track-ctx.ts` → `node:async_hooks`,
18
+ // `config.ts` → `node:fs`) that rolldown can't tree-shake past.
19
+
20
+ // Build-time SPA shell consumes this id; the prerender weaves it in.
21
+ export const SPA_STATE_SCRIPT_ID = "__MESOFACT_STATE__" as const;
22
+
23
+ // Per-request SSR Universal handoff consumes this id; see W173 § "Hydration
24
+ // handoff" — the name is fixed (not configurable) so the client snippet can
25
+ // be copy-pasteable across consumers.
26
+ export const SSR_DATA_SCRIPT_ID = "__mesofact_data__" as const;
27
+
28
+ // JSON-encode `value` and escape the characters that could close the parent
29
+ // `<script>` tag early or be reinterpreted by the HTML parser:
30
+ //
31
+ // `<`, `>`, `&` — `</script>`, `<!--`, `<![CDATA[`, `&amp;` injection
32
+ // U+2028 / U+2029 — JS line separators valid in JSON but break inline
33
+ // scripts when not escaped
34
+ //
35
+ // `JSON.parse` decodes the `\uXXXX` escapes transparently, so the client
36
+ // reads back the original value via `JSON.parse(el.textContent)` with no
37
+ // special handling. Non-negotiable per W173 § "XSS escape rule".
38
+ export function escapeJsonForScriptTag(value: unknown): string {
39
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (c) => {
40
+ switch (c) {
41
+ case "<":
42
+ return "\\u003c";
43
+ case ">":
44
+ return "\\u003e";
45
+ case "&":
46
+ return "\\u0026";
47
+ case "\u2028":
48
+ return "\\u2028";
49
+ case "\u2029":
50
+ return "\\u2029";
51
+ default:
52
+ return c;
53
+ }
54
+ });
55
+ }
56
+
57
+ // Serialize `data` into the data-handoff `<script>` tag an SSR route's
58
+ // Fetch handler inlines into its response body. Read back on the client via
59
+ // `JSON.parse(document.getElementById("__mesofact_data__").textContent)`.
60
+ export function hydrationDataTag(data: unknown): string {
61
+ return `<script id="${SSR_DATA_SCRIPT_ID}" type="application/json">${escapeJsonForScriptTag(data)}</script>`;
62
+ }
63
+
64
+ // Build the module-script tag that loads the route's hydrate bundle. `src`
65
+ // is escaped against `"`, `<`, and `&` so a manifest-derived URL containing
66
+ // a stray quote can't break the attribute or the tag boundary; belt-and-
67
+ // braces — the manifest paths the build emits don't contain quotes, but
68
+ // consumers may compose paths from request data.
69
+ export function hydrationScriptTag(src: string): string {
70
+ const safeSrc = src.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
71
+ return `<script type="module" src="${safeSrc}"></script>`;
72
+ }
package/src/index.ts ADDED
@@ -0,0 +1,113 @@
1
+ // @mesofact/runtime — render contract types and adapter API.
2
+ // See `.yah/docs/architecture/mesofact.md`.
3
+
4
+ export type {
5
+ Region,
6
+ User,
7
+ Project,
8
+ RenderRequest,
9
+ CachePolicy,
10
+ Hydration,
11
+ RenderResult,
12
+ RenderFn,
13
+ } from "./contract.js";
14
+
15
+ export type {
16
+ ListOpts,
17
+ R2Object,
18
+ Source,
19
+ BlobSource,
20
+ KeyValueSource,
21
+ } from "./source.js";
22
+ export { BaseSource } from "./source.js";
23
+
24
+ export {
25
+ SourceError,
26
+ SourceUnavailableError,
27
+ SourceTimeoutError,
28
+ SourceQueryError,
29
+ RowNotFoundError,
30
+ } from "./errors.js";
31
+
32
+ export type {
33
+ RouteMode,
34
+ Placement,
35
+ Requires,
36
+ CachePolicyConfig,
37
+ PrerenderConfig,
38
+ RouteEntry,
39
+ ErrorRoutes,
40
+ RoutesConfig,
41
+ RetryOn,
42
+ RetryPolicy,
43
+ QueuePolicy,
44
+ ResiliencePolicy,
45
+ } from "./routes.js";
46
+
47
+ export { defineRoutes, DEFAULT_RESILIENCE_TIMEOUT_MS } from "./routes.js";
48
+
49
+ export type { HookName, HooksConfig } from "./hooks.js";
50
+ export { HOOK_NAMES, HOOK_ROUTE_CLAIMS, isHookName } from "./hooks.js";
51
+
52
+ export type {
53
+ ManifestVersion,
54
+ ManifestCachePolicy,
55
+ ManifestHydration,
56
+ ManifestPrerender,
57
+ ManifestRoute,
58
+ ManifestStaticAsset,
59
+ ManifestErrorRoutes,
60
+ ManifestHook,
61
+ ManifestHooks,
62
+ Manifest,
63
+ ResolvedPlacement,
64
+ } from "./manifest.js";
65
+
66
+ export { MANIFEST_VERSION } from "./manifest.js";
67
+
68
+ export type {
69
+ SourceScope,
70
+ SourceCatalog,
71
+ ValidationError,
72
+ ValidationErrorKind,
73
+ ValidationResult,
74
+ } from "./validate.js";
75
+
76
+ export { validate } from "./validate.js";
77
+
78
+ export { runInTrackCtx, currentTrackCtx } from "./track-ctx.js";
79
+ export type { TrackCtx } from "./track-ctx.js";
80
+
81
+ export {
82
+ SPA_STATE_SCRIPT_ID,
83
+ SSR_DATA_SCRIPT_ID,
84
+ escapeJsonForScriptTag,
85
+ hydrationDataTag,
86
+ hydrationScriptTag,
87
+ } from "./hydration.js";
88
+
89
+ export type { OpenGraph, TwitterCard, HeadLink, Head } from "./head.js";
90
+ export { renderHead, weaveHead } from "./head.js";
91
+
92
+ export { defineReadyz } from "./health.js";
93
+ export type { ReadyCheck } from "./health.js";
94
+
95
+ export { R2Adapter, r2, registerR2, clearR2Registry } from "./adapters/r2.js";
96
+ export type { R2Config } from "./adapters/r2.js";
97
+
98
+ export { SqliteAdapter, sqlite, registerSqlite, clearSqliteRegistry } from "./adapters/sqlite.js";
99
+ export type { SqliteConfig, SqliteRunner } from "./adapters/sqlite.js";
100
+
101
+ export {
102
+ loadConfig,
103
+ parseConfig,
104
+ registerSourcesFromConfig,
105
+ ConfigError,
106
+ } from "./config.js";
107
+ export type {
108
+ BuildConfig,
109
+ MesofactConfig,
110
+ R2SourceConfig,
111
+ SqliteSourceConfig,
112
+ SourceConfig,
113
+ } from "./config.js";
@@ -0,0 +1,104 @@
1
+ // Manifest types — the single document the build emits and the proxy boots
2
+ // from. Authored shape lives in `routes.ts` (RoutesConfig); the build enriches
3
+ // it with `build_id`, `static_assets`, hydration, and resolved entrypoint
4
+ // paths.
5
+ //
6
+ // Versioned independently of the mesofact binary; major bumps force restart.
7
+ // See `.yah/docs/architecture/mesofact.md` §"Manifest schema".
8
+
9
+ import type { HookName } from "./hooks.js";
10
+ import type { Placement, ResiliencePolicy, RouteMode, Requires } from "./routes.js";
11
+
12
+ // Placement as carried in the manifest — the build resolves `"auto"` to
13
+ // `"host"` or `"edge"` before emission, so consumers never see `"auto"`.
14
+ // See W173 § "Placement: validation rules".
15
+ export type ResolvedPlacement = Exclude<Placement, "auto">;
16
+
17
+ export const MANIFEST_VERSION = "1" as const;
18
+
19
+ export type ManifestVersion = typeof MANIFEST_VERSION;
20
+
21
+ export type ManifestCachePolicy = {
22
+ ttl: number;
23
+ swr?: number;
24
+ negative_ttl?: number;
25
+ vary?: readonly string[];
26
+ };
27
+
28
+ export type ManifestHydration = {
29
+ script: string;
30
+ code_split: readonly string[];
31
+ };
32
+
33
+ export type ManifestPrerender =
34
+ | { params: ReadonlyArray<Record<string, string>> }
35
+ | { from: string; query: string; param: string }
36
+ | { from_data: string; items_key: string; param: string }
37
+ // Instance-addressed: params are minted after the build (publish time); the
38
+ // build emits the server bundle + manifest entry and prerenders nothing.
39
+ // Serving resolves each instance through the pointer store (W270 §2). Mirrors
40
+ // `Prerender::Deferred` in `crates/mesofact/src/manifest.rs`.
41
+ | { deferred: true };
42
+
43
+ export type ManifestRoute = {
44
+ route: string;
45
+ mode: RouteMode;
46
+ render_entrypoint: string;
47
+ requires?: readonly Requires[];
48
+ source_reads?: readonly string[];
49
+ // Build-time data artifact paths (relative to project root). Present when
50
+ // the route declared `data_inputs`; used by the reconciler to detect which
51
+ // file changes trigger a rebuild of this route.
52
+ data_inputs?: readonly string[];
53
+ cache_policy: ManifestCachePolicy;
54
+ concurrency?: number;
55
+ hydration?: ManifestHydration;
56
+ prerender?: ManifestPrerender;
57
+ // SSR routes only; never "auto" (resolved at build time per W173).
58
+ placement?: ResolvedPlacement;
59
+ // SSR routes only (W181). Carried verbatim from the route declaration so
60
+ // the Worker (prod) and the mesofact-dev proxy (dev) apply the same retry/
61
+ // timeout policy around the origin hop. Absent = one attempt, default
62
+ // timeout, fail with 502 — exactly the pre-W181 behavior.
63
+ resilience?: ResiliencePolicy;
64
+ };
65
+
66
+ export type ManifestStaticAsset = {
67
+ key: string;
68
+ content_hash: string;
69
+ content_type: string;
70
+ immutable: boolean;
71
+ };
72
+
73
+ export type ManifestErrorRoutes = {
74
+ "404"?: string;
75
+ "5xx"?: string;
76
+ };
77
+
78
+ // One declared Mode 2 hook (W311 §2 / R756-F6). An object rather than a bare
79
+ // path string because the per-hook options W311 left open — chiefly "may this
80
+ // hook be async" (Open Decision 3) — land here without a schema break.
81
+ export type ManifestHook = {
82
+ // Bundled module, `dist/server/hooks/<name>.js`. Same shape and resolution
83
+ // rule as a route's `render_entrypoint`.
84
+ entrypoint: string;
85
+ };
86
+
87
+ export type ManifestHooks = { readonly [K in HookName]?: ManifestHook };
88
+
89
+ export type Manifest = {
90
+ version: ManifestVersion;
91
+ build_id: string;
92
+ routes: readonly ManifestRoute[];
93
+ static_assets: readonly ManifestStaticAsset[];
94
+ error_routes?: ManifestErrorRoutes;
95
+ // Declared Mode 2 hooks, name → bundled module. Absent when the workload
96
+ // declares none. Host-only: the edge Worker never invokes a hook, because
97
+ // Mode 2's whole premise is that the Rust host owns the HTTP around it.
98
+ hooks?: ManifestHooks;
99
+ // Derived from every `mode:"ssr"` route per W173 § "SSR_PREFIXES derivation
100
+ // rule". Used by mesofact-dev (proxy) and the CF Worker to forward matching
101
+ // paths to the SSR runtime. Segment-aware match: `path === p || path.startsWith(p + "/")`.
102
+ // Absent when the workload has no SSR routes.
103
+ ssr_prefixes?: readonly string[];
104
+ };