@saastemly/voidcommerce 0.2.1 → 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.1",
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
 
@@ -2139,7 +2166,8 @@ export const emailNotifications = (emailer: Emailer): NotificationProvider => ({
2139
2166
  `;
2140
2167
  }
2141
2168
  function renderMintTs() {
2142
- return `import { type MintProvider, recordedMint } from "@saastemly/better-commerce/plugins/nft";
2169
+ return `import { getCurrentAuthContext } from "@better-auth/core/context";
2170
+ import { type MintProvider, recordedMint } from "@saastemly/better-commerce/plugins/nft";
2143
2171
 
2144
2172
  /**
2145
2173
  * Where a token comes from.
@@ -2458,6 +2486,7 @@ async function generateStrictRoot(root, manifest, result) {
2458
2486
  .wrangler
2459
2487
  .env
2460
2488
  .env.local
2489
+ .env.keys
2461
2490
  dist
2462
2491
  *.tsbuildinfo
2463
2492
  .DS_Store
@@ -2827,16 +2856,196 @@ ${ran.out.trim()}`);
2827
2856
  }
2828
2857
 
2829
2858
  // src/deploy/preflight.ts
2830
- import { existsSync as existsSync7, readFileSync as readFileSync2 } from "node:fs";
2831
- 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";
2832
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
2833
3042
  var UNSET = "unset";
