@saastemly/voidcommerce 0.3.0 → 0.5.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.
@@ -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
+ }>>;
@@ -17,3 +17,15 @@ export declare function preflightHelp(): Promise<number>;
17
17
  /** `vc secrets` — what the repository declares, and whether it is encrypted. */
18
18
  export declare function secretsCliCommand(args: string[]): Promise<number>;
19
19
  export declare function secretsHelp(): Promise<number>;
20
+ /** `vc keys` — what the token derives, and the warning that goes with it. */
21
+ export declare function keysCliCommand(args: string[]): Promise<number>;
22
+ export declare function keysHelp(): Promise<number>;
23
+ /**
24
+ * The pre-commit gate: refuse a commit that would put a secret in the clear.
25
+ *
26
+ * Exit code is the whole interface — a hook cares about nothing else.
27
+ */
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>;
@@ -0,0 +1,77 @@
1
+ import type { Project } from "../project";
2
+ /**
3
+ * The key that encrypts this repository's secrets.
4
+ *
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.
40
+ */
41
+ /** The public key the repository was encrypted under, from the committed file. */
42
+ export declare function committedPublicKey(root: string): string | null;
43
+ /** A fresh secp256k1 pair. The private half must reach GitHub and then be forgotten. */
44
+ export declare function generateKeypair(): Promise<{
45
+ publicKey: string;
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;
71
+ publicKey: string;
72
+ } | {
73
+ ok: false;
74
+ reason: string;
75
+ }>;
76
+ /** `vc keys` — where the key is, and what is missing. */
77
+ export declare function keysCommand(project: Project, args: string[]): Promise<number>;
@@ -0,0 +1,2 @@
1
+ import type { Project } from "../project";
2
+ export declare function linkCommand(project: Project, args: string[]): Promise<number>;
@@ -12,9 +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. The
16
- * private key is the one thing that stays out — a single value, set once
17
- * where the deploy runs.
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`.
18
20
  *
19
21
  * ── Why the values still become worker SECRETS ───────────────────────────
20
22
  *
@@ -27,19 +29,36 @@ import type { Project } from "../project";
27
29
  * applies ADDITIVELY, so a secret this file does not name is left alone
28
30
  * rather than deleted.
29
31
  *
30
- * ── The tradeoff, stated ─────────────────────────────────────────────────
32
+ * ── The tradeoffs, stated ────────────────────────────────────────────────
33
+ *
34
+ * Ciphertext in git is permanent. A key that ever leaks reads every secret
35
+ * in the history, including ones that were rotated — which is not true of
36
+ * `wrangler secret put`, where a rotation genuinely retires the old value.
31
37
  *
32
- * Ciphertext in git is permanent. If the private key ever leaks, every
33
- * secret in the history is readable, including ones that were rotated
34
- * which is not true of `wrangler secret put`, where a rotation genuinely
35
- * retires the old value. Rotating the dotenvx key re-encrypts the present,
36
- * not the past. That is the price of a shop that rebuilds from a checkout,
37
- * and it should be a decision rather than a surprise.
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
+ *
42
+ * Both are the price of a shop that rebuilds from a checkout, and both
43
+ * should be decisions rather than surprises.
38
44
  */
39
45
  export declare const SECRETS_FILE = ".env.secrets";
40
46
  export declare const PRIVATE_KEY_VAR = "DOTENV_PRIVATE_KEY_SECRETS";
41
47
  /** dotenvx, from the project's own install rather than a global one. */
42
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;
43
62
  /**
44
63
  * The names a `.env.secrets` declares, read WITHOUT decrypting.
45
64
  *
@@ -66,3 +85,33 @@ export declare function decryptSecrets(project: Project): Promise<DecryptedSecre
66
85
  }>;
67
86
  /** `vc secrets` — what the repository declares, and whether it is readable here. */
68
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
+ }>;
@@ -1,23 +1,38 @@
1
1
  import type { Manifest } from "../manifest";
2
2
  /**
3
- * The workflow that turns a push into a deploy.
4
- *
5
- * ── Why a branch and not a build ─────────────────────────────────────────
6
- *
7
- * Cloudflare's Workers Builds watches a repository and builds what it finds.
8
- * A strict repository has no app in it `.vc/app` is generated and
9
- * gitignored — and Cloudflare documents nothing about a build command that
10
- * writes the source it then builds. The install step's ordering against a
11
- * generated `package.json` is undocumented, which is not a thing to guess at
12
- * in the path that puts a shop on the internet.
13
- *
14
- * So the generator runs in GitHub Actions, where it is ordinary, and pushes
15
- * the result to its own branch. `main` stays the manifest and the data;
16
- * `void-dist` is a plain Void app with a committed `package.json`, lockfile
17
- * and `wrangler.jsonc` that anything can build from a cold checkout.
18
- * Cloudflare is pointed at that branch and needs to know nothing about
19
- * voidcommerce which also means the deploy path is one a person can run by
20
- * hand when CI is not the answer.
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. */
@@ -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>;
@@ -0,0 +1,35 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ for (let key of __getOwnPropNames(mod))
23
+ if (!__hasOwnProp.call(to, key))
24
+ __defProp(to, key, {
25
+ get: __accessProp.bind(mod, key),
26
+ enumerable: true
27
+ });
28
+ if (canCache)
29
+ cache.set(mod, to);
30
+ return to;
31
+ };
32
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
+
35
+ export { __toESM, __commonJS, __require };