void 0.10.8 → 0.10.11
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/AGENT_PROMPT.md +4 -0
- package/README.md +1 -1
- package/dist/{agents-Bmr5tFFb.mjs → agents-CtgBYqld.mjs} +1 -1
- package/dist/{auth-cmd-DlgNwByu.mjs → auth-cmd-BqsdZJp5.mjs} +3 -3
- package/dist/{better-auth-shared-BvnM9px6.d.mts → better-auth-shared-DealXecJ.d.mts} +1 -1
- package/dist/{build-cmd-Br8qL0rA.mjs → build-cmd-Bujrv5q-.mjs} +3 -3
- package/dist/{cache-wH-mP8UE.mjs → cache-C11V8Fxq.mjs} +3 -3
- package/dist/{cancel-deploy-BEBOEgtu.mjs → cancel-deploy-fwFYF04b.mjs} +2 -2
- package/dist/cli/cli.mjs +175 -43
- package/dist/cli/env-schema-probe.d.mts +96 -0
- package/dist/cli/env-schema-probe.mjs +272 -0
- package/dist/{client-Cj96iiBH.mjs → client-Gb71-XkG.mjs} +20 -2
- package/dist/{config-BdUctCZD.mjs → config-CutEMNGJ.mjs} +3 -3
- package/dist/{config-1twldYCW.mjs → config-E03l1C_h.mjs} +8 -2
- package/dist/{create-project-DD9n8Ho-.mjs → create-project-DsYvl3TB.mjs} +3 -3
- package/dist/{db-DjKE2-A-.mjs → db-PZBsLSGb.mjs} +27 -27
- package/dist/{delete-D2kr3Kmk.mjs → delete-mh6p-zkQ.mjs} +3 -3
- package/dist/deploy-DT8wsPZd.mjs +6990 -0
- package/dist/{discover-BuVVSAum.mjs → discover-CJHyvYfR.mjs} +2 -2
- package/dist/dist-DaKKDf8D.mjs +41 -0
- package/dist/{domain-_luIsM_B.mjs → domain-DiaNQbrl.mjs} +2 -2
- package/dist/entry-D7yy4xVH.mjs +100 -0
- package/dist/{env-CO5XAS9t.mjs → env-AAHU02L6.mjs} +5 -5
- package/dist/env-mask-Dd47NbR6.mjs +90 -0
- package/dist/env-public-BfiLcMBk.d.mts +140 -0
- package/dist/env-raw-Cx8ElDdj.mjs +58 -0
- package/dist/{env-types-D51bnR-c.mjs → env-types-QBj-ndax.mjs} +2 -2
- package/dist/env-validation-BdDlGhbN.mjs +1069 -0
- package/dist/{gen-Cf79J4aw.mjs → gen-oup1xBN0.mjs} +8 -8
- package/dist/{github-cmd-B6OX9c7d.mjs → github-cmd-BdNaOVNa.mjs} +102 -3
- package/dist/{handler-imD0UVDT.d.mts → handler-Cjh8uM3Y.d.mts} +1 -1
- package/dist/{headers-BwvFGhkx.mjs → headers-BQknpzkn.mjs} +2 -2
- package/dist/index.d.mts +14 -2
- package/dist/index.mjs +102 -55
- package/dist/{init-KirOzVDs.mjs → init-Dl2PKuQn.mjs} +13 -14
- package/dist/{link-CUmiosyb.mjs → link-CdGHSIy-.mjs} +4 -4
- package/dist/{list-3GEw7b6m.mjs → list-CPwFDZ_c.mjs} +3 -3
- package/dist/{login-DJReaT_Q.mjs → login-BT3H8PN3.mjs} +2 -2
- package/dist/{logs-D-rQ56Lq.mjs → logs-Bt313ax7.mjs} +3 -2
- package/dist/{mcp-D7yc0dXY.mjs → mcp-DoM3_nhd.mjs} +7 -2
- package/dist/{node-yFFk626c.mjs → node-BDx8pmhq.mjs} +5 -5
- package/dist/{package-json-B0NuUWGd.mjs → package-json-Cx1osYo6.mjs} +1 -1
- package/dist/pages/client.d.mts +1 -1
- package/dist/pages/index.d.mts +1 -23
- package/dist/pages/index.mjs +5 -5
- package/dist/pages/islands-plugin.mjs +2 -2
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/{plugin-inference-CJxi_fWI.mjs → plugin-inference-DMeavIJ6.mjs} +4 -98
- package/dist/{prepare-C_cVurhP.mjs → prepare-DOBTY0o4.mjs} +10 -10
- package/dist/preset-BGrvB4Bl.mjs +539 -0
- package/dist/{project-cmd-DnU7u9QF.mjs → project-cmd-D_w-4w5B.mjs} +13 -9
- package/dist/{project-paths-tpdR1mJR.mjs → project-paths-BQd7OmIo.mjs} +1 -1
- package/dist/project-paths-GpziKeQQ.d.mts +25 -0
- package/dist/{project-tsconfig-D9uSVVpA.mjs → project-tsconfig-B-QtXjLQ.mjs} +2 -2
- package/dist/{protocol-6hTJ04T1.d.mts → protocol-Bnb0LFp3.d.mts} +1 -1
- package/dist/provision-BLrCEBbI.mjs +2557 -0
- package/dist/requests-B8sZxaFM.mjs +50 -0
- package/dist/{resolve-project-D2HI3TrG.mjs → resolve-project-BBMtLLV9.mjs} +1 -1
- package/dist/{rollback-Yh7bCKob.mjs → rollback-CkvTFXx5.mjs} +2 -2
- package/dist/{route-types-CfKfhbIg.mjs → route-types-COI2DsZv.mjs} +2 -2
- package/dist/{runner-h272wcPj.mjs → runner-kapo9aPs.mjs} +3 -3
- package/dist/{runner-pg-waxJOnBb.mjs → runner-pg-CHM76xuC.mjs} +1 -1
- package/dist/runtime/ai.mjs +113 -15
- package/dist/runtime/better-auth-pg.d.mts +1 -1
- package/dist/runtime/better-auth.d.mts +1 -1
- package/dist/runtime/env-public.d.mts +1 -139
- package/dist/runtime/env-public.mjs +10 -93
- package/dist/runtime/env.d.mts +14 -1
- package/dist/runtime/env.mjs +24 -10
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/isr-cache.d.mts +207 -0
- package/dist/runtime/isr-cache.mjs +523 -0
- package/dist/runtime/isr.d.mts +4 -3
- package/dist/runtime/isr.mjs +44 -5
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/live.mjs +1 -1
- package/dist/runtime/sandbox.mjs +1 -1
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +1 -1
- package/dist/runtime/ws.d.mts +2 -2
- package/dist/{scan-Dp_Gyzs3.mjs → scan-DEwlM_Xy.mjs} +2 -2
- package/dist/{scan-i7Yz54fv.mjs → scan-DYXkrasO.mjs} +4 -4
- package/dist/{secret-u7FRvg8d.mjs → secret-Dt32J6RI.mjs} +3 -3
- package/dist/{skills-DsdNDtX3.mjs → skills-CLjN0uUO.mjs} +2 -2
- package/dist/{subcommand-prompt-DtES-oP6.mjs → subcommand-prompt-BzV8iQZo.mjs} +1 -1
- package/dist/sveltekit.mjs +1 -1
- package/dist/{validate-DT7nFMlf.mjs → validate-Cw_RLeTj.mjs} +1 -1
- package/dist/{yarn-pnp-CW8LB6g_.mjs → yarn-pnp-DJn3SAHF.mjs} +1 -1
- package/getting-started-prompt.txt +3 -1
- package/package.json +6 -3
- package/schema.json +11 -0
- package/skills/void/SKILL.md +34 -33
- package/skills/void/docs/guide/auth.md +8 -0
- package/skills/void/docs/guide/deployment.md +1 -1
- package/skills/void/docs/guide/env-vars.md +14 -6
- package/skills/void/docs/guide/queues.md +4 -0
- package/skills/void/docs/integrations/cloudflare.md +171 -5
- package/skills/void/docs/node_modules/void/AGENT_PROMPT.md +4 -0
- package/skills/void/docs/node_modules/void/{AGENTS.md → CLAUDE.md} +14 -0
- package/skills/void/docs/node_modules/void/README.md +1 -1
- package/skills/void/docs/node_modules/void/node_modules/@types/proper-lockfile/README.md +51 -0
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathslash/README.md +64 -0
- package/skills/void/docs/node_modules/void/node_modules/pathslash/README.md +64 -0
- package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/README.md +183 -0
- package/skills/void/docs/node_modules/void/skills/void/SKILL.md +34 -33
- package/skills/void/docs/node_modules/void/test/e2e/README.md +85 -0
- package/skills/void/docs/reference/cli.md +97 -14
- package/dist/deploy-C4PbkFyE.mjs +0 -3705
- package/dist/dotenv-D_UbC_vc.mjs +0 -173
- package/dist/env-raw-CoS20LHP.mjs +0 -32
- package/dist/env-validation-CeC2FL66.mjs +0 -163
- package/dist/pathe.M-eThtNZ-CQzLbt4c.mjs +0 -150
- package/dist/preset-CVvwCeIy.mjs +0 -208
- package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathe/README.md +0 -73
- package/skills/void/docs/node_modules/void/node_modules/pathe/README.md +0 -73
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
import { n as __exportAll } from "./rolldown-runtime-DJK8HYOj.mjs";
|
|
2
|
+
import { a as join, c as resolve, n as dirname } from "./dist-DaKKDf8D.mjs";
|
|
3
|
+
import { i as voidWarn } from "./log-ChfPKsVd.mjs";
|
|
4
|
+
import { c as mergeAndExpand, l as parseDotenvFile, o as resolveWranglerArgv } from "./preset-BGrvB4Bl.mjs";
|
|
5
|
+
import { t as findEnvFile } from "./env-types-QBj-ndax.mjs";
|
|
6
|
+
import { getSecretOverride } from "./runtime/env-helpers.mjs";
|
|
7
|
+
import { t as isLikelySecretKey } from "./env-mask-Dd47NbR6.mjs";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { parse } from "jsonc-parser";
|
|
12
|
+
//#region src/host/load-user-env.ts
|
|
13
|
+
/**
|
|
14
|
+
* Loader for the user's `env.ts`.
|
|
15
|
+
*
|
|
16
|
+
* Uses Node's native `import()` (type-stripping is on by default in the
|
|
17
|
+
* supported Node version), so only relative imports and bare package
|
|
18
|
+
* specifiers resolve. tsconfig path aliases (`@/foo`, `~/foo`) are NOT
|
|
19
|
+
* supported — if the user writes one, we detect the resulting resolution
|
|
20
|
+
* error and re-throw with guidance pointing at the offending specifier.
|
|
21
|
+
*
|
|
22
|
+
* Used by:
|
|
23
|
+
* - `plugin-env-schema.ts` (dev plugin, HMR re-imports)
|
|
24
|
+
* - `cli/env-validation.ts` (deploy + `void env check`)
|
|
25
|
+
*
|
|
26
|
+
* Side-effect contract: loading the file evaluates the user's `defineEnv`
|
|
27
|
+
* call, which registers the schema in the singleton state in
|
|
28
|
+
* `runtime/env-public.ts`. Callers read the result from there.
|
|
29
|
+
*
|
|
30
|
+
* Node-only (lives under `host/`). Must not be imported from anything
|
|
31
|
+
* that ships in worker bundles.
|
|
32
|
+
*/
|
|
33
|
+
let loadCounter = 0;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the specifier from a Node resolution error if the error looks
|
|
36
|
+
* like one — otherwise `null`. Both `ERR_MODULE_NOT_FOUND` and
|
|
37
|
+
* `ERR_UNSUPPORTED_DIR_IMPORT` have the specifier embedded in the message.
|
|
38
|
+
*/
|
|
39
|
+
function extractFailedSpecifier(err) {
|
|
40
|
+
if (!(err instanceof Error)) return null;
|
|
41
|
+
const code = err.code;
|
|
42
|
+
if (code !== "ERR_MODULE_NOT_FOUND" && code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") return null;
|
|
43
|
+
const match = /Cannot find (?:package|module) '([^']+)'/.exec(err.message);
|
|
44
|
+
return match ? match[1] ?? null : null;
|
|
45
|
+
}
|
|
46
|
+
/** A specifier is alias-shaped if it isn't relative, absolute, a node: URL, or a valid bare package name. */
|
|
47
|
+
function looksLikeAlias(specifier) {
|
|
48
|
+
if (specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/") || specifier.startsWith("node:") || specifier.startsWith("file:")) return false;
|
|
49
|
+
if (specifier.startsWith("@")) return !/^@[^/]+\/[^/]+/.test(specifier);
|
|
50
|
+
return specifier.startsWith("~") || specifier.startsWith("#");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Load the user's env.ts via native `import()`. The URL gets a cache-busting
|
|
54
|
+
* query param so HMR re-imports re-evaluate the file.
|
|
55
|
+
*
|
|
56
|
+
* On resolution failures, wraps the error with the source file path, and if
|
|
57
|
+
* the failed specifier looks like a tsconfig path alias, appends guidance
|
|
58
|
+
* explaining that env.ts doesn't resolve aliases.
|
|
59
|
+
*/
|
|
60
|
+
async function loadUserEnvFile(envFile) {
|
|
61
|
+
const url = pathToFileURL(envFile).href + `?t=${Date.now()}-${++loadCounter}`;
|
|
62
|
+
try {
|
|
63
|
+
await import(
|
|
64
|
+
/* @vite-ignore */
|
|
65
|
+
url
|
|
66
|
+
);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const specifier = extractFailedSpecifier(err);
|
|
69
|
+
const base = `env: Failed to load '${envFile}': '${err.message}'.`;
|
|
70
|
+
if (specifier && looksLikeAlias(specifier)) throw new Error(`${base}\n\nHint: '${specifier}' looks like a tsconfig path alias. env.ts is loaded directly by Node and does not resolve tsconfig "paths" — use a relative import (e.g. "./shared/...") instead.`);
|
|
71
|
+
throw new Error(base);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/cli/env-validation.ts
|
|
76
|
+
/**
|
|
77
|
+
* Shared env-schema validation for CLI commands.
|
|
78
|
+
*
|
|
79
|
+
* Loads the user's `env.ts` (which calls `defineEnv` as a side effect),
|
|
80
|
+
* collects sources of env values (`.env*` files, remote secrets), and
|
|
81
|
+
* validates each declared key against the registered schema.
|
|
82
|
+
*
|
|
83
|
+
* Used by both `void deploy` (hard error on missing prod env) and
|
|
84
|
+
* `void env check` (CI/pre-push validation).
|
|
85
|
+
*/
|
|
86
|
+
var env_validation_exports = /* @__PURE__ */ __exportAll({
|
|
87
|
+
SelfHostEnvValidationError: () => SelfHostEnvValidationError,
|
|
88
|
+
SelfHostFirstDeployError: () => SelfHostFirstDeployError,
|
|
89
|
+
SelfHostSchemaError: () => SelfHostSchemaError,
|
|
90
|
+
SelfHostSecretListError: () => SelfHostSecretListError,
|
|
91
|
+
collectPlaintextVarWarningsFromVars: () => collectPlaintextVarWarningsFromVars,
|
|
92
|
+
computeInvalidEmittedKeys: () => computeInvalidEmittedKeys,
|
|
93
|
+
createDefaultSecretListRunner: () => createDefaultSecretListRunner,
|
|
94
|
+
deriveSelfHostEnvContextFromVars: () => deriveSelfHostEnvContextFromVars,
|
|
95
|
+
deriveSelfHostSchemaFacts: () => deriveSelfHostSchemaFacts,
|
|
96
|
+
dischargeSelfHostEnvContext: () => dischargeSelfHostEnvContext,
|
|
97
|
+
fetchRemoteSecretNames: () => fetchRemoteSecretNames,
|
|
98
|
+
formatEnvReport: () => formatEnvReport,
|
|
99
|
+
getDeployEnvDefaults: () => getDeployEnvDefaults,
|
|
100
|
+
readEmittedWorkerVars: () => readEmittedWorkerVars,
|
|
101
|
+
secretListEnv: () => secretListEnv,
|
|
102
|
+
selfHostSchemaFailureMessage: () => selfHostSchemaFailureMessage,
|
|
103
|
+
validateProdEnv: () => validateProdEnv
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* After the user's env.ts has been imported (via `validateProdEnv` or
|
|
107
|
+
* `loadSchema`), read the registered schema's `.default(value)` entries.
|
|
108
|
+
* Returns `{}` if no schema was registered.
|
|
109
|
+
*/
|
|
110
|
+
async function getDeployEnvDefaults(root) {
|
|
111
|
+
const envFile = findEnvFile(root);
|
|
112
|
+
if (!envFile) return {};
|
|
113
|
+
const mod = await loadSchema(envFile);
|
|
114
|
+
if (!mod) return {};
|
|
115
|
+
return mod._getSchemaDefaults();
|
|
116
|
+
}
|
|
117
|
+
/** Load the registered schema by importing the user's env.ts. */
|
|
118
|
+
async function loadSchema(envFile) {
|
|
119
|
+
await loadUserEnvFile(envFile);
|
|
120
|
+
const mod = await import("./runtime/env-public.mjs");
|
|
121
|
+
if (!mod._hasRegisteredSchema()) return null;
|
|
122
|
+
return mod;
|
|
123
|
+
}
|
|
124
|
+
function emptyReport() {
|
|
125
|
+
return {
|
|
126
|
+
valid: true,
|
|
127
|
+
missing: [],
|
|
128
|
+
invalid: [],
|
|
129
|
+
values: /* @__PURE__ */ new Map()
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Validate the user's env schema against the union of .env files and
|
|
134
|
+
* remote secret names. Remote secrets are treated as "present" (we can't
|
|
135
|
+
* fetch their values), which is sufficient for the missing-key check.
|
|
136
|
+
*/
|
|
137
|
+
async function validateProdEnv(root, options) {
|
|
138
|
+
const envFile = findEnvFile(root);
|
|
139
|
+
if (!envFile) return {
|
|
140
|
+
hasSchema: false,
|
|
141
|
+
report: emptyReport()
|
|
142
|
+
};
|
|
143
|
+
const mod = await loadSchema(envFile);
|
|
144
|
+
if (!mod) return {
|
|
145
|
+
hasSchema: false,
|
|
146
|
+
report: emptyReport()
|
|
147
|
+
};
|
|
148
|
+
const merged = mergeAndExpand((options.productionOnly ? [".env", ".env.production"] : [
|
|
149
|
+
".env",
|
|
150
|
+
".env.local",
|
|
151
|
+
".env.production",
|
|
152
|
+
".env.production.local"
|
|
153
|
+
]).map((file) => parseDotenvFile(join(root, file), { expand: false })), { onWarn: (msg) => voidWarn(msg) });
|
|
154
|
+
const report = await mod.validateAllEnv(merged);
|
|
155
|
+
const remoteSet = new Set(options.remoteSecrets ?? []);
|
|
156
|
+
if (remoteSet.size > 0 && report.missing.length > 0) {
|
|
157
|
+
report.missing = report.missing.filter((key) => !remoteSet.has(key));
|
|
158
|
+
if (report.missing.length === 0 && report.invalid.length === 0) report.valid = true;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
hasSchema: true,
|
|
162
|
+
report
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Format an EnvValidationReport into a human-readable multi-line string. */
|
|
166
|
+
function formatEnvReport(report) {
|
|
167
|
+
const lines = [];
|
|
168
|
+
if (report.missing.length > 0) {
|
|
169
|
+
lines.push("Missing required env vars:");
|
|
170
|
+
for (const key of report.missing) lines.push(` • ${key}`);
|
|
171
|
+
}
|
|
172
|
+
if (report.invalid.length > 0) {
|
|
173
|
+
if (lines.length > 0) lines.push("");
|
|
174
|
+
lines.push("Invalid env vars:");
|
|
175
|
+
for (const { key, messages } of report.invalid) lines.push(` • ${key}: ${messages.join("; ")}`);
|
|
176
|
+
}
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
/** Convenience: fetch remote secret names from the platform. */
|
|
180
|
+
async function fetchRemoteSecretNames(client, projectId) {
|
|
181
|
+
try {
|
|
182
|
+
return (await client.listSecrets(projectId)).map((s) => s.name);
|
|
183
|
+
} catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A `scheme://user:password@host…` connection URL — the shape that carries a
|
|
189
|
+
* live credential in a key nothing in the NAME heuristic would catch. The
|
|
190
|
+
* motivating case is `DATABASE_URL`: `SECRET_KEY_RE` has no `URL`/`DATABASE`
|
|
191
|
+
* branch, and the shared `isLikelySecretValue` cannot help either — its
|
|
192
|
+
* `HIGH_ENTROPY_RE` character class excludes `@`, so a connection URL never
|
|
193
|
+
* matches it. Both halves of the userinfo must be non-empty, so a bare
|
|
194
|
+
* `postgres://host/db` or a password-less `https://user@host` is NOT flagged.
|
|
195
|
+
*
|
|
196
|
+
* Deliberately local to the DEPLOY-side warning rather than added to
|
|
197
|
+
* `shared/env-mask.ts`: that module is also the runtime/managed redaction
|
|
198
|
+
* heuristic, and widening it would change masking behaviour well outside this
|
|
199
|
+
* warning.
|
|
200
|
+
*/
|
|
201
|
+
const CREDENTIAL_URL_VALUE_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/;
|
|
202
|
+
/**
|
|
203
|
+
* `true` when a var's VALUE is a connection URL carrying inline credentials.
|
|
204
|
+
* Inspecting the value is safe here: only the KEY name ever leaves
|
|
205
|
+
* {@link collectPlaintextVarWarningsFromVars}.
|
|
206
|
+
*/
|
|
207
|
+
function isCredentialBearingUrlValue(value) {
|
|
208
|
+
return typeof value === "string" && CREDENTIAL_URL_VALUE_RE.test(value);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The required-key set, derived independently of how any validator treats
|
|
212
|
+
* the empty string. A key is required iff its validator REJECTS the ABSENT
|
|
213
|
+
* value (probe with `undefined`) — the SAME notion of "required" the managed
|
|
214
|
+
* gate uses (a key lands in `validateAllEnv().missing` when its absent value
|
|
215
|
+
* yields issues; `_getSchemaDefaults` probes `undefined` the exact same way).
|
|
216
|
+
* Requiredness is NEVER inferred from whether `""` validates, so a schema that
|
|
217
|
+
* ACCEPTS `""` (e.g. a bare Zod `z.string()`) is still correctly required.
|
|
218
|
+
*
|
|
219
|
+
* Runs the user's validators (arbitrary code), so callers MUST invoke this
|
|
220
|
+
* inside {@link withSchemaBoundary}: a validator that THROWS synchronously or
|
|
221
|
+
* REJECTS (async) on the probe surfaces as a values-free structural error, and
|
|
222
|
+
* the awaited async probe can never float as an unhandled rejection carrying a
|
|
223
|
+
* value. A NORMAL rejection (an `{ issues }` Result, not a thrown/rejected
|
|
224
|
+
* error) simply marks the key required.
|
|
225
|
+
*/
|
|
226
|
+
async function requiredSchemaKeys(schema) {
|
|
227
|
+
const required = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const [key, validator] of Object.entries(schema)) {
|
|
229
|
+
const resultMaybe = validator["~standard"].validate(void 0);
|
|
230
|
+
if ((resultMaybe instanceof Promise ? await resultMaybe : resultMaybe).issues) required.add(key);
|
|
231
|
+
}
|
|
232
|
+
return required;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Hard failure fetching the remote secret list (spawn error, signal, nonzero
|
|
236
|
+
* exit, timeout, or malformed output). DISTINCT from
|
|
237
|
+
* {@link SelfHostEnvValidationError}: this means "we could not learn what
|
|
238
|
+
* secrets exist" (fail closed), never "a required secret is missing".
|
|
239
|
+
*
|
|
240
|
+
* The message and attached fields are values-free by construction: only
|
|
241
|
+
* the structural {@link SecretListFailureReason} code and safe scalars
|
|
242
|
+
* ({@link SecretListFailureDetail}) are exposed. Raw wrangler output (stderr,
|
|
243
|
+
* stdout, rejection/spawn-error text) is NEVER stored here, so no env value the
|
|
244
|
+
* child may have echoed can reach `Error.message`, the `reason`/`detail`
|
|
245
|
+
* fields, or their JSON serialization — even under `VOID_ENV_UNMASK`.
|
|
246
|
+
*/
|
|
247
|
+
var SelfHostSecretListError = class extends Error {
|
|
248
|
+
name = "SelfHostSecretListError";
|
|
249
|
+
/** Stable, values-free failure classification. */
|
|
250
|
+
reason;
|
|
251
|
+
/** Safe scalars (exit code / signal name) — never contain wrangler output. */
|
|
252
|
+
detail;
|
|
253
|
+
constructor(message, reason, detail = {}) {
|
|
254
|
+
super(message);
|
|
255
|
+
this.reason = reason;
|
|
256
|
+
this.detail = detail;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* FIRST-DEPLOY specialization of {@link SelfHostSecretListError}: the remote
|
|
261
|
+
* secret list could not be fetched because the worker has NOT been deployed
|
|
262
|
+
* yet. wrangler 4.107's `secret list` catches the API worker-not-found error
|
|
263
|
+
* (codes 10007 / 10090 — `WORKER_NOT_FOUND` / `WORKER_LEGACY_ENVIRONMENT_NOT_FOUND`,
|
|
264
|
+
* per `isWorkerNotFoundError` in wrangler-dist/cli.js) and rethrows a `UserError`
|
|
265
|
+
* ("… not found. If this is a new Worker, run `wrangler deploy` first to create
|
|
266
|
+
* it."), exiting nonzero. Rather than surface that circular "deploy first" dead
|
|
267
|
+
* end, this carries the still-unsatisfied required key NAMES plus a
|
|
268
|
+
* worker-not-found flag so the deploy orchestrator can render actionable
|
|
269
|
+
* `wrangler secret put <NAME>` bootstrap guidance (wrangler can `secret put` to
|
|
270
|
+
* a draft worker). Still a HARD, fail-closed error — subclassing keeps existing
|
|
271
|
+
* `instanceof SelfHostSecretListError` fail-closed handling intact.
|
|
272
|
+
*/
|
|
273
|
+
var SelfHostFirstDeployError = class extends SelfHostSecretListError {
|
|
274
|
+
name = "SelfHostFirstDeployError";
|
|
275
|
+
/** Always true — distinguishes this from a generic list failure structurally. */
|
|
276
|
+
workerNotFound = true;
|
|
277
|
+
/** Required schema key NAMES still unsatisfied locally (values-free). */
|
|
278
|
+
missingKeys;
|
|
279
|
+
constructor(message, missingKeys, exitCode) {
|
|
280
|
+
super(message, "nonzero-exit", { exitCode });
|
|
281
|
+
this.missingKeys = missingKeys;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* The env schema is not satisfied for a self-host production deploy: required
|
|
286
|
+
* keys are missing (not in `.env`/`.env.production` and not present as remote
|
|
287
|
+
* worker secrets), local values are invalid, or an EMPTY local assignment would
|
|
288
|
+
* shadow a remote secret. The message and the attached `report` carry the
|
|
289
|
+
* offending key NAMES plus stable, values-free reason codes only — never raw
|
|
290
|
+
* input, never resolved values — so nothing on this path can echo a secret
|
|
291
|
+
* (see {@link selfHostEnvError}), even under `VOID_ENV_UNMASK`.
|
|
292
|
+
*/
|
|
293
|
+
var SelfHostEnvValidationError = class extends Error {
|
|
294
|
+
name = "SelfHostEnvValidationError";
|
|
295
|
+
report;
|
|
296
|
+
constructor(message, report) {
|
|
297
|
+
super(message);
|
|
298
|
+
this.report = report;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* A self-host schema evaluation/validation step threw. DISTINCT from
|
|
303
|
+
* {@link SelfHostEnvValidationError} (which means "a required value is missing
|
|
304
|
+
* or invalid", carrying key names): this means the schema machinery itself
|
|
305
|
+
* raised — env.ts failed to evaluate, or a validator threw/rejected rather than
|
|
306
|
+
* returning issues. Values-free by construction: the original error's
|
|
307
|
+
* message/stack/cause are DROPPED at the boundary and only the structural
|
|
308
|
+
* {@link SelfHostSchemaFailureReason} code + fixed guidance survive, so no env
|
|
309
|
+
* value the user's code may have embedded can reach the thrown error, even
|
|
310
|
+
* under `VOID_ENV_UNMASK`.
|
|
311
|
+
*/
|
|
312
|
+
var SelfHostSchemaError = class extends Error {
|
|
313
|
+
name = "SelfHostSchemaError";
|
|
314
|
+
/** Stable, values-free failure classification. */
|
|
315
|
+
reason;
|
|
316
|
+
constructor(message, reason) {
|
|
317
|
+
super(message);
|
|
318
|
+
this.reason = reason;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
/** The fixed, values-free message for each schema-failure reason. */
|
|
322
|
+
function selfHostSchemaFailureMessage(reason) {
|
|
323
|
+
switch (reason) {
|
|
324
|
+
case "schema-load-failed": return "env: the project env schema could not be loaded (reason: schema-load-failed). Check that env.ts evaluates without error, then run the deploy again.";
|
|
325
|
+
case "schema-validation-error": return "env: an env schema validator raised an error while checking production values (reason: schema-validation-error). Re-run the validator locally to see its own diagnostics.";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Values-free boundary around EVERY self-host schema evaluation +
|
|
330
|
+
* validation step. `loadSchema` evaluates the user's env.ts and each
|
|
331
|
+
* `validateProdEnv` / `validateAllEnv` pass runs the registered validators —
|
|
332
|
+
* all arbitrary code that can throw/reject an error EMBEDDING an env value in
|
|
333
|
+
* its message, stack, or cause (and `VOID_ENV_UNMASK` disables redaction). Any
|
|
334
|
+
* throw/reject from `fn` is caught, its original message/stack/cause DROPPED,
|
|
335
|
+
* and a fixed structural {@link SelfHostSchemaError} rethrown in its place. The
|
|
336
|
+
* managed `validateProdEnv` itself is NEVER wrapped — only the self-host CALL
|
|
337
|
+
* SITES pass through here, so the managed path is byte-identical.
|
|
338
|
+
*/
|
|
339
|
+
async function withSchemaBoundary(reason, fn) {
|
|
340
|
+
try {
|
|
341
|
+
return await fn();
|
|
342
|
+
} catch {
|
|
343
|
+
throw new SelfHostSchemaError(selfHostSchemaFailureMessage(reason), reason);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** A no-schema {@link SelfHostEnvContext} — nothing to gate. */
|
|
347
|
+
function emptyEnvContext() {
|
|
348
|
+
return {
|
|
349
|
+
hasSchema: false,
|
|
350
|
+
presentEmptyRequired: [],
|
|
351
|
+
presentEmptySources: {},
|
|
352
|
+
absentRequired: [],
|
|
353
|
+
invalidKeys: []
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Derive the {@link SelfHostSchemaFacts} by importing the user's env.ts. Runs
|
|
358
|
+
* ARBITRARY project code (env.ts evaluation) + the registered validators' probes,
|
|
359
|
+
* so the caller MUST run this in an ISOLATED SUBPROCESS ({@link
|
|
360
|
+
* ../cli/env-schema-probe.ts}) under a scrubbed env. Throws only a values-free
|
|
361
|
+
* {@link SelfHostSchemaError} (via {@link withSchemaBoundary}) when the schema
|
|
362
|
+
* machinery itself fails; the derived names/flags are returned as values-free data.
|
|
363
|
+
*/
|
|
364
|
+
async function deriveSelfHostSchemaFacts(root) {
|
|
365
|
+
const envFile = findEnvFile(root);
|
|
366
|
+
if (envFile == null) return {
|
|
367
|
+
hasSchema: false,
|
|
368
|
+
requiredKeys: [],
|
|
369
|
+
secretKeys: []
|
|
370
|
+
};
|
|
371
|
+
const mod = await withSchemaBoundary("schema-load-failed", () => loadSchema(envFile));
|
|
372
|
+
if (!mod) return {
|
|
373
|
+
hasSchema: false,
|
|
374
|
+
requiredKeys: [],
|
|
375
|
+
secretKeys: []
|
|
376
|
+
};
|
|
377
|
+
const schema = mod._getRegisteredSchema() ?? {};
|
|
378
|
+
const requiredKeys = await withSchemaBoundary("schema-validation-error", () => requiredSchemaKeys(schema));
|
|
379
|
+
const secretKeys = Object.keys(schema).filter((key) => getSecretOverride(schema[key]) === "secret");
|
|
380
|
+
return {
|
|
381
|
+
hasSchema: true,
|
|
382
|
+
requiredKeys: [...requiredKeys],
|
|
383
|
+
secretKeys
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Build the values-free {@link SelfHostEnvContext} from the REAL worker `vars` the
|
|
388
|
+
* build emitted into `dist/ssr/wrangler.json` (ground truth — never a facsimile)
|
|
389
|
+
* plus the env-insensitive {@link SelfHostSchemaFacts}. PURE (no project code, no
|
|
390
|
+
* I/O): the parent runs this AFTER the build with the real emitted map, so the
|
|
391
|
+
* gate reads exactly what the worker ships.
|
|
392
|
+
* - a required key present with an EMPTY value (`K=""` or `K=null`) → SHADOWS a
|
|
393
|
+
* same-named remote secret with an unusable value → fail closed ALWAYS
|
|
394
|
+
* (`presentEmptyRequired`). Both spellings are treated the same because the
|
|
395
|
+
* schema validators classify `null` and `""` as "missing" (not "invalid"), so
|
|
396
|
+
* they would otherwise slip past both the invalid-value probe and the absent
|
|
397
|
+
* fence while still shadowing the remote secret.
|
|
398
|
+
* - a required key ABSENT from the emitted vars → the only state a remote secret
|
|
399
|
+
* can discharge (`absentRequired`).
|
|
400
|
+
* `invalidKeys` is populated separately from the value-validation probe (the parent
|
|
401
|
+
* folds it in); a PRESENT, NON-EMPTY value that fails the schema validator lands there.
|
|
402
|
+
*/
|
|
403
|
+
function deriveSelfHostEnvContextFromVars(emittedVars, facts) {
|
|
404
|
+
if (!facts.hasSchema) return emptyEnvContext();
|
|
405
|
+
const isEmptyShadow = (value) => value === "" || value === null;
|
|
406
|
+
const presentEmptyRequired = facts.requiredKeys.filter((key) => Object.hasOwn(emittedVars, key) && isEmptyShadow(emittedVars[key]));
|
|
407
|
+
const absentRequired = facts.requiredKeys.filter((key) => !Object.hasOwn(emittedVars, key));
|
|
408
|
+
const presentEmptySources = {};
|
|
409
|
+
for (const key of presentEmptyRequired) presentEmptySources[key] = { kind: "emitted" };
|
|
410
|
+
return {
|
|
411
|
+
hasSchema: true,
|
|
412
|
+
presentEmptyRequired,
|
|
413
|
+
presentEmptySources,
|
|
414
|
+
absentRequired,
|
|
415
|
+
invalidKeys: []
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Follow the CF plugin's deploy pointer (`.wrangler/deploy/config.json` →
|
|
420
|
+
* `configPath`) to the generated wrangler config the build emitted, and return it
|
|
421
|
+
* parsed. Shared by the parent's ground-truth reads and the isolated
|
|
422
|
+
* `validate-emitted` probe child, so both resolve the SAME artifact. Returns a
|
|
423
|
+
* fail-closed reason (never throws) when the pointer/config is missing or malformed.
|
|
424
|
+
*/
|
|
425
|
+
function readGeneratedWranglerConfig(root) {
|
|
426
|
+
const deployConfigPath = join(root, ".wrangler", "deploy", "config.json");
|
|
427
|
+
if (!existsSync(deployConfigPath)) return {
|
|
428
|
+
ok: false,
|
|
429
|
+
reason: `the CF plugin's deploy pointer at ${deployConfigPath} was not written by the build.`
|
|
430
|
+
};
|
|
431
|
+
let redirectedPath;
|
|
432
|
+
try {
|
|
433
|
+
const parsed = JSON.parse(readFileSync(deployConfigPath, "utf-8"));
|
|
434
|
+
if (typeof parsed.configPath !== "string" || parsed.configPath.length === 0) return {
|
|
435
|
+
ok: false,
|
|
436
|
+
reason: `${deployConfigPath} has no "configPath" pointer.`
|
|
437
|
+
};
|
|
438
|
+
redirectedPath = resolve(dirname(deployConfigPath), parsed.configPath);
|
|
439
|
+
} catch {
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
reason: `${deployConfigPath} is not valid JSON.`
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (!existsSync(redirectedPath)) return {
|
|
446
|
+
ok: false,
|
|
447
|
+
reason: `generated wrangler config ${redirectedPath} does not exist.`
|
|
448
|
+
};
|
|
449
|
+
try {
|
|
450
|
+
return {
|
|
451
|
+
ok: true,
|
|
452
|
+
config: JSON.parse(readFileSync(redirectedPath, "utf-8"))
|
|
453
|
+
};
|
|
454
|
+
} catch {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
reason: `generated wrangler config ${redirectedPath} is not valid JSON.`
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Read the REAL `vars` map the build baked into `dist/ssr/wrangler.json` (ground
|
|
463
|
+
* truth — the exact plaintext the worker ships). A missing `vars` field is an empty
|
|
464
|
+
* map (a worker with no vars), not a failure; a missing/malformed artifact fails
|
|
465
|
+
* closed. The self-host orchestrator reads this in the parent; the `validate-emitted`
|
|
466
|
+
* probe reads it in the child to run the schema validators against those exact
|
|
467
|
+
* values in isolation.
|
|
468
|
+
*/
|
|
469
|
+
function readEmittedWorkerVars(root) {
|
|
470
|
+
const resolved = readGeneratedWranglerConfig(root);
|
|
471
|
+
if (!resolved.ok) return resolved;
|
|
472
|
+
const vars = resolved.config.vars;
|
|
473
|
+
if (vars === void 0) return {
|
|
474
|
+
ok: true,
|
|
475
|
+
vars: {}
|
|
476
|
+
};
|
|
477
|
+
if (vars === null || typeof vars !== "object" || Array.isArray(vars)) return {
|
|
478
|
+
ok: false,
|
|
479
|
+
reason: "the generated wrangler config has a malformed \"vars\" field (expected an object)."
|
|
480
|
+
};
|
|
481
|
+
return {
|
|
482
|
+
ok: true,
|
|
483
|
+
vars
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Validate the REAL emitted worker `vars` against the registered schema's
|
|
488
|
+
* validators and return the FAILING key NAMES only (values-free). A key FAILS iff
|
|
489
|
+
* the validator REJECTS the value it was SHIPPED WITH — the UNION of:
|
|
490
|
+
* - `report.invalid` (a PRESENT, NON-EMPTY value the validator rejected), plus
|
|
491
|
+
* - the `report.missing` keys that are STILL PRESENT in `emittedVars` (the key
|
|
492
|
+
* exists in the emitted map with value `null`/`""`). `validateAllEnv` buckets a
|
|
493
|
+
* REJECTED `undefined`/`null`/`""` as "missing", not "invalid" (see
|
|
494
|
+
* `runtime/env-public.ts` — `isMissing = raw === undefined || raw === null || raw
|
|
495
|
+
* === ''`), so an OPTIONAL-with-validator key shipped as `""`/`null` (e.g.
|
|
496
|
+
* `z.url().optional()`, which rejects those but the required-empty fence in
|
|
497
|
+
* `deriveSelfHostEnvContextFromVars` — `requiredKeys` only — never covers) would
|
|
498
|
+
* otherwise slip EVERY gate and boot-reject the deployed worker.
|
|
499
|
+
* A `report.missing` key ABSENT from `emittedVars` is NEVER counted: it is a
|
|
500
|
+
* genuinely-unset REQUIRED secret that a same-named remote `wrangler secret list`
|
|
501
|
+
* secret can still discharge, so it stays with the context's absent fence rather than
|
|
502
|
+
* becoming a hard invalid. Runs the user's validators (arbitrary code), so the caller
|
|
503
|
+
* MUST run this in the isolated {@link ../cli/env-schema-probe.ts} subprocess; a
|
|
504
|
+
* validator that THROWS/REJECTS surfaces as a values-free {@link SelfHostSchemaError}
|
|
505
|
+
* via {@link withSchemaBoundary}. NEVER reads `report.invalid[].messages` (which can
|
|
506
|
+
* embed the offending value) — only the key names leave here.
|
|
507
|
+
*/
|
|
508
|
+
async function computeInvalidEmittedKeys(root, emittedVars) {
|
|
509
|
+
const envFile = findEnvFile(root);
|
|
510
|
+
if (envFile == null) return [];
|
|
511
|
+
const mod = await withSchemaBoundary("schema-load-failed", () => loadSchema(envFile));
|
|
512
|
+
if (!mod) return [];
|
|
513
|
+
const report = await withSchemaBoundary("schema-validation-error", () => mod.validateAllEnv(emittedVars));
|
|
514
|
+
const invalid = new Set(report.invalid.map((entry) => entry.key));
|
|
515
|
+
for (const key of report.missing) if (Object.hasOwn(emittedVars, key)) invalid.add(key);
|
|
516
|
+
return Array.from(invalid).sort((a, b) => a.localeCompare(b));
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Classify the secret-looking keys among the REAL worker `vars` the build baked
|
|
520
|
+
* into `dist/ssr/wrangler.json` (ground truth). PURE: for each emitted var key, a
|
|
521
|
+
* `.secret()`-flagged key ({@link SelfHostSchemaFacts.secretKeys}) is
|
|
522
|
+
* `schema-secret`; otherwise the shared {@link isLikelySecretKey} name heuristic
|
|
523
|
+
* applies (so a secret-looking key ships plaintext regardless of any schema, and
|
|
524
|
+
* heuristic classification still works with no env.ts at all); failing both, a
|
|
525
|
+
* VALUE shaped like a credential-bearing connection URL
|
|
526
|
+
* ({@link isCredentialBearingUrlValue}) flags the key as `value-shape`, catching
|
|
527
|
+
* the `DATABASE_URL`-class leak no name heuristic sees.
|
|
528
|
+
*
|
|
529
|
+
* KEYS ONLY leave here — a value may be INSPECTED to classify it, but is never
|
|
530
|
+
* copied into the result, so no caller can format a value into a log.
|
|
531
|
+
*/
|
|
532
|
+
function collectPlaintextVarWarningsFromVars(emittedVars, secretFlaggedKeys) {
|
|
533
|
+
const flagged = new Set(secretFlaggedKeys);
|
|
534
|
+
const keys = Object.keys(emittedVars);
|
|
535
|
+
const secretKeys = [];
|
|
536
|
+
for (const key of keys) {
|
|
537
|
+
if (flagged.has(key)) {
|
|
538
|
+
secretKeys.push({
|
|
539
|
+
key,
|
|
540
|
+
reason: "schema-secret"
|
|
541
|
+
});
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (isLikelySecretKey(key)) {
|
|
545
|
+
secretKeys.push({
|
|
546
|
+
key,
|
|
547
|
+
reason: "heuristic"
|
|
548
|
+
});
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (isCredentialBearingUrlValue(emittedVars[key])) secretKeys.push({
|
|
552
|
+
key,
|
|
553
|
+
reason: "value-shape"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
secretKeys.sort((a, b) => a.key.localeCompare(b.key));
|
|
557
|
+
return {
|
|
558
|
+
secretKeys,
|
|
559
|
+
totalVarKeys: keys.length
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/** Build a values-free invalid {@link EnvValidationReport} from key names only. */
|
|
563
|
+
function invalidEnvReport(missing, invalidKeys) {
|
|
564
|
+
return {
|
|
565
|
+
valid: false,
|
|
566
|
+
missing,
|
|
567
|
+
invalid: invalidKeys.map((key) => ({
|
|
568
|
+
key,
|
|
569
|
+
messages: [SELF_HOST_INVALID_REASON]
|
|
570
|
+
})),
|
|
571
|
+
values: /* @__PURE__ */ new Map()
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* DISCHARGE half of the self-host gate — PURE parent work over the values-free
|
|
576
|
+
* {@link SelfHostEnvContext} the subprocess derived, plus at most one `wrangler
|
|
577
|
+
* secret list`. Runs NO project code, NO `loadEnv`, and imports NO env.ts, so the
|
|
578
|
+
* parent never evaluates arbitrary code with live credentials in scope. Applies
|
|
579
|
+
* the three fences unchanged:
|
|
580
|
+
* - a PRESENT-EMPTY required key fails closed ALWAYS, before any wrangler spawn;
|
|
581
|
+
* - the pinned worker `name` is reconciled against the config's `name`, and the
|
|
582
|
+
* target account against the config's `account_id`, BOTH BEFORE `secret list`
|
|
583
|
+
* is spawned;
|
|
584
|
+
* - an ABSENT required key is discharged only by a same-named remote secret,
|
|
585
|
+
* else fails closed ({@link SelfHostFirstDeployError} on a not-yet-deployed
|
|
586
|
+
* worker, carrying the missing names + `wrangler secret put` guidance).
|
|
587
|
+
*/
|
|
588
|
+
async function dischargeSelfHostEnvContext(context, options) {
|
|
589
|
+
if (!context.hasSchema) return {
|
|
590
|
+
hasSchema: false,
|
|
591
|
+
remoteSecretNames: null
|
|
592
|
+
};
|
|
593
|
+
if (context.presentEmptyRequired.length > 0) throw emptyAssignmentError(context.presentEmptyRequired, context.presentEmptySources);
|
|
594
|
+
const invalidKeys = context.invalidKeys;
|
|
595
|
+
if (context.absentRequired.length === 0) {
|
|
596
|
+
if (invalidKeys.length > 0) throw selfHostEnvError(invalidEnvReport([], invalidKeys));
|
|
597
|
+
return {
|
|
598
|
+
hasSchema: true,
|
|
599
|
+
remoteSecretNames: null
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
reconcileSelfHostWorkerName(options.configPath, options.workerName);
|
|
603
|
+
const effectiveAccountId = reconcileSelfHostAccount(options.configPath, options.accountId);
|
|
604
|
+
const remoteSecretNames = await fetchSelfHostSecretNames(options.runWrangler ?? createDefaultSecretListRunner(options.root, {
|
|
605
|
+
accountId: effectiveAccountId,
|
|
606
|
+
timeoutMs: options.timeoutMs
|
|
607
|
+
}), options.configPath, context.absentRequired);
|
|
608
|
+
const remoteSet = new Set(remoteSecretNames);
|
|
609
|
+
const stillMissing = context.absentRequired.filter((key) => !remoteSet.has(key));
|
|
610
|
+
if (stillMissing.length > 0 || invalidKeys.length > 0) throw selfHostEnvError(invalidEnvReport(stillMissing, invalidKeys));
|
|
611
|
+
return {
|
|
612
|
+
hasSchema: true,
|
|
613
|
+
remoteSecretNames
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Read the pinned config's top-level `account_id` and reconcile it with
|
|
618
|
+
* the caller's requested account, mirroring the provisioner's account gate
|
|
619
|
+
* (provision.ts `provisionCloudflareResourcesLocked`). wrangler 4.107 gives the
|
|
620
|
+
* config's `account_id` PRECEDENCE over `CLOUDFLARE_ACCOUNT_ID`, so a real
|
|
621
|
+
* config-vs-requested mismatch is a fail-closed error (thrown BEFORE any spawn).
|
|
622
|
+
* Returns the ONE effective account to pin (config wins, else requested), or
|
|
623
|
+
* undefined when neither is set (wrangler then resolves it as it normally would).
|
|
624
|
+
*/
|
|
625
|
+
function reconcileSelfHostAccount(configPath, requestedAccountId) {
|
|
626
|
+
const configAccountId = readConfigAccountId(configPath);
|
|
627
|
+
if (configAccountId && requestedAccountId && configAccountId !== requestedAccountId) throw new SelfHostSecretListError(`env: Cloudflare account mismatch — "${configPath}" pins account_id ${JSON.stringify(configAccountId)} but the deploy targets ${JSON.stringify(requestedAccountId)}. wrangler resolves the config's account_id before CLOUDFLARE_ACCOUNT_ID, so \`wrangler secret list\` would consult ${JSON.stringify(configAccountId)} while the deploy expects ${JSON.stringify(requestedAccountId)} — a same-named secret there could falsely pass this preflight. Reconcile them (correct or remove the config's account_id, or deploy to the pinned account) and retry.`, "account-mismatch");
|
|
628
|
+
return configAccountId ?? requestedAccountId;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Reconcile the config's top-level worker `name` against the name the deploy PINNED
|
|
632
|
+
* (deploy.ts step 5 writes `name: workerName` into the root config before the build).
|
|
633
|
+
* `wrangler secret list` resolves the worker from the config's `name`
|
|
634
|
+
* (`getLegacyScriptName`: `args.name ?? config.name` — we pass no `--name`), so a
|
|
635
|
+
* config whose `name` was CHANGED after it was pinned would make the gate list secrets
|
|
636
|
+
* for a DIFFERENT worker than the deploy ships — and a same-named secret there could
|
|
637
|
+
* falsely pass this preflight while the pinned worker deploys WITHOUT it.
|
|
638
|
+
*
|
|
639
|
+
* Fires ONLY when the config actually declares a `name` that DIFFERS from a passed
|
|
640
|
+
* pinned name (the same "both set and differ" guard as {@link reconcileSelfHostAccount})
|
|
641
|
+
* — post-pin the config's `name` IS `workerName`, so a later divergence is exactly the
|
|
642
|
+
* laundering signal, while an undefined pinned name or an undeclared config `name` never
|
|
643
|
+
* trips a legitimate deploy. Thrown BEFORE any spawn (its message names worker names,
|
|
644
|
+
* which are caller/config identifiers, never wrangler output).
|
|
645
|
+
*/
|
|
646
|
+
function reconcileSelfHostWorkerName(configPath, pinnedWorkerName) {
|
|
647
|
+
const configName = readConfigName(configPath);
|
|
648
|
+
if (configName && pinnedWorkerName && configName !== pinnedWorkerName) throw new SelfHostSecretListError(`env: Cloudflare worker name mismatch — "${configPath}" declares name ${JSON.stringify(configName)} but the deploy pinned ${JSON.stringify(pinnedWorkerName)}. wrangler resolves the worker name from the config's \`name\`, so \`wrangler secret list\` would consult ${JSON.stringify(configName)} while the deploy ships ${JSON.stringify(pinnedWorkerName)} — a same-named secret there could falsely pass this preflight. The config's name was changed after it was pinned; rebuild from a clean checkout, then rerun.`, "name-mismatch");
|
|
649
|
+
}
|
|
650
|
+
/** UTF-8 BOM, stripped before JSONC parsing (matches provision.ts readRootConfig). */
|
|
651
|
+
const UTF8_BOM = "";
|
|
652
|
+
/** True when `value` is a non-array object (JSONC-parsed config record). */
|
|
653
|
+
function isConfigRecord(value) {
|
|
654
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Parse the resolved root wrangler config (`-c` target) into a top-level record,
|
|
658
|
+
* mirroring the provisioner's `readRootConfig` (jsonc-parser, BOM-stripped,
|
|
659
|
+
* trailing commas allowed). We read the SAME file the deploy pins via `-c` and
|
|
660
|
+
* force top-level via `--env=`, so its top-level fields are exactly what
|
|
661
|
+
* wrangler's `secret list` and the build both resolve. An unreadable/malformed
|
|
662
|
+
* config yields undefined — callers then fall back safely (no account gate / no
|
|
663
|
+
* root-wrangler var layer), and wrangler itself later fails on a truly broken
|
|
664
|
+
* config with a distinct fail-closed {@link SelfHostSecretListError}.
|
|
665
|
+
*/
|
|
666
|
+
function parseRootConfig(configPath) {
|
|
667
|
+
if (!existsSync(configPath)) return;
|
|
668
|
+
let raw;
|
|
669
|
+
try {
|
|
670
|
+
raw = readFileSync(configPath, "utf-8");
|
|
671
|
+
} catch {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const content = raw.startsWith(UTF8_BOM) ? raw.slice(1) : raw;
|
|
675
|
+
const errors = [];
|
|
676
|
+
const parsed = parse(content, errors, { allowTrailingComma: true });
|
|
677
|
+
if (errors.length > 0 || !isConfigRecord(parsed)) return;
|
|
678
|
+
return parsed;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Read the top-level `account_id` string from the resolved wrangler config
|
|
682
|
+
* (`-c` target). Same file/parse as {@link parseRootConfig}; this is exactly the
|
|
683
|
+
* account wrangler's `secret list` will resolve. An unreadable/malformed config
|
|
684
|
+
* yields undefined (no reconciliation gate).
|
|
685
|
+
*/
|
|
686
|
+
function readConfigAccountId(configPath) {
|
|
687
|
+
const parsed = parseRootConfig(configPath);
|
|
688
|
+
if (!parsed) return;
|
|
689
|
+
const accountId = parsed.account_id;
|
|
690
|
+
return typeof accountId === "string" && accountId.length > 0 ? accountId : void 0;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Read the top-level `name` string from the resolved wrangler config (`-c` target).
|
|
694
|
+
* Same file/parse as {@link parseRootConfig}; this is exactly the worker name
|
|
695
|
+
* wrangler's `secret list` will resolve. An unreadable/malformed config or an
|
|
696
|
+
* absent/empty `name` yields undefined (no reconciliation gate).
|
|
697
|
+
*/
|
|
698
|
+
function readConfigName(configPath) {
|
|
699
|
+
const parsed = parseRootConfig(configPath);
|
|
700
|
+
if (!parsed) return;
|
|
701
|
+
const name = parsed.name;
|
|
702
|
+
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Stable, values-free reason code stamped on every invalid key on the
|
|
706
|
+
* self-host path. The raw validator message (e.g. `Expected a number, got
|
|
707
|
+
* "not-a-number"`) can EMBED the offending input, and redaction preserves
|
|
708
|
+
* benign values / can be disabled via `VOID_ENV_UNMASK`, so the message text is
|
|
709
|
+
* never surfaced here — only the key name and this fixed code.
|
|
710
|
+
*/
|
|
711
|
+
const SELF_HOST_INVALID_REASON = "failed schema validation";
|
|
712
|
+
/**
|
|
713
|
+
* Build the hard error for an unsatisfied schema from KEY NAMES + the
|
|
714
|
+
* stable {@link SELF_HOST_INVALID_REASON} code ONLY. It never reads
|
|
715
|
+
* `report.invalid[].messages` (which can embed raw input) and never resolved
|
|
716
|
+
* `report.values`, and it attaches a freshly-sanitized report of the same
|
|
717
|
+
* shape — so neither the thrown message, the attached report, nor its JSON
|
|
718
|
+
* serialization can echo a value, even with `VOID_ENV_UNMASK` set.
|
|
719
|
+
*/
|
|
720
|
+
function selfHostEnvError(report) {
|
|
721
|
+
const sortNames = (names) => [...names].sort((a, b) => a.localeCompare(b));
|
|
722
|
+
const missing = sortNames(report.missing);
|
|
723
|
+
const invalidKeys = sortNames(report.invalid.map((i) => i.key));
|
|
724
|
+
const lines = [];
|
|
725
|
+
if (missing.length > 0) {
|
|
726
|
+
lines.push("Missing required env vars:");
|
|
727
|
+
for (const key of missing) lines.push(` • ${key}`);
|
|
728
|
+
}
|
|
729
|
+
if (invalidKeys.length > 0) {
|
|
730
|
+
if (lines.length > 0) lines.push("");
|
|
731
|
+
lines.push("Invalid env vars:");
|
|
732
|
+
for (const key of invalidKeys) lines.push(` • ${key} (${SELF_HOST_INVALID_REASON})`);
|
|
733
|
+
}
|
|
734
|
+
if (missing.length > 0) lines.push("", "Set each missing production secret with `wrangler secret put <NAME>`, or add a value to .env / .env.production.");
|
|
735
|
+
const safeReport = {
|
|
736
|
+
valid: report.valid,
|
|
737
|
+
missing: report.missing,
|
|
738
|
+
invalid: report.invalid.map((i) => ({
|
|
739
|
+
key: i.key,
|
|
740
|
+
messages: [SELF_HOST_INVALID_REASON]
|
|
741
|
+
})),
|
|
742
|
+
values: /* @__PURE__ */ new Map()
|
|
743
|
+
};
|
|
744
|
+
return new SelfHostEnvValidationError(lines.join("\n"), safeReport);
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* The values-free description of a var's WINNING source, used to name the
|
|
748
|
+
* ACTUAL origin of a configured-empty key in the remediation. Only the source
|
|
749
|
+
* KIND is ever surfaced — never a value. The self-host gate reads ground truth
|
|
750
|
+
* (`dist/ssr/wrangler.json` `vars`), so every present key's source is `emitted`.
|
|
751
|
+
*/
|
|
752
|
+
function describeVarSource(source) {
|
|
753
|
+
switch (source?.kind) {
|
|
754
|
+
case "emitted": return "the built worker config (`dist/ssr/wrangler.json` `vars`)";
|
|
755
|
+
default: return "your build env configuration";
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* The hard error for a REQUIRED key set to an EMPTY value (`FOO=""`) in
|
|
760
|
+
* the effective build map. Values-free (key names + source KIND/filename only),
|
|
761
|
+
* thrown ALWAYS — independent of whether the key exists as a remote secret. The
|
|
762
|
+
* build bakes the empty string into plaintext `vars`, and wrangler 4.107 gives a
|
|
763
|
+
* config var precedence over a same-named remote secret (`checkRemoteSecretsOverride`
|
|
764
|
+
* in wrangler-dist/cli.js warns "… will replace these remote secrets with the
|
|
765
|
+
* configuration values"), so shipping this would ship an empty value for a
|
|
766
|
+
* required secret (overwriting a remote one if it exists). The fix is to REMOVE
|
|
767
|
+
* the empty assignment, not `secret put`.
|
|
768
|
+
*
|
|
769
|
+
* The empty value can come from ANY of the four build layers, so the
|
|
770
|
+
* remediation names each key's ACTUAL source (its {@link VarSource} provenance)
|
|
771
|
+
* — a dotenv file / `void.json` `worker.vars` / root `wrangler.jsonc` `vars` —
|
|
772
|
+
* instead of misdirecting the user to `.env` files.
|
|
773
|
+
*/
|
|
774
|
+
function emptyAssignmentError(keys, sources) {
|
|
775
|
+
const sorted = [...keys].sort((a, b) => a.localeCompare(b));
|
|
776
|
+
const lines = [
|
|
777
|
+
"Empty value for a required env var:",
|
|
778
|
+
...sorted.map((key) => ` • ${key} (from ${describeVarSource(sources[key])})`),
|
|
779
|
+
"",
|
|
780
|
+
"These keys are REQUIRED by your env schema but assigned an EMPTY value in the source noted above. The build bakes the empty value into the worker config as a plaintext var, and Cloudflare gives a config var precedence over any same-named remote secret — so the deploy would ship an empty value for a required secret (overwriting a remote secret if one exists).",
|
|
781
|
+
"",
|
|
782
|
+
"Remove the empty assignment (so a remote secret can take effect), or set a real value:",
|
|
783
|
+
...sorted.map((key) => ` • ${key} — from ${describeVarSource(sources[key])}`)
|
|
784
|
+
];
|
|
785
|
+
const safeReport = {
|
|
786
|
+
valid: false,
|
|
787
|
+
missing: sorted,
|
|
788
|
+
invalid: [],
|
|
789
|
+
values: /* @__PURE__ */ new Map()
|
|
790
|
+
};
|
|
791
|
+
return new SelfHostEnvValidationError(lines.join("\n"), safeReport);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Actionable, values-free bootstrap guidance for the FIRST-DEPLOY case:
|
|
795
|
+
* the worker does not exist yet, so its remote secrets can't be listed. Lists
|
|
796
|
+
* the still-unsatisfied required key NAMES and the `wrangler secret put <NAME>`
|
|
797
|
+
* command that bootstraps each on the draft worker (wrangler can `secret put`
|
|
798
|
+
* before a worker's first deploy). Key names only — no values.
|
|
799
|
+
*/
|
|
800
|
+
function firstDeployMessage(missingKeys) {
|
|
801
|
+
const sorted = [...missingKeys].sort((a, b) => a.localeCompare(b));
|
|
802
|
+
const names = sorted.length > 0 ? sorted : ["<NAME>"];
|
|
803
|
+
return [
|
|
804
|
+
"This Worker has not been deployed yet, so its remote secrets could not be listed.",
|
|
805
|
+
"",
|
|
806
|
+
"Required production secrets still unset locally:",
|
|
807
|
+
...sorted.map((key) => ` • ${key}`),
|
|
808
|
+
"",
|
|
809
|
+
"Bootstrap them on the draft Worker before deploying:",
|
|
810
|
+
...names.map((key) => ` wrangler secret put ${key}`),
|
|
811
|
+
"",
|
|
812
|
+
"or add a value to .env / .env.production, then run the deploy again."
|
|
813
|
+
].join("\n");
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* The exact `wrangler secret list` argv. Verified against the wrangler 4.107.0
|
|
817
|
+
* bundle (`wrangler-dist/cli.js`, `secretListCommand`):
|
|
818
|
+
* - `secret list` accepts `--format` with choices `json`/`pretty`; `json` is
|
|
819
|
+
* the default AND suppresses the banner (`printBanner` is format==='pretty'),
|
|
820
|
+
* printing `JSON.stringify(secrets, null, ' ')` — an array of secret
|
|
821
|
+
* objects (each with a string `name`, per `fetchSecrets` →
|
|
822
|
+
* `GET /accounts/{id}/workers/scripts/{name}/secrets`). Pinned explicitly
|
|
823
|
+
* so a future default change can't silently switch us to `pretty`.
|
|
824
|
+
* - the worker name resolves from the config's `name` (`getLegacyScriptName`:
|
|
825
|
+
* `args.name ?? config.name` — we pass no `--name`, the pinned config is
|
|
826
|
+
* the single source of truth).
|
|
827
|
+
* - `-c <configPath>` (global `--config` alias) pins the ONE resolved config;
|
|
828
|
+
* `--env=` (explicit EMPTY environment) forces the top-level config env so
|
|
829
|
+
* a project dotenv exporting `CLOUDFLARE_ENV` can never reselect a named
|
|
830
|
+
* `env.*` block — same rationale as the provisioner's wranglerConfigArgs.
|
|
831
|
+
*/
|
|
832
|
+
function secretListArgs(configPath) {
|
|
833
|
+
return [
|
|
834
|
+
"secret",
|
|
835
|
+
"list",
|
|
836
|
+
"--format=json",
|
|
837
|
+
"-c",
|
|
838
|
+
configPath,
|
|
839
|
+
"--env="
|
|
840
|
+
];
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* wrangler 4.107's `secret list` worker-not-found signal. When the worker has
|
|
844
|
+
* never been deployed, `fetchSecrets` throws the API worker-not-found error and
|
|
845
|
+
* `secretListCommand.handler` — ONLY when `isWorkerNotFoundError(e)` is true
|
|
846
|
+
* (`e.code === 10007` WORKER_NOT_FOUND or `10090`
|
|
847
|
+
* WORKER_LEGACY_ENVIRONMENT_NOT_FOUND, per wrangler-dist/cli.js) — rethrows a
|
|
848
|
+
* `UserError` with this exact fixed diagnostic block:
|
|
849
|
+
*
|
|
850
|
+
* Worker "<name>"[ (env: <env>)] not found.
|
|
851
|
+
*
|
|
852
|
+
* If this is a new Worker, run `wrangler deploy` first to create it.
|
|
853
|
+
* Otherwise, check that the Worker name is correct and you're logged into the right account.
|
|
854
|
+
*
|
|
855
|
+
* The API code itself is caught and never printed, so the child's stderr
|
|
856
|
+
* carries this MESSAGE, not `10007`/`10090`. Matching only the bare lead-in
|
|
857
|
+
* `If this is a new Worker` misclassifies any unrelated status-1
|
|
858
|
+
* diagnostic that merely contains that fragment. So we require the COMPLETE
|
|
859
|
+
* structured block — the `… not found.` lead AND both fixed sentences of the
|
|
860
|
+
* guidance — after normalizing whitespace (wrangler may wrap/indent the
|
|
861
|
+
* multi-line UserError when it prints). The block carries no secret content, so
|
|
862
|
+
* it survives `WRANGLER_LOG_SANITIZE=true`; the combined output is inspected
|
|
863
|
+
* INTERNALLY only and never copied into any thrown error.
|
|
864
|
+
*/
|
|
865
|
+
function isWorkerNotFoundOutput(output) {
|
|
866
|
+
const normalized = output.replace(/\s+/g, " ");
|
|
867
|
+
return /Worker ".*?"(?: \(env: [^)]*\))? not found\./.test(normalized) && normalized.includes("If this is a new Worker, run `wrangler deploy` first to create it.") && normalized.includes("Otherwise, check that the Worker name is correct and you're logged into the right account.");
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Build the THROWN message for a wrangler-run failure from the structural
|
|
871
|
+
* reason code + safe scalars ONLY. It NEVER interpolates `result.stderr`,
|
|
872
|
+
* `result.stdout`, a spawn-error string, or a rejection message — those are
|
|
873
|
+
* unconstrained (the child inherits `process.env`) and could embed an env
|
|
874
|
+
* value. Where wrangler's own diagnostics would help, the message tells the
|
|
875
|
+
* user to re-run wrangler themselves rather than echoing its output here.
|
|
876
|
+
*/
|
|
877
|
+
function secretListFailureMessage(reason, detail) {
|
|
878
|
+
const base = "env: `wrangler secret list` could not confirm the remote secrets";
|
|
879
|
+
switch (reason) {
|
|
880
|
+
case "spawn-error": return `${base} (reason: spawn-error) — the wrangler process could not be started. Check that wrangler is installed and the pinned config path is valid.`;
|
|
881
|
+
case "signal": return `${base} (reason: signal) — the process was terminated by ${detail.signal ?? "a signal"} before completing.`;
|
|
882
|
+
case "nonzero-exit": return `${base} (reason: nonzero-exit, status ${detail.exitCode ?? "unknown"}). Re-run \`wrangler secret list -c <config>\` to see wrangler’s own diagnostics.`;
|
|
883
|
+
case "malformed-output": return `${base} (reason: malformed-output) — \`wrangler secret list --format=json\` did not return the expected JSON array of secret objects.`;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/** Construct a values-free {@link SelfHostSecretListError} for a run failure. */
|
|
887
|
+
function secretListError(reason, detail = {}) {
|
|
888
|
+
return new SelfHostSecretListError(secretListFailureMessage(reason, detail), reason, detail);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Run `wrangler secret list` through the seam and parse STRICTLY, failing
|
|
892
|
+
* CLOSED: any signal, spawn error, nonzero exit, unparseable stdout, or
|
|
893
|
+
* malformed list member throws {@link SelfHostSecretListError}. A failure here
|
|
894
|
+
* is never interpreted as "no secrets exist" — that would let a deploy proceed
|
|
895
|
+
* with required secrets actually missing on the worker.
|
|
896
|
+
*
|
|
897
|
+
* A nonzero exit whose output carries wrangler's worker-not-found signal is a
|
|
898
|
+
* DISTINCT, structured {@link SelfHostFirstDeployError} carrying the still
|
|
899
|
+
* unsatisfied required `missingKeys` + `wrangler secret put` bootstrap guidance,
|
|
900
|
+
* instead of the generic "list exited nonzero" dead end.
|
|
901
|
+
*
|
|
902
|
+
* Raw child output (stderr/stdout, spawn-error text, rejection messages)
|
|
903
|
+
* is used ONLY internally: for worker-not-found classification the combined
|
|
904
|
+
* output is matched to a boolean that never leaves this function. Every THROWN
|
|
905
|
+
* error carries only a structural {@link SecretListFailureReason} code and safe
|
|
906
|
+
* scalars (exit code / signal name), so no env value the child may have echoed
|
|
907
|
+
* can reach the error's message, fields, or JSON — even under `VOID_ENV_UNMASK`.
|
|
908
|
+
*/
|
|
909
|
+
async function fetchSelfHostSecretNames(run, configPath, missingKeys) {
|
|
910
|
+
let result;
|
|
911
|
+
try {
|
|
912
|
+
result = await Promise.resolve(run(secretListArgs(configPath)));
|
|
913
|
+
} catch {
|
|
914
|
+
throw secretListError("spawn-error");
|
|
915
|
+
}
|
|
916
|
+
if (result.error != null && result.error !== "") throw secretListError("spawn-error");
|
|
917
|
+
if (result.signal != null) throw secretListError("signal", { signal: String(result.signal) });
|
|
918
|
+
if (typeof result.status !== "number" || result.status !== 0) {
|
|
919
|
+
const exitCode = typeof result.status === "number" ? result.status : void 0;
|
|
920
|
+
if (isWorkerNotFoundOutput(`${result.stdout}\n${result.stderr}`)) throw new SelfHostFirstDeployError(firstDeployMessage(missingKeys), missingKeys, exitCode);
|
|
921
|
+
throw secretListError("nonzero-exit", { exitCode });
|
|
922
|
+
}
|
|
923
|
+
let parsed;
|
|
924
|
+
try {
|
|
925
|
+
parsed = JSON.parse(result.stdout);
|
|
926
|
+
} catch {
|
|
927
|
+
throw secretListError("malformed-output");
|
|
928
|
+
}
|
|
929
|
+
if (!Array.isArray(parsed)) throw secretListError("malformed-output");
|
|
930
|
+
const names = [];
|
|
931
|
+
for (const member of parsed) {
|
|
932
|
+
const name = member !== null && typeof member === "object" && !Array.isArray(member) ? member.name : void 0;
|
|
933
|
+
if (typeof name !== "string" || name.length === 0) throw secretListError("malformed-output");
|
|
934
|
+
names.push(name);
|
|
935
|
+
}
|
|
936
|
+
return names;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Hard timeout for one `wrangler secret list` invocation — same bound as the
|
|
940
|
+
* provisioner's per-command timeout. A hung wrangler must never hang the
|
|
941
|
+
* whole deploy preflight.
|
|
942
|
+
*/
|
|
943
|
+
const SECRET_LIST_TIMEOUT_MS = 12e4;
|
|
944
|
+
/**
|
|
945
|
+
* Default {@link RunSecretList}: resolves wrangler's REAL CLI entry
|
|
946
|
+
* (`wrangler-dist/cli.js`, not the signal-swallowing `bin` wrapper) via
|
|
947
|
+
* {@link resolveWranglerArgv} and spawns it ASYNC with `shell: false`, so every
|
|
948
|
+
* argv element is passed literally (no shell → no metacharacter injection) and
|
|
949
|
+
* the event loop stays free. A hard timeout SIGKILLs a hung child; the result
|
|
950
|
+
* carries `{status, signal, error}` provenance so the caller fails closed on
|
|
951
|
+
* anything but a clean exit 0.
|
|
952
|
+
*
|
|
953
|
+
* Deliberately simpler than the provisioner's runner: this is a ONE-SHOT
|
|
954
|
+
* READ-ONLY query outside the provision lock — no lock heartbeat to protect,
|
|
955
|
+
* nothing a lingering child could mutate — so the group-kill/quarantine
|
|
956
|
+
* machinery is unnecessary. If the SIGKILL cannot be confirmed (close never
|
|
957
|
+
* fires), a bounded fallback settles the call as a HARD failure anyway.
|
|
958
|
+
*/
|
|
959
|
+
function createDefaultSecretListRunner(root, options) {
|
|
960
|
+
const launcher = options?.launcher ?? resolveWranglerArgv(root);
|
|
961
|
+
const timeoutMs = options?.timeoutMs ?? SECRET_LIST_TIMEOUT_MS;
|
|
962
|
+
const [command, ...prefix] = launcher;
|
|
963
|
+
return (args) => new Promise((resolvePromise) => {
|
|
964
|
+
const child = spawn(command, [...prefix, ...args], {
|
|
965
|
+
cwd: root,
|
|
966
|
+
shell: false,
|
|
967
|
+
env: secretListEnv(options?.accountId),
|
|
968
|
+
stdio: [
|
|
969
|
+
"ignore",
|
|
970
|
+
"pipe",
|
|
971
|
+
"pipe"
|
|
972
|
+
],
|
|
973
|
+
windowsHide: true
|
|
974
|
+
});
|
|
975
|
+
let stdout = "";
|
|
976
|
+
let stderr = "";
|
|
977
|
+
let settled = false;
|
|
978
|
+
let timedOut = false;
|
|
979
|
+
let killFallbackTimer;
|
|
980
|
+
child.stdout?.setEncoding("utf-8");
|
|
981
|
+
child.stderr?.setEncoding("utf-8");
|
|
982
|
+
child.stdout?.on("data", (chunk) => {
|
|
983
|
+
stdout += chunk;
|
|
984
|
+
});
|
|
985
|
+
child.stderr?.on("data", (chunk) => {
|
|
986
|
+
stderr += chunk;
|
|
987
|
+
});
|
|
988
|
+
const settle = (result) => {
|
|
989
|
+
if (settled) return;
|
|
990
|
+
settled = true;
|
|
991
|
+
clearTimeout(commandTimer);
|
|
992
|
+
clearTimeout(killFallbackTimer);
|
|
993
|
+
resolvePromise(result);
|
|
994
|
+
};
|
|
995
|
+
const timeoutNote = `env: wrangler secret list exceeded the ${timeoutMs}ms timeout`;
|
|
996
|
+
const commandTimer = setTimeout(() => {
|
|
997
|
+
timedOut = true;
|
|
998
|
+
try {
|
|
999
|
+
child.kill("SIGKILL");
|
|
1000
|
+
} catch {}
|
|
1001
|
+
killFallbackTimer = setTimeout(() => {
|
|
1002
|
+
settle({
|
|
1003
|
+
stdout,
|
|
1004
|
+
stderr: [stderr, timeoutNote].filter(Boolean).join("\n"),
|
|
1005
|
+
status: -1,
|
|
1006
|
+
signal: "SIGKILL",
|
|
1007
|
+
error: "timed out and termination could not be confirmed"
|
|
1008
|
+
});
|
|
1009
|
+
}, 5e3);
|
|
1010
|
+
}, timeoutMs);
|
|
1011
|
+
child.on("error", (error) => {
|
|
1012
|
+
settle({
|
|
1013
|
+
stdout,
|
|
1014
|
+
stderr,
|
|
1015
|
+
status: -1,
|
|
1016
|
+
signal: null,
|
|
1017
|
+
error: error.message
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
child.on("close", (code, signal) => {
|
|
1021
|
+
settle({
|
|
1022
|
+
stdout,
|
|
1023
|
+
stderr: timedOut ? [stderr, timeoutNote].filter(Boolean).join("\n") : stderr,
|
|
1024
|
+
status: typeof code === "number" ? code : -1,
|
|
1025
|
+
signal: signal ?? (timedOut ? "SIGKILL" : null)
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Child env for the spawned `wrangler secret list`. Mirrors deploy.ts's
|
|
1032
|
+
* `buildEnv` (not imported — deploy.ts imports this module): the CF Access
|
|
1033
|
+
* service-token pair is for the Void platform API client only and must never
|
|
1034
|
+
* leak into a spawned wrangler. Pins `WRANGLER_LOG_SANITIZE=true` (same as the
|
|
1035
|
+
* provisioner) and the caller's account id when provided. Unlike the
|
|
1036
|
+
* provisioner's `provisionEnv`, ambient transport env (`CLOUDFLARE_API_BASE_URL`
|
|
1037
|
+
* etc.) is left untouched: this preflight has no second transport to keep in
|
|
1038
|
+
* lockstep, and scrubbing would silently redirect a staging/FedRAMP user's
|
|
1039
|
+
* `secret list` at the production endpoint.
|
|
1040
|
+
*
|
|
1041
|
+
* Windows environment keys are CASE-INSENSITIVE (same hazard `provisionEnv`
|
|
1042
|
+
* guards): an ambient mixed-case spelling (`Cloudflare_Account_Id`) could
|
|
1043
|
+
* shadow an exact-case pin, so on win32 every case-variant of a scrubbed or
|
|
1044
|
+
* pinned key is removed first. Exported (with a platform seam) for direct
|
|
1045
|
+
* unit testing of that case rule; production callers pass no platform.
|
|
1046
|
+
*/
|
|
1047
|
+
function secretListEnv(accountId, platform) {
|
|
1048
|
+
const env = { ...process.env };
|
|
1049
|
+
const caseInsensitive = (platform ?? process.platform) === "win32";
|
|
1050
|
+
const scrub = (name) => {
|
|
1051
|
+
if (!caseInsensitive) {
|
|
1052
|
+
delete env[name];
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const lower = name.toLowerCase();
|
|
1056
|
+
for (const key of Object.keys(env)) if (key.toLowerCase() === lower) delete env[key];
|
|
1057
|
+
};
|
|
1058
|
+
scrub("CF_ACCESS_CLIENT_ID");
|
|
1059
|
+
scrub("CF_ACCESS_CLIENT_SECRET");
|
|
1060
|
+
scrub("WRANGLER_LOG_SANITIZE");
|
|
1061
|
+
env.WRANGLER_LOG_SANITIZE = "true";
|
|
1062
|
+
if (accountId) {
|
|
1063
|
+
scrub("CLOUDFLARE_ACCOUNT_ID");
|
|
1064
|
+
env.CLOUDFLARE_ACCOUNT_ID = accountId;
|
|
1065
|
+
}
|
|
1066
|
+
return env;
|
|
1067
|
+
}
|
|
1068
|
+
//#endregion
|
|
1069
|
+
export { collectPlaintextVarWarningsFromVars as a, env_validation_exports as c, getDeployEnvDefaults as d, readEmittedWorkerVars as f, loadUserEnvFile as h, SelfHostSecretListError as i, fetchRemoteSecretNames as l, validateProdEnv as m, SelfHostFirstDeployError as n, deriveSelfHostEnvContextFromVars as o, selfHostSchemaFailureMessage as p, SelfHostSchemaError as r, dischargeSelfHostEnvContext as s, SelfHostEnvValidationError as t, formatEnvReport as u };
|