2834
3043
  function productionEnv(appDir) {
2835
3044
  const out = new Map;
2836
- const path = join7(appDir, ".env.production");
2837
- if (!existsSync7(path))
3045
+ const path = join8(appDir, ".env.production");
3046
+ if (!existsSync8(path))
2838
3047
  return out;
2839
- for (const line2 of readFileSync2(path, "utf8").split(`
3048
+ for (const line2 of readFileSync3(path, "utf8").split(`
2840
3049
  `)) {
2841
3050
  const match = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line2);
2842
3051
  if (!match)
@@ -2861,12 +3070,12 @@ async function voidSecretNames(appDir) {
2861
3070
  return names;
2862
3071
  }
2863
3072
  function routeProblem(project) {
2864
- const path = join7(project.appDir, "wrangler.jsonc");
2865
- if (!existsSync7(path))
3073
+ const path = join8(project.appDir, "wrangler.jsonc");
3074
+ if (!existsSync8(path))
2866
3075
  return "wrangler.jsonc is missing";
2867
3076
  let routes = [];
2868
3077
  try {
2869
- routes = parseJsonc(readFileSync2(path, "utf8")).routes ?? [];
3078
+ routes = parseJsonc(readFileSync3(path, "utf8")).routes ?? [];
2870
3079
  } catch {
2871
3080
  return "wrangler.jsonc cannot be parsed";
2872
3081
  }
@@ -2886,17 +3095,27 @@ async function preflight(project, source) {
2886
3095
  }
2887
3096
  for (const name of remote ?? [])
2888
3097
  present.set(name, "<secret>");
3098
+ for (const name of declaredSecretNames(project.root))
3099
+ present.set(name, "<in the repository>");
2889
3100
  const missing = allEnvKeys(project.manifest).filter((key) => {
2890
3101
  const value = present.get(key.key);
2891
3102
  return value === undefined || value === "" || value === UNSET;
2892
3103
  });
2893
3104
  const problem = routeProblem(project);
2894
- 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
+ };
2895
3114
  }
2896
3115
  function printPreflight(project, result, source) {
2897
3116
  const keys = allEnvKeys(project.manifest);
2898
3117
  if (result.remote === null) {
2899
- 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.
2900
3119
  Anything not in .env.production is reported as missing.
2901
3120
  ` : `Could not read the project's secrets from Void — not logged in, or no linked project.
2902
3121
  Anything not in .env.production is reported as missing.
@@ -2906,34 +3125,41 @@ Anything not in .env.production is reported as missing.
2906
3125
  for (const key of keys) {
2907
3126
  const value = result.present.get(key.key);
2908
3127
  const set = value !== undefined && value !== "" && value !== UNSET;
2909
- 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)") : ""}`);
2910
3129
  }
2911
3130
  if (result.ready) {
2912
3131
  console.log(`
2913
- ${color3.green("Ready to go live.")}
3132
+ ${color4.green("Ready to go live.")}
2914
3133
  `);
2915
3134
  return;
2916
3135
  }
2917
3136
  console.log(`
2918
- ${color3.red("NOT ready to go live.")}
3137
+ ${color4.red("NOT ready to go live.")}
2919
3138
  `);
2920
3139
  if (result.routeProblem) {
2921
- console.log(`${color3.red("✗")} the worker's hostname
3140
+ console.log(`${color4.red("✗")} the worker's hostname
2922
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.
2923
3147
  `);
2924
3148
  }
2925
3149
  for (const key of result.missing) {
2926
- console.log(`${color3.red("✗")} ${key.key}`);
3150
+ console.log(`${color4.red("✗")} ${key.key}`);
2927
3151
  console.log(` ${key.breaks}`);
2928
3152
  if (key.where)
2929
- console.log(` ${color3.dim(`from: ${key.where}`)}`);
3153
+ console.log(` ${color4.dim(`from: ${key.where}`)}`);
2930
3154
  console.log("");
2931
3155
  }
2932
3156
  const secrets = result.missing.filter((key) => !key.plaintext);
2933
3157
  if (secrets.length > 0) {
2934
- 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:");
2935
- for (const key of secrets)
2936
- 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
+ }
2937
3163
  }
2938
3164
  const plain = result.missing.filter((key) => key.plaintext);
2939
3165
  if (plain.length > 0)
@@ -2942,17 +3168,17 @@ ${color3.red("NOT ready to go live.")}
2942
3168
  }
2943
3169
 
2944
3170
  // src/deploy/cloudflare.ts
2945
- import { existsSync as existsSync8, readFileSync as readFileSync3, writeFileSync } from "node:fs";
2946
- import { dirname as dirname5, join as join8 } from "node:path";
2947
- 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";
2948
3174
  var fail = (message) => {
2949
3175
  console.error(`
2950
- ${color4.red("✗")} ${message}
3176
+ ${color5.red("✗")} ${message}
2951
3177
  `);
2952
3178
  return 1;
2953
3179
  };
2954
3180
  function readConfig(path) {
2955
- return parseJsonc(readFileSync3(path, "utf8"));
3181
+ return parseJsonc(readFileSync4(path, "utf8"));
2956
3182
  }
2957
3183
  function findBuilder(from) {
2958
3184
  let dir = from;
@@ -2961,11 +3187,11 @@ function findBuilder(from) {
2961
3187
  ["vp", "vp build"],
2962
3188
  ["vite", "vite build"]
2963
3189
  ]) {
2964
- const path = join8(dir, "node_modules", ".bin", bin);
2965
- if (existsSync8(path))
3190
+ const path = join9(dir, "node_modules", ".bin", bin);
3191
+ if (existsSync9(path))
2966
3192
  return { cmd: path, label };
2967
3193
  }
2968
- const parent = dirname5(dir);
3194
+ const parent = dirname6(dir);
2969
3195
  if (parent === dir)
2970
3196
  return null;
2971
3197
  dir = parent;
@@ -2973,8 +3199,8 @@ function findBuilder(from) {
2973
3199
  }
2974
3200
  async function deployCloudflare(project, opts) {
2975
3201
  const app = project.appDir;
2976
- const configPath = join8(app, "wrangler.jsonc");
2977
- if (!existsSync8(configPath))
3202
+ const configPath = join9(app, "wrangler.jsonc");
3203
+ if (!existsSync9(configPath))
2978
3204
  return fail("wrangler.jsonc is missing — `vc generate` writes it.");
2979
3205
  const bin = findWrangler(app);
2980
3206
  if (!bin)
@@ -2987,16 +3213,16 @@ async function deployCloudflare(project, opts) {
2987
3213
  ${who.raw.trim().split(`
2988
3214
  `).slice(-4).join(`
2989
3215
  `)}`);
2990
- console.log(`${color4.green("✓")} wrangler is logged in`);
3216
+ console.log(`${color5.green("✓")} wrangler is logged in`);
2991
3217
  let config = readConfig(configPath);
2992
3218
  let accountId = config.account_id || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
2993
3219
  if (!accountId) {
2994
3220
  if (who.accounts.length === 1) {
2995
3221
  accountId = who.accounts[0].id;
2996
- writeFileSync(configPath, upsertJsonc(readFileSync3(configPath, "utf8"), "account_id", accountId));
3222
+ writeFileSync2(configPath, upsertJsonc(readFileSync4(configPath, "utf8"), "account_id", accountId));
2997
3223
  project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
2998
3224
  await writeManifest(project.root, project.manifest);
2999
- 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`);
3000
3226
  } else {
3001
3227
  return fail(`the account is not pinned and wrangler sees ${who.accounts.length}. Set account_id in wrangler.jsonc to one of:
3002
3228
  ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
@@ -3008,7 +3234,7 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3008
3234
  if (!check.ready) {
3009
3235
  if (!opts.force)
3010
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.");
3011
- 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.
3012
3238
  `));
3013
3239
  }
3014
3240
  const worker = config.name || project.manifest.shop.domain.split(".")[0];
@@ -3019,15 +3245,15 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3019
3245
  return fail("the D1 database is not provisioned. Run once with --provision to create it and record its id.");
3020
3246
  const db = await ensureD1(bin, app, `${worker}-db`);
3021
3247
  const entry = { binding: "DB", database_name: db.name, database_id: db.uuid, migrations_dir: "./db/migrations" };
3022
- writeFileSync(configPath, upsertJsonc(readFileSync3(configPath, "utf8"), "d1_databases", [entry]));
3248
+ writeFileSync2(configPath, upsertJsonc(readFileSync4(configPath, "utf8"), "d1_databases", [entry]));
3023
3249
  project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId, d1: { name: db.name, id: db.uuid } };
3024
3250
  await writeManifest(project.root, project.manifest);
3025
- 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`);
3026
3252
  config = readConfig(configPath);
3027
3253
  }
3028
3254
  if (opts.provision) {
3029
3255
  await ensureQueue(bin, app, "commerce");
3030
- console.log(`${color4.green("✓")} queue "commerce"`);
3256
+ console.log(`${color5.green("✓")} queue "commerce"`);
3031
3257
  }
3032
3258
  const builder = findBuilder(app);
3033
3259
  if (!builder)
@@ -3037,10 +3263,10 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3037
3263
  const built = await wrangler(builder.cmd, ["build"], app, true);
3038
3264
  if (built.code !== 0)
3039
3265
  return fail(`the build failed (exit ${built.code}).`);
3040
- const emittedPath = join8(app, "dist", "ssr", "wrangler.json");
3041
- if (!existsSync8(emittedPath))
3266
+ const emittedPath = join9(app, "dist", "ssr", "wrangler.json");
3267
+ if (!existsSync9(emittedPath))
3042
3268
  return fail(`the build emitted no ${emittedPath} — is this a Void app on the Cloudflare target?`);
3043
- const emitted = JSON.parse(readFileSync3(emittedPath, "utf8"));
3269
+ const emitted = JSON.parse(readFileSync4(emittedPath, "utf8"));
3044
3270
  const secretKeys = new Set(allEnvKeys(project.manifest).filter((key) => !key.plaintext).map((key) => key.key));
3045
3271
  const scrubbed = [];
3046
3272
  for (const [key, value] of Object.entries(emitted.vars ?? {})) {
@@ -3052,32 +3278,43 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
3052
3278
  const emittedD1 = emitted.d1_databases?.find((db) => db.binding === "DB");
3053
3279
  if (!emittedD1 || emittedD1.database_id === "local")
3054
3280
  return fail("the emitted config still carries a placeholder D1 id; wrangler.jsonc's DB binding was not picked up by the build.");
3055
- writeFileSync(emittedPath, JSON.stringify(emitted, null, 2));
3056
- 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(", ")}` : ""}`);
3057
3283
  console.log(`
3058
3284
  ▸ wrangler d1 migrations apply ${emittedD1.database_name} --remote`);
3059
3285
  const migrated = await wrangler(bin, ["d1", "migrations", "apply", emittedD1.database_name, "--remote"], app, true);
3060
3286
  if (migrated.code !== 0)
3061
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
+ }
3062
3298
  console.log(`
3063
3299
  ▸ wrangler deploy -c dist/ssr/wrangler.json`);
3064
- 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?.();
3065
3302
  if (deployed.code !== 0)
3066
3303
  return fail(`wrangler deploy failed (exit ${deployed.code}).`);
3067
3304
  const domain = project.manifest.shop.domain;
3068
3305
  const url = `https://${workerHosts(project.manifest)[0]}`;
3069
3306
  console.log(`
3070
- ${color4.green("Live:")} ${url}`);
3307
+ ${color5.green("Live:")} ${url}`);
3071
3308
  if (hasFrontend(project.manifest.layout))
3072
- 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."));
3073
3310
  return 0;
3074
3311
  }
3075
3312
 
3076
3313
  // src/import.ts
3077
3314
  import { readFile as readFile3 } from "node:fs/promises";
3078
- import { existsSync as existsSync9 } from "node:fs";
3079
- import { join as join9 } from "node:path";
3080
- 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";
3081
3318
  function describe(counts) {
3082
3319
  if (!counts)
3083
3320
  return "";
@@ -3116,7 +3353,7 @@ async function post(url, key, body) {
3116
3353
  return { ok: response.ok, status: response.status, body: { ...parsed, error: parsed.error ?? parsed.message } };
3117
3354
  }
3118
3355
  async function readJson(path) {
3119
- if (!existsSync9(path))
3356
+ if (!existsSync10(path))
3120
3357
  return null;
3121
3358
  try {
3122
3359
  return JSON.parse(await readFile3(path, "utf8"));
@@ -3144,41 +3381,41 @@ async function importCommand(args) {
3144
3381
  }
3145
3382
  const { url, how } = resolveTarget(project, args);
3146
3383
  if (!dry)
3147
- console.log(`${color5.dim("→")} ${url} ${color5.dim(`(${how})`)}
3384
+ console.log(`${color6.dim("→")} ${url} ${color6.dim(`(${how})`)}
3148
3385
  `);
3149
3386
  if (!dry) {
3150
3387
  let whoami2;
3151
3388
  try {
3152
3389
  whoami2 = await fetch(`${url}/api/auth/system/whoami`, { headers: { "x-system-key": key } });
3153
3390
  } catch (error) {
3154
- 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)}
3155
3392
  ` + ` Is the shop running? \`vc dev\` serves it locally; --local points here.
3156
3393
  `);
