@saastemly/voidcommerce 0.21.0 → 0.22.1
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.d.ts +13 -0
- package/dist/cli.js +5 -5
- package/dist/{index-pghh8nnq.js → index-51zee2vf.js} +2 -2
- package/dist/{index-940vpkwa.js → index-t1ggktg8.js} +16 -9
- package/dist/index.js +2 -2
- package/dist/{keys-mewxnkwg.js → keys-1h4zpvd7.js} +1 -1
- package/package.json +1 -1
- package/src/catalog.ts +13 -0
- package/src/deploy/secrets.ts +15 -5
- package/src/deploy/teardown.ts +9 -3
- package/src/generate/env.ts +10 -4
package/dist/catalog.d.ts
CHANGED
|
@@ -24,6 +24,19 @@ export interface EnvKey {
|
|
|
24
24
|
dev?: string | undefined;
|
|
25
25
|
/** Where the real value comes from. */
|
|
26
26
|
where?: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Needed to DEPLOY the shop, not to run it.
|
|
29
|
+
*
|
|
30
|
+
* The Cloudflare credentials are the case. They belong in `.env.secrets`
|
|
31
|
+
* so a push carries them, and preflight must insist on them — but the
|
|
32
|
+
* worker has no business seeing one. Left unmarked they ended up in three
|
|
33
|
+
* wrong places at once: `env.ts`, so the worker refused to boot without a
|
|
34
|
+
* deploy token; `.env`, where wrangler read `CLOUDFLARE_ACCOUNT_ID=unset`
|
|
35
|
+
* and addressed `accounts/unset/...`; and `--secrets-file`, which would
|
|
36
|
+
* have uploaded the deploy credential INTO the worker for any code
|
|
37
|
+
* running there to read.
|
|
38
|
+
*/
|
|
39
|
+
deployOnly?: boolean | undefined;
|
|
27
40
|
}
|
|
28
41
|
export interface Choice {
|
|
29
42
|
id: string;
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
version,
|
|
26
26
|
voidAppsIn,
|
|
27
27
|
wrangler
|
|
28
|
-
} from "./index-
|
|
28
|
+
} from "./index-51zee2vf.js";
|
|
29
29
|
import {
|
|
30
30
|
LOCAL_KEY_FILE,
|
|
31
31
|
PRIVATE_KEY_VAR,
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
run,
|
|
41
41
|
secretValue,
|
|
42
42
|
secretsCommand
|
|
43
|
-
} from "./index-
|
|
43
|
+
} from "./index-t1ggktg8.js";
|
|
44
44
|
import {
|
|
45
45
|
LAYOUTS,
|
|
46
46
|
oneOrigin,
|
|
@@ -126,7 +126,7 @@ ${color.bold("This will destroy, on Cloudflare:")}
|
|
|
126
126
|
` + ` ${color.cyan(`vc teardown --cloudflare --confirm ${project.manifest.shop.domain}`)}`);
|
|
127
127
|
}
|
|
128
128
|
console.log(`▸ wrangler delete (${plan.worker})`);
|
|
129
|
-
const deleted = await wrangler(bin, ["delete", "--name", plan.worker, "--force"], app
|
|
129
|
+
const deleted = await wrangler(bin, ["delete", "--name", plan.worker, "--force"], app);
|
|
130
130
|
if (deleted.code !== 0 && !/not found|does not exist|10007|10090/i.test(deleted.out)) {
|
|
131
131
|
return fail(`could not delete the worker:
|
|
132
132
|
${deleted.out.trim().split(`
|
|
@@ -135,7 +135,7 @@ ${deleted.out.trim().split(`
|
|
|
135
135
|
}
|
|
136
136
|
console.log(`${color.green("✓")} worker gone${/not found|does not exist/i.test(deleted.out) ? color.dim(" (it was already)") : ""}`);
|
|
137
137
|
for (const queue of plan.queues) {
|
|
138
|
-
const dropped = await wrangler(bin, ["queues", "delete", queue], app
|
|
138
|
+
const dropped = await wrangler(bin, ["queues", "delete", queue], app);
|
|
139
139
|
if (dropped.code !== 0 && !/not found|does not exist|11009|11018/i.test(dropped.out)) {
|
|
140
140
|
console.error(`${color.yellow("!")} queue "${queue}" could not be deleted: ${dropped.out.trim().split(`
|
|
141
141
|
`).slice(-1)[0]}`);
|
|
@@ -144,7 +144,7 @@ ${deleted.out.trim().split(`
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
if (plan.d1 && !opts.keepData) {
|
|
147
|
-
const dropped = await wrangler(bin, ["d1", "delete", plan.d1, "--skip-confirmation"], app
|
|
147
|
+
const dropped = await wrangler(bin, ["d1", "delete", plan.d1, "--skip-confirmation"], app);
|
|
148
148
|
if (dropped.code !== 0 && !/not found|does not exist|7404/i.test(dropped.out)) {
|
|
149
149
|
return fail(`the worker and queues are gone but D1 "${plan.d1}" is not:
|
|
150
150
|
${dropped.out.trim().split(`
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
renderEnvTs,
|
|
14
14
|
secretValue,
|
|
15
15
|
unsetSecretNames
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-t1ggktg8.js";
|
|
17
17
|
import {
|
|
18
18
|
MANIFEST_FILE,
|
|
19
19
|
has,
|
|
@@ -1383,7 +1383,7 @@ import color from "picocolors";
|
|
|
1383
1383
|
// package.json
|
|
1384
1384
|
var package_default = {
|
|
1385
1385
|
name: "@saastemly/voidcommerce",
|
|
1386
|
-
version: "0.
|
|
1386
|
+
version: "0.22.1",
|
|
1387
1387
|
description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
|
|
1388
1388
|
type: "module",
|
|
1389
1389
|
license: "MIT",
|
|
@@ -118,12 +118,14 @@ var BASE = [
|
|
|
118
118
|
{
|
|
119
119
|
key: "CLOUDFLARE_API_TOKEN",
|
|
120
120
|
breaks: "nothing deploys: no worker, no database, no migrations",
|
|
121
|
-
where: "dash.cloudflare.com/profile/api-tokens → Create Token → Custom token"
|
|
121
|
+
where: "dash.cloudflare.com/profile/api-tokens → Create Token → Custom token",
|
|
122
|
+
deployOnly: true
|
|
122
123
|
},
|
|
123
124
|
{
|
|
124
125
|
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
125
126
|
breaks: "wrangler cannot tell which account to deploy into, and refuses rather than guessing",
|
|
126
|
-
where: "the Cloudflare dashboard sidebar, or
|
|
127
|
+
where: "the Cloudflare dashboard sidebar, or read from the token on first deploy",
|
|
128
|
+
deployOnly: true
|
|
127
129
|
},
|
|
128
130
|
{
|
|
129
131
|
key: "SHOP_DOMAIN",
|
|
@@ -174,7 +176,7 @@ function allEnvKeys(manifest) {
|
|
|
174
176
|
}
|
|
175
177
|
var NUMERIC = new Set(["SHIPPING_DOMESTIC_MINOR", "SHIPPING_FREE_FROM_MINOR"]);
|
|
176
178
|
function renderEnvTs(manifest) {
|
|
177
|
-
const keys = allEnvKeys(manifest);
|
|
179
|
+
const keys = allEnvKeys(manifest).filter((key) => !key.deployOnly);
|
|
178
180
|
const lines = keys.map((key) => {
|
|
179
181
|
const helper = NUMERIC.has(key.key) ? "number()" : "string()";
|
|
180
182
|
const doc = [` /** ${key.breaks}${key.where ? ` — from: ${key.where}` : ""} */`];
|
|
@@ -203,7 +205,7 @@ ${lines.join(`
|
|
|
203
205
|
`;
|
|
204
206
|
}
|
|
205
207
|
function renderEnvExample(manifest) {
|
|
206
|
-
const keys = allEnvKeys(manifest);
|
|
208
|
+
const keys = allEnvKeys(manifest).filter((key) => !key.deployOnly);
|
|
207
209
|
return [
|
|
208
210
|
"# Every key is required. Copy to .env for local development.",
|
|
209
211
|
"# `unset` is the one value that reads as absent — for a credential you do not have yet.",
|
|
@@ -215,7 +217,7 @@ function renderEnvExample(manifest) {
|
|
|
215
217
|
}
|
|
216
218
|
var MANIFEST_OWNED_ENV = new Set(["SHOP_DOMAIN", "SHOP_ZONE", "EMAIL_FROM", "GITHUB_PAGES_HOST"]);
|
|
217
219
|
function renderEnvLocal(manifest) {
|
|
218
|
-
const keys = allEnvKeys(manifest);
|
|
220
|
+
const keys = allEnvKeys(manifest).filter((key) => !key.deployOnly);
|
|
219
221
|
const local = {
|
|
220
222
|
SHOP_DOMAIN: `${manifest.shop.domain.split(".")[0]}.test`,
|
|
221
223
|
GITHUB_PAGES_HOST: manifest.shop.pagesHost ?? "",
|
|
@@ -370,7 +372,7 @@ async function decryptSecrets(project) {
|
|
|
370
372
|
const root = project.root;
|
|
371
373
|
if (!existsSync2(join2(root, SECRETS_FILE)))
|
|
372
374
|
return { error: `${SECRETS_FILE} does not exist — \`vc secrets init\` writes one` };
|
|
373
|
-
const { localPrivateKey } = await import("./keys-
|
|
375
|
+
const { localPrivateKey } = await import("./keys-1h4zpvd7.js");
|
|
374
376
|
const privateKey = process.env[PRIVATE_KEY_VAR] || localPrivateKey(root) || "";
|
|
375
377
|
if (!privateKey) {
|
|
376
378
|
return {
|
|
@@ -382,7 +384,7 @@ async function decryptSecrets(project) {
|
|
|
382
384
|
}
|
|
383
385
|
const expected = committedPublicKey(root);
|
|
384
386
|
if (expected) {
|
|
385
|
-
const { publicKeyFor } = await import("./keys-
|
|
387
|
+
const { publicKeyFor } = await import("./keys-1h4zpvd7.js");
|
|
386
388
|
const derived = await publicKeyFor(privateKey);
|
|
387
389
|
if (derived && derived.toLowerCase() !== expected.toLowerCase()) {
|
|
388
390
|
return {
|
|
@@ -402,8 +404,13 @@ async function decryptSecrets(project) {
|
|
|
402
404
|
if (result.code !== 0)
|
|
403
405
|
return { error: `dotenvx could not decrypt ${SECRETS_FILE}: ${result.out.trim().split(`
|
|
404
406
|
`).slice(-2).join(" ")}` };
|
|
407
|
+
const deployOnly = new Set(allEnvKeys(project.manifest).filter((key) => key.deployOnly).map((key) => key.key));
|
|
405
408
|
const lines = result.out.split(`
|
|
406
|
-
`).filter((line) =>
|
|
409
|
+
`).filter((line) => {
|
|
410
|
+
if (!/^\s*[A-Z][A-Z0-9_]*\s*=/.test(line) || line.trimStart().startsWith("DOTENV_"))
|
|
411
|
+
return false;
|
|
412
|
+
return !deployOnly.has(line.slice(0, line.indexOf("=")).trim());
|
|
413
|
+
});
|
|
407
414
|
const names = lines.map((line) => line.slice(0, line.indexOf("=")).trim());
|
|
408
415
|
const path = join2(tmpdir(), `vc-secrets-${process.pid}-${Date.now()}.env`);
|
|
409
416
|
writeFileSync(path, `${lines.join(`
|
|
@@ -725,7 +732,7 @@ vc: no ${SECRETS_FILE} here — \`vc secrets --init\` starts a fresh one.
|
|
|
725
732
|
`);
|
|
726
733
|
return 1;
|
|
727
734
|
}
|
|
728
|
-
const { localPrivateKey, publicKeyFor, generateKeypair } = await import("./keys-
|
|
735
|
+
const { localPrivateKey, publicKeyFor, generateKeypair } = await import("./keys-1h4zpvd7.js");
|
|
729
736
|
const current = committedPublicKey(root);
|
|
730
737
|
const mine = process.env[PRIVATE_KEY_VAR] || localPrivateKey(root);
|
|
731
738
|
if (mine && current && (await publicKeyFor(mine))?.toLowerCase() === current.toLowerCase()) {
|
package/dist/index.js
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
routeProblem,
|
|
53
53
|
strictDependencies,
|
|
54
54
|
upsertJsonc
|
|
55
|
-
} from "./index-
|
|
55
|
+
} from "./index-51zee2vf.js";
|
|
56
56
|
import {
|
|
57
57
|
allEnvKeys,
|
|
58
58
|
envSummary,
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
renderEnvLocal,
|
|
61
61
|
renderEnvProduction,
|
|
62
62
|
renderEnvTs
|
|
63
|
-
} from "./index-
|
|
63
|
+
} from "./index-t1ggktg8.js";
|
|
64
64
|
import {
|
|
65
65
|
LAYOUTS,
|
|
66
66
|
MANIFEST_FILE,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saastemly/voidcommerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/catalog.ts
CHANGED
|
@@ -25,6 +25,19 @@ export interface EnvKey {
|
|
|
25
25
|
dev?: string | undefined;
|
|
26
26
|
/** Where the real value comes from. */
|
|
27
27
|
where?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Needed to DEPLOY the shop, not to run it.
|
|
30
|
+
*
|
|
31
|
+
* The Cloudflare credentials are the case. They belong in `.env.secrets`
|
|
32
|
+
* so a push carries them, and preflight must insist on them — but the
|
|
33
|
+
* worker has no business seeing one. Left unmarked they ended up in three
|
|
34
|
+
* wrong places at once: `env.ts`, so the worker refused to boot without a
|
|
35
|
+
* deploy token; `.env`, where wrangler read `CLOUDFLARE_ACCOUNT_ID=unset`
|
|
36
|
+
* and addressed `accounts/unset/...`; and `--secrets-file`, which would
|
|
37
|
+
* have uploaded the deploy credential INTO the worker for any code
|
|
38
|
+
* running there to read.
|
|
39
|
+
*/
|
|
40
|
+
deployOnly?: boolean | undefined;
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
export interface Choice {
|
package/src/deploy/secrets.ts
CHANGED
|
@@ -242,11 +242,21 @@ export async function decryptSecrets(project: Project): Promise<DecryptedSecrets
|
|
|
242
242
|
});
|
|
243
243
|
if (result.code !== 0) return { error: `dotenvx could not decrypt ${SECRETS_FILE}: ${result.out.trim().split("\n").slice(-2).join(" ")}` };
|
|
244
244
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Keep only the secrets the WORKER needs.
|
|
247
|
+
*
|
|
248
|
+
* dotenvx echoes its own public key back, which has no business being a
|
|
249
|
+
* worker secret. And the deploy-only keys are the important exclusion:
|
|
250
|
+
* `--secrets-file` uploads what it is given, so leaving CLOUDFLARE_API_TOKEN
|
|
251
|
+
* in here would put the credential that can deploy, delete and re-create
|
|
252
|
+
* this shop INSIDE the shop, readable by any code running in the worker.
|
|
253
|
+
* A compromised dependency would have found it there.
|
|
254
|
+
*/
|
|
255
|
+
const deployOnly = new Set(allEnvKeys(project.manifest).filter((key) => key.deployOnly).map((key) => key.key));
|
|
256
|
+
const lines = result.out.split("\n").filter((line) => {
|
|
257
|
+
if (!/^\s*[A-Z][A-Z0-9_]*\s*=/.test(line) || line.trimStart().startsWith("DOTENV_")) return false;
|
|
258
|
+
return !deployOnly.has(line.slice(0, line.indexOf("=")).trim());
|
|
259
|
+
});
|
|
250
260
|
const names = lines.map((line) => line.slice(0, line.indexOf("=")).trim());
|
|
251
261
|
|
|
252
262
|
const path = join(tmpdir(), `vc-secrets-${process.pid}-${Date.now()}.env`);
|
package/src/deploy/teardown.ts
CHANGED
|
@@ -116,12 +116,18 @@ export async function teardownCloudflare(project: Project, opts: TeardownOptions
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
+
* Every call CAPTURES its output rather than streaming it, because
|
|
120
|
+
* idempotency depends on reading what wrangler said. `wrangler(..., true)`
|
|
121
|
+
* inherits stdio, which leaves `out` empty — so "this worker does not
|
|
122
|
+
* exist" could never be recognised and a second teardown failed where it
|
|
123
|
+
* should have been a no-op.
|
|
124
|
+
*
|
|
119
125
|
* Order matters. The worker goes FIRST: while it is live it serves
|
|
120
126
|
* customers, and a shop that answers with a database that no longer
|
|
121
127
|
* exists is worse than a shop that does not answer.
|
|
122
128
|
*/
|
|
123
129
|
console.log(`▸ wrangler delete (${plan.worker})`);
|
|
124
|
-
const deleted = await wrangler(bin, ["delete", "--name", plan.worker, "--force"], app
|
|
130
|
+
const deleted = await wrangler(bin, ["delete", "--name", plan.worker, "--force"], app);
|
|
125
131
|
// "not found" is success here: teardown is idempotent by design.
|
|
126
132
|
if (deleted.code !== 0 && !/not found|does not exist|10007|10090/i.test(deleted.out)) {
|
|
127
133
|
return fail(`could not delete the worker:\n${deleted.out.trim().split("\n").slice(-3).join("\n")}`);
|
|
@@ -129,7 +135,7 @@ export async function teardownCloudflare(project: Project, opts: TeardownOptions
|
|
|
129
135
|
console.log(`${color.green("✓")} worker gone${/not found|does not exist/i.test(deleted.out) ? color.dim(" (it was already)") : ""}`);
|
|
130
136
|
|
|
131
137
|
for (const queue of plan.queues) {
|
|
132
|
-
const dropped = await wrangler(bin, ["queues", "delete", queue], app
|
|
138
|
+
const dropped = await wrangler(bin, ["queues", "delete", queue], app);
|
|
133
139
|
if (dropped.code !== 0 && !/not found|does not exist|11009|11018/i.test(dropped.out)) {
|
|
134
140
|
console.error(`${color.yellow("!")} queue "${queue}" could not be deleted: ${dropped.out.trim().split("\n").slice(-1)[0]}`);
|
|
135
141
|
} else {
|
|
@@ -138,7 +144,7 @@ export async function teardownCloudflare(project: Project, opts: TeardownOptions
|
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
if (plan.d1 && !opts.keepData) {
|
|
141
|
-
const dropped = await wrangler(bin, ["d1", "delete", plan.d1, "--skip-confirmation"], app
|
|
147
|
+
const dropped = await wrangler(bin, ["d1", "delete", plan.d1, "--skip-confirmation"], app);
|
|
142
148
|
if (dropped.code !== 0 && !/not found|does not exist|7404/i.test(dropped.out)) {
|
|
143
149
|
return fail(`the worker and queues are gone but D1 "${plan.d1}" is not:\n${dropped.out.trim().split("\n").slice(-3).join("\n")}`);
|
|
144
150
|
}
|
package/src/generate/env.ts
CHANGED
|
@@ -29,11 +29,13 @@ const BASE: EnvKey[] = [
|
|
|
29
29
|
key: "CLOUDFLARE_API_TOKEN",
|
|
30
30
|
breaks: "nothing deploys: no worker, no database, no migrations",
|
|
31
31
|
where: "dash.cloudflare.com/profile/api-tokens → Create Token → Custom token",
|
|
32
|
+
deployOnly: true,
|
|
32
33
|
},
|
|
33
34
|
{
|
|
34
35
|
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
35
36
|
breaks: "wrangler cannot tell which account to deploy into, and refuses rather than guessing",
|
|
36
|
-
where: "the Cloudflare dashboard sidebar, or
|
|
37
|
+
where: "the Cloudflare dashboard sidebar, or read from the token on first deploy",
|
|
38
|
+
deployOnly: true,
|
|
37
39
|
},
|
|
38
40
|
{
|
|
39
41
|
key: "SHOP_DOMAIN",
|
|
@@ -90,7 +92,9 @@ export function allEnvKeys(manifest: Manifest): EnvKey[] {
|
|
|
90
92
|
const NUMERIC = new Set(["SHIPPING_DOMESTIC_MINOR", "SHIPPING_FREE_FROM_MINOR"]);
|
|
91
93
|
|
|
92
94
|
export function renderEnvTs(manifest: Manifest): string {
|
|
93
|
-
|
|
95
|
+
// The worker validates what IT needs. A deploy credential here would make
|
|
96
|
+
// the shop refuse to boot without a token it never uses.
|
|
97
|
+
const keys = allEnvKeys(manifest).filter((key) => !key.deployOnly);
|
|
94
98
|
const lines = keys.map((key) => {
|
|
95
99
|
const helper = NUMERIC.has(key.key) ? "number()" : "string()";
|
|
96
100
|
const doc = [` /** ${key.breaks}${key.where ? ` — from: ${key.where}` : ""} */`];
|
|
@@ -117,7 +121,7 @@ ${lines.join("\n")}
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
export function renderEnvExample(manifest: Manifest): string {
|
|
120
|
-
const keys = allEnvKeys(manifest);
|
|
124
|
+
const keys = allEnvKeys(manifest).filter((key) => !key.deployOnly);
|
|
121
125
|
return [
|
|
122
126
|
"# Every key is required. Copy to .env for local development.",
|
|
123
127
|
"# `unset` is the one value that reads as absent — for a credential you do not have yet.",
|
|
@@ -139,7 +143,9 @@ export function renderEnvExample(manifest: Manifest): string {
|
|
|
139
143
|
export const MANIFEST_OWNED_ENV = new Set(["SHOP_DOMAIN", "SHOP_ZONE", "EMAIL_FROM", "GITHUB_PAGES_HOST"]);
|
|
140
144
|
|
|
141
145
|
export function renderEnvLocal(manifest: Manifest): string {
|
|
142
|
-
|
|
146
|
+
// Not in .env either: wrangler READS .env, so a deploy-only key sitting
|
|
147
|
+
// there as `unset` becomes a literal account id — `accounts/unset/...`.
|
|
148
|
+
const keys = allEnvKeys(manifest).filter((key) => !key.deployOnly);
|
|
143
149
|
const local: Record<string, string> = {
|
|
144
150
|
SHOP_DOMAIN: `${manifest.shop.domain.split(".")[0]}.test`,
|
|
145
151
|
GITHUB_PAGES_HOST: manifest.shop.pagesHost ?? "",
|