@saastemly/voidcommerce 0.19.1 → 0.20.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/cli.js +3 -2
- package/dist/deploy/token-link.d.ts +15 -0
- package/dist/{index-tk46w7yn.js → index-07ppzmgt.js} +17 -4
- package/dist/index-6e4eh08g.js +35 -0
- package/dist/{index-hsc97bf2.js → index-vpcnasre.js} +13 -2
- package/dist/index.js +3 -2
- package/dist/{keys-knmqsqz1.js → keys-fbw9fdby.js} +1 -1
- package/dist/token-link-wxpp5xzp.js +11 -0
- package/package.json +1 -1
- package/src/deploy/secrets.ts +15 -0
- package/src/deploy/token-link.ts +76 -0
- package/src/generate/ci.ts +13 -2
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
runVoid,
|
|
22
22
|
version,
|
|
23
23
|
voidAppsIn
|
|
24
|
-
} from "./index-
|
|
24
|
+
} from "./index-07ppzmgt.js";
|
|
25
25
|
import {
|
|
26
26
|
LOCAL_KEY_FILE,
|
|
27
27
|
PRIVATE_KEY_VAR,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
plaintextSecretNames,
|
|
36
36
|
run,
|
|
37
37
|
secretsCommand
|
|
38
|
-
} from "./index-
|
|
38
|
+
} from "./index-vpcnasre.js";
|
|
39
39
|
import {
|
|
40
40
|
LAYOUTS,
|
|
41
41
|
oneOrigin,
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
import {
|
|
47
47
|
GROUPS
|
|
48
48
|
} from "./index-xyjhy6kp.js";
|
|
49
|
+
import"./index-6e4eh08g.js";
|
|
49
50
|
import"./index-0v6na3yp.js";
|
|
50
51
|
|
|
51
52
|
// src/deploy/index.ts
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Manifest } from "../manifest";
|
|
2
|
+
/**
|
|
3
|
+
* The link, for this shop.
|
|
4
|
+
*
|
|
5
|
+
* The USER-token form, deliberately: it is the one that accepts the two
|
|
6
|
+
* user-scoped permissions wrangler wants, and account-owned tokens are
|
|
7
|
+
* documented as not taking `accountId`/`zoneId` at all. `zoneId=all` because
|
|
8
|
+
* a shop's zone is not known until the domain is on Cloudflare, and picking
|
|
9
|
+
* the wrong one is a worse failure than picking from a list.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cloudflareTokenLink(manifest: Manifest): string;
|
|
12
|
+
/** The same permissions as a table, for anyone who would rather tick them. */
|
|
13
|
+
export declare function cloudflareTokenTable(): string;
|
|
14
|
+
/** Markdown, for DEPLOY.md. */
|
|
15
|
+
export declare function cloudflareTokenMarkdown(): string;
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
renderEnvTs,
|
|
14
14
|
secretValue,
|
|
15
15
|
unsetSecretNames
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-vpcnasre.js";
|
|
17
17
|
import {
|
|
18
18
|
MANIFEST_FILE,
|
|
19
19
|
has,
|
|
@@ -30,6 +30,9 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
CHOICES
|
|
32
32
|
} from "./index-xyjhy6kp.js";
|
|
33
|
+
import {
|
|
34
|
+
cloudflareTokenMarkdown
|
|
35
|
+
} from "./index-6e4eh08g.js";
|
|
33
36
|
import {
|
|
34
37
|
__require
|
|
35
38
|
} from "./index-0v6na3yp.js";
|
|
@@ -1380,7 +1383,7 @@ import color from "picocolors";
|
|
|
1380
1383
|
// package.json
|
|
1381
1384
|
var package_default = {
|
|
1382
1385
|
name: "@saastemly/voidcommerce",
|
|
1383
|
-
version: "0.
|
|
1386
|
+
version: "0.20.0",
|
|
1384
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`.",
|
|
1385
1388
|
type: "module",
|
|
1386
1389
|
license: "MIT",
|
|
@@ -1859,6 +1862,7 @@ jobs:
|
|
|
1859
1862
|
}
|
|
1860
1863
|
function renderDeployReadme(manifest, zone2, hosts) {
|
|
1861
1864
|
const worker = manifest.shop.domain.split(".")[0];
|
|
1865
|
+
const TOKEN_TABLE = cloudflareTokenMarkdown();
|
|
1862
1866
|
return `# Going live
|
|
1863
1867
|
|
|
1864
1868
|
\`${manifest.shop.domain}\` on Cloudflare Workers. Generated by \`vc init\`;
|
|
@@ -1913,8 +1917,17 @@ ordinary secret in \`${SECRETS_FILE}\`: encrypted, committed, and read by the
|
|
|
1913
1917
|
deploy out of what you pushed.
|
|
1914
1918
|
|
|
1915
1919
|
The token itself has to be fetched by a person, once, because there is no
|
|
1916
|
-
OIDC between GitHub and Cloudflare and nothing else can issue one.
|
|
1917
|
-
|
|
1920
|
+
OIDC between GitHub and Cloudflare and nothing else can issue one. You do
|
|
1921
|
+
not have to find the permissions yourself:
|
|
1922
|
+
|
|
1923
|
+
\`\`\`sh
|
|
1924
|
+
vc secrets set CLOUDFLARE_API_TOKEN
|
|
1925
|
+
\`\`\`
|
|
1926
|
+
|
|
1927
|
+
prints a Cloudflare link with all of them **already selected**, then takes
|
|
1928
|
+
the token. It opens your own dashboard and the value is shown only to you.
|
|
1929
|
+
|
|
1930
|
+
${TOKEN_TABLE}
|
|
1918
1931
|
|
|
1919
1932
|
### The zone
|
|
1920
1933
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/deploy/token-link.ts
|
|
2
|
+
var PERMISSIONS = [
|
|
3
|
+
["Workers Scripts: Edit", "workers_scripts", "edit", "deploy the worker"],
|
|
4
|
+
["D1: Edit", "d1", "edit", "create the database and apply migrations"],
|
|
5
|
+
["Queues: Edit", "queues", "edit", "create the order queue"],
|
|
6
|
+
["Workers KV Storage: Edit", "workers_kv_storage", "edit", "sessions and caches"],
|
|
7
|
+
["Workers R2 Storage: Edit", "workers_r2", "edit", "product images"],
|
|
8
|
+
["Account Settings: Read", "account_settings", "read", "confirm which account this is"],
|
|
9
|
+
["Zone: Read", "zone", "read", "confirm the domain's zone is on this account"],
|
|
10
|
+
["DNS: Read", "dns", "read", "notice a hostname that already answers"],
|
|
11
|
+
["Workers Routes: Edit", "workers_routes", "edit", "answer on your domain"],
|
|
12
|
+
["User Details: Read", "user_details", "read", "wrangler asks at startup"],
|
|
13
|
+
["Memberships: Read", "memberships", "read", "the same"]
|
|
14
|
+
];
|
|
15
|
+
function cloudflareTokenLink(manifest) {
|
|
16
|
+
const groups = PERMISSIONS.map(([, key, type]) => ({ key, type }));
|
|
17
|
+
const params = new URLSearchParams({
|
|
18
|
+
permissionGroupKeys: JSON.stringify(groups),
|
|
19
|
+
accountId: "*",
|
|
20
|
+
zoneId: "all",
|
|
21
|
+
name: `${manifest.shop.domain} deploy`
|
|
22
|
+
});
|
|
23
|
+
return `https://dash.cloudflare.com/profile/api-tokens?${params.toString()}`;
|
|
24
|
+
}
|
|
25
|
+
function cloudflareTokenTable() {
|
|
26
|
+
const width = Math.max(...PERMISSIONS.map(([label]) => label.length));
|
|
27
|
+
return PERMISSIONS.map(([label, , , why]) => ` ${label.padEnd(width)} ${why}`).join(`
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
function cloudflareTokenMarkdown() {
|
|
31
|
+
return ["| permission | for |", "|---|---|", ...PERMISSIONS.map(([label, , , why]) => `| ${label} | ${why} |`)].join(`
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { cloudflareTokenLink, cloudflareTokenTable, cloudflareTokenMarkdown };
|
|
@@ -370,7 +370,7 @@ async function decryptSecrets(project) {
|
|
|
370
370
|
const root = project.root;
|
|
371
371
|
if (!existsSync2(join2(root, SECRETS_FILE)))
|
|
372
372
|
return { error: `${SECRETS_FILE} does not exist — \`vc secrets init\` writes one` };
|
|
373
|
-
const { localPrivateKey } = await import("./keys-
|
|
373
|
+
const { localPrivateKey } = await import("./keys-fbw9fdby.js");
|
|
374
374
|
const privateKey = process.env[PRIVATE_KEY_VAR] || localPrivateKey(root) || "";
|
|
375
375
|
if (!privateKey) {
|
|
376
376
|
return {
|
|
@@ -382,7 +382,7 @@ async function decryptSecrets(project) {
|
|
|
382
382
|
}
|
|
383
383
|
const expected = committedPublicKey(root);
|
|
384
384
|
if (expected) {
|
|
385
|
-
const { publicKeyFor } = await import("./keys-
|
|
385
|
+
const { publicKeyFor } = await import("./keys-fbw9fdby.js");
|
|
386
386
|
const derived = await publicKeyFor(privateKey);
|
|
387
387
|
if (derived && derived.toLowerCase() !== expected.toLowerCase()) {
|
|
388
388
|
return {
|
|
@@ -548,6 +548,17 @@ vc: ${SECRETS_FILE} has no DOTENV_PUBLIC_KEY_SECRETS, so there is nothing to enc
|
|
|
548
548
|
return 1;
|
|
549
549
|
}
|
|
550
550
|
const known = allEnvKeys(project.manifest).find((key) => key.key === name);
|
|
551
|
+
if (name === "CLOUDFLARE_API_TOKEN") {
|
|
552
|
+
const { cloudflareTokenLink, cloudflareTokenTable } = await import("./token-link-wxpp5xzp.js");
|
|
553
|
+
console.log(`
|
|
554
|
+
${color.bold("Open this — the permissions are already selected:")}
|
|
555
|
+
|
|
556
|
+
${color.cyan(cloudflareTokenLink(project.manifest))}
|
|
557
|
+
|
|
558
|
+
${color.dim("It opens your own dashboard; the token is shown only to you. What it grants:")}
|
|
559
|
+
${color.dim(cloudflareTokenTable())}
|
|
560
|
+
`);
|
|
561
|
+
}
|
|
551
562
|
const value = await readValue(name, known?.breaks, known?.where);
|
|
552
563
|
if (value === null)
|
|
553
564
|
return 1;
|
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-07ppzmgt.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-vpcnasre.js";
|
|
64
64
|
import {
|
|
65
65
|
LAYOUTS,
|
|
66
66
|
MANIFEST_FILE,
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
TAX,
|
|
100
100
|
UI
|
|
101
101
|
} from "./index-xyjhy6kp.js";
|
|
102
|
+
import"./index-6e4eh08g.js";
|
|
102
103
|
import"./index-0v6na3yp.js";
|
|
103
104
|
export {
|
|
104
105
|
zoneOf,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saastemly/voidcommerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
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/deploy/secrets.ts
CHANGED
|
@@ -405,6 +405,21 @@ export async function setSecretValue(project: Project, args: string[]): Promise<
|
|
|
405
405
|
// A known key gets its consequence shown, so a person setting one knows
|
|
406
406
|
// what it is for without leaving the terminal.
|
|
407
407
|
const known = allEnvKeys(project.manifest).find((key) => key.key === name);
|
|
408
|
+
|
|
409
|
+
// The one key whose "where" is ten checkboxes in a dashboard. Cloudflare
|
|
410
|
+
// takes the permissions as URL parameters, so this opens the form with all
|
|
411
|
+
// of them already ticked — which removes the failure where one missed row
|
|
412
|
+
// yields a token that authenticates and then fails mid-deploy.
|
|
413
|
+
if (name === "CLOUDFLARE_API_TOKEN") {
|
|
414
|
+
const { cloudflareTokenLink, cloudflareTokenTable } = await import("./token-link");
|
|
415
|
+
console.log(
|
|
416
|
+
`\n${color.bold("Open this — the permissions are already selected:")}\n\n` +
|
|
417
|
+
` ${color.cyan(cloudflareTokenLink(project.manifest))}\n\n` +
|
|
418
|
+
`${color.dim("It opens your own dashboard; the token is shown only to you. What it grants:")}\n` +
|
|
419
|
+
`${color.dim(cloudflareTokenTable())}\n`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
408
423
|
const value = await readValue(name, known?.breaks, known?.where);
|
|
409
424
|
if (value === null) return 1;
|
|
410
425
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Manifest } from "../manifest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Cloudflare dashboard link that opens the token form already filled in.
|
|
5
|
+
*
|
|
6
|
+
* ── Why this is worth having ─────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* Fetching an API token is the one step of this whole system that a person
|
|
9
|
+
* has to do by hand, and it was the worst-designed one: a table of ten
|
|
10
|
+
* permissions to find and tick, in two different scopes, where a single
|
|
11
|
+
* missed row produces a token that authenticates fine and then fails
|
|
12
|
+
* halfway through a deploy with a permissions error nobody can place.
|
|
13
|
+
*
|
|
14
|
+
* Cloudflare's dashboard accepts `permissionGroupKeys` — a JSON array of
|
|
15
|
+
* `{key, type}` — and opens Create Token with exactly those selected. So the
|
|
16
|
+
* ten rows become one link, and the failure mode goes away.
|
|
17
|
+
*
|
|
18
|
+
* @see https://developers.cloudflare.com/fundamentals/api/how-to/account-owned-token-template/
|
|
19
|
+
*
|
|
20
|
+
* ── On the keys below ────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* They are not guessable and a wrong one is silently dropped, which would
|
|
23
|
+
* produce a link that looks right and grants less than it claims. Every one
|
|
24
|
+
* was checked against the dashboard's own permission-group registry AND
|
|
25
|
+
* against links Cloudflare itself ships in its documentation — the Workers
|
|
26
|
+
* template, the DNS templates, the Prometheus link. None came from a
|
|
27
|
+
* third-party repository guessing at them, and several public repositories
|
|
28
|
+
* do guess: `account`, `user`, `workers`, `pages` match nothing at all.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** `[label, key, type, why]` — the why is what a person reads when it fails. */
|
|
32
|
+
const PERMISSIONS: Array<[string, string, string, string]> = [
|
|
33
|
+
["Workers Scripts: Edit", "workers_scripts", "edit", "deploy the worker"],
|
|
34
|
+
["D1: Edit", "d1", "edit", "create the database and apply migrations"],
|
|
35
|
+
["Queues: Edit", "queues", "edit", "create the order queue"],
|
|
36
|
+
["Workers KV Storage: Edit", "workers_kv_storage", "edit", "sessions and caches"],
|
|
37
|
+
["Workers R2 Storage: Edit", "workers_r2", "edit", "product images"],
|
|
38
|
+
["Account Settings: Read", "account_settings", "read", "confirm which account this is"],
|
|
39
|
+
["Zone: Read", "zone", "read", "confirm the domain's zone is on this account"],
|
|
40
|
+
["DNS: Read", "dns", "read", "notice a hostname that already answers"],
|
|
41
|
+
["Workers Routes: Edit", "workers_routes", "edit", "answer on your domain"],
|
|
42
|
+
["User Details: Read", "user_details", "read", "wrangler asks at startup"],
|
|
43
|
+
["Memberships: Read", "memberships", "read", "the same"],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The link, for this shop.
|
|
48
|
+
*
|
|
49
|
+
* The USER-token form, deliberately: it is the one that accepts the two
|
|
50
|
+
* user-scoped permissions wrangler wants, and account-owned tokens are
|
|
51
|
+
* documented as not taking `accountId`/`zoneId` at all. `zoneId=all` because
|
|
52
|
+
* a shop's zone is not known until the domain is on Cloudflare, and picking
|
|
53
|
+
* the wrong one is a worse failure than picking from a list.
|
|
54
|
+
*/
|
|
55
|
+
export function cloudflareTokenLink(manifest: Manifest): string {
|
|
56
|
+
const groups = PERMISSIONS.map(([, key, type]) => ({ key, type }));
|
|
57
|
+
const params = new URLSearchParams({
|
|
58
|
+
permissionGroupKeys: JSON.stringify(groups),
|
|
59
|
+
accountId: "*",
|
|
60
|
+
zoneId: "all",
|
|
61
|
+
// So a person with several tokens can tell later which shop this was for.
|
|
62
|
+
name: `${manifest.shop.domain} deploy`,
|
|
63
|
+
});
|
|
64
|
+
return `https://dash.cloudflare.com/profile/api-tokens?${params.toString()}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The same permissions as a table, for anyone who would rather tick them. */
|
|
68
|
+
export function cloudflareTokenTable(): string {
|
|
69
|
+
const width = Math.max(...PERMISSIONS.map(([label]) => label.length));
|
|
70
|
+
return PERMISSIONS.map(([label, , , why]) => ` ${label.padEnd(width)} ${why}`).join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Markdown, for DEPLOY.md. */
|
|
74
|
+
export function cloudflareTokenMarkdown(): string {
|
|
75
|
+
return ["| permission | for |", "|---|---|", ...PERMISSIONS.map(([label, , , why]) => `| ${label} | ${why} |`)].join("\n");
|
|
76
|
+
}
|
package/src/generate/ci.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DIST_BRANCH, DIST_DIR } from "../dist";
|
|
2
2
|
import type { Manifest } from "../manifest";
|
|
3
3
|
import { PRIVATE_KEY_VAR, SECRETS_FILE } from "../deploy/secrets";
|
|
4
|
+
import { cloudflareTokenMarkdown } from "../deploy/token-link";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The workflow that turns a push into a live shop.
|
|
@@ -190,6 +191,7 @@ jobs:
|
|
|
190
191
|
/** What a person still has to do once, and why each thing cannot be done for them. */
|
|
191
192
|
export function renderDeployReadme(manifest: Manifest, zone: string, hosts: string[]): string {
|
|
192
193
|
const worker = manifest.shop.domain.split(".")[0];
|
|
194
|
+
const TOKEN_TABLE = cloudflareTokenMarkdown();
|
|
193
195
|
return `# Going live
|
|
194
196
|
|
|
195
197
|
\`${manifest.shop.domain}\` on Cloudflare Workers. Generated by \`vc init\`;
|
|
@@ -244,8 +246,17 @@ ordinary secret in \`${SECRETS_FILE}\`: encrypted, committed, and read by the
|
|
|
244
246
|
deploy out of what you pushed.
|
|
245
247
|
|
|
246
248
|
The token itself has to be fetched by a person, once, because there is no
|
|
247
|
-
OIDC between GitHub and Cloudflare and nothing else can issue one.
|
|
248
|
-
|
|
249
|
+
OIDC between GitHub and Cloudflare and nothing else can issue one. You do
|
|
250
|
+
not have to find the permissions yourself:
|
|
251
|
+
|
|
252
|
+
\`\`\`sh
|
|
253
|
+
vc secrets set CLOUDFLARE_API_TOKEN
|
|
254
|
+
\`\`\`
|
|
255
|
+
|
|
256
|
+
prints a Cloudflare link with all of them **already selected**, then takes
|
|
257
|
+
the token. It opens your own dashboard and the value is shown only to you.
|
|
258
|
+
|
|
259
|
+
${TOKEN_TABLE}
|
|
249
260
|
|
|
250
261
|
### The zone
|
|
251
262
|
|