3157
3394
  return 1;
3158
3395
  }
3159
3396
  if (!whoami2.ok) {
3160
- 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}).
3161
3398
  ` + ` Is SYSTEM_API_KEY the one this deployment was given?
3162
3399
  `);
3163
3400
  return 1;
3164
3401
  }
3165
- console.log(`${color5.green("✓")} authenticated as the system identity`);
3402
+ console.log(`${color6.green("✓")} authenticated as the system identity`);
3166
3403
  }
3167
3404
  const root = project.root;
3168
- const products = await readJson(join9(root, "data", "catalog.json"));
3169
- const categories = await readJson(join9(root, "data", "categories.json"));
3170
- const faqs = has(project.manifest, "faqs") ? await readJson(join9(root, "content", "faqs.json")) : null;
3171
- 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;
3172
3409
  if (!products && !faqs?.length && !posts?.length) {
3173
3410
  console.log(`
3174
- ${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.
3175
3412
  ` + `Products go in data/catalog.json as a JSON array; \`vc import --dry-run\` checks it without pushing.
3176
3413
  `);
3177
3414
  return 0;
3178
3415
  }
3179
3416
  if (dry) {
3180
3417
  console.log(`
3181
- ${color5.dim("--dry-run: nothing was pushed.")}`);
3418
+ ${color6.dim("--dry-run: nothing was pushed.")}`);
3182
3419
  console.log(` ${products?.length ?? 0} products, ${categories?.length ?? 0} categories`);
3183
3420
  console.log(` ${faqs?.length ?? 0} FAQ entries, ${posts?.length ?? 0} posts
3184
3421
  `);
@@ -3194,15 +3431,15 @@ ${color5.dim("--dry-run: nothing was pushed.")}`);
3194
3431
  });
