@warlock.js/web 5.1.0 → 5.2.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.
- package/CHANGELOG.md +174 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +11 -14
- package/esm/build/contribution.mjs +46 -58
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +105 -45
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +47 -13
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +55 -13
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +48 -0
- package/esm/build/page-default-export.mjs.map +1 -0
- package/esm/build/page-routes-manifest.mjs +21 -0
- package/esm/build/page-routes-manifest.mjs.map +1 -0
- package/esm/build/public-files.mjs +45 -0
- package/esm/build/public-files.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +40 -9
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +1 -1
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/index.d.mts +2 -1
- package/esm/client/runtime/index.mjs +2 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +3 -2
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +13 -1
- package/esm/client/runtime/matcher.mjs +13 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +15 -2
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +40 -2
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +4 -2
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +6 -102
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +37 -8
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/loaders.d.mts +7 -36
- package/esm/metadata.d.mts +1 -1
- package/esm/metadata.mjs +1 -1
- package/esm/metadata.mjs.map +1 -1
- package/esm/props.d.mts +16 -8
- package/esm/route.d.mts +2 -17
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +1 -1
- package/esm/routing/url-fragment.mjs.map +1 -1
- package/esm/runtime/register-modules.d.mts +19 -0
- package/esm/runtime/register-modules.mjs +22 -0
- package/esm/runtime/register-modules.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +2 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +2 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +31 -6
- package/esm/server/create-page-route-handler.mjs +128 -43
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +2 -13
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +14 -0
- package/esm/server/error-page.mjs +39 -0
- package/esm/server/error-page.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +7 -4
- package/esm/server/execute-page-request.mjs +79 -80
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +33 -92
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +2 -2
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/index.d.mts +5 -5
- package/esm/server/index.mjs +3 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
- package/esm/server/install-page-routes-from-manifest.mjs +71 -10
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +32 -15
- package/esm/server/install-page-routes.mjs +91 -39
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +3 -36
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +2 -2
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -1
- package/esm/server/not-found-page.mjs +2 -2
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-context.d.mts +3 -9
- package/esm/server/page-context.mjs +3 -16
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +77 -0
- package/esm/server/page-file-change.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +25 -10
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +38 -0
- package/esm/server/page-render-bundle.mjs.map +1 -0
- package/esm/server/page-route-reload.mjs +54 -0
- package/esm/server/page-route-reload.mjs.map +1 -0
- package/esm/server/register-production-public-files.mjs +40 -0
- package/esm/server/register-production-public-files.mjs.map +1 -0
- package/esm/server/render-page.d.mts +11 -14
- package/esm/server/render-page.mjs +154 -28
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +2 -2
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +111 -46
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +41 -30
- package/esm/server/stylesheet-urls.mjs +137 -52
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +42 -0
- package/esm/server/unregistered-pages.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +2 -3
- package/esm/server/web-connector-factory.mjs +7 -6
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +138 -18
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +18 -3
- package/esm/shared.mjs +3 -7
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +2 -2
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +67 -13
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +35 -17
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +6 -6
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +2 -2
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +9 -6
- package/esm/vite/index.mjs +124 -11
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +22 -3
- package/esm/vite/page-registry-plugin.mjs +48 -14
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +2 -2
- package/esm/vite/projection.mjs +38 -15
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +220 -56
- package/llms.txt +5 -3
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +117 -10
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +1 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +14 -8
- package/skills/write-the-root/SKILL.md +3 -1
- package/esm/server/buffered-response.d.mts +0 -58
- package/esm/server/buffered-response.mjs +0 -115
- package/esm/server/buffered-response.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-b-secrets.mjs","names":[],"sources":["../../../../../../../web/src/vite/gate-b-secrets.ts"],"sourcesContent":["/**\r\n * Gate B — an AST transform gate for inline secrets.\r\n *\r\n * `resolveId` (Gate A) refuses import PATHS; it cannot see a bare property\r\n * read like `process.env.SECRET` or `import.meta.env.SOME_KEY` — there is no\r\n * import to refuse, just a member expression. Gate B is the orthogonal,\r\n * transform-time check that catches exactly that class. It is applied to\r\n * EVERY module Vite processes in the client build — NOT scoped to\r\n * `*.page.tsx` like projection, because a secret can leak from any helper,\r\n * component, or shared util, not only from a page file.\r\n *\r\n * The PUBLIC_ env-var convention this gate enforces is DEFINED here and\r\n * nowhere else: a client module may read\r\n * `import.meta.env.X` only when `X` starts with `PUBLIC_`. `process.env` is\r\n * never readable client-side at all — Node's `process` object does not exist\r\n * in the browser, so any static or computed read of `process.env.X` is\r\n * forbidden regardless of `X`.\r\n *\r\n * Gate A, Gate C and the SSR mirror rule are NOT this gate. Do not extend\r\n * this file to cover them; they are separate slices.\r\n *\r\n * A bare `import.meta.env` reference — used as a value directly (passed as\r\n * an argument, spread, destructured, or aliased to a variable) rather than\r\n * narrowed to a single static `.KEY` access — is caught here too: it leaks\r\n * the WHOLE env object, not one var, so it is the most\r\n * severe violation this gate judges, and is caught at the exact source line\r\n * like every other Gate B violation, not only as a whole-build\r\n * `generateBundle` failure. See `findViolation`'s `consumedEnvBases` note.\r\n */\r\nimport { parse } from \"@babel/parser\";\r\nimport type { Plugin } from \"vite\";\r\n\r\nconst PUBLIC_ENV_PREFIX = \"PUBLIC_\";\r\n\r\n/**\r\n * Vite injects these five `import.meta.env` keys itself (vite's own\r\n * `resolveConfig`: `env: { ...userEnv, BASE_URL, MODE, DEV, PROD }`, plus\r\n * `SSR` set per-environment by vite's `vite:define` plugin) — they are\r\n * framework metadata, not app secrets, and are legitimately readable\r\n * client-side with no `PUBLIC_` prefix. Named explicitly here so a future\r\n * false positive against these doesn't get\r\n * \"fixed\" by widening the PUBLIC_ rule instead of consulting this allowlist.\r\n */\r\nconst VITE_BUILTIN_ENV_KEYS = new Set([\"MODE\", \"DEV\", \"PROD\", \"BASE_URL\", \"SSR\"]);\r\n\r\nconst JS_MODULE_EXTENSIONS = /\\.(tsx?|jsx?|mjs|cjs)$/;\r\n\r\nfunction isJsModule(id: string): boolean {\r\n return JS_MODULE_EXTENSIONS.test(id.split(\"?\")[0]);\r\n}\r\n\r\n/**\r\n * Third-party dependencies (React among them) commonly guard dev-only code\r\n * with `process.env.NODE_ENV` checks that Vite's own dependency\r\n * pre-bundling/define step is responsible for handling — Gate B judges this\r\n * app's own source, not a vendor package's internal layout, the same\r\n * \"dependency internals are out of scope\" call Gate A's rule 4 makes for\r\n * `$module/web/` classification.\r\n */\r\nfunction isNodeModulesFile(id: string): boolean {\r\n return /(^|[\\\\/])node_modules([\\\\/]|$)/.test(id);\r\n}\r\n\r\n/**\r\n * Generic duck-typed AST walk (mirrors `projection.ts`'s `collectIdentifierNames`\r\n * — no `@babel/traverse` dependency, this package only needs `@babel/parser`).\r\n * Visits every node in the tree, including nested function bodies and JSX\r\n * expression containers, so a secret read buried inside a callback or a\r\n * conditional is caught just as one at the top level is.\r\n */\r\nfunction walk(node: unknown, visit: (node: Record<string, any>) => void): void {\r\n if (!node || typeof node !== \"object\") return;\r\n if (Array.isArray(node)) {\r\n for (const item of node) walk(item, visit);\r\n return;\r\n }\r\n const record = node as Record<string, any>;\r\n if (typeof record.type !== \"string\") return;\r\n visit(record);\r\n for (const key of Object.keys(record)) {\r\n if (key === \"type\" || key === \"start\" || key === \"end\" || key === \"loc\" || key === \"range\") continue;\r\n if (key === \"leadingComments\" || key === \"trailingComments\" || key === \"innerComments\" || key === \"extra\") {\r\n continue;\r\n }\r\n walk(record[key], visit);\r\n }\r\n}\r\n\r\nfunction isIdentifierNamed(node: any, name: string): boolean {\n return node?.type === \"Identifier\" && node.name === name;\n}\n\nfunction isMemberExpression(node: any): boolean {\n return node?.type === \"MemberExpression\" || node?.type === \"OptionalMemberExpression\";\n}\n\nfunction isProcessObject(node: any): boolean {\n if (isIdentifierNamed(node, \"process\")) return true;\n if (!isMemberExpression(node)) return false;\n\n const host = node.object;\n if (\n !isIdentifierNamed(host, \"globalThis\") &&\n !isIdentifierNamed(host, \"window\") &&\n !isIdentifierNamed(host, \"self\")\n ) {\n return false;\n }\n\n if (!node.computed) return isIdentifierNamed(node.property, \"process\");\n return node.property?.type === \"StringLiteral\" && node.property.value === \"process\";\n}\n\n/** Matches the `process.env` member expression itself (dot or static-bracket). */\nfunction isProcessEnvBase(node: any): boolean {\n if (!isMemberExpression(node)) return false;\n if (!isProcessObject(node.object)) return false;\n if (!node.computed) return isIdentifierNamed(node.property, \"env\");\n return node.property?.type === \"StringLiteral\" && node.property.value === \"env\";\n}\n\r\n/** Matches the `import.meta.env` member expression itself (dot or static-bracket). */\r\nfunction isImportMetaEnvBase(node: any): boolean {\n if (!isMemberExpression(node)) return false;\n const object = node.object;\r\n if (object?.type !== \"MetaProperty\") return false;\r\n if (object.meta?.name !== \"import\" || object.property?.name !== \"meta\") return false;\r\n if (!node.computed) return isIdentifierNamed(node.property, \"env\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"env\";\r\n}\r\n\r\ntype KeyResolution = { readonly static: true; readonly key: string } | { readonly static: false };\r\n\r\n/**\r\n * Resolves the key an outer `<base>.<key>` / `<base>[<key>]` member\r\n * expression reads off `process.env` or `import.meta.env`. Only a literal\r\n * dot-property or a static string-literal bracket key counts as static —\r\n * anything else (a variable, a template literal, a call expression, ...) is\r\n * a computed key and must fail closed (`c604f0bc` §5 / §3 \"never guess\").\r\n */\r\nfunction resolveKey(outer: any): KeyResolution {\r\n if (!outer.computed) {\r\n if (outer.property?.type === \"Identifier\") return { static: true, key: outer.property.name };\r\n return { static: false };\r\n }\r\n if (outer.property?.type === \"StringLiteral\") return { static: true, key: outer.property.value };\r\n return { static: false };\r\n}\r\n\r\nfunction expressionText(code: string, node: any): string {\r\n return code.slice(node.start as number, node.end as number);\r\n}\r\n\r\n/**\r\n * Tracks which declared `PUBLIC_*` keys were actually read anywhere in the\r\n * client build, and the resolved env values Vite would inline for them.\r\n * Shared, by construction, between `gateBSecrets` (which owns the writes —\r\n * `transform`'s `onPublicKeyRead` callback, `configResolved`'s declared-env\r\n * snapshot) and Gate C's inlined-value manifest (`gate-c-verify.ts`), so the\r\n * manifest's \"was this key actually inlined\" list and Gate B's own\r\n * unread-key exclusion (`generateBundle` below) can never drift apart into\r\n * two different answers — they read the same `Set`/object, not two\r\n * independently-recomputed ones.\r\n */\r\nexport interface PublicEnvTracker {\r\n readonly referencedKeys: Set<string>;\r\n declaredEnv: Record<string, unknown>;\r\n}\r\n\r\nexport function createPublicEnvTracker(): PublicEnvTracker {\r\n return { referencedKeys: new Set(), declaredEnv: {} };\r\n}\r\n\r\ninterface Violation {\r\n line: number;\r\n expression: string;\r\n cause: string;\r\n fix: string;\r\n}\r\n\r\n/**\r\n * Walks a module's AST looking for `process.env.*` and `import.meta.env.*`\r\n * reads and returns the first violation found, or `undefined` if the module\r\n * is clean. Only the first violation is reported per module, fail-fast, so a\r\n * build failure always points at one concrete fix.\r\n *\r\n * `onPublicKeyRead` fires for every statically-resolved `PUBLIC_*` key\r\n * allowed through, so the caller can track which declared `PUBLIC_*` env\r\n * vars are actually referenced anywhere in the client build (see\r\n * `gateBSecrets`'s `generateBundle` check below).\r\n *\r\n * `consumedEnvBases` tracks every `import.meta.env`\r\n * MemberExpression node that was already judged as the `.object` of a\r\n * further static/computed `.KEY` access (the two checks above) — i.e. a\r\n * NARROWED read, whether allowed or refused. Because `walk` visits a parent\r\n * node before its children, an outer `<base>.KEY` access always marks its\r\n * `base` sub-node as consumed BEFORE that same sub-node is visited on its\r\n * own. Any `import.meta.env` MemberExpression NOT found in this set when\r\n * visited on its own is therefore a bare reference used as a value —\r\n * `const env = import.meta.env`, `fn(import.meta.env)`,\r\n * `fn({...import.meta.env})`, `const { X } = import.meta.env` all take this\r\n * exact shape (the base is a direct child of a declarator/argument/spread,\r\n * never wrapped in one more `.KEY` MemberExpression) — and leaks the WHOLE\r\n * env object, not one key, so it fails regardless of what it's assigned to.\r\n */\r\nfunction findViolation(\r\n code: string,\r\n ast: any,\r\n onPublicKeyRead: (key: string) => void,\r\n): Violation | undefined {\r\n let found: Violation | undefined;\r\n const consumedEnvBases = new WeakSet<object>();\n\n walk(ast.program, (node) => {\n if (found || !isMemberExpression(node)) return;\n const outer = node;\r\n\r\n if (isProcessEnvBase(outer.object)) {\r\n const key = resolveKey(outer);\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: key.static\r\n ? `\"process.env.${key.key}\" is read in client-bound code. process.env does not exist in the browser — there is no \"public\" process.env key, static or computed.`\r\n : `a computed key is read off process.env in client-bound code. process.env does not exist in the browser, and the compiler cannot guess whether a computed key is safe.`,\r\n fix: `Move the code that needs this value into a *.server.ts file or a server export (loader/route/middleware/validation/metadata), or — if the client genuinely needs this value — expose it via import.meta.env.${PUBLIC_ENV_PREFIX}* instead.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isImportMetaEnvBase(outer.object)) {\r\n consumedEnvBases.add(outer.object);\r\n const key = resolveKey(outer);\r\n if (key.static && VITE_BUILTIN_ENV_KEYS.has(key.key)) return; // Vite built-in, not a secret\r\n if (key.static && key.key.startsWith(PUBLIC_ENV_PREFIX)) {\r\n onPublicKeyRead(key.key);\r\n return; // allowed\r\n }\r\n\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: key.static\r\n ? `\"import.meta.env.${key.key}\" is read in client-bound code, but its name does not start with \"${PUBLIC_ENV_PREFIX}\". Only import.meta.env keys prefixed \"${PUBLIC_ENV_PREFIX}\" are allowed in the client build — everything else is assumed to be a secret.`\r\n : `a computed key is read off import.meta.env in client-bound code. The compiler cannot guess whether a computed key resolves to a \"${PUBLIC_ENV_PREFIX}\"-prefixed name, so it fails closed rather than assume the key is public.`,\r\n fix: key.static\r\n ? `Rename the env var to start with \"${PUBLIC_ENV_PREFIX}\" (e.g. \"${PUBLIC_ENV_PREFIX}${key.key}\") if it is genuinely safe to ship to the browser, or move the code that reads it into a *.server.ts file / server export otherwise.`\r\n : `Use a static \"import.meta.env.${PUBLIC_ENV_PREFIX}*\" literal key instead of a computed one, or move the code that reads it into a *.server.ts file / server export if the key resolves to a secret.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isImportMetaEnvBase(outer) && !consumedEnvBases.has(outer)) {\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: `\"import.meta.env\" is referenced as a whole object in client-bound code (not narrowed to one static \"${PUBLIC_ENV_PREFIX}*\" key access) — used as a value like this (assigned, destructured, spread, or passed as an argument), it leaks every declared env var, public or not, to the client.`,\r\n fix: `Read only the specific \"import.meta.env.${PUBLIC_ENV_PREFIX}*\" key(s) you actually need, one at a time, instead of referencing the whole \"import.meta.env\" object.`,\r\n };\r\n }\r\n });\r\n\r\n return found;\r\n}\r\n\r\n/**\r\n * The client-build Vite plugin. `vite` is only imported for its types\r\n * (`import type`), matching Gate A / projection's peer-dependency shape.\r\n *\r\n * Beyond the `transform` gate itself, this plugin also owns two pieces of\r\n * Vite config surface, both load-bearing for\r\n * the `PUBLIC_` convention this file defines:\r\n *\r\n * 1. `config()` sets `envPrefix: \"PUBLIC_\"`. Vite's own `import.meta.env.*`\r\n * inlining (its `vite:define` plugin) only loads env vars from\r\n * `process.env`/`.env` files that match `envPrefix`, which defaults to\r\n * `\"VITE_\"` — NOT `\"PUBLIC_\"`. Verified against a real build: without this\r\n * line, `import.meta.env.PUBLIC_API_URL` silently compiles to `void 0` in\r\n * the emitted bundle (Vite's own definePlugin doesn't know the var\r\n * exists, so it replaces the read with its \"undefined\" fallback), even\r\n * though Gate B's `transform` gate allows the read through. This is not\r\n * just an optimization — without it, every `PUBLIC_*` read Gate B\r\n * approves is a silent runtime bug.\r\n * 2. `configResolved`/`generateBundle` track which declared `PUBLIC_*` keys\r\n * were actually read (via `findViolation`'s `onPublicKeyRead` callback)\r\n * and, once the bundle is emitted, scan the real output for any\r\n * unread key's value leaking in anyway (e.g. via `{...import.meta.env}`\r\n * or another whole-object read, which Vite serializes in full,\r\n * independent of Gate B's own per-key checks) — verified on the actual\r\n * emitted chunk code, not assumed from the transform/define config alone.\r\n *\r\n * `options.tracker` (default: a private, unshared one) is where that\r\n * read/declared-env state lives — pass the SAME `PublicEnvTracker` instance\r\n * used to build Gate C's inlined-value manifest (`warlockClientBoundary`\r\n * does this) so the manifest and this plugin's unread-key exclusion read off\r\n * one shared Set, not two.\r\n */\r\nexport function gateBSecrets(gateBOptions: { tracker?: PublicEnvTracker } = {}): Plugin {\r\n const tracker = gateBOptions.tracker ?? createPublicEnvTracker();\r\n\r\n return {\r\n name: \"warlock:gate-b-secrets\",\r\n enforce: \"pre\",\r\n config() {\r\n return { envPrefix: PUBLIC_ENV_PREFIX };\r\n },\r\n configResolved(config) {\r\n tracker.declaredEnv = config.env;\r\n },\r\n transform(code, id, options) {\r\n if (options?.ssr) return null;\r\n if (!isJsModule(id)) return null;\r\n if (isNodeModulesFile(id)) return null;\r\n\r\n const ast = parse(code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\r\n\r\n const violation = findViolation(code, ast, (key) => tracker.referencedKeys.add(key));\r\n if (violation) {\r\n this.error(\r\n [\r\n `Gate B refused a module: forbidden inline env read in the client build.`,\r\n ``,\r\n `File: ${id}:${violation.line}`,\r\n `Expression: ${violation.expression}`,\r\n `Cause: ${violation.cause}`,\r\n `Fix: ${violation.fix}`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n return null;\r\n },\r\n generateBundle(_options, bundle) {\r\n if (this.environment?.config?.consumer === \"server\") return;\r\n\r\n const unreadKeys = Object.keys(tracker.declaredEnv).filter(\r\n (key) => key.startsWith(PUBLIC_ENV_PREFIX) && !tracker.referencedKeys.has(key),\r\n );\r\n if (unreadKeys.length === 0) return;\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (file.type !== \"chunk\") continue;\r\n\r\n for (const key of unreadKeys) {\r\n const inlinedValue = JSON.stringify(tracker.declaredEnv[key]);\r\n if (!file.code.includes(inlinedValue)) continue;\r\n\r\n this.error(\r\n [\r\n `Gate B refused a build: an unread PUBLIC_ env var was inlined into the emitted bundle.`,\r\n ``,\r\n `File: ${file.fileName}`,\r\n `Key: import.meta.env.${key}`,\r\n `Cause: \"${key}\" is never read as \"import.meta.env.${key}\" anywhere in the client build, but its value was still inlined into the emitted output — most likely a whole-object \"import.meta.env\" read (e.g. a spread or destructure), which Vite serializes in full regardless of which individual keys are actually used.`,\r\n `Fix: read \"import.meta.env.${key}\" directly wherever its value is needed instead of spreading/destructuring the whole \"import.meta.env\" object, or remove the unused \"${key}\" declaration if the client build doesn't need it.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n }\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gate-b-secrets.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/gate-b-secrets.ts"],"sourcesContent":["/**\r\n * Gate B — an AST transform gate for inline secrets.\r\n *\r\n * `resolveId` (Gate A) refuses import PATHS; it cannot see a bare property\r\n * read like `process.env.SECRET` or `import.meta.env.SOME_KEY` — there is no\r\n * import to refuse, just a member expression. Gate B is the orthogonal,\r\n * transform-time check that catches exactly that class. It is applied to\r\n * EVERY module Vite processes in the client build — NOT scoped to\r\n * `*.page.tsx` like projection, because a secret can leak from any helper,\r\n * component, or shared util, not only from a page file.\r\n *\r\n * The PUBLIC_ env-var convention this gate enforces is DEFINED here and\r\n * nowhere else: a client module may read\r\n * `import.meta.env.X` only when `X` starts with `PUBLIC_`. `process.env` is\r\n * never readable client-side at all — Node's `process` object does not exist\r\n * in the browser, so any static or computed read of `process.env.X`, or of\r\n * `process.env` itself as a bare whole-object value (destructured, spread,\r\n * assigned, or passed as an argument), is forbidden regardless of `X`.\r\n *\r\n * Gate A, Gate C and the SSR mirror rule are NOT this gate. Do not extend\r\n * this file to cover them; they are separate slices.\r\n *\r\n * A bare `import.meta.env` reference — used as a value directly (passed as\r\n * an argument, spread, destructured, or aliased to a variable) rather than\r\n * narrowed to a single static `.KEY` access — is caught here too: it leaks\r\n * the WHOLE env object, not one var, so it is the most\r\n * severe violation this gate judges, and is caught at the exact source line\r\n * like every other Gate B violation, not only as a whole-build\r\n * `generateBundle` failure. See `findViolation`'s `consumedEnvBases` note.\r\n *\r\n * A bare `process.env` reference is caught the same way, for the same\r\n * reason — `findViolation`'s `consumedEnvBases` check applies to both bases,\r\n * and unlike `import.meta.env` there is no allowed key at all for\r\n * `process.env`, narrowed or bare.\r\n */\r\nimport { parse } from \"@babel/parser\";\r\nimport type { Plugin } from \"vite\";\r\n\r\nconst PUBLIC_ENV_PREFIX = \"PUBLIC_\";\r\n\r\n/**\r\n * Vite injects these five `import.meta.env` keys itself (vite's own\r\n * `resolveConfig`: `env: { ...userEnv, BASE_URL, MODE, DEV, PROD }`, plus\r\n * `SSR` set per-environment by vite's `vite:define` plugin) — they are\r\n * framework metadata, not app secrets, and are legitimately readable\r\n * client-side with no `PUBLIC_` prefix. Named explicitly here so a future\r\n * false positive against these doesn't get\r\n * \"fixed\" by widening the PUBLIC_ rule instead of consulting this allowlist.\r\n */\r\nconst VITE_BUILTIN_ENV_KEYS = new Set([\"MODE\", \"DEV\", \"PROD\", \"BASE_URL\", \"SSR\"]);\r\n\r\nconst JS_MODULE_EXTENSIONS = /\\.(tsx?|jsx?|mjs|cjs)$/;\r\n\r\nfunction isJsModule(id: string): boolean {\r\n return JS_MODULE_EXTENSIONS.test(id.split(\"?\")[0]);\r\n}\r\n\r\n/**\r\n * Third-party dependencies (React among them) commonly guard dev-only code\r\n * with `process.env.NODE_ENV` checks that Vite's own dependency\r\n * pre-bundling/define step is responsible for handling — Gate B judges this\r\n * app's own source, not a vendor package's internal layout, the same\r\n * \"dependency internals are out of scope\" call Gate A's rule 4 makes for\r\n * `$module/web/` classification.\r\n */\r\nfunction isNodeModulesFile(id: string): boolean {\r\n return /(^|[\\\\/])node_modules([\\\\/]|$)/.test(id);\r\n}\r\n\r\n/**\r\n * Generic duck-typed AST walk (mirrors `projection.ts`'s `collectIdentifierNames`\r\n * — no `@babel/traverse` dependency, this package only needs `@babel/parser`).\r\n * Visits every node in the tree, including nested function bodies and JSX\r\n * expression containers, so a secret read buried inside a callback or a\r\n * conditional is caught just as one at the top level is.\r\n */\r\nfunction walk(node: unknown, visit: (node: Record<string, any>) => void): void {\r\n if (!node || typeof node !== \"object\") return;\r\n if (Array.isArray(node)) {\r\n for (const item of node) walk(item, visit);\r\n return;\r\n }\r\n const record = node as Record<string, any>;\r\n if (typeof record.type !== \"string\") return;\r\n visit(record);\r\n for (const key of Object.keys(record)) {\r\n if (key === \"type\" || key === \"start\" || key === \"end\" || key === \"loc\" || key === \"range\") continue;\r\n if (key === \"leadingComments\" || key === \"trailingComments\" || key === \"innerComments\" || key === \"extra\") {\r\n continue;\r\n }\r\n walk(record[key], visit);\r\n }\r\n}\r\n\r\nfunction isIdentifierNamed(node: any, name: string): boolean {\r\n return node?.type === \"Identifier\" && node.name === name;\r\n}\r\n\r\nfunction isMemberExpression(node: any): boolean {\r\n return node?.type === \"MemberExpression\" || node?.type === \"OptionalMemberExpression\";\r\n}\r\n\r\nfunction isProcessObject(node: any): boolean {\r\n if (isIdentifierNamed(node, \"process\")) return true;\r\n if (!isMemberExpression(node)) return false;\r\n\r\n const host = node.object;\r\n if (\r\n !isIdentifierNamed(host, \"globalThis\") &&\r\n !isIdentifierNamed(host, \"window\") &&\r\n !isIdentifierNamed(host, \"self\")\r\n ) {\r\n return false;\r\n }\r\n\r\n if (!node.computed) return isIdentifierNamed(node.property, \"process\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"process\";\r\n}\r\n\r\n/** Matches the `process.env` member expression itself (dot or static-bracket). */\r\nfunction isProcessEnvBase(node: any): boolean {\r\n if (!isMemberExpression(node)) return false;\r\n if (!isProcessObject(node.object)) return false;\r\n if (!node.computed) return isIdentifierNamed(node.property, \"env\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"env\";\r\n}\r\n\r\n/** Matches the `import.meta.env` member expression itself (dot or static-bracket). */\r\nfunction isImportMetaEnvBase(node: any): boolean {\r\n if (!isMemberExpression(node)) return false;\r\n const object = node.object;\r\n if (object?.type !== \"MetaProperty\") return false;\r\n if (object.meta?.name !== \"import\" || object.property?.name !== \"meta\") return false;\r\n if (!node.computed) return isIdentifierNamed(node.property, \"env\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"env\";\r\n}\r\n\r\ntype KeyResolution = { readonly static: true; readonly key: string } | { readonly static: false };\r\n\r\n/**\r\n * Resolves the key an outer `<base>.<key>` / `<base>[<key>]` member\r\n * expression reads off `process.env` or `import.meta.env`. Only a literal\r\n * dot-property or a static string-literal bracket key counts as static —\r\n * anything else (a variable, a template literal, a call expression, ...) is\r\n * a computed key and must fail closed (`c604f0bc` §5 / §3 \"never guess\").\r\n */\r\nfunction resolveKey(outer: any): KeyResolution {\r\n if (!outer.computed) {\r\n if (outer.property?.type === \"Identifier\") return { static: true, key: outer.property.name };\r\n return { static: false };\r\n }\r\n if (outer.property?.type === \"StringLiteral\") return { static: true, key: outer.property.value };\r\n return { static: false };\r\n}\r\n\r\nfunction expressionText(code: string, node: any): string {\r\n return code.slice(node.start as number, node.end as number);\r\n}\r\n\r\n/**\r\n * Tracks which declared `PUBLIC_*` keys were actually read anywhere in the\r\n * client build, and the resolved env values Vite would inline for them.\r\n * Shared, by construction, between `gateBSecrets` (which owns the writes —\r\n * `transform`'s `onPublicKeyRead` callback, `configResolved`'s declared-env\r\n * snapshot) and Gate C's inlined-value manifest (`gate-c-verify.ts`), so the\r\n * manifest's \"was this key actually inlined\" list and Gate B's own\r\n * unread-key exclusion (`generateBundle` below) can never drift apart into\r\n * two different answers — they read the same `Set`/object, not two\r\n * independently-recomputed ones.\r\n */\r\nexport interface PublicEnvTracker {\r\n readonly referencedKeys: Set<string>;\r\n declaredEnv: Record<string, unknown>;\r\n}\r\n\r\nexport function createPublicEnvTracker(): PublicEnvTracker {\r\n return { referencedKeys: new Set(), declaredEnv: {} };\r\n}\r\n\r\ninterface Violation {\r\n line: number;\r\n expression: string;\r\n cause: string;\r\n fix: string;\r\n}\r\n\r\n/**\r\n * Walks a module's AST looking for `process.env.*` and `import.meta.env.*`\r\n * reads — narrowed member accesses and bare whole-object references alike —\r\n * and returns the first violation found, or `undefined` if the module is\r\n * clean. Only the first violation is reported per module, fail-fast, so a\r\n * build failure always points at one concrete fix.\r\n *\r\n * `onPublicKeyRead` fires for every statically-resolved `PUBLIC_*` key\r\n * allowed through, so the caller can track which declared `PUBLIC_*` env\r\n * vars are actually referenced anywhere in the client build (see\r\n * `gateBSecrets`'s `generateBundle` check below).\r\n *\r\n * `consumedEnvBases` tracks every `process.env` and `import.meta.env`\r\n * MemberExpression node that was already judged as the `.object` of a\r\n * further static/computed `.KEY` access (the narrowed-read checks above) —\r\n * i.e. a NARROWED read, whether allowed or refused. Because `walk` visits a\r\n * parent node before its children, an outer `<base>.KEY` access always marks\r\n * its `base` sub-node as consumed BEFORE that same sub-node is visited on\r\n * its own. Any `process.env` or `import.meta.env` MemberExpression NOT found\r\n * in this set when visited on its own is therefore a bare reference used as\r\n * a value — `const env = process.env`, `fn(process.env)`,\r\n * `fn({...process.env})`, `const { X } = process.env` (and the same four\r\n * shapes for `import.meta.env`) all take this exact shape (the base is a\r\n * direct child of a declarator/argument/spread, never wrapped in one more\r\n * `.KEY` MemberExpression) — and leaks the WHOLE env object, not one key, so\r\n * it fails regardless of what it's assigned to.\r\n */\r\nfunction findViolation(\r\n code: string,\r\n ast: any,\r\n onPublicKeyRead: (key: string) => void,\r\n): Violation | undefined {\r\n let found: Violation | undefined;\r\n const consumedEnvBases = new WeakSet<object>();\r\n\r\n walk(ast.program, (node) => {\r\n if (found || !isMemberExpression(node)) return;\r\n const outer = node;\r\n\r\n if (isProcessEnvBase(outer.object)) {\r\n consumedEnvBases.add(outer.object);\r\n const key = resolveKey(outer);\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: key.static\r\n ? `\"process.env.${key.key}\" is read in client-bound code. process.env does not exist in the browser — there is no \"public\" process.env key, static or computed.`\r\n : `a computed key is read off process.env in client-bound code. process.env does not exist in the browser, and the compiler cannot guess whether a computed key is safe.`,\r\n fix: `Move the code that needs this value into a *.server.ts file or a server export (loader/route/middleware/validation/metadata), or — if the client genuinely needs this value — expose it via import.meta.env.${PUBLIC_ENV_PREFIX}* instead.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isProcessEnvBase(outer) && !consumedEnvBases.has(outer)) {\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: `\"process.env\" is referenced as a whole object in client-bound code (not narrowed to one static \"process.env.<KEY>\" access) — process.env does not exist in the browser, so reading it as a value like this (assigned, destructured, spread, or passed as an argument) is never safe: there is no \"public\" process.env key, static or computed.`,\r\n fix: `Move the code that needs this value into a *.server.ts file or a server export (loader/route/middleware/validation/metadata), reading only the specific \"process.env.<KEY>\" value you actually need, or — if the client genuinely needs a value — expose it via import.meta.env.${PUBLIC_ENV_PREFIX}* instead.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isImportMetaEnvBase(outer.object)) {\r\n consumedEnvBases.add(outer.object);\r\n const key = resolveKey(outer);\r\n if (key.static && VITE_BUILTIN_ENV_KEYS.has(key.key)) return; // Vite built-in, not a secret\r\n if (key.static && key.key.startsWith(PUBLIC_ENV_PREFIX)) {\r\n onPublicKeyRead(key.key);\r\n return; // allowed\r\n }\r\n\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: key.static\r\n ? `\"import.meta.env.${key.key}\" is read in client-bound code, but its name does not start with \"${PUBLIC_ENV_PREFIX}\". Only import.meta.env keys prefixed \"${PUBLIC_ENV_PREFIX}\" are allowed in the client build — everything else is assumed to be a secret.`\r\n : `a computed key is read off import.meta.env in client-bound code. The compiler cannot guess whether a computed key resolves to a \"${PUBLIC_ENV_PREFIX}\"-prefixed name, so it fails closed rather than assume the key is public.`,\r\n fix: key.static\r\n ? `Rename the env var to start with \"${PUBLIC_ENV_PREFIX}\" (e.g. \"${PUBLIC_ENV_PREFIX}${key.key}\") if it is genuinely safe to ship to the browser, or move the code that reads it into a *.server.ts file / server export otherwise.`\r\n : `Use a static \"import.meta.env.${PUBLIC_ENV_PREFIX}*\" literal key instead of a computed one, or move the code that reads it into a *.server.ts file / server export if the key resolves to a secret.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isImportMetaEnvBase(outer) && !consumedEnvBases.has(outer)) {\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: `\"import.meta.env\" is referenced as a whole object in client-bound code (not narrowed to one static \"${PUBLIC_ENV_PREFIX}*\" key access) — used as a value like this (assigned, destructured, spread, or passed as an argument), it leaks every declared env var, public or not, to the client.`,\r\n fix: `Read only the specific \"import.meta.env.${PUBLIC_ENV_PREFIX}*\" key(s) you actually need, one at a time, instead of referencing the whole \"import.meta.env\" object.`,\r\n };\r\n }\r\n });\r\n\r\n return found;\r\n}\r\n\r\n/**\r\n * The client-build Vite plugin. `vite` is only imported for its types\r\n * (`import type`), matching Gate A / projection's peer-dependency shape.\r\n *\r\n * Beyond the `transform` gate itself, this plugin also owns two pieces of\r\n * Vite config surface, both load-bearing for\r\n * the `PUBLIC_` convention this file defines:\r\n *\r\n * 1. `config()` sets `envPrefix: \"PUBLIC_\"`. Vite's own `import.meta.env.*`\r\n * inlining (its `vite:define` plugin) only loads env vars from\r\n * `process.env`/`.env` files that match `envPrefix`, which defaults to\r\n * `\"VITE_\"` — NOT `\"PUBLIC_\"`. Verified against a real build: without this\r\n * line, `import.meta.env.PUBLIC_API_URL` silently compiles to `void 0` in\r\n * the emitted bundle (Vite's own definePlugin doesn't know the var\r\n * exists, so it replaces the read with its \"undefined\" fallback), even\r\n * though Gate B's `transform` gate allows the read through. This is not\r\n * just an optimization — without it, every `PUBLIC_*` read Gate B\r\n * approves is a silent runtime bug.\r\n * 2. `configResolved`/`generateBundle` track which declared `PUBLIC_*` keys\r\n * were actually read (via `findViolation`'s `onPublicKeyRead` callback)\r\n * and, once the bundle is emitted, scan the real output for any\r\n * unread key's value leaking in anyway (e.g. via `{...import.meta.env}`\r\n * or another whole-object read, which Vite serializes in full,\r\n * independent of Gate B's own per-key checks) — verified on the actual\r\n * emitted chunk code, not assumed from the transform/define config alone.\r\n *\r\n * `options.tracker` (default: a private, unshared one) is where that\r\n * read/declared-env state lives — pass the SAME `PublicEnvTracker` instance\r\n * used to build Gate C's inlined-value manifest (`warlockClientBoundary`\r\n * does this) so the manifest and this plugin's unread-key exclusion read off\r\n * one shared Set, not two.\r\n */\r\nexport function gateBSecrets(gateBOptions: { tracker?: PublicEnvTracker } = {}): Plugin {\r\n const tracker = gateBOptions.tracker ?? createPublicEnvTracker();\r\n\r\n return {\r\n name: \"warlock:gate-b-secrets\",\r\n enforce: \"pre\",\r\n config() {\r\n return { envPrefix: PUBLIC_ENV_PREFIX };\r\n },\r\n configResolved(config) {\r\n tracker.declaredEnv = config.env;\r\n },\r\n transform(code, id, options) {\r\n if (options?.ssr) return null;\r\n if (!isJsModule(id)) return null;\r\n if (isNodeModulesFile(id)) return null;\r\n\r\n const ast = parse(code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\r\n\r\n const violation = findViolation(code, ast, (key) => tracker.referencedKeys.add(key));\r\n if (violation) {\r\n this.error(\r\n [\r\n `Gate B refused a module: forbidden inline env read in the client build.`,\r\n ``,\r\n `File: ${id}:${violation.line}`,\r\n `Expression: ${violation.expression}`,\r\n `Cause: ${violation.cause}`,\r\n `Fix: ${violation.fix}`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n return null;\r\n },\r\n generateBundle(_options, bundle) {\r\n if (this.environment?.config?.consumer === \"server\") return;\r\n\r\n const unreadKeys = Object.keys(tracker.declaredEnv).filter(\r\n (key) => key.startsWith(PUBLIC_ENV_PREFIX) && !tracker.referencedKeys.has(key),\r\n );\r\n if (unreadKeys.length === 0) return;\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (file.type !== \"chunk\") continue;\r\n\r\n for (const key of unreadKeys) {\r\n const inlinedValue = JSON.stringify(tracker.declaredEnv[key]);\r\n if (!file.code.includes(inlinedValue)) continue;\r\n\r\n this.error(\r\n [\r\n `Gate B refused a build: an unread PUBLIC_ env var was inlined into the emitted bundle.`,\r\n ``,\r\n `File: ${file.fileName}`,\r\n `Key: import.meta.env.${key}`,\r\n `Cause: \"${key}\" is never read as \"import.meta.env.${key}\" anywhere in the client build, but its value was still inlined into the emitted output — most likely a whole-object \"import.meta.env\" read (e.g. a spread or destructure), which Vite serializes in full regardless of which individual keys are actually used.`,\r\n `Fix: read \"import.meta.env.${key}\" directly wherever its value is needed instead of spreading/destructuring the whole \"import.meta.env\" object, or remove the unused \"${key}\" declaration if the client build doesn't need it.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n }\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { EnvironmentClassifier, EnvironmentClassifierOptions } from "./gate-a-re
|
|
|
2
2
|
import { PublicEnvTracker } from "./gate-b-secrets.mjs";
|
|
3
3
|
import { Plugin } from "vite";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region web/src/vite/gate-c-verify.d.ts
|
|
6
6
|
interface ServerExportLeak {
|
|
7
7
|
fileName: string;
|
|
8
8
|
exportName: string;
|
|
@@ -3,19 +3,19 @@ import { createPublicEnvTracker } from "./gate-b-secrets.mjs";
|
|
|
3
3
|
import { SERVER_EXPORT_NAMES } from "./projection.mjs";
|
|
4
4
|
import { parse } from "@babel/parser";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region web/src/vite/gate-c-verify.ts
|
|
7
7
|
/**
|
|
8
8
|
* Gate C — emitted-output verification.
|
|
9
9
|
*
|
|
10
|
-
* Projection removes the
|
|
10
|
+
* Projection removes the six server exports before the client graph forms.
|
|
11
11
|
* Gate A refuses forbidden import PATHS. Gate B refuses inline secret reads.
|
|
12
12
|
* All three act BEFORE or DURING the build, on source or the module graph —
|
|
13
13
|
* none of them ever looks at what actually came out the other end. Gate C
|
|
14
14
|
* is that check: a build-time assertion on the EMITTED client bundle for
|
|
15
15
|
* every page, verifying:
|
|
16
16
|
*
|
|
17
|
-
* 1. The emitted code contains none of the
|
|
18
|
-
* (`route`, `middleware`, `validation`, `loader`, `metadata`) as a
|
|
17
|
+
* 1. The emitted code contains none of the six server export names
|
|
18
|
+
* (`route`, `middleware`, `validation`, `loader`, `metadata`, `prefix`) as a
|
|
19
19
|
* top-level binding, exported or not — `findLeakedServerExports`.
|
|
20
20
|
* 2. The emitted module graph (Rollup's `OutputBundle`, as seen in
|
|
21
21
|
* `generateBundle`) contains no import edge into a
|
|
@@ -78,7 +78,7 @@ var UnverifiableChunkError = class extends Error {
|
|
|
78
78
|
`Warlock stopped this build: a file in your client bundle could not be checked for server-only code.`,
|
|
79
79
|
``,
|
|
80
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.`,
|
|
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, prefix) were kept out of it.`,
|
|
82
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
83
|
].join("\n"));
|
|
84
84
|
this.name = "UnverifiableChunkError";
|
|
@@ -236,7 +236,7 @@ function gateCVerify(options = {}) {
|
|
|
236
236
|
``,
|
|
237
237
|
`File: ${exportLeak.fileName}${exportLeak.line ? `:${exportLeak.line}` : ""}`,
|
|
238
238
|
`Export: ${exportLeak.exportName}`,
|
|
239
|
-
`Cause: "${exportLeak.exportName}" is one of the
|
|
239
|
+
`Cause: "${exportLeak.exportName}" is one of the six server exports (route, middleware, validation, loader, metadata, prefix) 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
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
241
|
].join("\n"));
|
|
242
242
|
const importEdgeLeak = findLeakedServerImportEdges(bundle, classifier)[0];
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"gate-c-verify.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/gate-c-verify.ts"],"sourcesContent":["/**\r\n * Gate C — emitted-output verification.\r\n *\r\n * Projection removes the six 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 six server export names\r\n * (`route`, `middleware`, `validation`, `loader`, `metadata`, `prefix`) 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, prefix) 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 six server exports (route, middleware, validation, loader, metadata, prefix) 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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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region web/src/vite/hydration-entries.d.ts
|
|
2
2
|
/** Stable Rollup/Vite entry name shared by development and production wiring. */
|
|
3
3
|
declare const HYDRATION_CLIENT_ENTRY_NAME = "hydration";
|
|
4
4
|
type HydrationClientEntry = Readonly<{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region web/src/vite/hydration-entries.ts
|
|
5
5
|
/** Stable Rollup/Vite entry name shared by development and production wiring. */
|
|
6
6
|
const HYDRATION_CLIENT_ENTRY_NAME = "hydration";
|
|
7
7
|
function normalizeFileSystemPath(filePath) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydration-entries.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"hydration-entries.mjs","names":[],"sources":["C:/Work/Warlock.js/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 tarball ships `esm/`, `skills/` and the docs files — no `src/`\n * — so `src/` is absent from every real install. (Verified against\n * `@warlock.js/web@5.0.2`: 191 files, none under `src/`. Note the published\n * `package.json` carries no `files` key at all; the release tooling rewrites\n * the manifest, so do not treat this repo's `files` field as the mechanism.)\n * Resolving the entry to `<webRoot>/src/hydration/index.ts` unconditionally\n * therefore worked in this monorepo and failed for every consumer, with\n * `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;;;;;;;;;;;;;;;;;;;;AAqBA,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;;;;;;AAOvB,SAAgB,2BAA2B,SAAuC;CAChF,IAAI,OAAO,YAAY,YAAY,QAAQ,KAAK,EAAE,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"}
|
package/esm/vite/index.d.mts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { HYDRATION_CLIENT_ENTRY_NAME, HydrationClientEntry, createHydrationClientEntry } from "./hydration-entries.mjs";
|
|
2
2
|
import { BuildHydrationClientOptions, BuildHydrationClientResult, HydrationClientBuildOutput, buildHydrationClient } from "./build-client.mjs";
|
|
3
3
|
import { EnvironmentClassifier, EnvironmentClassifierOptions, WarlockEnvironment, gateAResolve } from "./gate-a-resolve.mjs";
|
|
4
|
+
import { CLIENT_PAGE_REGISTRY_ID, ClientPageRegistryPluginOptions, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry, invalidateClientPageRegistry } from "./page-registry-plugin.mjs";
|
|
4
5
|
import { PublicEnvTracker, createPublicEnvTracker, gateBSecrets } from "./gate-b-secrets.mjs";
|
|
5
6
|
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
7
|
import { ProjectionAmbiguityError, ProjectionResult, projection } from "./projection.mjs";
|
|
8
8
|
import { Plugin } from "vite";
|
|
9
9
|
|
|
10
|
-
//#region
|
|
11
|
-
type WarlockClientBoundaryOptions = Parameters<typeof gateAResolve>[0]
|
|
10
|
+
//#region web/src/vite/index.d.ts
|
|
11
|
+
type WarlockClientBoundaryOptions = Parameters<typeof gateAResolve>[0] & {
|
|
12
|
+
beforePageHotUpdate?: ClientPageRegistryPluginOptions["beforePageHotUpdate"];
|
|
13
|
+
};
|
|
12
14
|
type BuildWarlockHydrationClientOptions = Readonly<{
|
|
13
15
|
appRoot: string;
|
|
14
16
|
webRoot: string; /** Absolute client output dir — threaded to `buildHydrationClient` (`<outdir>/client`). */
|
|
15
17
|
outDir: string;
|
|
16
18
|
resolveAliases: BuildHydrationClientOptions["resolveAliases"];
|
|
17
|
-
external?: BuildHydrationClientOptions["external"];
|
|
19
|
+
external?: BuildHydrationClientOptions["external"]; /** App-configured plugins, appended after Warlock's client-boundary pipeline. */
|
|
20
|
+
plugins?: BuildHydrationClientOptions["plugins"];
|
|
18
21
|
}>;
|
|
19
22
|
/**
|
|
20
23
|
* The composed client-build pipeline: projection
|
|
@@ -114,7 +117,7 @@ type BuildWarlockHydrationClientOptions = Readonly<{
|
|
|
114
117
|
* chunk while the page's own component text is proven present. Inspecting this
|
|
115
118
|
* array's order would prove nothing about what reaches the browser.
|
|
116
119
|
*/
|
|
117
|
-
declare function warlockClientBoundary(options?:
|
|
120
|
+
declare function warlockClientBoundary(options?: WarlockClientBoundaryOptions): Plugin[];
|
|
118
121
|
/**
|
|
119
122
|
* Callable production seam: callers own their exact source aliases and the
|
|
120
123
|
* app-root classification boundary, while this module owns the one canonical
|
|
@@ -122,5 +125,5 @@ declare function warlockClientBoundary(options?: Parameters<typeof gateAResolve>
|
|
|
122
125
|
*/
|
|
123
126
|
declare function buildWarlockHydrationClient(options: BuildWarlockHydrationClientOptions): Promise<BuildHydrationClientResult>;
|
|
124
127
|
//#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 };
|
|
128
|
+
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, invalidateClientPageRegistry, projection, warlockClientBoundary };
|
|
126
129
|
//# sourceMappingURL=index.d.mts.map
|
package/esm/vite/index.mjs
CHANGED
|
@@ -1,18 +1,123 @@
|
|
|
1
1
|
import { HYDRATION_CLIENT_ENTRY_NAME, createHydrationClientEntry } from "./hydration-entries.mjs";
|
|
2
2
|
import { buildHydrationClient } from "./build-client.mjs";
|
|
3
|
-
import { gateAResolve } from "./gate-a-resolve.mjs";
|
|
3
|
+
import { gateAResolve, isAppSourcePath, isRecognizedUniversalSurface, isServerFile, isWithinModuleWebFolder } from "./gate-a-resolve.mjs";
|
|
4
4
|
import { createPublicEnvTracker, gateBSecrets } from "./gate-b-secrets.mjs";
|
|
5
|
-
import { ProjectionAmbiguityError, projection } from "./projection.mjs";
|
|
5
|
+
import { ProjectionAmbiguityError, isProjectableFile, projectModule, projection } from "./projection.mjs";
|
|
6
6
|
import { buildPublicEnvManifest, findLeakedServerExports, findLeakedServerImportEdges, gateCVerify } from "./gate-c-verify.mjs";
|
|
7
|
-
import { CLIENT_PAGE_REGISTRY_ID, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry } from "./page-registry-plugin.mjs";
|
|
7
|
+
import { CLIENT_PAGE_REGISTRY_ID, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry, invalidateClientPageRegistry } from "./page-registry-plugin.mjs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { parse } from "@babel/parser";
|
|
8
10
|
|
|
9
|
-
//#region
|
|
10
|
-
|
|
11
|
+
//#region web/src/vite/index.ts
|
|
12
|
+
/**
|
|
13
|
+
* `@warlock.js/web/vite` — build-tooling subpath, kept separate from the
|
|
14
|
+
* runtime barrel (`@warlock.js/web`) so importing it never pulls `vite` into
|
|
15
|
+
* a project that doesn't build with Vite.
|
|
16
|
+
*/
|
|
17
|
+
function moduleKey(id) {
|
|
18
|
+
return id.split("?")[0].replace(/\\/g, "/");
|
|
19
|
+
}
|
|
20
|
+
const CODE_MODULE_EXTENSION = /\.([cm]?[jt]sx?)$/;
|
|
21
|
+
function isStatelessClientSurface(id, appRoot) {
|
|
22
|
+
const bare = moduleKey(id);
|
|
23
|
+
if (!isAppSourcePath(bare, appRoot)) return false;
|
|
24
|
+
if (isServerFile(bare, appRoot)) return false;
|
|
25
|
+
return isProjectableFile(bare) || isRecognizedUniversalSurface(bare) || isWithinModuleWebFolder(bare, appRoot);
|
|
26
|
+
}
|
|
27
|
+
function collectImportSpecifiers(code) {
|
|
28
|
+
const ast = parse(code, {
|
|
29
|
+
sourceType: "module",
|
|
30
|
+
plugins: ["typescript", "jsx"]
|
|
31
|
+
});
|
|
32
|
+
const imports = /* @__PURE__ */ new Set();
|
|
33
|
+
function walk(node) {
|
|
34
|
+
if (!node || typeof node !== "object") return;
|
|
35
|
+
if (Array.isArray(node)) {
|
|
36
|
+
for (const child of node) walk(child);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const record = node;
|
|
40
|
+
if ((record.type === "ImportDeclaration" || record.type === "ExportNamedDeclaration" || record.type === "ExportAllDeclaration") && record.source?.type === "StringLiteral") imports.add(record.source.value);
|
|
41
|
+
if (record.type === "CallExpression" && record.callee?.type === "Import" && record.arguments?.[0]?.type === "StringLiteral") imports.add(record.arguments[0].value);
|
|
42
|
+
if (record.type === "ImportExpression" && record.source?.type === "StringLiteral") imports.add(record.source.value);
|
|
43
|
+
for (const [key, child] of Object.entries(record)) {
|
|
44
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range" || key.endsWith("Comments") || key === "extra") continue;
|
|
45
|
+
walk(child);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
walk(ast.program);
|
|
49
|
+
return imports;
|
|
50
|
+
}
|
|
51
|
+
function clientViewOf(state, code, id) {
|
|
52
|
+
const key = moduleKey(id);
|
|
53
|
+
if (!state.clientBoundModules.has(key) && !isStatelessClientSurface(key, state.appRoot)) return;
|
|
54
|
+
state.clientBoundModules.add(key);
|
|
55
|
+
if (!CODE_MODULE_EXTENSION.test(key)) {
|
|
56
|
+
state.clientImportsByModule.set(key, /* @__PURE__ */ new Set());
|
|
57
|
+
return code;
|
|
58
|
+
}
|
|
59
|
+
const clientCode = isProjectableFile(key) ? projectModule(code, key).code : code;
|
|
60
|
+
state.clientImportsByModule.set(key, collectImportSpecifiers(clientCode));
|
|
61
|
+
return clientCode;
|
|
62
|
+
}
|
|
63
|
+
function markResolvedClientModule(state, resolved) {
|
|
64
|
+
if (!resolved) return;
|
|
65
|
+
const id = typeof resolved === "string" ? resolved : resolved.id;
|
|
66
|
+
if (!id.includes("\0")) state.clientBoundModules.add(moduleKey(id));
|
|
67
|
+
}
|
|
68
|
+
function isServerEnvironment(context) {
|
|
69
|
+
return context.environment?.config.consumer === "server";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Keeps the production/client pipeline byte-identical, while giving Gate A
|
|
73
|
+
* and Gate B a validation-only view in Vite's development SSR environment.
|
|
74
|
+
* SSR still evaluates the original source: only the gates receive the
|
|
75
|
+
* projected client view, so loader/server exports retain their legitimate
|
|
76
|
+
* server access while component-visible code is refused before evaluation.
|
|
77
|
+
*/
|
|
78
|
+
function clientEnvironmentOnly(plugin, ssrState) {
|
|
79
|
+
const validatesDevSsr = plugin.name === "warlock:gate-a-resolve" || plugin.name === "warlock:gate-b-secrets";
|
|
80
|
+
const originalTransform = typeof plugin.transform === "function" ? plugin.transform : plugin.transform?.handler;
|
|
81
|
+
const originalResolveId = typeof plugin.resolveId === "function" ? plugin.resolveId : plugin.resolveId?.handler;
|
|
82
|
+
const originalBuildStart = typeof plugin.buildStart === "function" ? plugin.buildStart : plugin.buildStart?.handler;
|
|
11
83
|
return {
|
|
12
84
|
...plugin,
|
|
13
85
|
applyToEnvironment(environment) {
|
|
14
|
-
return environment.config.consumer === "client";
|
|
15
|
-
}
|
|
86
|
+
return environment.config.consumer === "client" || validatesDevSsr && environment.config.consumer === "server";
|
|
87
|
+
},
|
|
88
|
+
buildStart: originalBuildStart ? function(...args) {
|
|
89
|
+
if (isServerEnvironment(this)) return;
|
|
90
|
+
return originalBuildStart.apply(this, args);
|
|
91
|
+
} : void 0,
|
|
92
|
+
transform: originalTransform ? async function(code, id, options) {
|
|
93
|
+
if (!isServerEnvironment(this)) return originalTransform.call(this, code, id, options);
|
|
94
|
+
const clientCode = clientViewOf(ssrState, code, id);
|
|
95
|
+
if (clientCode === void 0) return null;
|
|
96
|
+
const transformed = await originalTransform.call(this, clientCode, id, {
|
|
97
|
+
...options,
|
|
98
|
+
ssr: false
|
|
99
|
+
});
|
|
100
|
+
if (plugin.name === "warlock:gate-a-resolve" && originalResolveId) for (const source of ssrState.clientImportsByModule.get(moduleKey(id)) ?? []) markResolvedClientModule(ssrState, await originalResolveId.call(this, source, id, {
|
|
101
|
+
attributes: {},
|
|
102
|
+
isEntry: false,
|
|
103
|
+
ssr: false
|
|
104
|
+
}));
|
|
105
|
+
return transformed;
|
|
106
|
+
} : void 0,
|
|
107
|
+
resolveId: originalResolveId ? async function(source, importer, options) {
|
|
108
|
+
if (!isServerEnvironment(this)) return originalResolveId.call(this, source, importer, options);
|
|
109
|
+
if (!importer) return null;
|
|
110
|
+
const importerKey = moduleKey(importer);
|
|
111
|
+
if (!(ssrState.clientBoundModules.has(importerKey) || isStatelessClientSurface(importerKey, ssrState.appRoot))) return null;
|
|
112
|
+
const survivingImports = ssrState.clientImportsByModule.get(importerKey);
|
|
113
|
+
if (survivingImports && !survivingImports.has(source)) return null;
|
|
114
|
+
const resolved = await originalResolveId.call(this, source, importer, {
|
|
115
|
+
...options,
|
|
116
|
+
ssr: false
|
|
117
|
+
});
|
|
118
|
+
markResolvedClientModule(ssrState, resolved);
|
|
119
|
+
return resolved;
|
|
120
|
+
} : void 0
|
|
16
121
|
};
|
|
17
122
|
}
|
|
18
123
|
/**
|
|
@@ -115,8 +220,16 @@ function clientEnvironmentOnly(plugin) {
|
|
|
115
220
|
*/
|
|
116
221
|
function warlockClientBoundary(options = {}) {
|
|
117
222
|
const tracker = createPublicEnvTracker();
|
|
223
|
+
const ssrState = {
|
|
224
|
+
appRoot: path.resolve(options.appRoot ?? process.cwd()),
|
|
225
|
+
clientBoundModules: /* @__PURE__ */ new Set(),
|
|
226
|
+
clientImportsByModule: /* @__PURE__ */ new Map()
|
|
227
|
+
};
|
|
118
228
|
return [
|
|
119
|
-
clientPageRegistry({
|
|
229
|
+
clientPageRegistry({
|
|
230
|
+
appRoot: options.appRoot,
|
|
231
|
+
beforePageHotUpdate: options.beforePageHotUpdate
|
|
232
|
+
}),
|
|
120
233
|
projection(),
|
|
121
234
|
gateBSecrets({ tracker }),
|
|
122
235
|
gateAResolve(options),
|
|
@@ -124,7 +237,7 @@ function warlockClientBoundary(options = {}) {
|
|
|
124
237
|
...options,
|
|
125
238
|
tracker
|
|
126
239
|
})
|
|
127
|
-
].map(clientEnvironmentOnly);
|
|
240
|
+
].map((plugin) => clientEnvironmentOnly(plugin, ssrState));
|
|
128
241
|
}
|
|
129
242
|
/**
|
|
130
243
|
* Callable production seam: callers own their exact source aliases and the
|
|
@@ -137,10 +250,10 @@ async function buildWarlockHydrationClient(options) {
|
|
|
137
250
|
outDir: options.outDir,
|
|
138
251
|
resolveAliases: options.resolveAliases,
|
|
139
252
|
external: options.external,
|
|
140
|
-
plugins: warlockClientBoundary({ appRoot: options.appRoot })
|
|
253
|
+
plugins: [...warlockClientBoundary({ appRoot: options.appRoot }), ...options.plugins ?? []]
|
|
141
254
|
});
|
|
142
255
|
}
|
|
143
256
|
|
|
144
257
|
//#endregion
|
|
145
|
-
export { CLIENT_PAGE_REGISTRY_ID, HYDRATION_CLIENT_ENTRY_NAME, ProjectionAmbiguityError, RESOLVED_CLIENT_PAGE_REGISTRY_ID, buildHydrationClient, buildPublicEnvManifest, buildWarlockHydrationClient, clientPageRegistry, createHydrationClientEntry, createPublicEnvTracker, findLeakedServerExports, findLeakedServerImportEdges, gateAResolve, gateBSecrets, gateCVerify, projection, warlockClientBoundary };
|
|
258
|
+
export { CLIENT_PAGE_REGISTRY_ID, HYDRATION_CLIENT_ENTRY_NAME, ProjectionAmbiguityError, RESOLVED_CLIENT_PAGE_REGISTRY_ID, buildHydrationClient, buildPublicEnvManifest, buildWarlockHydrationClient, clientPageRegistry, createHydrationClientEntry, createPublicEnvTracker, findLeakedServerExports, findLeakedServerImportEdges, gateAResolve, gateBSecrets, gateCVerify, invalidateClientPageRegistry, projection, warlockClientBoundary };
|
|
146
259
|
//# sourceMappingURL=index.mjs.map
|