@warlock.js/web 5.0.0

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 (182) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/esm/build/contribution.d.mts +55 -0
  5. package/esm/build/contribution.mjs +132 -0
  6. package/esm/build/contribution.mjs.map +1 -0
  7. package/esm/build/discover-pages.mjs +294 -0
  8. package/esm/build/discover-pages.mjs.map +1 -0
  9. package/esm/build/generate-client-registry.mjs +123 -0
  10. package/esm/build/generate-client-registry.mjs.map +1 -0
  11. package/esm/build/generate-pages-barrel.mjs +330 -0
  12. package/esm/build/generate-pages-barrel.mjs.map +1 -0
  13. package/esm/build/read-route-exports.mjs +212 -0
  14. package/esm/build/read-route-exports.mjs.map +1 -0
  15. package/esm/client/build-hydrated-tree.mjs +113 -0
  16. package/esm/client/build-hydrated-tree.mjs.map +1 -0
  17. package/esm/client/hydrate-page.mjs +73 -0
  18. package/esm/client/hydrate-page.mjs.map +1 -0
  19. package/esm/client/navigation/current-route.d.mts +91 -0
  20. package/esm/client/navigation/current-route.mjs +66 -0
  21. package/esm/client/navigation/current-route.mjs.map +1 -0
  22. package/esm/client/navigation/fetch-page-data.mjs +97 -0
  23. package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
  24. package/esm/client/navigation/navigation-root.mjs +196 -0
  25. package/esm/client/navigation/navigation-root.mjs.map +1 -0
  26. package/esm/client/navigation/prefetch.mjs +110 -0
  27. package/esm/client/navigation/prefetch.mjs.map +1 -0
  28. package/esm/client/navigation/refresh.d.mts +40 -0
  29. package/esm/client/navigation/refresh.mjs +146 -0
  30. package/esm/client/navigation/refresh.mjs.map +1 -0
  31. package/esm/client/navigation/verbs.d.mts +55 -0
  32. package/esm/client/navigation/verbs.mjs +66 -0
  33. package/esm/client/navigation/verbs.mjs.map +1 -0
  34. package/esm/client/runtime/index.d.mts +4 -0
  35. package/esm/client/runtime/index.mjs +4 -0
  36. package/esm/client/runtime/manifest.d.mts +8 -0
  37. package/esm/client/runtime/manifest.mjs +77 -0
  38. package/esm/client/runtime/manifest.mjs.map +1 -0
  39. package/esm/client/runtime/matcher.d.mts +7 -0
  40. package/esm/client/runtime/matcher.mjs +169 -0
  41. package/esm/client/runtime/matcher.mjs.map +1 -0
  42. package/esm/client/runtime/types.d.mts +22 -0
  43. package/esm/components/default-app.mjs +32 -0
  44. package/esm/components/default-app.mjs.map +1 -0
  45. package/esm/components/document-context.d.mts +1 -0
  46. package/esm/components/document-context.mjs +24 -0
  47. package/esm/components/document-context.mjs.map +1 -0
  48. package/esm/components/head.d.mts +14 -0
  49. package/esm/components/head.mjs +63 -0
  50. package/esm/components/head.mjs.map +1 -0
  51. package/esm/components/link.d.mts +157 -0
  52. package/esm/components/link.mjs +194 -0
  53. package/esm/components/link.mjs.map +1 -0
  54. package/esm/components/scripts.d.mts +14 -0
  55. package/esm/components/scripts.mjs +21 -0
  56. package/esm/components/scripts.mjs.map +1 -0
  57. package/esm/connector/index.d.mts +6 -0
  58. package/esm/connector/index.mjs +7 -0
  59. package/esm/context.d.mts +107 -0
  60. package/esm/hydration/index.d.mts +1 -0
  61. package/esm/hydration/index.mjs +33 -0
  62. package/esm/hydration/index.mjs.map +1 -0
  63. package/esm/hydration-payload.d.mts +1 -0
  64. package/esm/hydration-payload.mjs +72 -0
  65. package/esm/hydration-payload.mjs.map +1 -0
  66. package/esm/index.d.mts +35 -0
  67. package/esm/index.mjs +12 -0
  68. package/esm/loaders.d.mts +47 -0
  69. package/esm/metadata.d.mts +54 -0
  70. package/esm/props.d.mts +42 -0
  71. package/esm/route.d.mts +30 -0
  72. package/esm/routing/compose-route-path.mjs +33 -0
  73. package/esm/routing/compose-route-path.mjs.map +1 -0
  74. package/esm/routing/data-request.mjs +52 -0
  75. package/esm/routing/data-request.mjs.map +1 -0
  76. package/esm/routing/layout-policy.mjs +78 -0
  77. package/esm/routing/layout-policy.mjs.map +1 -0
  78. package/esm/routing/navigator.mjs +21 -0
  79. package/esm/routing/navigator.mjs.map +1 -0
  80. package/esm/routing/query-string.d.mts +250 -0
  81. package/esm/routing/query-string.mjs +340 -0
  82. package/esm/routing/query-string.mjs.map +1 -0
  83. package/esm/routing/route-identity.mjs +68 -0
  84. package/esm/routing/route-identity.mjs.map +1 -0
  85. package/esm/routing/route-table.d.mts +34 -0
  86. package/esm/routing/route-table.mjs +182 -0
  87. package/esm/routing/route-table.mjs.map +1 -0
  88. package/esm/routing/router-events.d.mts +159 -0
  89. package/esm/routing/router-events.mjs +83 -0
  90. package/esm/routing/router-events.mjs.map +1 -0
  91. package/esm/server/buffered-response.mjs +115 -0
  92. package/esm/server/buffered-response.mjs.map +1 -0
  93. package/esm/server/build-hydration-payload.mjs +27 -0
  94. package/esm/server/build-hydration-payload.mjs.map +1 -0
  95. package/esm/server/client-asset-url-prefix.d.mts +22 -0
  96. package/esm/server/client-asset-url-prefix.mjs +23 -0
  97. package/esm/server/client-asset-url-prefix.mjs.map +1 -0
  98. package/esm/server/create-page-module-loader.mjs +49 -0
  99. package/esm/server/create-page-module-loader.mjs.map +1 -0
  100. package/esm/server/create-page-route-handler.mjs +107 -0
  101. package/esm/server/create-page-route-handler.mjs.map +1 -0
  102. package/esm/server/dev-server.mjs +167 -0
  103. package/esm/server/dev-server.mjs.map +1 -0
  104. package/esm/server/execute-page-request.d.mts +1 -0
  105. package/esm/server/execute-page-request.mjs +166 -0
  106. package/esm/server/execute-page-request.mjs.map +1 -0
  107. package/esm/server/execute-page-request.types.d.mts +1 -0
  108. package/esm/server/hydration-client-url.d.mts +37 -0
  109. package/esm/server/hydration-client-url.mjs +101 -0
  110. package/esm/server/hydration-client-url.mjs.map +1 -0
  111. package/esm/server/index.mjs +13 -0
  112. package/esm/server/install-page-routes-from-manifest.mjs +133 -0
  113. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
  114. package/esm/server/install-page-routes.d.mts +1 -0
  115. package/esm/server/install-page-routes.mjs +6 -0
  116. package/esm/server/install-production-page-routes.mjs +55 -0
  117. package/esm/server/install-production-page-routes.mjs.map +1 -0
  118. package/esm/server/match-page-route.mjs +48 -0
  119. package/esm/server/match-page-route.mjs.map +1 -0
  120. package/esm/server/page-context.d.mts +1 -0
  121. package/esm/server/page-context.mjs +33 -0
  122. package/esm/server/page-context.mjs.map +1 -0
  123. package/esm/server/page-manifest.d.mts +102 -0
  124. package/esm/server/page-manifest.mjs +31 -0
  125. package/esm/server/page-manifest.mjs.map +1 -0
  126. package/esm/server/render-page.mjs +214 -0
  127. package/esm/server/render-page.mjs.map +1 -0
  128. package/esm/server/resolve-page-metadata.mjs +33 -0
  129. package/esm/server/resolve-page-metadata.mjs.map +1 -0
  130. package/esm/server/resolve-validation-data.mjs +27 -0
  131. package/esm/server/resolve-validation-data.mjs.map +1 -0
  132. package/esm/server/settle-page-response.d.mts +1 -0
  133. package/esm/server/settle-page-response.mjs +90 -0
  134. package/esm/server/settle-page-response.mjs.map +1 -0
  135. package/esm/server/stylesheet-urls.mjs +65 -0
  136. package/esm/server/stylesheet-urls.mjs.map +1 -0
  137. package/esm/server/web-connector-factory.d.mts +25 -0
  138. package/esm/server/web-connector-factory.mjs +121 -0
  139. package/esm/server/web-connector-factory.mjs.map +1 -0
  140. package/esm/server/web-connector.d.mts +34 -0
  141. package/esm/server/web-connector.mjs +516 -0
  142. package/esm/server/web-connector.mjs.map +1 -0
  143. package/esm/shared.d.mts +22 -0
  144. package/esm/shared.mjs +223 -0
  145. package/esm/shared.mjs.map +1 -0
  146. package/esm/validation.d.mts +31 -0
  147. package/esm/vite/app-convention-aliases.mjs +43 -0
  148. package/esm/vite/app-convention-aliases.mjs.map +1 -0
  149. package/esm/vite/build-client.d.mts +38 -0
  150. package/esm/vite/build-client.mjs +72 -0
  151. package/esm/vite/build-client.mjs.map +1 -0
  152. package/esm/vite/gate-a-resolve.d.mts +57 -0
  153. package/esm/vite/gate-a-resolve.mjs +834 -0
  154. package/esm/vite/gate-a-resolve.mjs.map +1 -0
  155. package/esm/vite/gate-b-secrets.d.mts +57 -0
  156. package/esm/vite/gate-b-secrets.mjs +299 -0
  157. package/esm/vite/gate-b-secrets.mjs.map +1 -0
  158. package/esm/vite/gate-c-verify.d.mts +118 -0
  159. package/esm/vite/gate-c-verify.mjs +263 -0
  160. package/esm/vite/gate-c-verify.mjs.map +1 -0
  161. package/esm/vite/hydration-entries.d.mts +17 -0
  162. package/esm/vite/hydration-entries.mjs +45 -0
  163. package/esm/vite/hydration-entries.mjs.map +1 -0
  164. package/esm/vite/index.d.mts +126 -0
  165. package/esm/vite/index.mjs +146 -0
  166. package/esm/vite/index.mjs.map +1 -0
  167. package/esm/vite/page-registry-plugin.d.mts +43 -0
  168. package/esm/vite/page-registry-plugin.mjs +138 -0
  169. package/esm/vite/page-registry-plugin.mjs.map +1 -0
  170. package/esm/vite/projection.d.mts +31 -0
  171. package/esm/vite/projection.mjs +384 -0
  172. package/esm/vite/projection.mjs.map +1 -0
  173. package/llms-full.txt +1041 -0
  174. package/llms.txt +21 -0
  175. package/package.json +77 -0
  176. package/skills/add-web-to-an-app/SKILL.md +103 -0
  177. package/skills/create-a-page/SKILL.md +141 -0
  178. package/skills/load-page-data/SKILL.md +183 -0
  179. package/skills/navigate-on-the-client/SKILL.md +182 -0
  180. package/skills/serve-styles/SKILL.md +145 -0
  181. package/skills/use-layouts/SKILL.md +121 -0
  182. package/skills/write-the-root/SKILL.md +131 -0
