@saastemly/voidcommerce 0.4.0 → 0.6.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 +269 -76
- package/dist/deploy/github.d.ts +85 -0
- package/dist/deploy/index.d.ts +3 -0
- package/dist/deploy/keys.d.ts +68 -25
- package/dist/deploy/link.d.ts +2 -0
- package/dist/deploy/secrets.d.ts +51 -8
- package/dist/generate/ci.d.ts +33 -18
- package/dist/generate/frontend.d.ts +1 -1
- package/dist/generate/index.d.ts +2 -0
- package/dist/index-2qt2yh3z.js +797 -0
- package/dist/{index-yzezvy5h.js → index-f1wds190.js} +322 -663
- package/dist/{index-pz6m2hkm.js → index-xx5p4b8d.js} +5 -4
- package/dist/index.js +12 -8
- package/dist/keys-qwvt324e.js +19 -0
- package/dist/manifest.d.ts +27 -5
- package/dist/manifest.js +3 -1
- package/package.json +1 -1
- package/src/cli.ts +14 -1
- package/src/deploy/cloudflare.ts +68 -4
- package/src/deploy/github.ts +198 -0
- package/src/deploy/index.ts +66 -18
- package/src/deploy/keys.ts +208 -144
- package/src/deploy/link.ts +185 -0
- package/src/deploy/secrets.ts +217 -35
- package/src/dotenvx-primitives.d.ts +4 -0
- package/src/generate/ci.ts +176 -121
- package/src/generate/frontend.ts +26 -4
- package/src/generate/index.ts +46 -8
- package/src/generate/support.ts +7 -5
- package/src/init.ts +2 -1
- package/src/manifest.ts +33 -8
- package/src/regenerate.ts +1 -0
- package/src/wizard.ts +7 -3
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub as the root of trust.
|
|
3
|
+
*
|
|
4
|
+
* ── Why GitHub and not Cloudflare ────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The shop's credentials have to be reachable by whatever deploys it, and
|
|
7
|
+
* whatever deploys it has to be reachable by a push. The first design
|
|
8
|
+
* derived the encryption key from a Cloudflare API token, which put the
|
|
9
|
+
* bootstrap in the wrong order: the token authorises a worker that does not
|
|
10
|
+
* exist yet, and getting one means `wrangler login` — the exact step this is
|
|
11
|
+
* supposed to remove.
|
|
12
|
+
*
|
|
13
|
+
* GitHub is already there. A person who can push is logged into `gh`, and
|
|
14
|
+
* `gh` can write repository secrets. So GitHub holds the credentials, GitHub
|
|
15
|
+
* Actions is the thing that deploys, and the local machine holds nothing.
|
|
16
|
+
*
|
|
17
|
+
* ── What this cannot do, stated plainly ──────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* It cannot mint a Cloudflare credential. There is no OIDC or workload
|
|
20
|
+
* identity federation from GitHub Actions to the Cloudflare API — the
|
|
21
|
+
* request has been open since 2025 with no Cloudflare commitment, and the
|
|
22
|
+
* official CI guidance is still "store an API token in your CI provider's
|
|
23
|
+
* secrets". The Cloudflare GitHub App runs one way: it grants Cloudflare
|
|
24
|
+
* access to the repository, not the repository access to Cloudflare.
|
|
25
|
+
*
|
|
26
|
+
* So exactly one long-lived Cloudflare token must exist, and this module's
|
|
27
|
+
* job is to make sure it exists in ONE place — GitHub's encrypted secrets —
|
|
28
|
+
* rather than on a laptop, in a shell profile, or in `~/.wrangler`.
|
|
29
|
+
*
|
|
30
|
+
* @see https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/
|
|
31
|
+
* @see https://github.com/cloudflare/workers-sdk/discussions/11434
|
|
32
|
+
*/
|
|
33
|
+
export interface GhAuth {
|
|
34
|
+
/** Logged in, with a token that can write repository secrets. */
|
|
35
|
+
ok: boolean;
|
|
36
|
+
user?: string | undefined;
|
|
37
|
+
scopes?: string[] | undefined;
|
|
38
|
+
reason?: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
export declare function findGh(): string | null;
|
|
41
|
+
export declare function run(cmd: string, args: string[], cwd: string, stdin?: string): Promise<{
|
|
42
|
+
code: number;
|
|
43
|
+
out: string;
|
|
44
|
+
}>;
|
|
45
|
+
/** Is `gh` here, logged in, and scoped to write secrets? */
|
|
46
|
+
export declare function ghAuth(cwd: string): Promise<GhAuth>;
|
|
47
|
+
/** `owner/repo` for this checkout, or null when there is no GitHub remote yet. */
|
|
48
|
+
export declare function repoSlug(cwd: string): Promise<string | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Write a repository secret, with the value on STDIN.
|
|
51
|
+
*
|
|
52
|
+
* Never as an argument: an argument is visible in `ps` to every other
|
|
53
|
+
* process on the machine for as long as the call runs, and this is called
|
|
54
|
+
* with private keys.
|
|
55
|
+
*/
|
|
56
|
+
export declare function setSecret(cwd: string, name: string, value: string): Promise<{
|
|
57
|
+
ok: boolean;
|
|
58
|
+
error?: string;
|
|
59
|
+
}>;
|
|
60
|
+
/** Repository secret NAMES. GitHub never gives a value back, by design. */
|
|
61
|
+
export declare function secretNames(cwd: string): Promise<Set<string>>;
|
|
62
|
+
/** A repository VARIABLE — readable, for things that are configuration rather than credentials. */
|
|
63
|
+
export declare function setVariable(cwd: string, name: string, value: string): Promise<{
|
|
64
|
+
ok: boolean;
|
|
65
|
+
error?: string;
|
|
66
|
+
}>;
|
|
67
|
+
export declare function variableNames(cwd: string): Promise<Set<string>>;
|
|
68
|
+
/**
|
|
69
|
+
* Ask Cloudflare whether a token is real, before it is stored anywhere.
|
|
70
|
+
*
|
|
71
|
+
* A token that is wrong should be caught at the moment it is typed, by the
|
|
72
|
+
* person who has it, and not two minutes into a CI run by someone reading a
|
|
73
|
+
* log. This is the one Cloudflare call vc makes with the token in hand.
|
|
74
|
+
*
|
|
75
|
+
* @see https://developers.cloudflare.com/fundamentals/api/reference/verify-token/
|
|
76
|
+
*/
|
|
77
|
+
export declare function verifyCloudflareToken(token: string): Promise<{
|
|
78
|
+
ok: boolean;
|
|
79
|
+
detail: string;
|
|
80
|
+
}>;
|
|
81
|
+
/** The accounts a token can see, so the account id never has to be typed. */
|
|
82
|
+
export declare function cloudflareAccounts(token: string): Promise<Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
}>>;
|
package/dist/deploy/index.d.ts
CHANGED
|
@@ -26,3 +26,6 @@ export declare function keysHelp(): Promise<number>;
|
|
|
26
26
|
* Exit code is the whole interface — a hook cares about nothing else.
|
|
27
27
|
*/
|
|
28
28
|
export declare function guardCommand(): Promise<number>;
|
|
29
|
+
/** `vc link` — GitHub holds the credentials; this is what puts them there. */
|
|
30
|
+
export declare function linkCliCommand(args: string[]): Promise<number>;
|
|
31
|
+
export declare function linkHelp(): Promise<number>;
|
package/dist/deploy/keys.d.ts
CHANGED
|
@@ -1,34 +1,77 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/** Where the token comes from, in the order wrangler itself reads them. */
|
|
3
|
-
export declare function apiToken(): string | null;
|
|
1
|
+
import type { Project } from "../project";
|
|
4
2
|
/**
|
|
5
|
-
* The
|
|
3
|
+
* The key that encrypts this repository's secrets.
|
|
6
4
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* ── The realisation that shaped this ─────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* dotenvx is ASYMMETRIC. Encryption takes the public key; only decryption
|
|
8
|
+
* takes the private one. The public key is committed at the top of
|
|
9
|
+
* `.env.secrets`, so ADDING OR CHANGING A SECRET NEEDS NO CREDENTIAL AT
|
|
10
|
+
* ALL — not a Cloudflare token, not a wrangler login, not even this file.
|
|
11
|
+
* Anyone who can clone the repository can set a secret in it; nobody who
|
|
12
|
+
* can clone it can read one.
|
|
13
|
+
*
|
|
14
|
+
* That collapses the problem. The private key is needed in exactly one
|
|
15
|
+
* place — wherever the deploy runs — and nowhere else, ever.
|
|
16
|
+
*
|
|
17
|
+
* ── Why the key lives in GitHub ──────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* The first design derived it from a Cloudflare API token, which put the
|
|
20
|
+
* bootstrap in the wrong order: the token authorises a worker that does not
|
|
21
|
+
* exist yet, and obtaining one means `wrangler login`. It also made every
|
|
22
|
+
* secret unreadable the day the token rotated, and gave two admins two
|
|
23
|
+
* different keys.
|
|
24
|
+
*
|
|
25
|
+
* So the key is generated at random and handed to GitHub, which is where
|
|
26
|
+
* the deploy runs and the one place a person who can push is already
|
|
27
|
+
* authenticated. It is never written to disk. `gh secret set` takes it on
|
|
28
|
+
* stdin, GitHub encrypts it, and no API can read it back — which is the
|
|
29
|
+
* property that makes it a good home and, unavoidably, the property that
|
|
30
|
+
* makes rotation a re-key rather than a re-encrypt.
|
|
31
|
+
*
|
|
32
|
+
* ── Rotation, and why losing the key is survivable ───────────────────────
|
|
33
|
+
*
|
|
34
|
+
* Nothing can read a GitHub secret back, so re-encrypting the existing
|
|
35
|
+
* ciphertext needs a local copy of the old key. Usually there is none, and
|
|
36
|
+
* that is fine: the only reason to rotate an encryption key is that it may
|
|
37
|
+
* have leaked, and a key that may have leaked means the VALUES may have
|
|
38
|
+
* leaked. Those must be replaced at Stripe and everywhere else regardless.
|
|
39
|
+
* Re-entering them is not extra work — it is the work.
|
|
11
40
|
*/
|
|
12
|
-
|
|
13
|
-
/** The public key for a private one, via dotenvx's own primitive. */
|
|
14
|
-
export declare function publicKeyFor(privateKey: string): Promise<string | null>;
|
|
15
|
-
/** The public key the repository was actually encrypted under. */
|
|
41
|
+
/** The public key the repository was encrypted under, from the committed file. */
|
|
16
42
|
export declare function committedPublicKey(root: string): string | null;
|
|
17
|
-
|
|
18
|
-
|
|
43
|
+
/** A fresh secp256k1 pair. The private half must reach GitHub and then be forgotten. */
|
|
44
|
+
export declare function generateKeypair(): Promise<{
|
|
45
|
+
publicKey: string;
|
|
19
46
|
privateKey: string;
|
|
47
|
+
} | null>;
|
|
48
|
+
export declare function publicKeyFor(privateKey: string): Promise<string | null>;
|
|
49
|
+
export interface KeyState {
|
|
50
|
+
/** The public key in `.env.secrets`, if the file exists. */
|
|
51
|
+
publicKey: string | null;
|
|
52
|
+
/** Does GitHub hold a private key for this repository? Names only — values never come back. */
|
|
53
|
+
inGitHub: boolean;
|
|
54
|
+
/** `owner/repo`, or null when there is no GitHub remote yet. */
|
|
55
|
+
slug: string | null;
|
|
56
|
+
/** A local override, for deploying by hand without CI. */
|
|
57
|
+
localKey: string | null;
|
|
58
|
+
/** Set when the local override does not match the committed public key. */
|
|
59
|
+
mismatch?: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
export declare function keyState(project: Project): Promise<KeyState>;
|
|
62
|
+
/**
|
|
63
|
+
* Make a key and give it to GitHub.
|
|
64
|
+
*
|
|
65
|
+
* Returns the PUBLIC key for the caller to write into `.env.secrets`. The
|
|
66
|
+
* private key is deliberately not returned and not logged: it exists as a
|
|
67
|
+
* local variable for the length of one `gh` call and then goes out of scope.
|
|
68
|
+
*/
|
|
69
|
+
export declare function provisionKey(project: Project): Promise<{
|
|
70
|
+
ok: true;
|
|
20
71
|
publicKey: string;
|
|
21
|
-
fresh: boolean;
|
|
22
72
|
} | {
|
|
23
73
|
ok: false;
|
|
24
74
|
reason: string;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
* `fresh` means the repository has no secrets yet, so there is nothing to
|
|
30
|
-
* check the derivation against and any token is as good as another.
|
|
31
|
-
*/
|
|
32
|
-
export declare function keyFor(manifest: Manifest, root: string): Promise<KeyState>;
|
|
33
|
-
/** `vc keys` — what the derivation says, without printing anything secret. */
|
|
34
|
-
export declare function keysCommand(manifest: Manifest, root: string, args: string[]): Promise<number>;
|
|
75
|
+
}>;
|
|
76
|
+
/** `vc keys` — where the key is, and what is missing. */
|
|
77
|
+
export declare function keysCommand(project: Project, args: string[]): Promise<number>;
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -12,10 +12,11 @@ import type { Project } from "../project";
|
|
|
12
12
|
*
|
|
13
13
|
* dotenvx closes it. `.env.secrets` holds ciphertext and a public key, and
|
|
14
14
|
* it is COMMITTED: the key names are readable, the values are not, and a
|
|
15
|
-
* diff shows when a secret changed without showing what it changed to.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* diff shows when a secret changed without showing what it changed to.
|
|
16
|
+
*
|
|
17
|
+
* Because dotenvx is asymmetric, SETTING a secret needs only the committed
|
|
18
|
+
* public key — no credential, no login, no key file. The private key lives
|
|
19
|
+
* in one place, GitHub Actions, and only the deploy uses it. See `./keys.ts`.
|
|
19
20
|
*
|
|
20
21
|
* ── Why the values still become worker SECRETS ───────────────────────────
|
|
21
22
|
*
|
|
@@ -34,10 +35,9 @@ import type { Project } from "../project";
|
|
|
34
35
|
* in the history, including ones that were rotated — which is not true of
|
|
35
36
|
* `wrangler secret put`, where a rotation genuinely retires the old value.
|
|
36
37
|
*
|
|
37
|
-
* And
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* cannot recover values, only explain them.
|
|
38
|
+
* And GitHub will not hand a secret back once set, so re-keying means
|
|
39
|
+
* entering the values again. That is the right trade: a key worth rotating
|
|
40
|
+
* is a key that may have leaked, and the values then need replacing anyway.
|
|
41
41
|
*
|
|
42
42
|
* Both are the price of a shop that rebuilds from a checkout, and both
|
|
43
43
|
* should be decisions rather than surprises.
|
|
@@ -46,6 +46,19 @@ export declare const SECRETS_FILE = ".env.secrets";
|
|
|
46
46
|
export declare const PRIVATE_KEY_VAR = "DOTENV_PRIVATE_KEY_SECRETS";
|
|
47
47
|
/** dotenvx, from the project's own install rather than a global one. */
|
|
48
48
|
export declare function findDotenvx(from: string): string | null;
|
|
49
|
+
export declare function run(cmd: string, args: string[], cwd: string, env?: Record<string, string>): Promise<{
|
|
50
|
+
code: number;
|
|
51
|
+
out: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Put a public key at the top of `.env.secrets`, creating the file if it is
|
|
55
|
+
* not there yet.
|
|
56
|
+
*
|
|
57
|
+
* Only the public half is ever written to disk, and it is meant to be
|
|
58
|
+
* committed: it is what lets anyone with a clone SET a secret without
|
|
59
|
+
* holding a credential.
|
|
60
|
+
*/
|
|
61
|
+
export declare function committedPublicKeyInto(root: string, publicKey: string): void;
|
|
49
62
|
/**
|
|
50
63
|
* The names a `.env.secrets` declares, read WITHOUT decrypting.
|
|
51
64
|
*
|
|
@@ -72,3 +85,33 @@ export declare function decryptSecrets(project: Project): Promise<DecryptedSecre
|
|
|
72
85
|
}>;
|
|
73
86
|
/** `vc secrets` — what the repository declares, and whether it is readable here. */
|
|
74
87
|
export declare function secretsCommand(project: Project, args: string[]): Promise<number>;
|
|
88
|
+
/** Write a `.env.secrets` with every required key as the `unset` sentinel. */
|
|
89
|
+
export declare function initSecrets(project: Project): Promise<number>;
|
|
90
|
+
/**
|
|
91
|
+
* `vc secrets set KEY` — change one value, with no credential of any kind.
|
|
92
|
+
*
|
|
93
|
+
* This is the command the whole design exists to make possible. dotenvx
|
|
94
|
+
* encrypts with the public key sitting in the committed file, so a
|
|
95
|
+
* contributor with a clone and nothing else can rotate the Stripe key. They
|
|
96
|
+
* cannot read the one that is there, which is exactly right.
|
|
97
|
+
*
|
|
98
|
+
* The value is read from the terminal or from stdin, never from argv: an
|
|
99
|
+
* argument is visible in `ps` to every process on the machine while the
|
|
100
|
+
* command runs, and shells keep it in history besides.
|
|
101
|
+
*/
|
|
102
|
+
export declare function setSecretValue(project: Project, args: string[]): Promise<number>;
|
|
103
|
+
/**
|
|
104
|
+
* Encrypt one value into `.env.secrets`, in process.
|
|
105
|
+
*
|
|
106
|
+
* Deliberately NOT `dotenvx set NAME value -f …`: that takes the value as an
|
|
107
|
+
* argument, and an argument is visible in `ps` to every other process on the
|
|
108
|
+
* machine for as long as the call runs. This uses the same secp256k1 the CLI
|
|
109
|
+
* uses — it is the CLI's own library — so what it writes is byte-compatible
|
|
110
|
+
* with what `dotenvx decrypt` expects.
|
|
111
|
+
*/
|
|
112
|
+
export declare function encryptInto(root: string, publicKey: string, name: string, value: string): Promise<{
|
|
113
|
+
ok: true;
|
|
114
|
+
} | {
|
|
115
|
+
ok: false;
|
|
116
|
+
error: string;
|
|
117
|
+
}>;
|
package/dist/generate/ci.d.ts
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
import type { Manifest } from "../manifest";
|
|
2
2
|
/**
|
|
3
|
-
* The workflow that turns a push into a
|
|
4
|
-
*
|
|
5
|
-
* ── Why
|
|
6
|
-
*
|
|
7
|
-
* Cloudflare's Workers Builds
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
3
|
+
* The workflow that turns a push into a live shop.
|
|
4
|
+
*
|
|
5
|
+
* ── Why GitHub Actions deploys, and not Cloudflare's own build ───────────
|
|
6
|
+
*
|
|
7
|
+
* Cloudflare's Workers Builds can watch a repository, and for a while that
|
|
8
|
+
* was the plan. Three things make it the wrong end to drive from.
|
|
9
|
+
*
|
|
10
|
+
* The build secret. `.env.secrets` is ciphertext, so something must hold
|
|
11
|
+
* the key that opens it. Workers Builds takes build variables ONLY from the
|
|
12
|
+
* dashboard — there is no repository file for them — so the one credential
|
|
13
|
+
* that makes push-to-deploy work would have to be pasted into a web page.
|
|
14
|
+
*
|
|
15
|
+
* The token. The API token Cloudflare generates for its own builds has no
|
|
16
|
+
* D1 and no Queues permission, so it cannot create this shop's database.
|
|
17
|
+
* Replacing it is another dashboard visit.
|
|
18
|
+
*
|
|
19
|
+
* The bootstrap. Connecting the repository is itself a dashboard step, on a
|
|
20
|
+
* Worker that has to exist first.
|
|
21
|
+
*
|
|
22
|
+
* GitHub already has what is needed. A person who can push is logged into
|
|
23
|
+
* `gh`, and `gh` can write repository secrets from the terminal — so
|
|
24
|
+
* `vc link` puts the key and the token there in one command, and this
|
|
25
|
+
* workflow spends them. Nothing is typed into a web page except the token
|
|
26
|
+
* itself, once, because Cloudflare will not issue one any other way.
|
|
27
|
+
*
|
|
28
|
+
* ── The `void-dist` branch is still built ────────────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* It is no longer the deploy path, but it stays: a plain Void app with a
|
|
31
|
+
* committed lockfile and `wrangler.jsonc` that anything can build from a
|
|
32
|
+
* cold checkout. It is what makes the deploy reproducible by hand, and it
|
|
33
|
+
* is the escape hatch for anyone who does want Cloudflare's build after all.
|
|
34
|
+
*
|
|
35
|
+
* @see https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#api-token
|
|
21
36
|
*/
|
|
22
37
|
export declare function renderDistWorkflow(manifest: Manifest): string;
|
|
23
38
|
/** What a person still has to do once, and why each thing cannot be done for them. */
|
package/dist/generate/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { envSummary } from "./env";
|
|
|
22
22
|
export interface GenerateResult {
|
|
23
23
|
written: string[];
|
|
24
24
|
kept: string[];
|
|
25
|
+
/** Generated files that no longer belong, and were deleted. */
|
|
26
|
+
retired: string[];
|
|
25
27
|
packages: string[];
|
|
26
28
|
}
|
|
27
29
|
export declare function generate(root: string, manifest: Manifest): Promise<GenerateResult>;
|