3195
3432
  if (result.ok) {
3196
3433
  const report = result.body.report ?? {};
3197
- console.log(`${color5.green("✓")} products: ${describe(report.products)}`);
3434
+ console.log(`${color6.green("✓")} products: ${describe(report.products)}`);
3198
3435
  if (report.categories)
3199
- console.log(`${color5.green("✓")} categories: ${describe(report.categories)}`);
3436
+ console.log(`${color6.green("✓")} categories: ${describe(report.categories)}`);
3200
3437
  if (report.prices)
3201
- console.log(`${color5.green("✓")} prices: ${describe(report.prices)}`);
3438
+ console.log(`${color6.green("✓")} prices: ${describe(report.prices)}`);
3202
3439
  if (report.addons)
3203
- console.log(`${color5.green("✓")} addons: ${describe(report.addons)}`);
3440
+ console.log(`${color6.green("✓")} addons: ${describe(report.addons)}`);
3204
3441
  } else {
3205
- console.error(`${color5.red("✗")} catalogue: ${result.body.error ?? `HTTP ${result.status}`}`);
3442
+ console.error(`${color6.red("✗")} catalogue: ${result.body.error ?? `HTTP ${result.status}`}`);
3206
3443
  failed = true;
3207
3444
  }
3208
3445
  }
@@ -3214,20 +3451,20 @@ ${color5.dim("--dry-run: nothing was pushed.")}`);
3214
3451
  continue;
3215
3452
  const result = await post(`${url}${path}`, key, { entries: rows, posts: rows });
3216
3453
  if (result.ok) {
3217
- console.log(`${color5.green("✓")} ${label}: ${describe(result.body)}`);
3454
+ console.log(`${color6.green("✓")} ${label}: ${describe(result.body)}`);
3218
3455
  } else {
3219
- console.error(`${color5.red("✗")} ${label}: ${result.body.error ?? `HTTP ${result.status}`}`);
3456
+ console.error(`${color6.red("✗")} ${label}: ${result.body.error ?? `HTTP ${result.status}`}`);
3220
3457
  failed = true;
3221
3458
  }
3222
3459
  }
3223
3460
  if (failed) {
3224
3461
  console.error(`
3225
- ${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.
3226
3463
  `);
3227
3464
  return 1;
3228
3465
  }
3229
3466
  console.log(`
3230
- ${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.
3231
3468
  `);
3232
3469
  return 0;
3233
3470
  }
@@ -3248,4 +3485,4 @@ async function importHelp() {
3248
3485
  return 0;
3249
3486
  }
3250
3487
 
3251
- 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 };