@saastemly/voidcommerce 0.2.2 → 0.4.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.
@@ -11,11 +11,14 @@ import {
11
11
  workerHosts,
12
12
  writeManifest,
13
13
  zone
14
- } from "./index-zh3tmj08.js";
14
+ } from "./index-pz6m2hkm.js";
15
15
  import {
16
- CHOICES,
17
- __require
18
- } from "./index-z4qazajn.js";
16
+ CHOICES
17
+ } from "./index-844b3qn9.js";
18
+ import {
19
+ __require,
20
+ __toESM
21
+ } from "./index-0v6na3yp.js";
19
22
 
20
23
  // src/generate/auth.ts
21
24
  var VAT = {
@@ -840,6 +843,8 @@ function strictDependencies(manifest) {
840
843
  const devDependencies = {};
841
844
  for (const name of [
842
845
  "@hono/node-server",
846
+ "@dotenvx/dotenvx",
847
+ "husky",
843
848
  "@rolldown/plugin-babel",
844
849
  "@tailwindcss/vite",
845
850
  "@types/node",
@@ -1436,7 +1441,7 @@ import color from "picocolors";
1436
1441
  // package.json
1437
1442
  var package_default = {
1438
1443
  name: "@saastemly/voidcommerce",
1439
- version: "0.2.2",
1444
+ version: "0.4.0",
1440
1445
  description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
1441
1446
  type: "module",
1442
1447
  license: "MIT",
@@ -1470,6 +1475,7 @@ var package_default = {
1470
1475
  },
1471
1476
  dependencies: {
1472
1477
  "@clack/prompts": "^0.11.0",
1478
+ "@dotenvx/primitives": "^2.2.0",
1473
1479
  picocolors: "^1.1.1"
1474
1480
  },
1475
1481
  devDependencies: {
@@ -1492,6 +1498,7 @@ var EXTENDED_ROWS = [
1492
1498
  ["vc dev | build | preview", "the app's own script, run where the app is — api/ for a monorepo, .vc/app for strict"],
1493
1499
  ["vc dist", "the deployable app as a self-contained tree — what the void-dist branch carries"],
1494
1500
  ["vc import", "push data/ and content/ into the running shop, as the shop itself — an upsert, safe on every deploy"],
1501
+ ["vc secrets", "the shop's secrets, encrypted and committed — one key where the deploy runs, instead of one wrangler secret put per value"],
1495
1502
  ["vc preflight", "is the shop ready to advertise on? every required key, and what breaks without it"],
1496
1503
  ["vc deploy", "preflight, then void deploy; or --cloudflare: wrangler, your own account, no Void login"]
1497
1504
  ];
@@ -1721,7 +1728,7 @@ async function distCommand(args) {
1721
1728
  appPkg["patchedDependencies"] = rootPkg["patchedDependencies"];
1722
1729
  await writeFile(appPkgPath, `${JSON.stringify(appPkg, null, 2)}
1723
1730
  `, "utf8");
1724
- for (const file of ["bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml"]) {
1731
+ for (const file of ["bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml", ".env.secrets"]) {
1725
1732
  const source = join3(project.root, file);
1726
1733
  if (existsSync3(source))
1727
1734
  await cp(source, join3(target, file));
@@ -1876,13 +1883,27 @@ and \`voidcommerce.json\` so every later generate carries the real ids.
1876
1883
 
1877
1884
  ### 3. The secrets
1878
1885
 
1886
+ They live in the repository, encrypted:
1887
+
1879
1888
  \`\`\`sh
1880
- vc preflight --cloudflare
1889
+ vc secrets --init # every required key, as \`unset\`
1890
+ # put the real values in, then
1891
+ bunx dotenvx encrypt -f .env.secrets # ciphertext; commit this
1881
1892
  \`\`\`
1882
1893
 
1883
- lists every required key, says what breaks without it, and prints the
1884
- \`wrangler secret put\` line. On a Worker that has never been deployed, setting
1885
- the first secret is what creates it.
1894
+ \`.env.secrets\` is committed and \`.env.keys\` is not. The deploy decrypts it and
1895
+ hands the values to \`wrangler deploy --secrets-file\`, which stores them as
1896
+ real Worker secrets not as plaintext \`vars\`, which anyone with dashboard
1897
+ access can read.
1898
+
1899
+ That turns one \`wrangler secret put\` per value into one build variable, set
1900
+ once in step 5. It also means the shop rebuilds from a checkout.
1901
+
1902
+ **Know the tradeoff.** Ciphertext in git is permanent: if the private key ever
1903
+ leaks, every secret in the history is readable, including ones you rotated.
1904
+ \`wrangler secret put\` does not have that property, and stays available for
1905
+ anything you would rather never commit. \`vc preflight\` counts a secret the
1906
+ repository declares as present, and refuses any value committed in the clear.
1886
1907
 
1887
1908
  ### 4. An API token the build can use
1888
1909
 
@@ -1917,9 +1938,20 @@ the \`wrangler.jsonc\` at the root directory, or the build fails.
1917
1938
  | branch | \`${DIST_BRANCH}\` |
1918
1939
  | root directory | \`/\` |
1919
1940
  | build command | \`bun install && bunx void prepare && bunx vp build\` |
1920
- | deploy command | \`bunx wrangler d1 migrations apply DB --remote && bunx wrangler deploy -c dist/ssr/wrangler.json\` |
1941
+ | deploy command | see below |
1942
+ | build variable | \`DOTENV_PRIVATE_KEY_SECRETS\`, marked as a secret — the one value that is not in the repository |
1921
1943
  | API token | the one from step 4 |
1922
1944
 
1945
+ The deploy command, on one line:
1946
+
1947
+ \`\`\`sh
1948
+ bunx dotenvx decrypt -f .env.secrets --stdout > .vc-secrets.env && bunx wrangler d1 migrations apply DB --remote && bunx wrangler deploy -c dist/ssr/wrangler.json --secrets-file .vc-secrets.env
1949
+ \`\`\`
1950
+
1951
+ Plain \`sh\`, so it does not rely on process substitution. The decrypted file
1952
+ exists only inside the build sandbox, and \`--secrets-file\` applies additively:
1953
+ a secret it does not name is left alone rather than deleted.
1954
+
1923
1955
  The migration command names the **binding** (\`DB\`), not the database, so it
1924
1956
  still points at the right database if the name ever differs.
1925
1957
 
@@ -2451,6 +2483,8 @@ async function generateStrictRoot(root, manifest, result) {
2451
2483
  scripts["deploy"] ??= "vc deploy";
2452
2484
  scripts["import:catalog"] ??= "vc import";
2453
2485
  scripts["maildev"] ??= "maildev --smtp 1025 --web 1080";
2486
+ scripts["secrets"] ??= "vc secrets";
2487
+ scripts["prepare"] ??= "husky";
2454
2488
  pkg["scripts"] = scripts;
2455
2489
  }, result);
2456
2490
  await put(root, ".gitignore", `node_modules
@@ -2459,11 +2493,18 @@ async function generateStrictRoot(root, manifest, result) {
2459
2493
  .wrangler
2460
2494
  .env
2461
2495
  .env.local
2496
+ .env.keys
2462
2497
  dist
2463
2498
  *.tsbuildinfo
2464
2499
  .DS_Store
2465
2500
  `, result, "own");
2466
2501
  await put(root, "patches/void@0.10.13.patch", renderVoidPatch(), result, "regenerate");
2502
+ await put(root, ".husky/pre-commit", `#!/usr/bin/env sh
2503
+ # Generated by \`vc init\`. Refuses a commit that would put a secret in the
2504
+ # clear in .env.secrets — which cannot be undone by a later commit,
2505
+ # because the value stays in the history.
2506
+ bunx vc guard
2507
+ `, result, "regenerate");
2467
2508
  await put(root, ".github/workflows/void-dist.yml", renderDistWorkflow(manifest), result, "regenerate");
2468
2509
  await put(root, "DEPLOY.md", renderDeployReadme(manifest, zone(manifest), workerHosts(manifest)), result, "regenerate");
2469
2510
  await put(root, ".env", renderEnvLocal(manifest), result, "own");
@@ -2828,16 +2869,346 @@ ${ran.out.trim()}`);
2828
2869
  }
2829
2870
 
2830
2871
  // src/deploy/preflight.ts
2872
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
2873
+ import { join as join9 } from "node:path";
2874
+ import color5 from "picocolors";
2875
+
2876
+ // src/deploy/secrets.ts
2877
+ import { existsSync as existsSync8, readFileSync as readFileSync3, writeFileSync } from "node:fs";
2878
+ import { spawn as spawn3 } from "node:child_process";
2879
+ import { delimiter as delimiter3, dirname as dirname5, join as join8 } from "node:path";
2880
+ import { tmpdir } from "node:os";
2881
+ import color4 from "picocolors";
2882
+
2883
+ // src/deploy/keys.ts
2884
+ import { hkdfSync } from "node:crypto";
2831
2885
  import { existsSync as existsSync7, readFileSync as readFileSync2 } from "node:fs";
2832
2886
  import { join as join7 } from "node:path";
2833
2887
  import color3 from "picocolors";
2888
+ var INFO = "voidcommerce/dotenvx/secrets/v1";
2889
+ function apiToken() {
2890
+ return process.env["CLOUDFLARE_API_TOKEN"] || process.env["CF_API_TOKEN"] || null;
2891
+ }
2892
+ function derivePrivateKey(token, accountId) {
2893
+ const bytes = hkdfSync("sha256", Buffer.from(token, "utf8"), Buffer.from(accountId, "utf8"), Buffer.from(INFO), 32);
2894
+ return Buffer.from(bytes).toString("hex");
2895
+ }
2896
+ async function publicKeyFor(privateKey) {
2897
+ try {
2898
+ const { derive } = await import("./index-mpr7gm6k.js").then((m)=>__toESM(m.default,1));
2899
+ return derive(privateKey);
2900
+ } catch {
2901
+ return null;
2902
+ }
2903
+ }
2904
+ function committedPublicKey(root) {
2905
+ const path = join7(root, SECRETS_FILE);
2906
+ if (!existsSync7(path))
2907
+ return null;
2908
+ const match = /^\s*DOTENV_PUBLIC_KEY_SECRETS\s*=\s*["']?([0-9a-fA-F]+)["']?/m.exec(readFileSync2(path, "utf8"));
2909
+ return match?.[1] ?? null;
2910
+ }
2911
+ async function keyFor(manifest, root) {
2912
+ const token = apiToken();
2913
+ if (!token) {
2914
+ return {
2915
+ ok: false,
2916
+ reason: `CLOUDFLARE_API_TOKEN is not set.
2917
+ The key is derived from it, so there is nothing to derive from. Note that
2918
+ ` + " `wrangler login` does NOT set one — this scheme needs an API token, from\n" + " My Profile → API Tokens."
2919
+ };
2920
+ }
2921
+ const accountId = manifest.cloudflare?.accountId || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
2922
+ if (!accountId) {
2923
+ return {
2924
+ ok: false,
2925
+ reason: "the account is not pinned, and it salts the derivation.\n Run `vc deploy --cloudflare --provision` once, or set CLOUDFLARE_ACCOUNT_ID."
2926
+ };
2927
+ }
2928
+ const privateKey = derivePrivateKey(token, accountId);
2929
+ const publicKey = await publicKeyFor(privateKey);
2930
+ if (!publicKey) {
2931
+ return { ok: false, reason: "@dotenvx/dotenvx is not installed here, so a key cannot be derived. `bun add -d @dotenvx/dotenvx`" };
2932
+ }
2933
+ const committed = committedPublicKey(root);
2934
+ if (!committed)
2935
+ return { ok: true, privateKey, publicKey, fresh: true };
2936
+ if (committed.toLowerCase() !== publicKey.toLowerCase()) {
2937
+ return {
2938
+ ok: false,
2939
+ reason: `this token does not derive the key ${SECRETS_FILE} was encrypted under.
2940
+
2941
+ encrypted under: ${committed.slice(0, 16)}…
2942
+ ` + ` this token gives: ${publicKey.slice(0, 16)}…
2943
+
2944
+ ` + ` That is one of three things, and all of them are the same fix:
2945
+ ` + ` · the token was rotated since the secrets were encrypted
2946
+ ` + ` · this is a different admin's token
2947
+ ` + ` · CLOUDFLARE_ACCOUNT_ID is not the account they were encrypted for
2948
+
2949
+ ` + ` If you still have the ORIGINAL token, \`vc keys rotate\` re-encrypts
2950
+ everything under the new one. If you do not, the values are unrecoverable
2951
+ and must be entered again.`
2952
+ };
2953
+ }
2954
+ return { ok: true, privateKey, publicKey, fresh: false };
2955
+ }
2956
+ async function keysCommand(manifest, root, args) {
2957
+ if (args.includes("--rotate"))
2958
+ return rotate(manifest, root);
2959
+ const state = await keyFor(manifest, root);
2960
+ console.log(`
2961
+ The key is DERIVED from CLOUDFLARE_API_TOKEN, salted with the account id.`);
2962
+ console.log(color3.dim(`Nothing is stored, so nothing can leak — and nothing can be recovered.
2963
+ `));
2964
+ if (!state.ok) {
2965
+ console.error(`${color3.red("✗")} ${state.reason}
2966
+ `);
2967
+ return 1;
2968
+ }
2969
+ console.log(` derives public key ${state.publicKey.slice(0, 20)}…`);
2970
+ console.log(state.fresh ? ` ${color3.dim(`${SECRETS_FILE} does not exist yet, so there is nothing to check against`)}` : ` ${color3.green("✓")} matches what ${SECRETS_FILE} was encrypted under`);
2971
+ console.log(color3.yellow(`
2972
+ ! Rotating this API token makes every secret in ${SECRETS_FILE} unreadable.
2973
+ Run \`vc keys --rotate\` with the NEW token exported and the old one in
2974
+ CLOUDFLARE_API_TOKEN_OLD, BEFORE the old one stops working.
2975
+ `));
2976
+ return 0;
2977
+ }
2978
+ async function rotate(manifest, root) {
2979
+ const oldToken = process.env["CLOUDFLARE_API_TOKEN_OLD"];
2980
+ const newToken = apiToken();
2981
+ if (!oldToken || !newToken) {
2982
+ console.error(`
2983
+ vc: rotation needs BOTH tokens:
2984
+ CLOUDFLARE_API_TOKEN_OLD=<the one the secrets were encrypted under>
2985
+ CLOUDFLARE_API_TOKEN=<the new one>
2986
+
2987
+ The old one is the only thing that can read the current values.
2988
+ `);
2989
+ return 1;
2990
+ }
2991
+ const accountId = manifest.cloudflare?.accountId || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
2992
+ if (!accountId) {
2993
+ console.error("vc: the account is not pinned, and it salts the derivation.");
2994
+ return 1;
2995
+ }
2996
+ const oldKey = derivePrivateKey(oldToken, accountId);
2997
+ const oldPublic = await publicKeyFor(oldKey);
2998
+ const committed = committedPublicKey(root);
2999
+ if (committed && oldPublic && committed.toLowerCase() !== oldPublic.toLowerCase()) {
3000
+ console.error(`
3001
+ vc: CLOUDFLARE_API_TOKEN_OLD does not derive the key ${SECRETS_FILE} was encrypted under either.
3002
+ `);
3003
+ return 1;
3004
+ }
3005
+ console.log(`
3006
+ Rotation is a decrypt with the old key and an encrypt with the new one.
3007
+ Run these two, in this order, from ${root}:
3008
+
3009
+ ${color3.cyan(`DOTENV_PRIVATE_KEY_SECRETS=<old> bunx dotenvx decrypt -f ${SECRETS_FILE}`)}
3010
+ ${color3.cyan(`bunx dotenvx encrypt -f ${SECRETS_FILE}`)} ${color3.dim("# under the new derived key")}
3011
+
3012
+ ` + color3.dim(`vc does not run them for you: the middle state is your secrets in
3013
+ plaintext on disk, and that is a moment to be deliberate about.
3014
+ `));
3015
+ return 0;
3016
+ }
3017
+
3018
+ // src/deploy/secrets.ts
3019
+ var SECRETS_FILE = ".env.secrets";
3020
+ var PRIVATE_KEY_VAR = "DOTENV_PRIVATE_KEY_SECRETS";
3021
+ function findDotenvx(from) {
3022
+ let dir = from;
3023
+ for (;; ) {
3024
+ const local = join8(dir, "node_modules", ".bin", "dotenvx");
3025
+ if (existsSync8(local))
3026
+ return local;
3027
+ const parent = dirname5(dir);
3028
+ if (parent === dir)
3029
+ break;
3030
+ dir = parent;
3031
+ }
3032
+ return (process.env["PATH"] ?? "").split(delimiter3).filter(Boolean).some((entry) => existsSync8(join8(entry, "dotenvx"))) ? "dotenvx" : null;
3033
+ }
3034
+ function run(cmd, args, cwd, env = {}) {
3035
+ return new Promise((resolve) => {
3036
+ const child = spawn3(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...env } });
3037
+ let out = "";
3038
+ child.stdout.on("data", (chunk) => {
3039
+ out += chunk;
3040
+ });
3041
+ child.stderr.on("data", (chunk) => {
3042
+ out += chunk;
3043
+ });
3044
+ child.on("error", (error) => resolve({ code: 1, out: String(error) }));
3045
+ child.on("exit", (code) => resolve({ code: code ?? 1, out }));
3046
+ });
3047
+ }
3048
+ function declaredSecretNames(root) {
3049
+ const path = join8(root, SECRETS_FILE);
3050
+ if (!existsSync8(path))
3051
+ return new Set;
3052
+ const names = new Set;
3053
+ for (const line2 of readFileSync3(path, "utf8").split(`
3054
+ `)) {
3055
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=/.exec(line2);
3056
+ if (match && !match[1].startsWith("DOTENV_"))
3057
+ names.add(match[1]);
3058
+ }
3059
+ return names;
3060
+ }
3061
+ function plaintextSecretNames(root) {
3062
+ const path = join8(root, SECRETS_FILE);
3063
+ if (!existsSync8(path))
3064
+ return [];
3065
+ const bare = [];
3066
+ for (const line2 of readFileSync3(path, "utf8").split(`
3067
+ `)) {
3068
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line2);
3069
+ if (!match || match[1].startsWith("DOTENV_"))
3070
+ continue;
3071
+ const value = match[2].trim().replace(/^['"]|['"]$/g, "");
3072
+ if (value && value !== "unset" && !value.startsWith("encrypted:"))
3073
+ bare.push(match[1]);
3074
+ }
3075
+ return bare;
3076
+ }
3077
+ async function decryptSecrets(project) {
3078
+ const root = project.root;
3079
+ if (!existsSync8(join8(root, SECRETS_FILE)))
3080
+ return { error: `${SECRETS_FILE} does not exist — \`vc secrets init\` writes one` };
3081
+ const dotenvx = findDotenvx(root);
3082
+ if (!dotenvx)
3083
+ return { error: "dotenvx is not installed. `bun add -d @dotenvx/dotenvx`" };
3084
+ const key = await keyFor(project.manifest, root);
3085
+ if (!key.ok)
3086
+ return { error: key.reason };
3087
+ const result = await run(dotenvx, ["decrypt", "-f", SECRETS_FILE, "--stdout"], root, {
3088
+ [PRIVATE_KEY_VAR]: key.privateKey
3089
+ });
3090
+ if (result.code !== 0)
3091
+ return { error: `dotenvx could not decrypt ${SECRETS_FILE}: ${result.out.trim().split(`
3092
+ `).slice(-2).join(" ")}` };
3093
+ const lines = result.out.split(`
3094
+ `).filter((line2) => /^\s*[A-Z][A-Z0-9_]*\s*=/.test(line2) && !line2.trimStart().startsWith("DOTENV_"));
3095
+ const names = lines.map((line2) => line2.slice(0, line2.indexOf("=")).trim());
3096
+ const path = join8(tmpdir(), `vc-secrets-${process.pid}-${Date.now()}.env`);
3097
+ writeFileSync(path, `${lines.join(`
3098
+ `)}
3099
+ `, { mode: 384 });
3100
+ return {
3101
+ path,
3102
+ names,
3103
+ cleanup: () => {
3104
+ try {
3105
+ writeFileSync(path, "", { mode: 384 });
3106
+ __require("node:fs").unlinkSync(path);
3107
+ } catch {}
3108
+ }
3109
+ };
3110
+ }
3111
+ async function secretsCommand(project, args) {
3112
+ const root = project.root;
3113
+ const required = allEnvKeys(project.manifest).filter((key) => !key.plaintext);
3114
+ const declared = declaredSecretNames(root);
3115
+ const bare = plaintextSecretNames(root);
3116
+ if (args.includes("--init"))
3117
+ return initSecrets(project);
3118
+ if (!existsSync8(join8(root, SECRETS_FILE))) {
3119
+ console.log(`
3120
+ ${color4.yellow("No " + SECRETS_FILE + " yet.")} Secrets are set by hand with \`wrangler secret put\`,
3121
+ ` + `which means the shop cannot be rebuilt from a checkout.
3122
+
3123
+ ` + ` vc secrets --init write one, with every required key as \`unset\`
3124
+ `);
3125
+ return 1;
3126
+ }
3127
+ console.log(`
3128
+ ${SECRETS_FILE} — committed, encrypted, and read by \`vc deploy --cloudflare\`
3129
+ `);
3130
+ for (const key of required) {
3131
+ const state = declared.has(key.key) ? color4.green("declared") : color4.red("MISSING ");
3132
+ console.log(` ${state} ${key.key}`);
3133
+ }
3134
+ const extra = [...declared].filter((name) => !required.some((key) => key.key === name));
3135
+ for (const name of extra)
3136
+ console.log(` ${color4.dim("extra ")} ${name} ${color4.dim("— not required by this shop")}`);
3137
+ if (bare.length > 0) {
3138
+ console.log(`
3139
+ ${color4.red("✗")} ${bare.length} value${bare.length === 1 ? " is" : "s are"} committed IN THE CLEAR: ${bare.join(", ")}
3140
+ ` + ` Run \`dotenvx encrypt -f ${SECRETS_FILE}\` before committing again.
3141
+ `);
3142
+ return 1;
3143
+ }
3144
+ const missing = required.filter((key) => !declared.has(key.key));
3145
+ if (missing.length > 0) {
3146
+ console.log(`
3147
+ ${color4.red("✗")} ${missing.length} required secret${missing.length === 1 ? "" : "s"} not declared.
3148
+ ` + ` dotenvx set ${missing[0].key} '…' -f ${SECRETS_FILE}
3149
+ `);
3150
+ return 1;
3151
+ }
3152
+ console.log(`
3153
+ ${color4.green("✓")} every secret this shop needs is declared and encrypted.
3154
+ `);
3155
+ return 0;
3156
+ }
3157
+ async function initSecrets(project) {
3158
+ const path = join8(project.root, SECRETS_FILE);
3159
+ if (existsSync8(path)) {
3160
+ console.error(`vc: ${SECRETS_FILE} already exists; not overwriting it.`);
3161
+ return 1;
3162
+ }
3163
+ const key = await keyFor(project.manifest, project.root);
3164
+ if (!key.ok) {
3165
+ console.error(`
3166
+ vc: ${key.reason}
3167
+ `);
3168
+ return 1;
3169
+ }
3170
+ const required = allEnvKeys(project.manifest).filter((key2) => !key2.plaintext);
3171
+ const body = [
3172
+ "# Secrets, encrypted, and COMMITTED.",
3173
+ "#",
3174
+ "# Values are ciphertext; the key names are readable so a diff shows WHICH",
3175
+ "# secret changed without showing what it changed to. `unset` is the",
3176
+ "# documented placeholder and preflight refuses it.",
3177
+ "#",
3178
+ "# The private key is DERIVED from CLOUDFLARE_API_TOKEN, salted with the",
3179
+ "# account id — it is stored nowhere, so there is no .env.keys to leak.",
3180
+ "#",
3181
+ "# The cost of that: rotating the token makes every value below",
3182
+ "# permanently unreadable. `vc keys --rotate` re-encrypts while you still",
3183
+ "# have the old token; after that there is no recovery.",
3184
+ "",
3185
+ `DOTENV_PUBLIC_KEY_SECRETS="${key.publicKey}"`,
3186
+ "",
3187
+ ...required.flatMap((key2) => [`# ${key2.breaks}${key2.where ? ` — from: ${key2.where}` : ""}`, `${key2.key}=unset`, ""])
3188
+ ].join(`
3189
+ `);
3190
+ writeFileSync(path, body, { mode: 384 });
3191
+ console.log(`
3192
+ ${color4.green("+")} ${SECRETS_FILE} — ${required.length} keys, all \`unset\`, under the key this token derives
3193
+
3194
+ ` + `Next:
3195
+ ` + ` 1. put the real values in, then
3196
+ ` + ` 2. ${color4.cyan(`bunx dotenvx encrypt -f ${SECRETS_FILE}`)}
3197
+ ` + ` 3. commit it — there is no key file to keep out
3198
+
3199
+ ` + color4.yellow(`! Rotating CLOUDFLARE_API_TOKEN makes these unreadable. \`vc keys\` explains.
3200
+ `));
3201
+ return 0;
3202
+ }
3203
+
3204
+ // src/deploy/preflight.ts
2834
3205
  var UNSET = "unset";
2835
3206
  function productionEnv(appDir) {
2836
3207
  const out = new Map;
2837
- const path = join7(appDir, ".env.production");
2838
- if (!existsSync7(path))
3208
+ const path = join9(appDir, ".env.production");
3209
+ if (!existsSync9(path))
2839
3210
  return out;
2840
- for (const line2 of readFileSync2(path, "utf8").split(`
3211
+ for (const line2 of readFileSync4(path, "utf8").split(`
2841
3212
  `)) {
2842
3213
  const match = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line2);
2843
3214
  if (!match)
@@ -2862,12 +3233,12 @@ async function voidSecretNames(appDir) {
2862
3233
  return names;
2863
3234
  }
2864
3235
  function routeProblem(project) {
2865
- const path = join7(project.appDir, "wrangler.jsonc");
2866
- if (!existsSync7(path))
3236
+ const path = join9(project.appDir, "wrangler.jsonc");
3237
+ if (!existsSync9(path))
2867
3238
  return "wrangler.jsonc is missing";
2868
3239
  let routes = [];
2869
3240
  try {
2870
- routes = parseJsonc(readFileSync2(path, "utf8")).routes ?? [];
3241
+ routes = parseJsonc(readFileSync4(path, "utf8")).routes ?? [];
2871
3242
  } catch {
2872
3243
  return "wrangler.jsonc cannot be parsed";
2873
3244
  }
@@ -2887,17 +3258,27 @@ async function preflight(project, source) {
2887
3258
  }
2888
3259
  for (const name of remote ?? [])
2889
3260
  present.set(name, "<secret>");
3261
+ for (const name of declaredSecretNames(project.root))
3262
+ present.set(name, "<in the repository>");
2890
3263
  const missing = allEnvKeys(project.manifest).filter((key) => {
2891
3264
  const value = present.get(key.key);
2892
3265
  return value === undefined || value === "" || value === UNSET;
2893
3266
  });
2894
3267
  const problem = routeProblem(project);
2895
- return { present, remote, missing, routeProblem: problem, ready: missing.length === 0 && problem === null };
3268
+ const bare = plaintextSecretNames(project.root);
3269
+ return {
3270
+ present,
3271
+ remote,
3272
+ missing,
3273
+ routeProblem: problem,
3274
+ bareSecrets: bare,
3275
+ ready: missing.length === 0 && problem === null && bare.length === 0
3276
+ };
2896
3277
  }
2897
3278
  function printPreflight(project, result, source) {
2898
3279
  const keys = allEnvKeys(project.manifest);
2899
3280
  if (result.remote === null) {
2900
- console.log(color3.dim(source === "wrangler" ? `Could not read the worker's secrets — not deployed yet, or wrangler is not logged in.
3281
+ console.log(color5.dim(source === "wrangler" ? `Could not read the worker's secrets — not deployed yet, or wrangler is not logged in.
2901
3282
  Anything not in .env.production is reported as missing.
2902
3283
  ` : `Could not read the project's secrets from Void — not logged in, or no linked project.
2903
3284
  Anything not in .env.production is reported as missing.
@@ -2907,34 +3288,41 @@ Anything not in .env.production is reported as missing.
2907
3288
  for (const key of keys) {
2908
3289
  const value = result.present.get(key.key);
2909
3290
  const set = value !== undefined && value !== "" && value !== UNSET;
2910
- console.log(` ${set ? color3.green("set ") : color3.dim("unset")} ${key.key}${key.plaintext ? color3.dim(" (plaintext)") : ""}`);
3291
+ console.log(` ${set ? color5.green("set ") : color5.dim("unset")} ${key.key}${key.plaintext ? color5.dim(" (plaintext)") : ""}`);
2911
3292
  }
2912
3293
  if (result.ready) {
2913
3294
  console.log(`
2914
- ${color3.green("Ready to go live.")}
3295
+ ${color5.green("Ready to go live.")}
2915
3296
  `);
2916
3297
  return;
2917
3298
  }
2918
3299
  console.log(`
2919
- ${color3.red("NOT ready to go live.")}
3300
+ ${color5.red("NOT ready to go live.")}
2920
3301
  `);
2921
3302
  if (result.routeProblem) {
2922
- console.log(`${color3.red("✗")} the worker's hostname
3303
+ console.log(`${color5.red("✗")} the worker's hostname
2923
3304
  ${result.routeProblem}
3305
+ `);
3306
+ }
3307
+ if (result.bareSecrets.length > 0) {
3308
+ console.log(`${color5.red("✗")} committed in the clear in ${SECRETS_FILE}: ${result.bareSecrets.join(", ")}
3309
+ ` + ` Run \`dotenvx encrypt -f ${SECRETS_FILE}\` — and treat those values as burned.
2924
3310
  `);
2925
3311
  }
2926
3312
  for (const key of result.missing) {
2927
- console.log(`${color3.red("✗")} ${key.key}`);
3313
+ console.log(`${color5.red("✗")} ${key.key}`);
2928
3314
  console.log(` ${key.breaks}`);
2929
3315
  if (key.where)
2930
- console.log(` ${color3.dim(`from: ${key.where}`)}`);
3316
+ console.log(` ${color5.dim(`from: ${key.where}`)}`);
2931
3317
  console.log("");
2932
3318
  }
2933
3319
  const secrets = result.missing.filter((key) => !key.plaintext);
2934
3320
  if (secrets.length > 0) {
2935
- console.log(source === "wrangler" ? "Set each secret on the worker (this also creates the draft worker on a first deploy):" : "Set each secret on the project:");
2936
- for (const key of secrets)
2937
- console.log(` ${source === "wrangler" ? "wrangler secret put" : "void secret put"} ${key.key}`);
3321
+ const inRepo = existsSync9(join9(project.root, SECRETS_FILE));
3322
+ console.log(inRepo ? `Put each in ${SECRETS_FILE}, which the deploy uploads:` : source === "wrangler" ? "Set each secret on the worker (this also creates the draft worker on a first deploy):" : "Set each secret on the project:");
3323
+ for (const key of secrets) {
3324
+ console.log(inRepo ? ` dotenvx set ${key.key} '…' -f ${SECRETS_FILE}` : ` ${source === "wrangler" ? "wrangler secret put" : "void secret put"} ${key.key}`);
3325
+ }
2938
3326
  }
2939
3327
  const plain = result.missing.filter((key) => key.plaintext);
2940
3328
  if (plain.length > 0)
@@ -2943,17 +3331,17 @@ ${color3.red("NOT ready to go live.")}
2943
3331
  }
2944
3332
 
2945
3333
  // src/deploy/cloudflare.ts
2946
- import { existsSync as existsSync8, readFileSync as readFileSync3, writeFileSync } from "node:fs";
2947
- import { dirname as dirname5, join as join8 } from "node:path";
2948
- import color4 from "picocolors";
3334
+ import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
3335
+ import { dirname as dirname6, join as join10 } from "node:path";
3336
+ import color6 from "picocolors";
2949
3337
  var fail = (message) => {
2950
3338
  console.error(`
2951
- ${color4.red("✗")} ${message}
3339
+ ${color6.red("✗")} ${message}
2952
3340
  `);
2953
3341
  return 1;
2954
3342
  };
2955
3343
  function readConfig(path) {
2956
- return parseJsonc(readFileSync3(path, "utf8"));
3344
+ return parseJsonc(readFileSync5(path, "utf8"));
2957
3345
  }
2958
3346
  function findBuilder(from) {
2959
3347
  let dir = from;
@@ -2962,11 +3350,11 @@ function findBuilder(from) {
2962
3350
  ["vp", "vp build"],
2963
3351
  ["vite", "vite build"]
2964
3352
  ]) {
2965
- const path = join8(dir, "node_modules", ".bin", bin);
2966
- if (existsSync8(path))
3353
+ const path = join10(dir, "node_modules", ".bin", bin);
3354
+ if (existsSync10(path))
2967
3355
  return { cmd: path, label };
2968
3356
  }
2969
- const parent = dirname5(dir);
3357
+ const parent = dirname6(dir);
2970
3358
  if (parent === dir)
2971
3359
  return null;
2972
3360
  dir = parent;
@@ -2974,8 +3362,8 @@ function findBuilder(from) {
2974
3362
  }
2975
3363
  async function deployCloudflare(project, opts) {
2976
3364
  const app = project.appDir;
2977
- const configPath = join8(app, "wrangler.jsonc");
2978
- if (!existsSync8(configPath))
3365
+ const configPath = join10(app, "wrangler.jsonc");
3366
+ if (!existsSync10(configPath))
2979
3367
  return fail("wrangler.jsonc is missing — `vc generate` writes it.");
2980
3368
  const bin = findWrangler(app);
2981
3369
  if (!bin)
@@ -2988,16 +3376,16 @@ async function deployCloudflare(project, opts) {
2988
3376
  ${who.raw.trim().split(`
2989
3377
  `).slice(-4).join(`
2990
3378
  `)}`);
2991
- console.log(`${color4.green("✓")} wrangler is logged in`);
3379
+ console.log(`${color6.green("✓")} wrangler is logged in`);
2992
3380
  let config = readConfig(configPath);
2993
3381
  let accountId = config.account_id || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
2994
3382
  if (!accountId) {
2995
3383
  if (who.accounts.length === 1) {
2996
3384
  accountId = who.accounts[0].id;
2997
- writeFileSync(configPath, upsertJsonc(readFileSync3(configPath, "utf8"), "account_id", accountId));
3385
+ writeFileSync2(configPath, upsertJsonc(readFileSync5(configPath, "utf8"), "account_id", accountId));
2998
3386
  project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
2999
3387
  await writeManifest(project.root, project.manifest);
3000
- console.log(`${color4.green("✓")} pinned the account "${who.accounts[0].name}" in wrangler.jsonc`);
3388
+ console.log(`${color6.green("✓")} pinned the account "${who.accounts[0].name}" in wrangler.jsonc`);
3001
3389
  } else {
3002
3390
  return fail(`the account is not pinned and wrangler sees ${who.accounts.length}. Set account_id in wrangler.jsonc to one of:
3003
3391
  ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
@@ -3009,7 +3397,7 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3009
3397
  if (!check.ready) {
3010
3398
  if (!opts.force)
3011
3399
  return fail("refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force for a deliberate partial deploy.");
3012
- console.error(color4.yellow(`--force: deploying a shop that is NOT ready for customers.
3400
+ console.error(color6.yellow(`--force: deploying a shop that is NOT ready for customers.
3013
3401
  `));
3014
3402
  }
3015
3403
  const worker = config.name || project.manifest.shop.domain.split(".")[0];
@@ -3020,15 +3408,15 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3020
3408
  return fail("the D1 database is not provisioned. Run once with --provision to create it and record its id.");
3021
3409
  const db = await ensureD1(bin, app, `${worker}-db`);
3022
3410
  const entry = { binding: "DB", database_name: db.name, database_id: db.uuid, migrations_dir: "./db/migrations" };
3023
- writeFileSync(configPath, upsertJsonc(readFileSync3(configPath, "utf8"), "d1_databases", [entry]));
3411
+ writeFileSync2(configPath, upsertJsonc(readFileSync5(configPath, "utf8"), "d1_databases", [entry]));
3024
3412
  project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId, d1: { name: db.name, id: db.uuid } };
3025
3413
  await writeManifest(project.root, project.manifest);
3026
- console.log(`${color4.green("✓")} D1 "${db.name}" recorded in wrangler.jsonc and voidcommerce.json`);
3414
+ console.log(`${color6.green("✓")} D1 "${db.name}" recorded in wrangler.jsonc and voidcommerce.json`);
3027
3415
  config = readConfig(configPath);
3028
3416
  }
3029
3417
  if (opts.provision) {
3030
3418
  await ensureQueue(bin, app, "commerce");
3031
- console.log(`${color4.green("✓")} queue "commerce"`);
3419
+ console.log(`${color6.green("✓")} queue "commerce"`);
3032
3420
  }
3033
3421
  const builder = findBuilder(app);
3034
3422
  if (!builder)
@@ -3038,10 +3426,10 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3038
3426
  const built = await wrangler(builder.cmd, ["build"], app, true);
3039
3427
  if (built.code !== 0)
3040
3428
  return fail(`the build failed (exit ${built.code}).`);
3041
- const emittedPath = join8(app, "dist", "ssr", "wrangler.json");
3042
- if (!existsSync8(emittedPath))
3429
+ const emittedPath = join10(app, "dist", "ssr", "wrangler.json");
3430
+ if (!existsSync10(emittedPath))
3043
3431
  return fail(`the build emitted no ${emittedPath} — is this a Void app on the Cloudflare target?`);
3044
- const emitted = JSON.parse(readFileSync3(emittedPath, "utf8"));
3432
+ const emitted = JSON.parse(readFileSync5(emittedPath, "utf8"));
3045
3433
  const secretKeys = new Set(allEnvKeys(project.manifest).filter((key) => !key.plaintext).map((key) => key.key));
3046
3434
  const scrubbed = [];
3047
3435
  for (const [key, value] of Object.entries(emitted.vars ?? {})) {
@@ -3053,32 +3441,43 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3053
3441
  const emittedD1 = emitted.d1_databases?.find((db) => db.binding === "DB");
3054
3442
  if (!emittedD1 || emittedD1.database_id === "local")
3055
3443
  return fail("the emitted config still carries a placeholder D1 id; wrangler.jsonc's DB binding was not picked up by the build.");
3056
- writeFileSync(emittedPath, JSON.stringify(emitted, null, 2));
3057
- console.log(`${color4.green("✓")} scrubbed ${scrubbed.length} baked value${scrubbed.length === 1 ? "" : "s"} from the worker's vars${scrubbed.length ? `: ${scrubbed.join(", ")}` : ""}`);
3444
+ writeFileSync2(emittedPath, JSON.stringify(emitted, null, 2));
3445
+ console.log(`${color6.green("✓")} scrubbed ${scrubbed.length} baked value${scrubbed.length === 1 ? "" : "s"} from the worker's vars${scrubbed.length ? `: ${scrubbed.join(", ")}` : ""}`);
3058
3446
  console.log(`
3059
3447
  ▸ wrangler d1 migrations apply ${emittedD1.database_name} --remote`);
3060
3448
  const migrated = await wrangler(bin, ["d1", "migrations", "apply", emittedD1.database_name, "--remote"], app, true);
3061
3449
  if (migrated.code !== 0)
3062
3450
  return fail(`applying migrations failed (exit ${migrated.code}); nothing was deployed.`);
3451
+ const secretArgs = [];
3452
+ let cleanupSecrets;
3453
+ if (existsSync10(join10(project.root, SECRETS_FILE))) {
3454
+ const decrypted = await decryptSecrets(project);
3455
+ if ("error" in decrypted)
3456
+ return fail(`the repository's secrets could not be read: ${decrypted.error}`);
3457
+ secretArgs.push("--secrets-file", decrypted.path);
3458
+ cleanupSecrets = decrypted.cleanup;
3459
+ console.log(`${color6.green("✓")} ${decrypted.names.length} secrets from ${SECRETS_FILE}, uploaded with this version`);
3460
+ }
3063
3461
  console.log(`
3064
3462
  ▸ wrangler deploy -c dist/ssr/wrangler.json`);
3065
- const deployed = await wrangler(bin, ["deploy", "-c", join8("dist", "ssr", "wrangler.json")], app, true);
3463
+ const deployed = await wrangler(bin, ["deploy", "-c", join10("dist", "ssr", "wrangler.json"), ...secretArgs], app, true);
3464
+ cleanupSecrets?.();
3066
3465
  if (deployed.code !== 0)
3067
3466
  return fail(`wrangler deploy failed (exit ${deployed.code}).`);
3068
3467
  const domain = project.manifest.shop.domain;
3069
3468
  const url = `https://${workerHosts(project.manifest)[0]}`;
3070
3469
  console.log(`
3071
- ${color4.green("Live:")} ${url}`);
3470
+ ${color6.green("Live:")} ${url}`);
3072
3471
  if (hasFrontend(project.manifest.layout))
3073
- console.log(color4.dim("The storefront deploys itself from GitHub Actions on push."));
3472
+ console.log(color6.dim("The storefront deploys itself from GitHub Actions on push."));
3074
3473
  return 0;
3075
3474
  }
3076
3475
 
3077
3476
  // src/import.ts
3078
3477
  import { readFile as readFile3 } from "node:fs/promises";
3079
- import { existsSync as existsSync9 } from "node:fs";
3080
- import { join as join9 } from "node:path";
3081
- import color5 from "picocolors";
3478
+ import { existsSync as existsSync11 } from "node:fs";
3479
+ import { join as join11 } from "node:path";
3480
+ import color7 from "picocolors";
3082
3481
  function describe(counts) {
3083
3482
  if (!counts)
3084
3483
  return "";
@@ -3117,7 +3516,7 @@ async function post(url, key, body) {
3117
3516
  return { ok: response.ok, status: response.status, body: { ...parsed, error: parsed.error ?? parsed.message } };
3118
3517
  }
3119
3518
  async function readJson(path) {
3120
- if (!existsSync9(path))
3519
+ if (!existsSync11(path))
3121
3520
  return null;
3122
3521
  try {
3123
3522
  return JSON.parse(await readFile3(path, "utf8"));
@@ -3145,41 +3544,41 @@ async function importCommand(args) {
3145
3544
  }
3146
3545
  const { url, how } = resolveTarget(project, args);
3147
3546
  if (!dry)
3148
- console.log(`${color5.dim("→")} ${url} ${color5.dim(`(${how})`)}
3547
+ console.log(`${color7.dim("→")} ${url} ${color7.dim(`(${how})`)}
3149
3548
  `);
3150
3549
  if (!dry) {
3151
3550
  let whoami2;
3152
3551
  try {
3153
3552
  whoami2 = await fetch(`${url}/api/auth/system/whoami`, { headers: { "x-system-key": key } });
3154
3553
  } catch (error) {
3155
- console.error(`${color5.red("✗")} could not reach ${url}: ${error instanceof Error ? error.message : String(error)}
3554
+ console.error(`${color7.red("✗")} could not reach ${url}: ${error instanceof Error ? error.message : String(error)}
3156
3555
  ` + ` Is the shop running? \`vc dev\` serves it locally; --local points here.
3157
3556
  `);
3158
3557
  return 1;
3159
3558
  }
3160
3559
  if (!whoami2.ok) {
3161
- console.error(`${color5.red("✗")} the shop refused the system key (HTTP ${whoami2.status}).
3560
+ console.error(`${color7.red("✗")} the shop refused the system key (HTTP ${whoami2.status}).
3162
3561
  ` + ` Is SYSTEM_API_KEY the one this deployment was given?
3163
3562
  `);
3164
3563
  return 1;
3165
3564
  }
3166
- console.log(`${color5.green("✓")} authenticated as the system identity`);
3565
+ console.log(`${color7.green("✓")} authenticated as the system identity`);
3167
3566
  }
3168
3567
  const root = project.root;
3169
- const products = await readJson(join9(root, "data", "catalog.json"));
3170
- const categories = await readJson(join9(root, "data", "categories.json"));
3171
- const faqs = has(project.manifest, "faqs") ? await readJson(join9(root, "content", "faqs.json")) : null;
3172
- const posts = has(project.manifest, "blogs") ? await readJson(join9(root, "content", "posts.json")) : null;
3568
+ const products = await readJson(join11(root, "data", "catalog.json"));
3569
+ const categories = await readJson(join11(root, "data", "categories.json"));
3570
+ const faqs = has(project.manifest, "faqs") ? await readJson(join11(root, "content", "faqs.json")) : null;
3571
+ const posts = has(project.manifest, "blogs") ? await readJson(join11(root, "content", "posts.json")) : null;
3173
3572
  if (!products && !faqs?.length && !posts?.length) {
3174
3573
  console.log(`
3175
- ${color5.yellow("Nothing to import.")} data/catalog.json is absent and the content files are empty.
3574
+ ${color7.yellow("Nothing to import.")} data/catalog.json is absent and the content files are empty.
3176
3575
  ` + `Products go in data/catalog.json as a JSON array; \`vc import --dry-run\` checks it without pushing.
3177
3576
  `);
3178
3577
  return 0;
3179
3578
  }
3180
3579
  if (dry) {
3181
3580
  console.log(`
3182
- ${color5.dim("--dry-run: nothing was pushed.")}`);
3581
+ ${color7.dim("--dry-run: nothing was pushed.")}`);
3183
3582
  console.log(` ${products?.length ?? 0} products, ${categories?.length ?? 0} categories`);
3184
3583
  console.log(` ${faqs?.length ?? 0} FAQ entries, ${posts?.length ?? 0} posts
3185
3584
  `);
@@ -3195,15 +3594,15 @@ ${color5.dim("--dry-run: nothing was pushed.")}`);
3195
3594
  });
3196
3595
  if (result.ok) {
3197
3596
  const report = result.body.report ?? {};
3198
- console.log(`${color5.green("✓")} products: ${describe(report.products)}`);
3597
+ console.log(`${color7.green("✓")} products: ${describe(report.products)}`);
3199
3598
  if (report.categories)
3200
- console.log(`${color5.green("✓")} categories: ${describe(report.categories)}`);
3599
+ console.log(`${color7.green("✓")} categories: ${describe(report.categories)}`);
3201
3600
  if (report.prices)
3202
- console.log(`${color5.green("✓")} prices: ${describe(report.prices)}`);
3601
+ console.log(`${color7.green("✓")} prices: ${describe(report.prices)}`);
3203
3602
  if (report.addons)
3204
- console.log(`${color5.green("✓")} addons: ${describe(report.addons)}`);
3603
+ console.log(`${color7.green("✓")} addons: ${describe(report.addons)}`);
3205
3604
  } else {
3206
- console.error(`${color5.red("✗")} catalogue: ${result.body.error ?? `HTTP ${result.status}`}`);
3605
+ console.error(`${color7.red("✗")} catalogue: ${result.body.error ?? `HTTP ${result.status}`}`);
3207
3606
  failed = true;
3208
3607
  }
3209
3608
  }
@@ -3215,20 +3614,20 @@ ${color5.dim("--dry-run: nothing was pushed.")}`);
3215
3614
  continue;
3216
3615
  const result = await post(`${url}${path}`, key, { entries: rows, posts: rows });
3217
3616
  if (result.ok) {
3218
- console.log(`${color5.green("✓")} ${label}: ${describe(result.body)}`);
3617
+ console.log(`${color7.green("✓")} ${label}: ${describe(result.body)}`);
3219
3618
  } else {
3220
- console.error(`${color5.red("✗")} ${label}: ${result.body.error ?? `HTTP ${result.status}`}`);
3619
+ console.error(`${color7.red("✗")} ${label}: ${result.body.error ?? `HTTP ${result.status}`}`);
3221
3620
  failed = true;
3222
3621
  }
3223
3622
  }
3224
3623
  if (failed) {
3225
3624
  console.error(`
3226
- ${color5.red("Some of it did not land.")} The import is an upsert, so fixing the cause and running it again is safe.
3625
+ ${color7.red("Some of it did not land.")} The import is an upsert, so fixing the cause and running it again is safe.
3227
3626
  `);
3228
3627
  return 1;
3229
3628
  }
3230
3629
  console.log(`
3231
- ${color5.green("Imported.")} It is an upsert, so running it again costs one pass and changes nothing.
3630
+ ${color7.green("Imported.")} It is an upsert, so running it again costs one pass and changes nothing.
3232
3631
  `);
3233
3632
  return 0;
3234
3633
  }
@@ -3249,4 +3648,4 @@ async function importHelp() {
3249
3648
  return 0;
3250
3649
  }
3251
3650
 
3252
- export { renderAuthTs, allEnvKeys, renderEnvTs, renderEnvExample, renderEnvLocal, renderEnvProduction, envSummary, FRONTEND_BRANCH, renderFrontendApiTs, renderFrontendEnvProduction, renderStorefrontPage, renderFrontendWorkflow, runVoid, runInherit, captureVoid, isVoidApp, voidAppsIn, STRICT_APP, VOID_VERSION, strictDependencies, renderVoidPatch, renderViteConfig, renderStrictTsconfig, renderVoidJson, renderDbSchema, renderDbSeed, renderLayoutTsx, renderAppCss, renderIndexServer, INDEX_PAGE, renderDashboardPage, CLIENT_ONLY, renderSignInPage, renderAuthClient, renderContentTs, renderCron, renderQueue, renderLiveStream, renderLiveRoute, renderStrictAppPackageJson, DATA_README, BRANDING_README, MIGRATIONS_README, finishStrict, line, row, box, fullHelp, initHelp, version, findProject, ensureGenerated, DIST_DIR, DIST_BRANCH, distCommand, distHelp, renderDistWorkflow, renderDeployReadme, renderRequirementsTs, renderDomainTs, generate, parseJsonc, upsertJsonc, findWrangler, parseWhoAmI, productionEnv, routeProblem, preflight, printPreflight, deployCloudflare, importCommand, importHelp };
3651
+ export { renderAuthTs, allEnvKeys, renderEnvTs, renderEnvExample, renderEnvLocal, renderEnvProduction, envSummary, FRONTEND_BRANCH, renderFrontendApiTs, renderFrontendEnvProduction, renderStorefrontPage, renderFrontendWorkflow, runVoid, runInherit, captureVoid, isVoidApp, voidAppsIn, STRICT_APP, VOID_VERSION, strictDependencies, renderVoidPatch, renderViteConfig, renderStrictTsconfig, renderVoidJson, renderDbSchema, renderDbSeed, renderLayoutTsx, renderAppCss, renderIndexServer, INDEX_PAGE, renderDashboardPage, CLIENT_ONLY, renderSignInPage, renderAuthClient, renderContentTs, renderCron, renderQueue, renderLiveStream, renderLiveRoute, renderStrictAppPackageJson, DATA_README, BRANDING_README, MIGRATIONS_README, finishStrict, line, row, box, fullHelp, initHelp, version, findProject, ensureGenerated, DIST_DIR, DIST_BRANCH, distCommand, distHelp, renderDistWorkflow, renderDeployReadme, renderRequirementsTs, renderDomainTs, generate, parseJsonc, upsertJsonc, findWrangler, parseWhoAmI, keysCommand, SECRETS_FILE, PRIVATE_KEY_VAR, plaintextSecretNames, secretsCommand, productionEnv, routeProblem, preflight, printPreflight, deployCloudflare, importCommand, importHelp };