@@ -0,0 +1,118 @@
1
+ import { EnvironmentClassifier, EnvironmentClassifierOptions } from "./gate-a-resolve.mjs";
2
+ import { PublicEnvTracker } from "./gate-b-secrets.mjs";
3
+ import { Plugin } from "vite";
4
+
5
+ //#region ../web/src/vite/gate-c-verify.d.ts
6
+ interface ServerExportLeak {
7
+ fileName: string;
8
+ exportName: string;
9
+ line?: number;
10
+ }
11
+ interface ServerImportEdgeLeak {
12
+ fileName: string;
13
+ moduleId: string;
14
+ packageName: string;
15
+ }
16
+ interface PublicEnvManifestEntry {
17
+ key: string;
18
+ /** `null` when `redacted` is true — see `isSafeToShowValue`. */
19
+ value: string | null;
20
+ redacted: boolean;
21
+ }
22
+ /**
23
+ * A structurally-minimal view of Rollup's `OutputBundle` — only the fields
24
+ * Gate C actually reads. Deliberately NOT the real `OutputBundle`/`OutputChunk`
25
+ * types: Part 1's own discipline requires proving Gate C catches a bundle
26
+ * that a real pipeline could never produce (a hand-built leaked chunk), and
27
+ * forcing every test fixture to satisfy Rollup's full chunk shape would
28
+ * fight that requirement for no safety benefit — Vite's real `generateBundle`
29
+ * hook still hands Gate C a real `OutputBundle`, which is a structural
30
+ * superset of this.
31
+ */
32
+ type BundleLike = Record<string, {
33
+ type?: string;
34
+ fileName?: string;
35
+ code?: string;
36
+ moduleIds?: readonly string[];
37
+ }>;
38
+ /**
39
+ * Thrown when an emitted chunk cannot be parsed, which means Gate C could not
40
+ * inspect it. A gate that skips what it cannot read reports "no violation
41
+ * found" on precisely the input it failed to look at — the one input where
42
+ * that answer is worthless. So an unreadable chunk fails the build instead.
43
+ */
44
+ /**
45
+ * Part 1, item 1: parses each emitted chunk's ACTUAL code (never pre-transform
46
+ * source) and looks for a top-level binding named one of the five server
47
+ * exports. A parse failure on an emitted chunk FAILS the gate
48
+ * (`UnverifiableChunkError`) — it is never skipped. Skipping would report the
49
+ * bundle clean on the one chunk the gate did not actually inspect, which is a
50
+ * safety check failing open; a build stopped on an unreadable chunk is
51
+ * recoverable, a boundary silently unenforced is not.
52
+ *
53
+ * This SURVIVES real minification (pinned by the `gate-c-verify.spec.ts`
54
+ * "D.8" describe block). A minifier (esbuild,
55
+ * Vite's default) is only free to rename LOCAL bindings; the exported name
56
+ * itself is part of the module's public interface and is never mangled — a
57
+ * minified `export const route = ...` still emits `export { e as route }`,
58
+ * with the local identifier renamed (`e`) but `route` intact as the specifier's
59
+ * `exported` name, which is exactly the field `collectServerExportNames`
60
+ * reads for the `ExportNamedDeclaration` specifier form. No name-independent
61
+ * redesign needed here — verified against a real `build.minify: true` output,
62
+ * not assumed.
63
+ */
64
+ declare function findLeakedServerExports(bundle: BundleLike): ServerExportLeak[];
65
+ /**
66
+ * Part 1, item 2: walks every emitted chunk's `moduleIds` (the resolved,
67
+ * absolute file paths of every source module Rollup actually bundled into
68
+ * that chunk) and classifies each one via the SAME `EnvironmentClassifier`
69
+ * Gate A's `resolveId` uses, re-derived from `gate-a-resolve.ts` rather than
70
+ * hand-rolled a second time. A moduleId that maps to a governed-scope
71
+ * package classified `"server"` is an import edge Gate A should already
72
+ * have refused — if it's here anyway, the earlier gate has a bug.
73
+ *
74
+ * D.8 verdict: this SURVIVES real minification unconditionally, and for a
75
+ * stronger reason than item 1 above — it never reads emitted code text or
76
+ * identifier names at all. `moduleIds` is Rollup's own bundling metadata
77
+ * (which resolved source files ended up in this chunk), untouched by
78
+ * minification, which only rewrites code, not that metadata. Verified
79
+ * against a real `build.minify: true` output in `gate-c-verify.spec.ts`.
80
+ */
81
+ declare function findLeakedServerImportEdges(bundle: BundleLike, classifier: Pick<EnvironmentClassifier, "environmentOf" | "packageNameForFilePath">): ServerImportEdgeLeak[];
82
+ /**
83
+ * The enumerated, human-reviewable list of
84
+ * every `PUBLIC_*` key actually inlined into the client bundle. Built
85
+ * directly from `tracker.referencedKeys` — the exact same `Set` Gate B's own
86
+ * `generateBundle` unread-key check (`gate-b-secrets.ts`) reads from — so
87
+ * this manifest and that exclusion logic agree BY CONSTRUCTION: they are two
88
+ * readers of one Set, not two independent recomputations of "which keys were
89
+ * read" that could drift apart. A key only reaches `referencedKeys` once
90
+ * `findViolation` in `gate-b-secrets.ts` has seen a statically-resolved
91
+ * `import.meta.env.PUBLIC_X` read for it, which is also precisely the
92
+ * condition under which Gate B allows its value to be inlined at all — an
93
+ * unread key that somehow leaked in anyway is a Gate B `generateBundle`
94
+ * BUILD FAILURE (see `gate-b-secrets.ts`), never silently listed here.
95
+ */
96
+ declare function buildPublicEnvManifest(tracker: PublicEnvTracker): PublicEnvManifestEntry[];
97
+ interface GateCOptions extends EnvironmentClassifierOptions {
98
+ /**
99
+ * Shared with `gateBSecrets({ tracker })` — required for the manifest to
100
+ * reflect what THIS build's Gate B pass actually saw. Defaults to a
101
+ * private, unshared tracker (always empty) if omitted, which is only ever
102
+ * correct when Gate C runs standalone in a test.
103
+ */
104
+ tracker?: PublicEnvTracker;
105
+ /** Defaults to `"warlock-env-manifest.json"` (Suki's suggested name). */
106
+ manifestFileName?: string;
107
+ }
108
+ /**
109
+ * The client-build Vite plugin. Runs only at `generateBundle` — Gate C has
110
+ * nothing to say about source, only about the bundle Rollup actually wrote.
111
+ * Skipped for the SSR/server build, same as Gate B (`gate-b-secrets.ts`):
112
+ * a page's server exports are meant to survive in that build; only the
113
+ * client build is judged.
114
+ */
115
+ declare function gateCVerify(options?: GateCOptions): Plugin;
116
+ //#endregion
117
+ export { GateCOptions, PublicEnvManifestEntry, ServerExportLeak, ServerImportEdgeLeak, buildPublicEnvManifest, findLeakedServerExports, findLeakedServerImportEdges, gateCVerify };
118
+ //# sourceMappingURL=gate-c-verify.d.mts.map
@@ -0,0 +1,263 @@
1
+ import { createEnvironmentClassifier } from "./gate-a-resolve.mjs";
2
+ import { createPublicEnvTracker } from "./gate-b-secrets.mjs";
3
+ import { SERVER_EXPORT_NAMES } from "./projection.mjs";
4
+ import { parse } from "@babel/parser";
5
+
6
+ //#region ../web/src/vite/gate-c-verify.ts
7
+ /**
8
+ * Gate C — emitted-output verification.
9
+ *
10
+ * Projection removes the five server exports before the client graph forms.
11
+ * Gate A refuses forbidden import PATHS. Gate B refuses inline secret reads.
12
+ * All three act BEFORE or DURING the build, on source or the module graph —
13
+ * none of them ever looks at what actually came out the other end. Gate C
14
+ * is that check: a build-time assertion on the EMITTED client bundle for
15
+ * every page, verifying:
16
+ *
17
+ * 1. The emitted code contains none of the five server export names
18
+ * (`route`, `middleware`, `validation`, `loader`, `metadata`) as a
19
+ * top-level binding, exported or not — `findLeakedServerExports`.
20
+ * 2. The emitted module graph (Rollup's `OutputBundle`, as seen in
21
+ * `generateBundle`) contains no import edge into a
22
+ * `warlock.environment: "server"` (or absent-defaulting-to-server)
23
+ * package — re-derived from the exact classifier Gate A uses
24
+ * (`gate-a-resolve.ts`'s `createEnvironmentClassifier`), never a second,
25
+ * independently-drifting classification scheme — `findLeakedServerImportEdges`.
26
+ *
27
+ * This is defense in depth: if projection or Gate A has a bug that lets
28
+ * something through, Gate C is what turns "shipped a leak" into "build
29
+ * fails." Both check functions are exported as PURE functions over a bundle
30
+ * object precisely so a test can construct a bundle where projection/Gate A
31
+ * are bypassed entirely (a hand-built leaked chunk, never produced by a real
32
+ * pipeline run) and still prove Gate C catches it independently — re-running
33
+ * the D.1-D.5 happy-path fixtures through the composed pipeline only proves
34
+ * the happy path, never that Gate C itself has teeth.
35
+ *
36
+ * Gate C also emits the inlined-`PUBLIC_*`-value manifest — see
37
+ * `buildPublicEnvManifest` below — and is the only
38
+ * one of the three gates that runs exclusively at `generateBundle` time: it
39
+ * has nothing to say about source, only about output.
40
+ */
41
+ /**
42
+ * Recursively collects every top-level statement's bound name(s), matching:
43
+ * - `const route = ...` / `function loader() {}` (exported or not — the
44
+ * check is on top-level BINDINGS, not top-level exports).
45
+ * - `export const route = ...` / `export function loader() {}` (the
46
+ * `ExportNamedDeclaration` wrapper form).
47
+ * - `export { route }` (the bare re-export-specifier form Rollup sometimes
48
+ * emits instead of inlining the declaration itself).
49
+ */
50
+ function collectServerExportNames(stmt) {
51
+ const matches = [];
52
+ const line = stmt.loc?.start?.line;
53
+ function record(name) {
54
+ if (name && SERVER_EXPORT_NAMES.has(name)) matches.push({
55
+ name,
56
+ line: line ?? 0
57
+ });
58
+ }
59
+ if (stmt.type === "VariableDeclaration") {
60
+ for (const decl of stmt.declarations) if (decl.id?.type === "Identifier") record(decl.id.name);
61
+ } else if (stmt.type === "FunctionDeclaration") record(stmt.id?.name);
62
+ else if (stmt.type === "ExportNamedDeclaration") {
63
+ if (stmt.declaration) matches.push(...collectServerExportNames(stmt.declaration));
64
+ for (const specifier of stmt.specifiers ?? []) record(specifier.exported?.name ?? specifier.exported?.value);
65
+ }
66
+ return matches;
67
+ }
68
+ /**
69
+ * Thrown when an emitted chunk cannot be parsed, which means Gate C could not
70
+ * inspect it. A gate that skips what it cannot read reports "no violation
71
+ * found" on precisely the input it failed to look at — the one input where
72
+ * that answer is worthless. So an unreadable chunk fails the build instead.
73
+ */
74
+ var UnverifiableChunkError = class extends Error {
75
+ fileName;
76
+ constructor(fileName, cause) {
77
+ super([
78
+ `Warlock stopped this build: a file in your client bundle could not be checked for server-only code.`,
79
+ ``,
80
+ `File: ${fileName}`,
81
+ `Cause: this file could not be parsed as JavaScript, so the client/server boundary check could not be performed on it. Warlock cannot confirm that the server-only exports (route, middleware, validation, loader, metadata) were kept out of it.`,
82
+ `Fix: the build is being stopped rather than passed, because a file that was never checked is not a file known to be safe. This emitted file is outside the JavaScript syntax Warlock can currently verify. Two things commonly put it there, and this error cannot tell which: the output uses syntax newer than the parser Warlock ships with, or a plugin or loader emitted non-standard syntax into the client bundle. Check the compatibility of whatever produced this file, then build again.`
83
+ ].join("\n"));
84
+ this.name = "UnverifiableChunkError";
85
+ this.fileName = fileName;
86
+ if (cause !== void 0) this.cause = cause;
87
+ }
88
+ };
89
+ /**
90
+ * Part 1, item 1: parses each emitted chunk's ACTUAL code (never pre-transform
91
+ * source) and looks for a top-level binding named one of the five server
92
+ * exports. A parse failure on an emitted chunk FAILS the gate
93
+ * (`UnverifiableChunkError`) — it is never skipped. Skipping would report the
94
+ * bundle clean on the one chunk the gate did not actually inspect, which is a
95
+ * safety check failing open; a build stopped on an unreadable chunk is
96
+ * recoverable, a boundary silently unenforced is not.
97
+ *
98
+ * This SURVIVES real minification (pinned by the `gate-c-verify.spec.ts`
99
+ * "D.8" describe block). A minifier (esbuild,
100
+ * Vite's default) is only free to rename LOCAL bindings; the exported name
101
+ * itself is part of the module's public interface and is never mangled — a
102
+ * minified `export const route = ...` still emits `export { e as route }`,
103
+ * with the local identifier renamed (`e`) but `route` intact as the specifier's
104
+ * `exported` name, which is exactly the field `collectServerExportNames`
105
+ * reads for the `ExportNamedDeclaration` specifier form. No name-independent
106
+ * redesign needed here — verified against a real `build.minify: true` output,
107
+ * not assumed.
108
+ */
109
+ function findLeakedServerExports(bundle) {
110
+ const leaks = [];
111
+ for (const file of Object.values(bundle)) {
112
+ if (!file || file.type !== "chunk" || typeof file.code !== "string") continue;
113
+ let ast;
114
+ try {
115
+ ast = parse(file.code, {
116
+ sourceType: "module",
117
+ plugins: ["typescript", "jsx"]
118
+ });
119
+ } catch (error) {
120
+ throw new UnverifiableChunkError(file.fileName ?? "(unnamed chunk)", error);
121
+ }
122
+ for (const stmt of ast.program.body) for (const match of collectServerExportNames(stmt)) leaks.push({
123
+ fileName: file.fileName,
124
+ exportName: match.name,
125
+ line: match.line
126
+ });
127
+ }
128
+ return leaks;
129
+ }
130
+ /**
131
+ * Part 1, item 2: walks every emitted chunk's `moduleIds` (the resolved,
132
+ * absolute file paths of every source module Rollup actually bundled into
133
+ * that chunk) and classifies each one via the SAME `EnvironmentClassifier`
134
+ * Gate A's `resolveId` uses, re-derived from `gate-a-resolve.ts` rather than
135
+ * hand-rolled a second time. A moduleId that maps to a governed-scope
136
+ * package classified `"server"` is an import edge Gate A should already
137
+ * have refused — if it's here anyway, the earlier gate has a bug.
138
+ *
139
+ * D.8 verdict: this SURVIVES real minification unconditionally, and for a
140
+ * stronger reason than item 1 above — it never reads emitted code text or
141
+ * identifier names at all. `moduleIds` is Rollup's own bundling metadata
142
+ * (which resolved source files ended up in this chunk), untouched by
143
+ * minification, which only rewrites code, not that metadata. Verified
144
+ * against a real `build.minify: true` output in `gate-c-verify.spec.ts`.
145
+ */
146
+ function findLeakedServerImportEdges(bundle, classifier) {
147
+ const leaks = [];
148
+ for (const file of Object.values(bundle)) {
149
+ if (!file || file.type !== "chunk") continue;
150
+ for (const moduleId of file.moduleIds ?? []) {
151
+ const packageName = classifier.packageNameForFilePath(moduleId);
152
+ if (!packageName) continue;
153
+ if (classifier.environmentOf(packageName) === "server") leaks.push({
154
+ fileName: file.fileName,
155
+ moduleId,
156
+ packageName
157
+ });
158
+ }
159
+ }
160
+ return leaks;
161
+ }
162
+ /**
163
+ * Conservative "does this PUBLIC_* value look safe to print in the reviewable
164
+ * manifest" heuristic: if genuinely unsure, show the key only — never guess
165
+ * that a value is safe. The whole point of this manifest is that the `PUBLIC_`
166
+ * prefix rule alone is not trustworthy — someone can and will name an actual
167
+ * secret `PUBLIC_STRIPE_KEY` — so a value that LOOKS like a credential is
168
+ * redacted regardless of what its key is named. Non-string values (booleans,
169
+ * numbers) are never secret-shaped and always shown.
170
+ */
171
+ const CREDENTIALED_URL_RE = /:\/\/[^/\s]+:[^/\s@]+@/;
172
+ const KNOWN_SECRET_PREFIX_RES = [
173
+ /^sk_(live|test)_/i,
174
+ /^rk_(live|test)_/i,
175
+ /^AKIA[0-9A-Z]{12,}/,
176
+ /^gh[pousr]_[A-Za-z0-9]{20,}/,
177
+ /^glpat-[A-Za-z0-9_-]{20,}/,
178
+ /^xox[baprs]-[A-Za-z0-9-]{10,}/,
179
+ /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/
180
+ ];
181
+ const HIGH_ENTROPY_TOKEN_RE = /^[A-Za-z0-9+/_=-]{32,}$/;
182
+ function isSafeToShowValue(value) {
183
+ if (typeof value !== "string" || value.length === 0) return true;
184
+ if (CREDENTIALED_URL_RE.test(value)) return false;
185
+ if (KNOWN_SECRET_PREFIX_RES.some((re) => re.test(value))) return false;
186
+ if (HIGH_ENTROPY_TOKEN_RE.test(value) && /[0-9]/.test(value) && /[a-zA-Z]/.test(value)) return false;
187
+ return true;
188
+ }
189
+ function stringifyEnvValue(value) {
190
+ if (typeof value === "string") return value;
191
+ return JSON.stringify(value) ?? "undefined";
192
+ }
193
+ /**
194
+ * The enumerated, human-reviewable list of
195
+ * every `PUBLIC_*` key actually inlined into the client bundle. Built
196
+ * directly from `tracker.referencedKeys` — the exact same `Set` Gate B's own
197
+ * `generateBundle` unread-key check (`gate-b-secrets.ts`) reads from — so
198
+ * this manifest and that exclusion logic agree BY CONSTRUCTION: they are two
199
+ * readers of one Set, not two independent recomputations of "which keys were
200
+ * read" that could drift apart. A key only reaches `referencedKeys` once
201
+ * `findViolation` in `gate-b-secrets.ts` has seen a statically-resolved
202
+ * `import.meta.env.PUBLIC_X` read for it, which is also precisely the
203
+ * condition under which Gate B allows its value to be inlined at all — an
204
+ * unread key that somehow leaked in anyway is a Gate B `generateBundle`
205
+ * BUILD FAILURE (see `gate-b-secrets.ts`), never silently listed here.
206
+ */
207
+ function buildPublicEnvManifest(tracker) {
208
+ return [...tracker.referencedKeys].sort().map((key) => {
209
+ const value = tracker.declaredEnv[key];
210
+ const safe = isSafeToShowValue(value);
211
+ return {
212
+ key,
213
+ value: safe ? stringifyEnvValue(value) : null,
214
+ redacted: !safe
215
+ };
216
+ });
217
+ }
218
+ /**
219
+ * The client-build Vite plugin. Runs only at `generateBundle` — Gate C has
220
+ * nothing to say about source, only about the bundle Rollup actually wrote.
221
+ * Skipped for the SSR/server build, same as Gate B (`gate-b-secrets.ts`):
222
+ * a page's server exports are meant to survive in that build; only the
223
+ * client build is judged.
224
+ */
225
+ function gateCVerify(options = {}) {
226
+ const classifier = createEnvironmentClassifier(options);
227
+ const tracker = options.tracker ?? createPublicEnvTracker();
228
+ const manifestFileName = options.manifestFileName ?? "warlock-env-manifest.json";
229
+ return {
230
+ name: "warlock:gate-c-verify",
231
+ generateBundle(_outputOptions, bundle) {
232
+ if (this.environment?.config?.consumer === "server") return;
233
+ const exportLeak = findLeakedServerExports(bundle)[0];
234
+ if (exportLeak) this.error([
235
+ `Gate C refused a build: a server export survived into the emitted client bundle.`,
236
+ ``,
237
+ `File: ${exportLeak.fileName}${exportLeak.line ? `:${exportLeak.line}` : ""}`,
238
+ `Export: ${exportLeak.exportName}`,
239
+ `Cause: "${exportLeak.exportName}" is one of the five server exports (route, middleware, validation, loader, metadata) and is still present as a top-level binding in the EMITTED client chunk — projection and/or Gate A should have removed or refused it before the bundle was written.`,
240
+ `Fix: this should already be impossible if projection and Gate A ran correctly — investigate why "${exportLeak.exportName}" reached the emitted output (a projection bug, a build config that bypasses these plugins, or a plugin ordering change) rather than assuming this build is a one-off; Gate C is defense in depth, not the primary fence.`
241
+ ].join("\n"));
242
+ const importEdgeLeak = findLeakedServerImportEdges(bundle, classifier)[0];
243
+ if (importEdgeLeak) this.error([
244
+ `Gate C refused a build: a server-only import edge survived into the emitted client bundle's module graph.`,
245
+ ``,
246
+ `File: ${importEdgeLeak.fileName}`,
247
+ `Module: ${importEdgeLeak.moduleId}`,
248
+ `Cause: "${importEdgeLeak.moduleId}" belongs to ${importEdgeLeak.packageName}, a server-only package (it declares "warlock": { "environment": "server" } in its package.json), and is present among the bundled modules of the emitted client chunk "${importEdgeLeak.fileName}" — Gate A's resolveId should have refused this import before it ever reached the bundle.`,
249
+ `Fix: this should already be impossible if Gate A ran on this build — investigate why ${importEdgeLeak.packageName} reached the emitted output (a Gate A bypass, a custom resolveId/external override, or a plugin ordering change) rather than assuming this build is a one-off; Gate C is defense in depth, not the primary fence.`
250
+ ].join("\n"));
251
+ const manifest = buildPublicEnvManifest(tracker);
252
+ this.emitFile({
253
+ type: "asset",
254
+ fileName: manifestFileName,
255
+ source: JSON.stringify(manifest, null, 2)
256
+ });
257
+ }
258
+ };
259
+ }
260
+
261
+ //#endregion
262
+ export { buildPublicEnvManifest, findLeakedServerExports, findLeakedServerImportEdges, gateCVerify };
263
+ //# sourceMappingURL=gate-c-verify.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-c-verify.mjs","names":[],"sources":["../../../../../../../web/src/vite/gate-c-verify.ts"],"sourcesContent":["/**\r\n * Gate C — emitted-output verification.\r\n *\r\n * Projection removes the five server exports before the client graph forms.\r\n * Gate A refuses forbidden import PATHS. Gate B refuses inline secret reads.\r\n * All three act BEFORE or DURING the build, on source or the module graph —\r\n * none of them ever looks at what actually came out the other end. Gate C\r\n * is that check: a build-time assertion on the EMITTED client bundle for\r\n * every page, verifying:\r\n *\r\n * 1. The emitted code contains none of the five server export names\r\n * (`route`, `middleware`, `validation`, `loader`, `metadata`) as a\r\n * top-level binding, exported or not — `findLeakedServerExports`.\r\n * 2. The emitted module graph (Rollup's `OutputBundle`, as seen in\r\n * `generateBundle`) contains no import edge into a\r\n * `warlock.environment: \"server\"` (or absent-defaulting-to-server)\r\n * package — re-derived from the exact classifier Gate A uses\r\n * (`gate-a-resolve.ts`'s `createEnvironmentClassifier`), never a second,\r\n * independently-drifting classification scheme — `findLeakedServerImportEdges`.\r\n *\r\n * This is defense in depth: if projection or Gate A has a bug that lets\r\n * something through, Gate C is what turns \"shipped a leak\" into \"build\r\n * fails.\" Both check functions are exported as PURE functions over a bundle\r\n * object precisely so a test can construct a bundle where projection/Gate A\r\n * are bypassed entirely (a hand-built leaked chunk, never produced by a real\r\n * pipeline run) and still prove Gate C catches it independently — re-running\r\n * the D.1-D.5 happy-path fixtures through the composed pipeline only proves\r\n * the happy path, never that Gate C itself has teeth.\r\n *\r\n * Gate C also emits the inlined-`PUBLIC_*`-value manifest — see\r\n * `buildPublicEnvManifest` below — and is the only\r\n * one of the three gates that runs exclusively at `generateBundle` time: it\r\n * has nothing to say about source, only about output.\r\n */\r\nimport { parse } from \"@babel/parser\";\r\nimport type { Plugin } from \"vite\";\r\nimport {\r\n createEnvironmentClassifier,\r\n type EnvironmentClassifier,\r\n type EnvironmentClassifierOptions,\r\n} from \"./gate-a-resolve\";\r\nimport { createPublicEnvTracker, type PublicEnvTracker } from \"./gate-b-secrets\";\r\nimport { SERVER_EXPORT_NAMES } from \"./projection\";\r\n\r\nexport interface ServerExportLeak {\r\n fileName: string;\r\n exportName: string;\r\n line?: number;\r\n}\r\n\r\nexport interface ServerImportEdgeLeak {\r\n fileName: string;\r\n moduleId: string;\r\n packageName: string;\r\n}\r\n\r\nexport interface PublicEnvManifestEntry {\r\n key: string;\r\n /** `null` when `redacted` is true — see `isSafeToShowValue`. */\r\n value: string | null;\r\n redacted: boolean;\r\n}\r\n\r\n/**\r\n * A structurally-minimal view of Rollup's `OutputBundle` — only the fields\r\n * Gate C actually reads. Deliberately NOT the real `OutputBundle`/`OutputChunk`\r\n * types: Part 1's own discipline requires proving Gate C catches a bundle\r\n * that a real pipeline could never produce (a hand-built leaked chunk), and\r\n * forcing every test fixture to satisfy Rollup's full chunk shape would\r\n * fight that requirement for no safety benefit — Vite's real `generateBundle`\r\n * hook still hands Gate C a real `OutputBundle`, which is a structural\r\n * superset of this.\r\n */\r\ntype BundleLike = Record<\r\n string,\r\n {\r\n type?: string;\r\n fileName?: string;\r\n code?: string;\r\n moduleIds?: readonly string[];\r\n }\r\n>;\r\n\r\n/**\r\n * Recursively collects every top-level statement's bound name(s), matching:\r\n * - `const route = ...` / `function loader() {}` (exported or not — the\r\n * check is on top-level BINDINGS, not top-level exports).\r\n * - `export const route = ...` / `export function loader() {}` (the\r\n * `ExportNamedDeclaration` wrapper form).\r\n * - `export { route }` (the bare re-export-specifier form Rollup sometimes\r\n * emits instead of inlining the declaration itself).\r\n */\r\nfunction collectServerExportNames(stmt: any): Array<{ name: string; line: number }> {\r\n const matches: Array<{ name: string; line: number }> = [];\r\n const line = stmt.loc?.start?.line as number | undefined;\r\n\r\n function record(name: string | undefined) {\r\n if (name && SERVER_EXPORT_NAMES.has(name)) matches.push({ name, line: line ?? 0 });\r\n }\r\n\r\n if (stmt.type === \"VariableDeclaration\") {\r\n for (const decl of stmt.declarations) {\r\n if (decl.id?.type === \"Identifier\") record(decl.id.name);\r\n }\r\n } else if (stmt.type === \"FunctionDeclaration\") {\r\n record(stmt.id?.name);\r\n } else if (stmt.type === \"ExportNamedDeclaration\") {\r\n if (stmt.declaration) matches.push(...collectServerExportNames(stmt.declaration));\r\n for (const specifier of stmt.specifiers ?? []) {\r\n record(specifier.exported?.name ?? specifier.exported?.value);\r\n }\r\n }\r\n\r\n return matches;\r\n}\r\n\r\n/**\r\n * Thrown when an emitted chunk cannot be parsed, which means Gate C could not\r\n * inspect it. A gate that skips what it cannot read reports \"no violation\r\n * found\" on precisely the input it failed to look at — the one input where\r\n * that answer is worthless. So an unreadable chunk fails the build instead.\r\n */\r\nexport class UnverifiableChunkError extends Error {\r\n readonly fileName: string;\r\n\r\n constructor(fileName: string, cause?: unknown) {\r\n super(\r\n [\r\n `Warlock stopped this build: a file in your client bundle could not be checked for server-only code.`,\r\n ``,\r\n `File: ${fileName}`,\r\n `Cause: this file could not be parsed as JavaScript, so the client/server boundary check could not be performed on it. Warlock cannot confirm that the server-only exports (route, middleware, validation, loader, metadata) were kept out of it.`,\r\n `Fix: the build is being stopped rather than passed, because a file that was never checked is not a file known to be safe. This emitted file is outside the JavaScript syntax Warlock can currently verify. Two things commonly put it there, and this error cannot tell which: the output uses syntax newer than the parser Warlock ships with, or a plugin or loader emitted non-standard syntax into the client bundle. Check the compatibility of whatever produced this file, then build again.`,\r\n ].join(\"\\n\"),\r\n );\r\n this.name = \"UnverifiableChunkError\";\r\n this.fileName = fileName;\r\n if (cause !== undefined) (this as { cause?: unknown }).cause = cause;\r\n }\r\n}\r\n\r\n/**\r\n * Part 1, item 1: parses each emitted chunk's ACTUAL code (never pre-transform\r\n * source) and looks for a top-level binding named one of the five server\r\n * exports. A parse failure on an emitted chunk FAILS the gate\r\n * (`UnverifiableChunkError`) — it is never skipped. Skipping would report the\r\n * bundle clean on the one chunk the gate did not actually inspect, which is a\r\n * safety check failing open; a build stopped on an unreadable chunk is\r\n * recoverable, a boundary silently unenforced is not.\r\n *\r\n * This SURVIVES real minification (pinned by the `gate-c-verify.spec.ts`\r\n * \"D.8\" describe block). A minifier (esbuild,\r\n * Vite's default) is only free to rename LOCAL bindings; the exported name\r\n * itself is part of the module's public interface and is never mangled — a\r\n * minified `export const route = ...` still emits `export { e as route }`,\r\n * with the local identifier renamed (`e`) but `route` intact as the specifier's\r\n * `exported` name, which is exactly the field `collectServerExportNames`\r\n * reads for the `ExportNamedDeclaration` specifier form. No name-independent\r\n * redesign needed here — verified against a real `build.minify: true` output,\r\n * not assumed.\r\n */\r\nexport function findLeakedServerExports(bundle: BundleLike): ServerExportLeak[] {\r\n const leaks: ServerExportLeak[] = [];\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (!file || file.type !== \"chunk\" || typeof file.code !== \"string\") continue;\r\n\r\n let ast: any;\r\n try {\r\n ast = parse(file.code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\r\n } catch (error) {\r\n throw new UnverifiableChunkError(file.fileName ?? \"(unnamed chunk)\", error);\r\n }\r\n\r\n for (const stmt of ast.program.body as any[]) {\r\n for (const match of collectServerExportNames(stmt)) {\r\n leaks.push({ fileName: file.fileName as string, exportName: match.name, line: match.line });\r\n }\r\n }\r\n }\r\n\r\n return leaks;\r\n}\r\n\r\n/**\r\n * Part 1, item 2: walks every emitted chunk's `moduleIds` (the resolved,\r\n * absolute file paths of every source module Rollup actually bundled into\r\n * that chunk) and classifies each one via the SAME `EnvironmentClassifier`\r\n * Gate A's `resolveId` uses, re-derived from `gate-a-resolve.ts` rather than\r\n * hand-rolled a second time. A moduleId that maps to a governed-scope\r\n * package classified `\"server\"` is an import edge Gate A should already\r\n * have refused — if it's here anyway, the earlier gate has a bug.\r\n *\r\n * D.8 verdict: this SURVIVES real minification unconditionally, and for a\r\n * stronger reason than item 1 above — it never reads emitted code text or\r\n * identifier names at all. `moduleIds` is Rollup's own bundling metadata\r\n * (which resolved source files ended up in this chunk), untouched by\r\n * minification, which only rewrites code, not that metadata. Verified\r\n * against a real `build.minify: true` output in `gate-c-verify.spec.ts`.\r\n */\r\nexport function findLeakedServerImportEdges(\r\n bundle: BundleLike,\r\n classifier: Pick<EnvironmentClassifier, \"environmentOf\" | \"packageNameForFilePath\">,\r\n): ServerImportEdgeLeak[] {\r\n const leaks: ServerImportEdgeLeak[] = [];\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (!file || file.type !== \"chunk\") continue;\r\n\r\n for (const moduleId of file.moduleIds ?? []) {\r\n const packageName = classifier.packageNameForFilePath(moduleId);\r\n if (!packageName) continue;\r\n if (classifier.environmentOf(packageName) === \"server\") {\r\n leaks.push({ fileName: file.fileName as string, moduleId, packageName });\r\n }\r\n }\r\n }\r\n\r\n return leaks;\r\n}\r\n\r\n/**\r\n * Conservative \"does this PUBLIC_* value look safe to print in the reviewable\r\n * manifest\" heuristic: if genuinely unsure, show the key only — never guess\r\n * that a value is safe. The whole point of this manifest is that the `PUBLIC_`\r\n * prefix rule alone is not trustworthy — someone can and will name an actual\r\n * secret `PUBLIC_STRIPE_KEY` — so a value that LOOKS like a credential is\r\n * redacted regardless of what its key is named. Non-string values (booleans,\r\n * numbers) are never secret-shaped and always shown.\r\n */\r\nconst CREDENTIALED_URL_RE = /:\\/\\/[^/\\s]+:[^/\\s@]+@/; // scheme://user:pass@host\r\nconst KNOWN_SECRET_PREFIX_RES = [\r\n /^sk_(live|test)_/i, // Stripe secret key (pk_ publishable is a different prefix)\r\n /^rk_(live|test)_/i, // Stripe restricted key\r\n /^AKIA[0-9A-Z]{12,}/, // AWS access key id\r\n /^gh[pousr]_[A-Za-z0-9]{20,}/, // GitHub personal/OAuth/user/server tokens\r\n /^glpat-[A-Za-z0-9_-]{20,}/, // GitLab personal access token\r\n /^xox[baprs]-[A-Za-z0-9-]{10,}/, // Slack tokens\r\n /^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/, // JWT (3 base64url segments)\r\n];\r\n// A long opaque token made up only of base64url/hex-ish characters, with both\r\n// letters and digits, and no separators a human-authored public value (a\r\n// URL, a short id) would normally contain — conservatively treated as a\r\n// secret-shaped random token even though it might genuinely be safe.\r\nconst HIGH_ENTROPY_TOKEN_RE = /^[A-Za-z0-9+/_=-]{32,}$/;\r\n\r\nfunction isSafeToShowValue(value: unknown): boolean {\r\n if (typeof value !== \"string\" || value.length === 0) return true;\r\n if (CREDENTIALED_URL_RE.test(value)) return false;\r\n if (KNOWN_SECRET_PREFIX_RES.some((re) => re.test(value))) return false;\r\n if (HIGH_ENTROPY_TOKEN_RE.test(value) && /[0-9]/.test(value) && /[a-zA-Z]/.test(value)) return false;\r\n return true;\r\n}\r\n\r\nfunction stringifyEnvValue(value: unknown): string {\r\n if (typeof value === \"string\") return value;\r\n return JSON.stringify(value) ?? \"undefined\";\r\n}\r\n\r\n/**\r\n * The enumerated, human-reviewable list of\r\n * every `PUBLIC_*` key actually inlined into the client bundle. Built\r\n * directly from `tracker.referencedKeys` — the exact same `Set` Gate B's own\r\n * `generateBundle` unread-key check (`gate-b-secrets.ts`) reads from — so\r\n * this manifest and that exclusion logic agree BY CONSTRUCTION: they are two\r\n * readers of one Set, not two independent recomputations of \"which keys were\r\n * read\" that could drift apart. A key only reaches `referencedKeys` once\r\n * `findViolation` in `gate-b-secrets.ts` has seen a statically-resolved\r\n * `import.meta.env.PUBLIC_X` read for it, which is also precisely the\r\n * condition under which Gate B allows its value to be inlined at all — an\r\n * unread key that somehow leaked in anyway is a Gate B `generateBundle`\r\n * BUILD FAILURE (see `gate-b-secrets.ts`), never silently listed here.\r\n */\r\nexport function buildPublicEnvManifest(tracker: PublicEnvTracker): PublicEnvManifestEntry[] {\r\n return [...tracker.referencedKeys]\r\n .sort()\r\n .map((key) => {\r\n const value = tracker.declaredEnv[key];\r\n const safe = isSafeToShowValue(value);\r\n return { key, value: safe ? stringifyEnvValue(value) : null, redacted: !safe };\r\n });\r\n}\r\n\r\nexport interface GateCOptions extends EnvironmentClassifierOptions {\r\n /**\r\n * Shared with `gateBSecrets({ tracker })` — required for the manifest to\r\n * reflect what THIS build's Gate B pass actually saw. Defaults to a\r\n * private, unshared tracker (always empty) if omitted, which is only ever\r\n * correct when Gate C runs standalone in a test.\r\n */\r\n tracker?: PublicEnvTracker;\r\n /** Defaults to `\"warlock-env-manifest.json\"` (Suki's suggested name). */\r\n manifestFileName?: string;\r\n}\r\n\r\n/**\r\n * The client-build Vite plugin. Runs only at `generateBundle` — Gate C has\r\n * nothing to say about source, only about the bundle Rollup actually wrote.\r\n * Skipped for the SSR/server build, same as Gate B (`gate-b-secrets.ts`):\r\n * a page's server exports are meant to survive in that build; only the\r\n * client build is judged.\r\n */\r\nexport function gateCVerify(options: GateCOptions = {}): Plugin {\r\n const classifier = createEnvironmentClassifier(options);\r\n const tracker = options.tracker ?? createPublicEnvTracker();\r\n const manifestFileName = options.manifestFileName ?? \"warlock-env-manifest.json\";\r\n\r\n return {\r\n name: \"warlock:gate-c-verify\",\r\n generateBundle(_outputOptions, bundle) {\r\n if (this.environment?.config?.consumer === \"server\") return;\r\n\r\n const exportLeak = findLeakedServerExports(bundle)[0];\r\n if (exportLeak) {\r\n this.error(\r\n [\r\n `Gate C refused a build: a server export survived into the emitted client bundle.`,\r\n ``,\r\n `File: ${exportLeak.fileName}${exportLeak.line ? `:${exportLeak.line}` : \"\"}`,\r\n `Export: ${exportLeak.exportName}`,\r\n `Cause: \"${exportLeak.exportName}\" is one of the five server exports (route, middleware, validation, loader, metadata) and is still present as a top-level binding in the EMITTED client chunk — projection and/or Gate A should have removed or refused it before the bundle was written.`,\r\n `Fix: this should already be impossible if projection and Gate A ran correctly — investigate why \"${exportLeak.exportName}\" reached the emitted output (a projection bug, a build config that bypasses these plugins, or a plugin ordering change) rather than assuming this build is a one-off; Gate C is defense in depth, not the primary fence.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n const importEdgeLeak = findLeakedServerImportEdges(bundle, classifier)[0];\r\n if (importEdgeLeak) {\r\n this.error(\r\n [\r\n `Gate C refused a build: a server-only import edge survived into the emitted client bundle's module graph.`,\r\n ``,\r\n `File: ${importEdgeLeak.fileName}`,\r\n `Module: ${importEdgeLeak.moduleId}`,\r\n `Cause: \"${importEdgeLeak.moduleId}\" belongs to ${importEdgeLeak.packageName}, a server-only package (it declares \"warlock\": { \"environment\": \"server\" } in its package.json), and is present among the bundled modules of the emitted client chunk \"${importEdgeLeak.fileName}\" — Gate A's resolveId should have refused this import before it ever reached the bundle.`,\r\n `Fix: this should already be impossible if Gate A ran on this build — investigate why ${importEdgeLeak.packageName} reached the emitted output (a Gate A bypass, a custom resolveId/external override, or a plugin ordering change) rather than assuming this build is a one-off; Gate C is defense in depth, not the primary fence.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n const manifest = buildPublicEnvManifest(tracker);\r\n\r\n // EMITTED through `this.emitFile`, not written into `bundle` by hand.\r\n //\r\n // This used to assign a hand-built record directly:\r\n //\r\n // bundle[manifestFileName] = { type: \"asset\", fileName, name, source } as any;\r\n //\r\n // and the `as any` was load-bearing, which was the warning sign. A Rollup\r\n // asset record carries `names` and `originalFileNames` ARRAYS; that object\r\n // had neither, so it was not the shape Rollup produces — it merely\r\n // type-asserted its way into the bundle.\r\n //\r\n // Nothing noticed until the production build got far enough to render\r\n // chunks, at which point Vite's own `vite:manifest` plugin read\r\n // `chunk.names.length` on every asset and died on `undefined`:\r\n //\r\n // [vite:manifest] Cannot read properties of undefined (reading 'length')\r\n //\r\n // `emitFile` makes Rollup construct the record, so the shape is correct by\r\n // construction and stays correct when Rollup adds fields. Hand-building a\r\n // bundle entry is signing up to track someone else's internal type forever.\r\n this.emitFile({\r\n type: \"asset\",\r\n fileName: manifestFileName,\r\n source: JSON.stringify(manifest, null, 2),\r\n });\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ //#region ../web/src/vite/hydration-entries.d.ts
2
+ /** Stable Rollup/Vite entry name shared by development and production wiring. */
3
+ declare const HYDRATION_CLIENT_ENTRY_NAME = "hydration";
4
+ type HydrationClientEntry = Readonly<{
5
+ name: typeof HYDRATION_CLIENT_ENTRY_NAME;
6
+ sourcePath: string;
7
+ devUrl: string;
8
+ }>;
9
+ /**
10
+ * Describes the single framework hydration entry without importing Vite.
11
+ * Vite's `/@fs/` prefix accepts an absolute normalized file-system path;
12
+ * keeping the drive colon produces `/@fs/D:/...` consistently on Windows.
13
+ */
14
+ declare function createHydrationClientEntry(webRoot: string): HydrationClientEntry;
15
+ //#endregion
16
+ export { HYDRATION_CLIENT_ENTRY_NAME, HydrationClientEntry, createHydrationClientEntry };
17
+ //# sourceMappingURL=hydration-entries.d.mts.map
@@ -0,0 +1,45 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ //#region ../web/src/vite/hydration-entries.ts
5
+ /** Stable Rollup/Vite entry name shared by development and production wiring. */
6
+ const HYDRATION_CLIENT_ENTRY_NAME = "hydration";
7
+ function normalizeFileSystemPath(filePath) {
8
+ return filePath.replace(/\\/g, "/");
9
+ }
10
+ /**
11
+ * Where the hydration entry lives inside an INSTALLED `@warlock.js/web`, and
12
+ * where it lives inside this checkout — in that order of preference.
13
+ *
14
+ * The published package declares `"files": ["esm"]`, so `src/` is absent from
15
+ * every real install. Resolving the entry to `<webRoot>/src/hydration/index.ts`
16
+ * unconditionally therefore worked in this monorepo and failed for every
17
+ * consumer, with `warlock build` unable to emit a client bundle at all.
18
+ *
19
+ * The built artifact is preferred rather than the source being published,
20
+ * because shipping `src/` beside `esm/` would put TWO instances of
21
+ * `routing/route-table` in one client bundle — the app's own imports resolve
22
+ * through `esm/`, the hydration entry's relative imports through `src/`.
23
+ * `publishRouteTable()` would write to one and `<Link>` would read the other.
24
+ */
25
+ const PACKAGED_ENTRY = "esm/hydration/index.mjs";
26
+ const CHECKOUT_ENTRY = "src/hydration/index.ts";
27
+ /**
28
+ * Describes the single framework hydration entry without importing Vite.
29
+ * Vite's `/@fs/` prefix accepts an absolute normalized file-system path;
30
+ * keeping the drive colon produces `/@fs/D:/...` consistently on Windows.
31
+ */
32
+ function createHydrationClientEntry(webRoot) {
33
+ if (typeof webRoot !== "string" || webRoot.trim().length === 0) throw new TypeError("Cannot create the hydration client entry: webRoot must be a non-empty path.");
34
+ const packagedPath = path.resolve(webRoot, PACKAGED_ENTRY);
35
+ const sourcePath = normalizeFileSystemPath(existsSync(packagedPath) ? packagedPath : path.resolve(webRoot, CHECKOUT_ENTRY));
36
+ return {
37
+ name: HYDRATION_CLIENT_ENTRY_NAME,
38
+ sourcePath,
39
+ devUrl: `/@fs/${sourcePath}`
40
+ };
41
+ }
42
+
43
+ //#endregion
44
+ export { HYDRATION_CLIENT_ENTRY_NAME, createHydrationClientEntry };
45
+ //# sourceMappingURL=hydration-entries.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydration-entries.mjs","names":[],"sources":["../../../../../../../web/src/vite/hydration-entries.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/** Stable Rollup/Vite entry name shared by development and production wiring. */\nexport const HYDRATION_CLIENT_ENTRY_NAME = \"hydration\";\n\nexport type HydrationClientEntry = Readonly<{\n name: typeof HYDRATION_CLIENT_ENTRY_NAME;\n sourcePath: string;\n devUrl: string;\n}>;\n\nfunction normalizeFileSystemPath(filePath: string): string {\n return filePath.replace(/\\\\/g, \"/\");\n}\n\n/**\n * Where the hydration entry lives inside an INSTALLED `@warlock.js/web`, and\n * where it lives inside this checkout — in that order of preference.\n *\n * The published package declares `\"files\": [\"esm\"]`, so `src/` is absent from\n * every real install. Resolving the entry to `<webRoot>/src/hydration/index.ts`\n * unconditionally therefore worked in this monorepo and failed for every\n * consumer, with `warlock build` unable to emit a client bundle at all.\n *\n * The built artifact is preferred rather than the source being published,\n * because shipping `src/` beside `esm/` would put TWO instances of\n * `routing/route-table` in one client bundle — the app's own imports resolve\n * through `esm/`, the hydration entry's relative imports through `src/`.\n * `publishRouteTable()` would write to one and `<Link>` would read the other.\n */\nconst PACKAGED_ENTRY = \"esm/hydration/index.mjs\";\nconst CHECKOUT_ENTRY = \"src/hydration/index.ts\";\n\n/**\n * Describes the single framework hydration entry without importing Vite.\n * Vite's `/@fs/` prefix accepts an absolute normalized file-system path;\n * keeping the drive colon produces `/@fs/D:/...` consistently on Windows.\n */\nexport function createHydrationClientEntry(webRoot: string): HydrationClientEntry {\n if (typeof webRoot !== \"string\" || webRoot.trim().length === 0) {\n throw new TypeError(\"Cannot create the hydration client entry: webRoot must be a non-empty path.\");\n }\n\n const packagedPath = path.resolve(webRoot, PACKAGED_ENTRY);\n const sourcePath = normalizeFileSystemPath(\n existsSync(packagedPath) ? packagedPath : path.resolve(webRoot, CHECKOUT_ENTRY),\n );\n\n return {\n name: HYDRATION_CLIENT_ENTRY_NAME,\n sourcePath,\n devUrl: `/@fs/${sourcePath}`,\n };\n}\n"],"mappings":";;;;;AAIA,MAAa,8BAA8B;AAQ3C,SAAS,wBAAwB,UAA0B;CACzD,OAAO,SAAS,QAAQ,OAAO,GAAG;AACpC;;;;;;;;;;;;;;;;AAiBA,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;;;;;;AAOvB,SAAgB,2BAA2B,SAAuC;CAChF,IAAI,OAAO,YAAY,YAAY,QAAQ,KAAK,CAAC,CAAC,WAAW,GAC3D,MAAM,IAAI,UAAU,6EAA6E;CAGnG,MAAM,eAAe,KAAK,QAAQ,SAAS,cAAc;CACzD,MAAM,aAAa,wBACjB,WAAW,YAAY,IAAI,eAAe,KAAK,QAAQ,SAAS,cAAc,CAChF;CAEA,OAAO;EACL,MAAM;EACN;EACA,QAAQ,QAAQ;CAClB;AACF"}
@@ -0,0 +1,126 @@
1
+ import { HYDRATION_CLIENT_ENTRY_NAME, HydrationClientEntry, createHydrationClientEntry } from "./hydration-entries.mjs";
2
+ import { BuildHydrationClientOptions, BuildHydrationClientResult, HydrationClientBuildOutput, buildHydrationClient } from "./build-client.mjs";
3
+ import { EnvironmentClassifier, EnvironmentClassifierOptions, WarlockEnvironment, gateAResolve } from "./gate-a-resolve.mjs";
4
+ import { PublicEnvTracker, createPublicEnvTracker, gateBSecrets } from "./gate-b-secrets.mjs";
5
+ import { GateCOptions, PublicEnvManifestEntry, ServerExportLeak, ServerImportEdgeLeak, buildPublicEnvManifest, findLeakedServerExports, findLeakedServerImportEdges, gateCVerify } from "./gate-c-verify.mjs";
6
+ import { CLIENT_PAGE_REGISTRY_ID, ClientPageRegistryPluginOptions, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry } from "./page-registry-plugin.mjs";
7
+ import { ProjectionAmbiguityError, ProjectionResult, projection } from "./projection.mjs";
8
+ import { Plugin } from "vite";
9
+
10
+ //#region ../web/src/vite/index.d.ts
11
+ type WarlockClientBoundaryOptions = Parameters<typeof gateAResolve>[0];
12
+ type BuildWarlockHydrationClientOptions = Readonly<{
13
+ appRoot: string;
14
+ webRoot: string; /** Absolute client output dir — threaded to `buildHydrationClient` (`<outdir>/client`). */
15
+ outDir: string;
16
+ resolveAliases: BuildHydrationClientOptions["resolveAliases"];
17
+ external?: BuildHydrationClientOptions["external"];
18
+ }>;
19
+ /**
20
+ * The composed client-build pipeline: projection
21
+ * strips the 5 server exports first, THEN Gate B's `transform` checks
22
+ * whatever source remains for inline secret reads, THEN Gate A's
23
+ * `resolveId` judges whatever imports remain. Array order here is
24
+ * `[projection(), gateBSecrets(), gateAResolve()]` to match Vite's own
25
+ * pipeline shape (`transform` before `resolveId`), but array order alone
26
+ * does not guarantee this — see the hook-ordering fact below, which is what
27
+ * actually makes the composition correct.
28
+ *
29
+ * Empirically observed fact (via an instrumented real `vite.build()`, not
30
+ * assumed from plugin array order): for a given module M, Vite/Rollup calls
31
+ * `transform(M)` BEFORE it calls `resolveId` for any of M's own import
32
+ * specifiers — because Rollup must parse M's post-transform source to even
33
+ * discover which specifiers to resolve next. Concretely: `transform` ran on
34
+ * `entry.page.tsx` first, and only after that did `resolveId("./dep", ...)`
35
+ * fire for the import statement still present in the transformed code. A
36
+ * consequence follows directly: if projection's `transform` removes an
37
+ * import statement from a page module entirely (e.g. `loader`'s
38
+ * `@warlock.js/core` import, stripped because `loader` itself is removed),
39
+ * `resolveId` is never invoked for that specifier at all — Gate A doesn't
40
+ * "let it pass", it never sees it. Gate A's `resolveId` only fires for
41
+ * imports that survive projection's `transform`, which is exactly why a
42
+ * component-level `@warlock.js/core` import (never touched by projection)
43
+ * still reaches and is refused by Gate A.
44
+ *
45
+ * This is OBSERVED Rollup behavior, not a documented contract — a future
46
+ * Vite/Rollup upgrade could invert it. Pinned by a
47
+ * regression test (`index.spec.ts`, "D.3 hook ordering pin") that fails
48
+ * loudly if the ordering ever inverts, and by the `vite` peer floor in
49
+ * `web/package.json` (`>=7.3.5`, the version this was verified against). If
50
+ * that test ever fails after a Vite bump: the failure mode of the ordering
51
+ * assumption breaking is SAFE — projection would stop removing an import
52
+ * statement Gate A still sees, so Gate A would refuse an import it used to
53
+ * silently let a stripped server export take with it. That is a loud build
54
+ * failure ("Gate A refused an import"), never a silent client-bundle leak.
55
+ * Do NOT "fix" an apparent Gate A false-positive after a Vite upgrade by
56
+ * weakening Gate A (e.g. widening what it lets through) — investigate
57
+ * whether this ordering assumption broke instead; loosening Gate A to work
58
+ * around it would turn a loud failure into the exact silent leak this
59
+ * pipeline exists to prevent.
60
+ *
61
+ * Gate B is placed between the two for the same reason, but for a `transform`
62
+ * hook rather than `resolveId`: within a plugin array, Rollup runs each
63
+ * module's registered `transform` hooks in array order, each one receiving
64
+ * the PREVIOUS plugin's output. Running Gate B after projection means it
65
+ * inspects the POST-projection source — a `process.env.SECRET` read inside
66
+ * `loader` (a server export, legitimately reading a real secret server-side)
67
+ * is invisible to Gate B once projection has already removed `loader`
68
+ * entirely, exactly as it should be: Gate B's job is to fence client-bound
69
+ * code, and projection is what decides what counts as client-bound. A
70
+ * component-level secret read is untouched by projection and still reaches
71
+ * Gate B, which refuses it. Gate B does not depend on Gate A's `resolveId`
72
+ * output at all (orthogonal concern, raw source vs. import paths), so its
73
+ * position relative to Gate A is not load-bearing — it is placed before Gate
74
+ * A only to keep both `transform` hooks adjacent in the array.
75
+ *
76
+ * Gate C (`gate-c-verify.ts`) runs last and only at `generateBundle` — after
77
+ * the entire `transform`/`resolveId` build phase has completed for every
78
+ * plugin, regardless of array position (a Rollup lifecycle fact, not
79
+ * something this array order enforces). It verifies the EMITTED output the
80
+ * other three produced: no server export survived as a top-level binding, no
81
+ * import edge into a server-only package survived into the module graph, and
82
+ * it emits the reviewable `PUBLIC_*` inlined-value manifest. `gateBSecrets` and `gateCVerify` share
83
+ * one `PublicEnvTracker` instance so the manifest and Gate B's own unread-key
84
+ * exclusion check agree by construction, not by coincidence.
85
+ *
86
+ * `clientPageRegistry()` is FIRST, ahead of projection. It contributes no
87
+ * `transform` at all — only a `resolveId`/`load` pair for one synthetic id —
88
+ * so it cannot displace or pre-empt any gate's inspection of any real file.
89
+ * Two reasons for the position, one of which is not load-bearing and is
90
+ * labelled as such:
91
+ *
92
+ * 1. Load-bearing: it must own `virtual:warlock/pages` before Gate A's
93
+ * `resolveId` (also `enforce: "pre"`) reaches its `this.resolve(...)` call
94
+ * for that specifier. Gate A's nested resolve would find it anyway, but
95
+ * routing the id through Gate A's importer-chain bookkeeping only to have
96
+ * it come back means a synthetic id can surface in a user-facing "Import
97
+ * chain:" message. Resolving it first keeps ownership of the id in one
98
+ * place.
99
+ * 2. NOT load-bearing: the position relative to `projection()`. Projection is
100
+ * `enforce: "pre"` and selects by file BASENAME (`projection.ts:242-248`),
101
+ * so it transforms every `*.page.tsx` / `layout.tsx` / `root.tsx` that
102
+ * enters the graph regardless of who imported it or where this plugin sits.
103
+ * The registry emits absolute POSIX specifiers that Rollup resolves and
104
+ * loads as ORDINARY file modules — they are not inlined into the virtual
105
+ * module — so each one is transformed exactly as a page imported from a
106
+ * real file would be. Projection declines the virtual module itself
107
+ * (`\0virtual:warlock/pages` has no matching basename), which is correct:
108
+ * generated code has no server exports to strip.
109
+ *
110
+ * Point 2 is asserted, not assumed, by a real `vite.build()` in
111
+ * `page-registry-plugin.spec.ts`: a fixture page whose `loader` — and only its
112
+ * `loader` — imports a marker module that Gate A independently PERMITS, built
113
+ * through this exact array, with the marker proven absent from every emitted
114
+ * chunk while the page's own component text is proven present. Inspecting this
115
+ * array's order would prove nothing about what reaches the browser.
116
+ */
117
+ declare function warlockClientBoundary(options?: Parameters<typeof gateAResolve>[0]): Plugin[];
118
+ /**
119
+ * Callable production seam: callers own their exact source aliases and the
120
+ * app-root classification boundary, while this module owns the one canonical
121
+ * projection/Gate B/Gate A/Gate C composition.
122
+ */
123
+ declare function buildWarlockHydrationClient(options: BuildWarlockHydrationClientOptions): Promise<BuildHydrationClientResult>;
124
+ //#endregion
125
+ export { type BuildHydrationClientOptions, type BuildHydrationClientResult, BuildWarlockHydrationClientOptions, CLIENT_PAGE_REGISTRY_ID, type ClientPageRegistryPluginOptions, type EnvironmentClassifier, type EnvironmentClassifierOptions, type GateCOptions, HYDRATION_CLIENT_ENTRY_NAME, type HydrationClientBuildOutput, type HydrationClientEntry, ProjectionAmbiguityError, type ProjectionResult, type PublicEnvManifestEntry, type PublicEnvTracker, RESOLVED_CLIENT_PAGE_REGISTRY_ID, type ServerExportLeak, type ServerImportEdgeLeak, WarlockClientBoundaryOptions, type WarlockEnvironment, buildHydrationClient, buildPublicEnvManifest, buildWarlockHydrationClient, clientPageRegistry, createHydrationClientEntry, createPublicEnvTracker, findLeakedServerExports, findLeakedServerImportEdges, gateAResolve, gateBSecrets, gateCVerify, projection, warlockClientBoundary };
126
+ //# sourceMappingURL=index.d.mts.map