@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
@@ -0,0 +1,388 @@
1
+ // Manifest validator — structural shape + semantic rules. Used by the build
2
+ // (fail before bad HTML hits R2) and mirrored in `crates/mesofact/src/validate.rs`
3
+ // for the proxy. Shared fixtures live at `tests/fixtures/manifests/`.
4
+ //
5
+ // Semantic rules:
6
+ // R1. Mode 1 (static) + `source_reads` referencing a non-`global` source
7
+ // is rejected. Mode 1 builds can't enumerate per-user/per-project keys.
8
+ // R2. Mode 1 + `requires` containing "user" is rejected. The build can't
9
+ // know who the user is.
10
+ //
11
+ // See `.yah/docs/architecture/mesofact.md` §"Render axis × source axis".
12
+
13
+ import type {
14
+ Manifest,
15
+ ManifestHooks,
16
+ ManifestRoute,
17
+ ManifestStaticAsset,
18
+ ManifestErrorRoutes,
19
+ } from "./manifest.js";
20
+ import { MANIFEST_VERSION } from "./manifest.js";
21
+ import { HOOK_NAMES, isHookName } from "./hooks.js";
22
+
23
+ export type SourceScope = "global" | "project" | "user";
24
+
25
+ export type SourceCatalog = Record<string, { scope: SourceScope }>;
26
+
27
+ export type ValidationErrorKind =
28
+ | "shape"
29
+ | "unsupported_version"
30
+ | "mode1_scoped_source"
31
+ | "mode1_requires_user"
32
+ // R756-F6 — a `hooks` entry naming a hook the engine does not invoke. Its
33
+ // own kind rather than `shape` because the shape is fine; the name is not,
34
+ // and the Rust validator (which gets structure for free from serde) can
35
+ // only report it as a semantic rule.
36
+ | "unknown_hook";
37
+
38
+ export type ValidationError = {
39
+ kind: ValidationErrorKind;
40
+ path: string;
41
+ message: string;
42
+ };
43
+
44
+ export type ValidationResult =
45
+ | { ok: true; manifest: Manifest }
46
+ | { ok: false; errors: ValidationError[] };
47
+
48
+ const MODES = new Set(["static", "ssr", "spa"]);
49
+ const REQUIRES = new Set(["user", "project", "region"]);
50
+ const RESOLVED_PLACEMENTS = new Set(["host", "edge"]);
51
+
52
+ function isObject(x: unknown): x is Record<string, unknown> {
53
+ return typeof x === "object" && x !== null && !Array.isArray(x);
54
+ }
55
+
56
+ function shape(path: string, message: string): ValidationError {
57
+ return { kind: "shape", path, message };
58
+ }
59
+
60
+ function checkRoute(idx: number, raw: unknown, errs: ValidationError[]): ManifestRoute | null {
61
+ const base = `routes[${idx}]`;
62
+ if (!isObject(raw)) {
63
+ errs.push(shape(base, "expected object"));
64
+ return null;
65
+ }
66
+
67
+ const out: Partial<ManifestRoute> = {};
68
+
69
+ if (typeof raw.route !== "string") errs.push(shape(`${base}.route`, "expected string"));
70
+ else out.route = raw.route;
71
+
72
+ if (typeof raw.mode !== "string" || !MODES.has(raw.mode)) {
73
+ errs.push(shape(`${base}.mode`, "expected 'static' | 'ssr' | 'spa'"));
74
+ } else {
75
+ out.mode = raw.mode as ManifestRoute["mode"];
76
+ }
77
+
78
+ if (typeof raw.render_entrypoint !== "string") {
79
+ errs.push(shape(`${base}.render_entrypoint`, "expected string"));
80
+ } else {
81
+ out.render_entrypoint = raw.render_entrypoint;
82
+ }
83
+
84
+ if (raw.requires !== undefined) {
85
+ if (!Array.isArray(raw.requires) || raw.requires.some((r) => typeof r !== "string" || !REQUIRES.has(r))) {
86
+ errs.push(shape(`${base}.requires`, "expected ('user' | 'project' | 'region')[]"));
87
+ } else {
88
+ out.requires = raw.requires as ManifestRoute["requires"];
89
+ }
90
+ }
91
+
92
+ if (raw.source_reads !== undefined) {
93
+ if (!Array.isArray(raw.source_reads) || raw.source_reads.some((s) => typeof s !== "string")) {
94
+ errs.push(shape(`${base}.source_reads`, "expected string[]"));
95
+ } else {
96
+ out.source_reads = raw.source_reads as readonly string[];
97
+ }
98
+ }
99
+
100
+ if (raw.data_inputs !== undefined) {
101
+ if (!Array.isArray(raw.data_inputs) || raw.data_inputs.some((s) => typeof s !== "string")) {
102
+ errs.push(shape(`${base}.data_inputs`, "expected string[]"));
103
+ } else {
104
+ out.data_inputs = raw.data_inputs as readonly string[];
105
+ }
106
+ }
107
+
108
+ if (!isObject(raw.cache_policy) || typeof raw.cache_policy.ttl !== "number") {
109
+ errs.push(shape(`${base}.cache_policy`, "expected { ttl: number, ... }"));
110
+ } else {
111
+ out.cache_policy = raw.cache_policy as ManifestRoute["cache_policy"];
112
+ }
113
+
114
+ if (raw.concurrency !== undefined) {
115
+ if (typeof raw.concurrency !== "number") {
116
+ errs.push(shape(`${base}.concurrency`, "expected number"));
117
+ } else {
118
+ out.concurrency = raw.concurrency;
119
+ }
120
+ }
121
+
122
+ if (raw.hydration !== undefined) {
123
+ const h = raw.hydration;
124
+ if (!isObject(h) || typeof h.script !== "string" || !Array.isArray(h.code_split)) {
125
+ errs.push(shape(`${base}.hydration`, "expected { script: string, code_split: string[] }"));
126
+ } else {
127
+ out.hydration = h as ManifestRoute["hydration"];
128
+ }
129
+ }
130
+
131
+ if (raw.prerender !== undefined) {
132
+ if (!isObject(raw.prerender)) {
133
+ errs.push(shape(`${base}.prerender`, "expected object"));
134
+ } else {
135
+ out.prerender = raw.prerender as ManifestRoute["prerender"];
136
+ }
137
+ }
138
+
139
+ if (raw.placement !== undefined) {
140
+ if (typeof raw.placement !== "string" || !RESOLVED_PLACEMENTS.has(raw.placement)) {
141
+ // The build resolves `"auto"` to a concrete value before emission, so
142
+ // the manifest only ever carries `"host"` or `"edge"`.
143
+ errs.push(shape(`${base}.placement`, "expected 'host' | 'edge'"));
144
+ } else if (out.mode !== undefined && out.mode !== "ssr") {
145
+ errs.push(shape(`${base}.placement`, "placement is only valid on mode:'ssr'"));
146
+ } else {
147
+ out.placement = raw.placement as ManifestRoute["placement"];
148
+ }
149
+ }
150
+
151
+ if (raw.resilience !== undefined) {
152
+ const res = checkResilience(base, raw.resilience, out.mode, errs);
153
+ if (res) out.resilience = res;
154
+ }
155
+
156
+ if (errs.length && errs.some((e) => e.path.startsWith(base))) return null;
157
+ return out as ManifestRoute;
158
+ }
159
+
160
+ const RETRY_ON_VALUES = new Set(["connection", "5xx", "any"]);
161
+
162
+ // Structural check for the W181 resilience block as carried in the manifest.
163
+ // `defineRoutes` already enforced the semantic rules (ssr-only, queue
164
+ // reserved, backoff length); this pass keeps a hand-written manifest honest
165
+ // and — like data_inputs before it (R014) — prevents the field from being
166
+ // silently stripped when the validator rebuilds the route object.
167
+ function checkResilience(
168
+ base: string,
169
+ raw: unknown,
170
+ mode: ManifestRoute["mode"] | undefined,
171
+ errs: ValidationError[],
172
+ ): ManifestRoute["resilience"] | null {
173
+ const path = `${base}.resilience`;
174
+ if (!isObject(raw)) {
175
+ errs.push(shape(path, "expected object"));
176
+ return null;
177
+ }
178
+ if (mode !== undefined && mode !== "ssr") {
179
+ errs.push(shape(path, "resilience is only valid on mode:'ssr'"));
180
+ return null;
181
+ }
182
+ if (raw.queue !== undefined) {
183
+ errs.push(shape(`${path}.queue`, "queue policy is reserved for v2 (W181) and must not be emitted"));
184
+ return null;
185
+ }
186
+ if (raw.timeout_ms !== undefined && typeof raw.timeout_ms !== "number") {
187
+ errs.push(shape(`${path}.timeout_ms`, "expected number"));
188
+ return null;
189
+ }
190
+ if (raw.retry !== undefined) {
191
+ const retry = raw.retry;
192
+ if (
193
+ !isObject(retry) ||
194
+ typeof retry.attempts !== "number" ||
195
+ !Array.isArray(retry.backoff_ms) ||
196
+ retry.backoff_ms.some((b) => typeof b !== "number") ||
197
+ retry.backoff_ms.length !== retry.attempts - 1
198
+ ) {
199
+ errs.push(
200
+ shape(`${path}.retry`, "expected { attempts: number, backoff_ms: number[attempts - 1], ... }"),
201
+ );
202
+ return null;
203
+ }
204
+ if (retry.retry_on !== undefined && (typeof retry.retry_on !== "string" || !RETRY_ON_VALUES.has(retry.retry_on))) {
205
+ errs.push(shape(`${path}.retry.retry_on`, "expected 'connection' | '5xx' | 'any'"));
206
+ return null;
207
+ }
208
+ if (retry.budget_ms !== undefined && typeof retry.budget_ms !== "number") {
209
+ errs.push(shape(`${path}.retry.budget_ms`, "expected number"));
210
+ return null;
211
+ }
212
+ }
213
+ return raw as ManifestRoute["resilience"];
214
+ }
215
+
216
+ function checkStaticAsset(idx: number, raw: unknown, errs: ValidationError[]): ManifestStaticAsset | null {
217
+ const base = `static_assets[${idx}]`;
218
+ if (!isObject(raw)) {
219
+ errs.push(shape(base, "expected object"));
220
+ return null;
221
+ }
222
+ if (
223
+ typeof raw.key !== "string" ||
224
+ typeof raw.content_hash !== "string" ||
225
+ typeof raw.content_type !== "string" ||
226
+ typeof raw.immutable !== "boolean"
227
+ ) {
228
+ errs.push(shape(base, "expected { key, content_hash, content_type, immutable }"));
229
+ return null;
230
+ }
231
+ return {
232
+ key: raw.key,
233
+ content_hash: raw.content_hash,
234
+ content_type: raw.content_type,
235
+ immutable: raw.immutable,
236
+ };
237
+ }
238
+
239
+ function checkErrorRoutes(raw: unknown, errs: ValidationError[]): ManifestErrorRoutes | undefined {
240
+ if (raw === undefined) return undefined;
241
+ if (!isObject(raw)) {
242
+ errs.push(shape("error_routes", "expected object"));
243
+ return undefined;
244
+ }
245
+ const out: ManifestErrorRoutes = {};
246
+ if (raw["404"] !== undefined) {
247
+ if (typeof raw["404"] !== "string") errs.push(shape("error_routes.404", "expected string"));
248
+ else out["404"] = raw["404"];
249
+ }
250
+ if (raw["5xx"] !== undefined) {
251
+ if (typeof raw["5xx"] !== "string") errs.push(shape("error_routes.5xx", "expected string"));
252
+ else out["5xx"] = raw["5xx"];
253
+ }
254
+ return out;
255
+ }
256
+
257
+ // Mode 2 hooks (R756-F6). The name set is closed — only the engine invokes a
258
+ // hook, so an unrecognised name in a manifest is a build that emitted
259
+ // something nothing will ever call, not a forward-compatible extension.
260
+ function checkHooks(raw: unknown, errs: ValidationError[]): ManifestHooks | undefined {
261
+ if (raw === undefined) return undefined;
262
+ if (!isObject(raw)) {
263
+ errs.push(shape("hooks", "expected object"));
264
+ return undefined;
265
+ }
266
+ const out: Record<string, { entrypoint: string }> = {};
267
+ for (const [name, value] of Object.entries(raw)) {
268
+ if (!isHookName(name)) {
269
+ errs.push({
270
+ kind: "unknown_hook",
271
+ path: `hooks.${name}`,
272
+ message: `unknown hook '${name}' (known: ${HOOK_NAMES.join(", ")})`,
273
+ });
274
+ continue;
275
+ }
276
+ if (!isObject(value) || typeof value.entrypoint !== "string") {
277
+ errs.push(shape(`hooks.${name}`, "expected { entrypoint: string }"));
278
+ continue;
279
+ }
280
+ out[name] = { entrypoint: value.entrypoint };
281
+ }
282
+ return out as ManifestHooks;
283
+ }
284
+
285
+ function checkRules(manifest: Manifest, catalog: SourceCatalog, errs: ValidationError[]): void {
286
+ manifest.routes.forEach((route, idx) => {
287
+ if (route.mode !== "static") return;
288
+
289
+ if (route.source_reads) {
290
+ for (const name of route.source_reads) {
291
+ const entry = catalog[name];
292
+ if (!entry) {
293
+ errs.push({
294
+ kind: "shape",
295
+ path: `routes[${idx}].source_reads`,
296
+ message: `route ${route.route}: source '${name}' not declared in catalog`,
297
+ });
298
+ continue;
299
+ }
300
+ if (entry.scope !== "global") {
301
+ errs.push({
302
+ kind: "mode1_scoped_source",
303
+ path: `routes[${idx}].source_reads`,
304
+ message: `route ${route.route}: Mode 1 cannot read from non-'global' source '${name}' (scope='${entry.scope}')`,
305
+ });
306
+ }
307
+ }
308
+ }
309
+
310
+ if (route.requires?.includes("user")) {
311
+ errs.push({
312
+ kind: "mode1_requires_user",
313
+ path: `routes[${idx}].requires`,
314
+ message: `route ${route.route}: Mode 1 cannot require 'user' (the build can't enumerate users)`,
315
+ });
316
+ }
317
+ });
318
+ }
319
+
320
+ export function validate(input: unknown, catalog: SourceCatalog = {}): ValidationResult {
321
+ const errs: ValidationError[] = [];
322
+
323
+ if (!isObject(input)) {
324
+ return { ok: false, errors: [shape("$", "expected object")] };
325
+ }
326
+
327
+ if (input.version !== MANIFEST_VERSION) {
328
+ errs.push({
329
+ kind: "unsupported_version",
330
+ path: "version",
331
+ message: `unsupported manifest version '${String(input.version)}' (expected '${MANIFEST_VERSION}')`,
332
+ });
333
+ }
334
+
335
+ if (typeof input.build_id !== "string") errs.push(shape("build_id", "expected string"));
336
+
337
+ if (!Array.isArray(input.routes)) {
338
+ errs.push(shape("routes", "expected array"));
339
+ return { ok: false, errors: errs };
340
+ }
341
+
342
+ const routes: ManifestRoute[] = [];
343
+ input.routes.forEach((r, i) => {
344
+ const route = checkRoute(i, r, errs);
345
+ if (route) routes.push(route);
346
+ });
347
+
348
+ const staticAssetsRaw = input.static_assets ?? [];
349
+ if (!Array.isArray(staticAssetsRaw)) {
350
+ errs.push(shape("static_assets", "expected array"));
351
+ return { ok: false, errors: errs };
352
+ }
353
+ const staticAssets: ManifestStaticAsset[] = [];
354
+ staticAssetsRaw.forEach((a, i) => {
355
+ const asset = checkStaticAsset(i, a, errs);
356
+ if (asset) staticAssets.push(asset);
357
+ });
358
+
359
+ const errorRoutes = checkErrorRoutes(input.error_routes, errs);
360
+
361
+ let ssrPrefixes: readonly string[] | undefined;
362
+ if (input.ssr_prefixes !== undefined) {
363
+ if (!Array.isArray(input.ssr_prefixes) || input.ssr_prefixes.some((s) => typeof s !== "string")) {
364
+ errs.push(shape("ssr_prefixes", "expected string[]"));
365
+ } else {
366
+ ssrPrefixes = input.ssr_prefixes as readonly string[];
367
+ }
368
+ }
369
+
370
+ const hooks = checkHooks(input.hooks, errs);
371
+
372
+ if (errs.length) return { ok: false, errors: errs };
373
+
374
+ const manifest: Manifest = {
375
+ version: input.version as Manifest["version"],
376
+ build_id: input.build_id as string,
377
+ routes,
378
+ static_assets: staticAssets,
379
+ ...(errorRoutes ? { error_routes: errorRoutes } : {}),
380
+ ...(ssrPrefixes !== undefined ? { ssr_prefixes: ssrPrefixes } : {}),
381
+ ...(hooks !== undefined ? { hooks } : {}),
382
+ };
383
+
384
+ checkRules(manifest, catalog, errs);
385
+
386
+ if (errs.length) return { ok: false, errors: errs };
387
+ return { ok: true, manifest };
388
+ }