@saastemly/voidcommerce 0.2.2 → 0.3.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.
@@ -840,6 +840,7 @@ function strictDependencies(manifest) {
840
840
  const devDependencies = {};
841
841
  for (const name of [
842
842
  "@hono/node-server",
843
+ "@dotenvx/dotenvx",
843
844
  "@rolldown/plugin-babel",
844
845
  "@tailwindcss/vite",
845
846
  "@types/node",
@@ -1436,7 +1437,7 @@ import color from "picocolors";
1436
1437
  // package.json
1437
1438
  var package_default = {
1438
1439
  name: "@saastemly/voidcommerce",
1439
- version: "0.2.2",
1440
+ version: "0.3.0",
1440
1441
  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
1442
  type: "module",
1442
1443
  license: "MIT",
@@ -1492,6 +1493,7 @@ var EXTENDED_ROWS = [
1492
1493
  ["vc dev | build | preview", "the app's own script, run where the app is — api/ for a monorepo, .vc/app for strict"],
1493
1494
  ["vc dist", "the deployable app as a self-contained tree — what the void-dist branch carries"],
1494
1495
  ["vc import", "push data/ and content/ into the running shop, as the shop itself — an upsert, safe on every deploy"],
1496
+ ["vc secrets", "the shop's secrets, encrypted and committed — one key where the deploy runs, instead of one wrangler secret put per value"],
1495
1497
  ["vc preflight", "is the shop ready to advertise on? every required key, and what breaks without it"],
1496
1498
  ["vc deploy", "preflight, then void deploy; or --cloudflare: wrangler, your own account, no Void login"]
1497
1499
  ];
@@ -1721,7 +1723,7 @@ async function distCommand(args) {
1721
1723
  appPkg["patchedDependencies"] = rootPkg["patchedDependencies"];
1722
1724
  await writeFile(appPkgPath, `${JSON.stringify(appPkg, null, 2)}
1723
1725
  `, "utf8");
1724
- for (const file of ["bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml"]) {
1726
+ for (const file of ["bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml", ".env.secrets"]) {
1725
1727
  const source = join3(project.root, file);
1726
1728
  if (existsSync3(source))
1727
1729
  await cp(source, join3(target, file));
@@ -1876,13 +1878,27 @@ and \`voidcommerce.json\` so every later generate carries the real ids.
1876
1878
 
1877
1879
  ### 3. The secrets
1878
1880
 
1881
+ They live in the repository, encrypted:
1882
+
1879
1883
  \`\`\`sh
1880
- vc preflight --cloudflare
1884
+ vc secrets --init # every required key, as \`unset\`
1885
+ # put the real values in, then
1886
+ bunx dotenvx encrypt -f .env.secrets # ciphertext; commit this
1881
1887
  \`\`\`
1882
1888
 
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.
1889
+ \`.env.secrets\` is committed and \`.env.keys\` is not. The deploy decrypts it and
1890
+ hands the values to \`wrangler deploy --secrets-file\`, which stores them as
1891
+ real Worker secrets not as plaintext \`vars\`, which anyone with dashboard
1892
+ access can read.
1893
+
1894
+ That turns one \`wrangler secret put\` per value into one build variable, set
1895
+ once in step 5. It also means the shop rebuilds from a checkout.
1896
+
1897
+ **Know the tradeoff.** Ciphertext in git is permanent: if the private key ever
1898
+ leaks, every secret in the history is readable, including ones you rotated.
1899
+ \`wrangler secret put\` does not have that property, and stays available for
1900
+ anything you would rather never commit. \`vc preflight\` counts a secret the
1901
+ repository declares as present, and refuses any value committed in the clear.
1886
1902
 
1887
1903
  ### 4. An API token the build can use
1888
1904
 
@@ -1917,9 +1933,20 @@ the \`wrangler.jsonc\` at the root directory, or the build fails.
1917
1933
  | branch | \`${DIST_BRANCH}\` |
1918
1934
  | root directory | \`/\` |
1919
1935
  | 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\` |
1936
+ | deploy command | see below |
1937
+ | build variable | \`DOTENV_PRIVATE_KEY_SECRETS\`, marked as a secret — the one value that is not in the repository |
1921
1938
  | API token | the one from step 4 |
1922
1939
 
1940
+ The deploy command, on one line:
1941
+
1942
+ \`\`\`sh
1943
+ 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
1944
+ \`\`\`
1945
+
1946
+ Plain \`sh\`, so it does not rely on process substitution. The decrypted file
1947
+ exists only inside the build sandbox, and \`--secrets-file\` applies additively:
1948
+ a secret it does not name is left alone rather than deleted.
1949
+
1923
1950
  The migration command names the **binding** (\`DB\`), not the database, so it
1924
1951
  still points at the right database if the name ever differs.
1925
1952
 
@@ -2459,6 +2486,7 @@ async function generateStrictRoot(root, manifest, result) {
2459
2486
  .wrangler
2460
2487
  .env
2461
2488
  .env.local
2489
+ .env.keys
2462
2490
  dist
2463
2491
  *.tsbuildinfo
2464
2492
  .DS_Store
@@ -2828,16 +2856,196 @@ ${ran.out.trim()}`);
2828
2856
  }
2829
2857
 
2830
2858
  // src/deploy/preflight.ts
2831
- import { existsSync as existsSync7, readFileSync as readFileSync2 } from "node:fs";
2832
- import { join as join7 } from "node:path";
2859
+ import { existsSync as existsSync8, readFileSync as readFileSync3 } from "node:fs";
2860
+ import { join as join8 } from "node:path";
2861
+ import color4 from "picocolors";
2862
+
2863
+ // src/deploy/secrets.ts
2864
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2865
+ import { spawn as spawn3 } from "node:child_process";
2866
+ import { delimiter as delimiter3, dirname as dirname5, join as join7 } from "node:path";
2867
+ import { tmpdir } from "node:os";
2833
2868
  import color3 from "picocolors";
2869
+ var SECRETS_FILE = ".env.secrets";
2870
+ var PRIVATE_KEY_VAR = "DOTENV_PRIVATE_KEY_SECRETS";
2871
+ function findDotenvx(from) {
2872
+ let dir = from;
2873
+ for (;; ) {
2874
+ const local = join7(dir, "node_modules", ".bin", "dotenvx");
2875
+ if (existsSync7(local))
2876
+ return local;
2877
+ const parent = dirname5(dir);
2878
+ if (parent === dir)
2879
+ break;
2880
+ dir = parent;
2881
+ }
2882
+ return (process.env["PATH"] ?? "").split(delimiter3).filter(Boolean).some((entry) => existsSync7(join7(entry, "dotenvx"))) ? "dotenvx" : null;
2883
+ }
2884
+ function run(cmd, args, cwd, env = {}) {
2885
+ return new Promise((resolve) => {
2886
+ const child = spawn3(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...env } });
2887
+ let out = "";
2888
+ child.stdout.on("data", (chunk) => {
2889
+ out += chunk;
2890
+ });
2891
+ child.stderr.on("data", (chunk) => {
2892
+ out += chunk;
2893
+ });
2894
+ child.on("error", (error) => resolve({ code: 1, out: String(error) }));
2895
+ child.on("exit", (code) => resolve({ code: code ?? 1, out }));
2896
+ });
2897
+ }
2898
+ function declaredSecretNames(root) {
2899
+ const path = join7(root, SECRETS_FILE);
2900
+ if (!existsSync7(path))
2901
+ return new Set;
2902
+ const names = new Set;
2903
+ for (const line2 of readFileSync2(path, "utf8").split(`
2904
+ `)) {
2905
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=/.exec(line2);
2906
+ if (match && !match[1].startsWith("DOTENV_"))
2907
+ names.add(match[1]);
2908
+ }
2909
+ return names;
2910
+ }
2911
+ function plaintextSecretNames(root) {
2912
+ const path = join7(root, SECRETS_FILE);
2913
+ if (!existsSync7(path))
2914
+ return [];
2915
+ const bare = [];
2916
+ for (const line2 of readFileSync2(path, "utf8").split(`
2917
+ `)) {
2918
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line2);
2919
+ if (!match || match[1].startsWith("DOTENV_"))
2920
+ continue;
2921
+ const value = match[2].trim().replace(/^['"]|['"]$/g, "");
2922
+ if (value && value !== "unset" && !value.startsWith("encrypted:"))
2923
+ bare.push(match[1]);
2924
+ }
2925
+ return bare;
2926
+ }
2927
+ async function decryptSecrets(project) {
2928
+ const root = project.root;
2929
+ if (!existsSync7(join7(root, SECRETS_FILE)))
2930
+ return { error: `${SECRETS_FILE} does not exist — \`vc secrets init\` writes one` };
2931
+ const dotenvx = findDotenvx(root);
2932
+ if (!dotenvx)
2933
+ return { error: "dotenvx is not installed. `bun add -d @dotenvx/dotenvx`" };
2934
+ if (!process.env[PRIVATE_KEY_VAR] && !existsSync7(join7(root, ".env.keys"))) {
2935
+ return {
2936
+ error: `${PRIVATE_KEY_VAR} is not set and there is no .env.keys here.
2937
+ It is the one value that stays out of the repository; set it where the deploy runs.`
2938
+ };
2939
+ }
2940
+ const result = await run(dotenvx, ["decrypt", "-f", SECRETS_FILE, "--stdout"], root);
2941
+ if (result.code !== 0)
2942
+ return { error: `dotenvx could not decrypt ${SECRETS_FILE}: ${result.out.trim().split(`
2943
+ `).slice(-2).join(" ")}` };
2944
+ const lines = result.out.split(`
2945
+ `).filter((line2) => /^\s*[A-Z][A-Z0-9_]*\s*=/.test(line2) && !line2.trimStart().startsWith("DOTENV_"));
2946
+ const names = lines.map((line2) => line2.slice(0, line2.indexOf("=")).trim());
2947
+ const path = join7(tmpdir(), `vc-secrets-${process.pid}-${Date.now()}.env`);
2948
+ writeFileSync(path, `${lines.join(`
2949
+ `)}
2950
+ `, { mode: 384 });
2951
+ return {
2952
+ path,
2953
+ names,
2954
+ cleanup: () => {
2955
+ try {
2956
+ writeFileSync(path, "", { mode: 384 });
2957
+ __require("node:fs").unlinkSync(path);
2958
+ } catch {}
2959
+ }
2960
+ };
2961
+ }
2962
+ async function secretsCommand(project, args) {
2963
+ const root = project.root;
2964
+ const required = allEnvKeys(project.manifest).filter((key) => !key.plaintext);
2965
+ const declared = declaredSecretNames(root);
2966
+ const bare = plaintextSecretNames(root);
2967
+ if (args.includes("--init"))
2968
+ return initSecrets(project);
2969
+ if (!existsSync7(join7(root, SECRETS_FILE))) {
2970
+ console.log(`
2971
+ ${color3.yellow("No " + SECRETS_FILE + " yet.")} Secrets are set by hand with \`wrangler secret put\`,
2972
+ ` + `which means the shop cannot be rebuilt from a checkout.
2973
+
2974
+ ` + ` vc secrets --init write one, with every required key as \`unset\`
2975
+ `);
2976
+ return 1;
2977
+ }
2978
+ console.log(`
2979
+ ${SECRETS_FILE} — committed, encrypted, and read by \`vc deploy --cloudflare\`
2980
+ `);
2981
+ for (const key of required) {
2982
+ const state = declared.has(key.key) ? color3.green("declared") : color3.red("MISSING ");
2983
+ console.log(` ${state} ${key.key}`);
2984
+ }
2985
+ const extra = [...declared].filter((name) => !required.some((key) => key.key === name));
2986
+ for (const name of extra)
2987
+ console.log(` ${color3.dim("extra ")} ${name} ${color3.dim("— not required by this shop")}`);
2988
+ if (bare.length > 0) {
2989
+ console.log(`
2990
+ ${color3.red("✗")} ${bare.length} value${bare.length === 1 ? " is" : "s are"} committed IN THE CLEAR: ${bare.join(", ")}
2991
+ ` + ` Run \`dotenvx encrypt -f ${SECRETS_FILE}\` before committing again.
2992
+ `);
2993
+ return 1;
2994
+ }
2995
+ const missing = required.filter((key) => !declared.has(key.key));
2996
+ if (missing.length > 0) {
2997
+ console.log(`
2998
+ ${color3.red("✗")} ${missing.length} required secret${missing.length === 1 ? "" : "s"} not declared.
2999
+ ` + ` dotenvx set ${missing[0].key} '…' -f ${SECRETS_FILE}
3000
+ `);
3001
+ return 1;
3002
+ }
3003
+ console.log(`
3004
+ ${color3.green("✓")} every secret this shop needs is declared and encrypted.
3005
+ `);
3006
+ return 0;
3007
+ }
3008
+ async function initSecrets(project) {
3009
+ const path = join7(project.root, SECRETS_FILE);
3010
+ if (existsSync7(path)) {
3011
+ console.error(`vc: ${SECRETS_FILE} already exists; not overwriting it.`);
3012
+ return 1;
3013
+ }
3014
+ const required = allEnvKeys(project.manifest).filter((key) => !key.plaintext);
3015
+ const body = [
3016
+ "# Secrets, encrypted, and COMMITTED.",
3017
+ "#",
3018
+ "# Values are ciphertext; the key names are readable so a diff shows WHICH",
3019
+ "# secret changed without showing what it changed to. `unset` is the",
3020
+ "# documented placeholder and preflight refuses it.",
3021
+ "#",
3022
+ "# The private key stays out of the repository. It lives in .env.keys",
3023
+ "# locally (gitignored) and as one build variable where the deploy runs.",
3024
+ "",
3025
+ ...required.flatMap((key) => [`# ${key.breaks}${key.where ? ` — from: ${key.where}` : ""}`, `${key.key}=unset`, ""])
3026
+ ].join(`
3027
+ `);
3028
+ writeFileSync(path, body, { mode: 384 });
3029
+ console.log(`
3030
+ ${color3.green("+")} ${SECRETS_FILE} — ${required.length} keys, all \`unset\`
3031
+
3032
+ ` + `Next:
3033
+ ` + ` 1. put the real values in, then
3034
+ ` + ` 2. ${color3.cyan(`dotenvx encrypt -f ${SECRETS_FILE}`)}
3035
+ ` + ` 3. commit ${SECRETS_FILE}; never commit .env.keys
3036
+ ` + ` 4. set ${PRIVATE_KEY_VAR} where the deploy runs
3037
+ `);
3038
+ return 0;
3039
+ }
3040
+
3041
+ // src/deploy/preflight.ts
2834
3042
  var UNSET = "unset";
2835
3043
  function productionEnv(appDir) {
2836
3044
  const out = new Map;
2837
- const path = join7(appDir, ".env.production");
2838
- if (!existsSync7(path))
3045
+ const path = join8(appDir, ".env.production");
3046
+ if (!existsSync8(path))
2839
3047
  return out;
2840
- for (const line2 of readFileSync2(path, "utf8").split(`
3048
+ for (const line2 of readFileSync3(path, "utf8").split(`
2841
3049
  `)) {
2842
3050
  const match = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line2);
2843
3051
  if (!match)
@@ -2862,12 +3070,12 @@ async function voidSecretNames(appDir) {
2862
3070
  return names;
2863
3071
  }
2864
3072
  function routeProblem(project) {
2865
- const path = join7(project.appDir, "wrangler.jsonc");
2866
- if (!existsSync7(path))
3073
+ const path = join8(project.appDir, "wrangler.jsonc");
3074
+ if (!existsSync8(path))
2867
3075
  return "wrangler.jsonc is missing";
2868
3076
  let routes = [];
2869
3077
  try {
2870
- routes = parseJsonc(readFileSync2(path, "utf8")).routes ?? [];
3078
+ routes = parseJsonc(readFileSync3(path, "utf8")).routes ?? [];
2871
3079
  } catch {
2872
3080
  return "wrangler.jsonc cannot be parsed";
2873
3081
  }
@@ -2887,17 +3095,27 @@ async function preflight(project, source) {
2887
3095
  }
2888
3096
  for (const name of remote ?? [])
2889
3097
  present.set(name, "<secret>");
3098
+ for (const name of declaredSecretNames(project.root))
3099
+ present.set(name, "<in the repository>");
2890
3100
  const missing = allEnvKeys(project.manifest).filter((key) => {
2891
3101
  const value = present.get(key.key);
2892
3102
  return value === undefined || value === "" || value === UNSET;
2893
3103
  });
2894
3104
  const problem = routeProblem(project);
2895
- return { present, remote, missing, routeProblem: problem, ready: missing.length === 0 && problem === null };
3105
+ const bare = plaintextSecretNames(project.root);
3106
+ return {
3107
+ present,
3108
+ remote,
3109
+ missing,
3110
+ routeProblem: problem,
3111
+ bareSecrets: bare,
3112
+ ready: missing.length === 0 && problem === null && bare.length === 0
3113
+ };
2896
3114
  }
2897
3115
  function printPreflight(project, result, source) {
2898
3116
  const keys = allEnvKeys(project.manifest);
2899
3117
  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.
3118
+ console.log(color4.dim(source === "wrangler" ? `Could not read the worker's secrets — not deployed yet, or wrangler is not logged in.
2901
3119
  Anything not in .env.production is reported as missing.
2902
3120
  ` : `Could not read the project's secrets from Void — not logged in, or no linked project.
2903
3121
  Anything not in .env.production is reported as missing.
@@ -2907,34 +3125,41 @@ Anything not in .env.production is reported as missing.
2907
3125
  for (const key of keys) {
2908
3126
  const value = result.present.get(key.key);
2909
3127
  const set = value !== undefined && value !== "" && value !== UNSET;
2910
- console.log(` ${set ? color3.green("set ") : color3.dim("unset")} ${key.key}${key.plaintext ? color3.dim(" (plaintext)") : ""}`);
3128
+ console.log(` ${set ? color4.green("set ") : color4.dim("unset")} ${key.key}${key.plaintext ? color4.dim(" (plaintext)") : ""}`);
2911
3129
  }
2912
3130
  if (result.ready) {
2913
3131
  console.log(`
2914
- ${color3.green("Ready to go live.")}
3132
+ ${color4.green("Ready to go live.")}
2915
3133
  `);
2916
3134
  return;
2917
3135
  }
2918
3136
  console.log(`
2919
- ${color3.red("NOT ready to go live.")}
3137
+ ${color4.red("NOT ready to go live.")}
2920
3138
  `);
2921
3139
  if (result.routeProblem) {
2922
- console.log(`${color3.red("✗")} the worker's hostname
3140
+ console.log(`${color4.red("✗")} the worker's hostname
2923
3141
  ${result.routeProblem}
3142
+ `);
3143
+ }
3144
+ if (result.bareSecrets.length > 0) {
3145
+ console.log(`${color4.red("✗")} committed in the clear in ${SECRETS_FILE}: ${result.bareSecrets.join(", ")}
3146
+ ` + ` Run \`dotenvx encrypt -f ${SECRETS_FILE}\` — and treat those values as burned.
2924
3147
  `);
2925
3148
  }
2926
3149
  for (const key of result.missing) {
2927
- console.log(`${color3.red("✗")} ${key.key}`);
3150
+ console.log(`${color4.red("✗")} ${key.key}`);
2928
3151
  console.log(` ${key.breaks}`);
2929
3152
  if (key.where)
2930
- console.log(` ${color3.dim(`from: ${key.where}`)}`);
3153
+ console.log(` ${color4.dim(`from: ${key.where}`)}`);
2931
3154
  console.log("");
2932
3155
  }
2933
3156
  const secrets = result.missing.filter((key) => !key.plaintext);
2934
3157
  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}`);
3158
+ const inRepo = existsSync8(join8(project.root, SECRETS_FILE));
3159
+ 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:");
3160
+ for (const key of secrets) {
3161
+ console.log(inRepo ? ` dotenvx set ${key.key} '…' -f ${SECRETS_FILE}` : ` ${source === "wrangler" ? "wrangler secret put" : "void secret put"} ${key.key}`);
3162
+ }
2938
3163
  }
2939
3164
  const plain = result.missing.filter((key) => key.plaintext);
2940
3165
  if (plain.length > 0)
@@ -2943,17 +3168,17 @@ ${color3.red("NOT ready to go live.")}
2943
3168
  }
2944
3169
 
2945
3170
  // 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";
3171
+ import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
3172
+ import { dirname as dirname6, join as join9 } from "node:path";
3173
+ import color5 from "picocolors";
2949
3174
  var fail = (message) => {
2950
3175
  console.error(`
2951
- ${color4.red("✗")} ${message}
3176
+ ${color5.red("✗")} ${message}
2952
3177
  `);
2953
3178
  return 1;
2954
3179
  };
2955
3180
  function readConfig(path) {
2956
- return parseJsonc(readFileSync3(path, "utf8"));
3181
+ return parseJsonc(readFileSync4(path, "utf8"));
2957
3182
  }
2958
3183
  function findBuilder(from) {
2959
3184
  let dir = from;
@@ -2962,11 +3187,11 @@ function findBuilder(from) {
2962
3187
  ["vp", "vp build"],
2963
3188
  ["vite", "vite build"]
2964
3189
  ]) {
2965
- const path = join8(dir, "node_modules", ".bin", bin);
2966
- if (existsSync8(path))
3190
+ const path = join9(dir, "node_modules", ".bin", bin);
3191
+ if (existsSync9(path))
2967
3192
  return { cmd: path, label };
2968
3193
  }
2969
- const parent = dirname5(dir);
3194
+ const parent = dirname6(dir);
2970
3195
  if (parent === dir)
2971
3196
  return null;
2972
3197
  dir = parent;
@@ -2974,8 +3199,8 @@ function findBuilder(from) {
2974
3199
  }
2975
3200
  async function deployCloudflare(project, opts) {
2976
3201
  const app = project.appDir;
2977
- const configPath = join8(app, "wrangler.jsonc");
2978
- if (!existsSync8(configPath))
3202
+ const configPath = join9(app, "wrangler.jsonc");
3203
+ if (!existsSync9(configPath))
2979
3204
  return fail("wrangler.jsonc is missing — `vc generate` writes it.");
2980
3205
  const bin = findWrangler(app);
2981
3206
  if (!bin)
@@ -2988,16 +3213,16 @@ async function deployCloudflare(project, opts) {
2988
3213
  ${who.raw.trim().split(`
2989
3214
  `).slice(-4).join(`
2990
3215
  `)}`);
2991
- console.log(`${color4.green("✓")} wrangler is logged in`);
3216
+ console.log(`${color5.green("✓")} wrangler is logged in`);
2992
3217
  let config = readConfig(configPath);
2993
3218
  let accountId = config.account_id || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
2994
3219
  if (!accountId) {
2995
3220
  if (who.accounts.length === 1) {
2996
3221
  accountId = who.accounts[0].id;
2997
- writeFileSync(configPath, upsertJsonc(readFileSync3(configPath, "utf8"), "account_id", accountId));
3222
+ writeFileSync2(configPath, upsertJsonc(readFileSync4(configPath, "utf8"), "account_id", accountId));
2998
3223
  project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
2999
3224
  await writeManifest(project.root, project.manifest);
3000
- console.log(`${color4.green("✓")} pinned the account "${who.accounts[0].name}" in wrangler.jsonc`);
3225
+ console.log(`${color5.green("✓")} pinned the account "${who.accounts[0].name}" in wrangler.jsonc`);
3001
3226
  } else {
3002
3227
  return fail(`the account is not pinned and wrangler sees ${who.accounts.length}. Set account_id in wrangler.jsonc to one of:
3003
3228
  ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
@@ -3009,7 +3234,7 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3009
3234
  if (!check.ready) {
3010
3235
  if (!opts.force)
3011
3236
  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.
3237
+ console.error(color5.yellow(`--force: deploying a shop that is NOT ready for customers.
3013
3238
  `));
3014
3239
  }
3015
3240
  const worker = config.name || project.manifest.shop.domain.split(".")[0];
@@ -3020,15 +3245,15 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3020
3245
  return fail("the D1 database is not provisioned. Run once with --provision to create it and record its id.");
3021
3246
  const db = await ensureD1(bin, app, `${worker}-db`);
3022
3247
  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]));
3248
+ writeFileSync2(configPath, upsertJsonc(readFileSync4(configPath, "utf8"), "d1_databases", [entry]));
3024
3249
  project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId, d1: { name: db.name, id: db.uuid } };
3025
3250
  await writeManifest(project.root, project.manifest);
3026
- console.log(`${color4.green("✓")} D1 "${db.name}" recorded in wrangler.jsonc and voidcommerce.json`);
3251
+ console.log(`${color5.green("✓")} D1 "${db.name}" recorded in wrangler.jsonc and voidcommerce.json`);
3027
3252
  config = readConfig(configPath);
3028
3253
  }
3029
3254
  if (opts.provision) {
3030
3255
  await ensureQueue(bin, app, "commerce");
3031
- console.log(`${color4.green("✓")} queue "commerce"`);
3256
+ console.log(`${color5.green("✓")} queue "commerce"`);
3032
3257
  }
3033
3258
  const builder = findBuilder(app);
3034
3259
  if (!builder)
@@ -3038,10 +3263,10 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3038
3263
  const built = await wrangler(builder.cmd, ["build"], app, true);
3039
3264
  if (built.code !== 0)
3040
3265
  return fail(`the build failed (exit ${built.code}).`);
3041
- const emittedPath = join8(app, "dist", "ssr", "wrangler.json");
3042
- if (!existsSync8(emittedPath))
3266
+ const emittedPath = join9(app, "dist", "ssr", "wrangler.json");
3267
+ if (!existsSync9(emittedPath))
3043
3268
  return fail(`the build emitted no ${emittedPath} — is this a Void app on the Cloudflare target?`);
3044
- const emitted = JSON.parse(readFileSync3(emittedPath, "utf8"));
3269
+ const emitted = JSON.parse(readFileSync4(emittedPath, "utf8"));
3045
3270
  const secretKeys = new Set(allEnvKeys(project.manifest).filter((key) => !key.plaintext).map((key) => key.key));
3046
3271
  const scrubbed = [];
3047
3272
  for (const [key, value] of Object.entries(emitted.vars ?? {})) {
@@ -3053,32 +3278,43 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3053
3278
  const emittedD1 = emitted.d1_databases?.find((db) => db.binding === "DB");
3054
3279
  if (!emittedD1 || emittedD1.database_id === "local")
3055
3280
  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(", ")}` : ""}`);
3281
+ writeFileSync2(emittedPath, JSON.stringify(emitted, null, 2));
3282
+ console.log(`${color5.green("✓")} scrubbed ${scrubbed.length} baked value${scrubbed.length === 1 ? "" : "s"} from the worker's vars${scrubbed.length ? `: ${scrubbed.join(", ")}` : ""}`);
3058
3283
  console.log(`
3059
3284
  ▸ wrangler d1 migrations apply ${emittedD1.database_name} --remote`);
3060
3285
  const migrated = await wrangler(bin, ["d1", "migrations", "apply", emittedD1.database_name, "--remote"], app, true);
3061
3286
  if (migrated.code !== 0)
3062
3287
  return fail(`applying migrations failed (exit ${migrated.code}); nothing was deployed.`);
3288
+ const secretArgs = [];
3289
+ let cleanupSecrets;
3290
+ if (existsSync9(join9(project.root, SECRETS_FILE))) {
3291
+ const decrypted = await decryptSecrets(project);
3292
+ if ("error" in decrypted)
3293
+ return fail(`the repository's secrets could not be read: ${decrypted.error}`);
3294
+ secretArgs.push("--secrets-file", decrypted.path);
3295
+ cleanupSecrets = decrypted.cleanup;
3296
+ console.log(`${color5.green("✓")} ${decrypted.names.length} secrets from ${SECRETS_FILE}, uploaded with this version`);
3297
+ }
3063
3298
  console.log(`
3064
3299
  ▸ wrangler deploy -c dist/ssr/wrangler.json`);
3065
- const deployed = await wrangler(bin, ["deploy", "-c", join8("dist", "ssr", "wrangler.json")], app, true);
3300
+ const deployed = await wrangler(bin, ["deploy", "-c", join9("dist", "ssr", "wrangler.json"), ...secretArgs], app, true);
3301
+ cleanupSecrets?.();
3066
3302
  if (deployed.code !== 0)
3067
3303
  return fail(`wrangler deploy failed (exit ${deployed.code}).`);
3068
3304
  const domain = project.manifest.shop.domain;
3069
3305
  const url = `https://${workerHosts(project.manifest)[0]}`;
3070
3306
  console.log(`
3071
- ${color4.green("Live:")} ${url}`);
3307
+ ${color5.green("Live:")} ${url}`);
3072
3308
  if (hasFrontend(project.manifest.layout))
3073
- console.log(color4.dim("The storefront deploys itself from GitHub Actions on push."));
3309
+ console.log(color5.dim("The storefront deploys itself from GitHub Actions on push."));
3074
3310
  return 0;
3075
3311
  }
3076
3312
 
3077
3313
  // src/import.ts
3078
3314
  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";
3315
+ import { existsSync as existsSync10 } from "node:fs";
3316
+ import { join as join10 } from "node:path";
3317
+ import color6 from "picocolors";
3082
3318
  function describe(counts) {
3083
3319
  if (!counts)
3084
3320
  return "";
@@ -3117,7 +3353,7 @@ async function post(url, key, body) {
3117
3353
  return { ok: response.ok, status: response.status, body: { ...parsed, error: parsed.error ?? parsed.message } };
3118
3354
  }
3119
3355
  async function readJson(path) {
3120
- if (!existsSync9(path))
3356
+ if (!existsSync10(path))
3121
3357
  return null;
3122
3358
  try {
3123
3359
  return JSON.parse(await readFile3(path, "utf8"));
@@ -3145,41 +3381,41 @@ async function importCommand(args) {
3145
3381
  }
3146
3382
  const { url, how } = resolveTarget(project, args);
3147
3383
  if (!dry)
3148
- console.log(`${color5.dim("→")} ${url} ${color5.dim(`(${how})`)}
3384
+ console.log(`${color6.dim("→")} ${url} ${color6.dim(`(${how})`)}
3149
3385
  `);
3150
3386
  if (!dry) {
3151
3387
  let whoami2;
3152
3388
  try {
3153
3389
  whoami2 = await fetch(`${url}/api/auth/system/whoami`, { headers: { "x-system-key": key } });
3154
3390
  } catch (error) {
3155
- console.error(`${color5.red("✗")} could not reach ${url}: ${error instanceof Error ? error.message : String(error)}
3391
+ console.error(`${color6.red("✗")} could not reach ${url}: ${error instanceof Error ? error.message : String(error)}
3156
3392
  ` + ` Is the shop running? \`vc dev\` serves it locally; --local points here.
3157
3393
  `);
3158
3394
  return 1;
3159
3395
  }
3160
3396
  if (!whoami2.ok) {
3161
- console.error(`${color5.red("✗")} the shop refused the system key (HTTP ${whoami2.status}).
3397
+ console.error(`${color6.red("✗")} the shop refused the system key (HTTP ${whoami2.status}).
3162
3398
  ` + ` Is SYSTEM_API_KEY the one this deployment was given?
3163
3399
  `);
3164
3400
  return 1;
3165
3401
  }
3166
- console.log(`${color5.green("✓")} authenticated as the system identity`);
3402
+ console.log(`${color6.green("✓")} authenticated as the system identity`);
3167
3403
  }
3168
3404
  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;
3405
+ const products = await readJson(join10(root, "data", "catalog.json"));
3406
+ const categories = await readJson(join10(root, "data", "categories.json"));
3407
+ const faqs = has(project.manifest, "faqs") ? await readJson(join10(root, "content", "faqs.json")) : null;
3408
+ const posts = has(project.manifest, "blogs") ? await readJson(join10(root, "content", "posts.json")) : null;
3173
3409
  if (!products && !faqs?.length && !posts?.length) {
3174
3410
  console.log(`
3175
- ${color5.yellow("Nothing to import.")} data/catalog.json is absent and the content files are empty.
3411
+ ${color6.yellow("Nothing to import.")} data/catalog.json is absent and the content files are empty.
3176
3412
  ` + `Products go in data/catalog.json as a JSON array; \`vc import --dry-run\` checks it without pushing.
3177
3413
  `);
3178
3414
  return 0;
3179
3415
  }
3180
3416
  if (dry) {
3181
3417
  console.log(`
3182
- ${color5.dim("--dry-run: nothing was pushed.")}`);
3418
+ ${color6.dim("--dry-run: nothing was pushed.")}`);
3183
3419
  console.log(` ${products?.length ?? 0} products, ${categories?.length ?? 0} categories`);
3184
3420
  console.log(` ${faqs?.length ?? 0} FAQ entries, ${posts?.length ?? 0} posts
3185
3421
  `);
@@ -3195,15 +3431,15 @@ ${color5.dim("--dry-run: nothing was pushed.")}`);
3195
3431
  });
3196
3432
  if (result.ok) {
3197
3433
  const report = result.body.report ?? {};
3198
- console.log(`${color5.green("✓")} products: ${describe(report.products)}`);
3434
+ console.log(`${color6.green("✓")} products: ${describe(report.products)}`);
3199
3435
  if (report.categories)
3200
- console.log(`${color5.green("✓")} categories: ${describe(report.categories)}`);
3436
+ console.log(`${color6.green("✓")} categories: ${describe(report.categories)}`);
3201
3437
  if (report.prices)
3202
- console.log(`${color5.green("✓")} prices: ${describe(report.prices)}`);
3438
+ console.log(`${color6.green("✓")} prices: ${describe(report.prices)}`);
3203
3439
  if (report.addons)
3204
- console.log(`${color5.green("✓")} addons: ${describe(report.addons)}`);
3440
+ console.log(`${color6.green("✓")} addons: ${describe(report.addons)}`);
3205
3441
  } else {
3206
- console.error(`${color5.red("✗")} catalogue: ${result.body.error ?? `HTTP ${result.status}`}`);
3442
+ console.error(`${color6.red("✗")} catalogue: ${result.body.error ?? `HTTP ${result.status}`}`);
3207
3443
  failed = true;
3208
3444
  }
3209
3445
  }
@@ -3215,20 +3451,20 @@ ${color5.dim("--dry-run: nothing was pushed.")}`);
3215
3451
  continue;
3216
3452
  const result = await post(`${url}${path}`, key, { entries: rows, posts: rows });
3217
3453
  if (result.ok) {
3218
- console.log(`${color5.green("✓")} ${label}: ${describe(result.body)}`);
3454
+ console.log(`${color6.green("✓")} ${label}: ${describe(result.body)}`);
3219
3455
  } else {
3220
- console.error(`${color5.red("✗")} ${label}: ${result.body.error ?? `HTTP ${result.status}`}`);
3456
+ console.error(`${color6.red("✗")} ${label}: ${result.body.error ?? `HTTP ${result.status}`}`);
3221
3457
  failed = true;
3222
3458
  }
3223
3459
  }
3224
3460
  if (failed) {
3225
3461
  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.
3462
+ ${color6.red("Some of it did not land.")} The import is an upsert, so fixing the cause and running it again is safe.
3227
3463
  `);
3228
3464
  return 1;
3229
3465
  }
3230
3466
  console.log(`
3231
- ${color5.green("Imported.")} It is an upsert, so running it again costs one pass and changes nothing.
3467
+ ${color6.green("Imported.")} It is an upsert, so running it again costs one pass and changes nothing.
3232
3468
  `);
3233
3469
  return 0;
3234
3470
  }
@@ -3249,4 +3485,4 @@ async function importHelp() {
3249
3485
  return 0;
3250
3486
  }
3251
3487
 
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 };
3488
+ 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, SECRETS_FILE, PRIVATE_KEY_VAR, secretsCommand, productionEnv, routeProblem, preflight, printPreflight, deployCloudflare, importCommand, importHelp };