@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.
- package/dist/catalog.js +2 -1
- package/dist/cli.js +87 -3
- package/dist/deploy/cloudflare.d.ts +3 -1
- package/dist/deploy/index.d.ts +12 -0
- package/dist/deploy/keys.d.ts +34 -0
- package/dist/deploy/preflight.d.ts +2 -0
- package/dist/deploy/secrets.d.ts +74 -0
- package/dist/index-0v6na3yp.js +35 -0
- package/dist/{index-z4qazajn.js → index-844b3qn9.js} +1 -4
- package/dist/index-mpr7gm6k.js +4597 -0
- package/dist/{index-zh3tmj08.js → index-pz6m2hkm.js} +1 -1
- package/dist/{index-sbchkpa6.js → index-yzezvy5h.js} +476 -77
- package/dist/index.js +4 -3
- package/dist/manifest.js +3 -2
- package/package.json +2 -1
- package/src/cli.ts +5 -1
- package/src/deploy/cloudflare.ts +25 -3
- package/src/deploy/index.ts +88 -0
- package/src/deploy/keys.ts +212 -0
- package/src/deploy/preflight.ts +38 -3
- package/src/deploy/secrets.ts +266 -0
- package/src/dist.ts +3 -1
- package/src/dotenvx-primitives.d.ts +10 -0
- package/src/generate/ci.ts +30 -5
- package/src/generate/index.ts +30 -1
- package/src/generate/strict.ts +5 -0
- package/src/help.ts +1 -0
|
@@ -11,11 +11,14 @@ import {
|
|
|
11
11
|
workerHosts,
|
|
12
12
|
writeManifest,
|
|
13
13
|
zone
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-pz6m2hkm.js";
|
|
15
15
|
import {
|
|
16
|
-
CHOICES
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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 |
|
|
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 =
|
|
2838
|
-
if (!
|
|
3208
|
+
const path = join9(appDir, ".env.production");
|
|
3209
|
+
if (!existsSync9(path))
|
|
2839
3210
|
return out;
|
|
2840
|
-
for (const line2 of
|
|
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 =
|
|
2866
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 ?
|
|
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
|
-
${
|
|
3295
|
+
${color5.green("Ready to go live.")}
|
|
2915
3296
|
`);
|
|
2916
3297
|
return;
|
|
2917
3298
|
}
|
|
2918
3299
|
console.log(`
|
|
2919
|
-
${
|
|
3300
|
+
${color5.red("NOT ready to go live.")}
|
|
2920
3301
|
`);
|
|
2921
3302
|
if (result.routeProblem) {
|
|
2922
|
-
console.log(`${
|
|
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(`${
|
|
3313
|
+
console.log(`${color5.red("✗")} ${key.key}`);
|
|
2928
3314
|
console.log(` ${key.breaks}`);
|
|
2929
3315
|
if (key.where)
|
|
2930
|
-
console.log(` ${
|
|
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
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
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
|
|
2947
|
-
import { dirname as
|
|
2948
|
-
import
|
|
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
|
-
${
|
|
3339
|
+
${color6.red("✗")} ${message}
|
|
2952
3340
|
`);
|
|
2953
3341
|
return 1;
|
|
2954
3342
|
};
|
|
2955
3343
|
function readConfig(path) {
|
|
2956
|
-
return parseJsonc(
|
|
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 =
|
|
2966
|
-
if (
|
|
3353
|
+
const path = join10(dir, "node_modules", ".bin", bin);
|
|
3354
|
+
if (existsSync10(path))
|
|
2967
3355
|
return { cmd: path, label };
|
|
2968
3356
|
}
|
|
2969
|
-
const parent =
|
|
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 =
|
|
2978
|
-
if (!
|
|
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(`${
|
|
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
|
-
|
|
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(`${
|
|
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(
|
|
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
|
-
|
|
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(`${
|
|
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(`${
|
|
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 =
|
|
3042
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
3057
|
-
console.log(`${
|
|
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",
|
|
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
|
-
${
|
|
3470
|
+
${color6.green("Live:")} ${url}`);
|
|
3072
3471
|
if (hasFrontend(project.manifest.layout))
|
|
3073
|
-
console.log(
|
|
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
|
|
3080
|
-
import { join as
|
|
3081
|
-
import
|
|
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 (!
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
3565
|
+
console.log(`${color7.green("✓")} authenticated as the system identity`);
|
|
3167
3566
|
}
|
|
3168
3567
|
const root = project.root;
|
|
3169
|
-
const products = await readJson(
|
|
3170
|
-
const categories = await readJson(
|
|
3171
|
-
const faqs = has(project.manifest, "faqs") ? await readJson(
|
|
3172
|
-
const posts = has(project.manifest, "blogs") ? await readJson(
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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(`${
|
|
3597
|
+
console.log(`${color7.green("✓")} products: ${describe(report.products)}`);
|
|
3199
3598
|
if (report.categories)
|
|
3200
|
-
console.log(`${
|
|
3599
|
+
console.log(`${color7.green("✓")} categories: ${describe(report.categories)}`);
|
|
3201
3600
|
if (report.prices)
|
|
3202
|
-
console.log(`${
|
|
3601
|
+
console.log(`${color7.green("✓")} prices: ${describe(report.prices)}`);
|
|
3203
3602
|
if (report.addons)
|
|
3204
|
-
console.log(`${
|
|
3603
|
+
console.log(`${color7.green("✓")} addons: ${describe(report.addons)}`);
|
|
3205
3604
|
} else {
|
|
3206
|
-
console.error(`${
|
|
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(`${
|
|
3617
|
+
console.log(`${color7.green("✓")} ${label}: ${describe(result.body)}`);
|
|
3219
3618
|
} else {
|
|
3220
|
-
console.error(`${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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 };
|