@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 CHANGED
@@ -14,7 +14,8 @@ import {
14
14
  SIGN_IN,
15
15
  TAX,
16
16
  UI
17
- } from "./index-z4qazajn.js";
17
+ } from "./index-844b3qn9.js";
18
+ import"./index-0v6na3yp.js";
18
19
  export {
19
20
  UI,
20
21
  TAX,
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import {
2
+ PRIVATE_KEY_VAR,
3
+ SECRETS_FILE,
2
4
  box,
3
5
  captureVoid,
4
6
  deployCloudflare,
@@ -14,24 +16,28 @@ import {
14
16
  importHelp,
15
17
  initHelp,
16
18
  isVoidApp,
19
+ keysCommand,
17
20
  line,
21
+ plaintextSecretNames,
18
22
  preflight,
19
23
  printPreflight,
20
24
  row,
21
25
  runInherit,
22
26
  runVoid,
27
+ secretsCommand,
23
28
  version,
24
29
  voidAppsIn
25
- } from "./index-sbchkpa6.js";
30
+ } from "./index-yzezvy5h.js";
26
31
  import {
27
32
  LAYOUTS,
28
33
  readManifest,
29
34
  validate,
30
35
  zoneOf
31
- } from "./index-zh3tmj08.js";
36
+ } from "./index-pz6m2hkm.js";
32
37
  import {
33
38
  GROUPS
34
- } from "./index-z4qazajn.js";
39
+ } from "./index-844b3qn9.js";
40
+ import"./index-0v6na3yp.js";
35
41
 
36
42
  // src/deploy/index.ts
37
43
  import color from "picocolors";
@@ -113,6 +119,81 @@ async function preflightHelp() {
113
119
  ], width));
114
120
  return 0;
115
121
  }
122
+ async function secretsCliCommand(args) {
123
+ const project = await findProject();
124
+ if (!project) {
125
+ console.error("vc: no voidcommerce.json here.");
126
+ return 1;
127
+ }
128
+ return secretsCommand(project, args);
129
+ }
130
+ async function secretsHelp() {
131
+ const width = 80;
132
+ console.log(box("vc secrets", [
133
+ line(`Secrets that live in the repository, encrypted, in ${SECRETS_FILE}.`, width),
134
+ line("", width),
135
+ ...row("vc secrets", "what this shop needs, and whether it is declared and encrypted", width, 2),
136
+ ...row("vc secrets --init", `write ${SECRETS_FILE} with every required key as \`unset\``, width, 2),
137
+ ...row(`dotenvx set KEY '…' -f ${SECRETS_FILE}`, "set one, without decrypting the file", width, 2),
138
+ ...row(`dotenvx encrypt -f ${SECRETS_FILE}`, "encrypt anything still in the clear", width, 2),
139
+ line("", width),
140
+ line("The file is COMMITTED: values are ciphertext, key names are not, so a", width),
141
+ line("diff shows which secret changed without showing what it changed to.", width),
142
+ line(`The private key stays out — .env.keys locally, and ${PRIVATE_KEY_VAR}`, width),
143
+ line("as one build variable where the deploy runs.", width),
144
+ line("", width),
145
+ line("Tradeoff worth knowing: ciphertext in git is permanent, so a leaked key", width),
146
+ line("exposes rotated secrets too. `wrangler secret put` does not have that", width),
147
+ line("property, and stays available for anything you would rather keep out.", width)
148
+ ], width));
149
+ return 0;
150
+ }
151
+ async function keysCliCommand(args) {
152
+ const project = await findProject();
153
+ if (!project) {
154
+ console.error("vc: no voidcommerce.json here.");
155
+ return 1;
156
+ }
157
+ return keysCommand(project.manifest, project.root, args);
158
+ }
159
+ async function keysHelp() {
160
+ const width = 80;
161
+ console.log(box("vc keys", [
162
+ line("The key that encrypts this repository's secrets is DERIVED from", width),
163
+ line("CLOUDFLARE_API_TOKEN, salted with the account id. Nothing is stored.", width),
164
+ line("", width),
165
+ ...row("vc keys", "what this token derives, and whether it matches the repository", width, 2),
166
+ ...row("vc keys --rotate", "re-encrypt under a new token, while the old one still works", width, 2),
167
+ line("", width),
168
+ line("Whoever can deploy the worker can read its secrets, and nobody else can.", width),
169
+ line("The price: rotating the token orphans every encrypted value, a second", width),
170
+ line("admin's token derives a different key, and the build must use the same", width),
171
+ line("token the values were encrypted under. `vc keys` checks the derived", width),
172
+ line("public key against the committed one, so a mismatch is a clear refusal", width),
173
+ line("rather than a decryption error nobody can place.", width)
174
+ ], width));
175
+ return 0;
176
+ }
177
+ async function guardCommand() {
178
+ const project = await findProject();
179
+ if (!project)
180
+ return 0;
181
+ const bare = plaintextSecretNames(project.root);
182
+ if (bare.length === 0)
183
+ return 0;
184
+ console.error(`
185
+ ${color.red("✗ refusing the commit")}: ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE} ${bare.length === 1 ? "is" : "are"} not encrypted.
186
+
187
+ ` + ` ${bare.join(`
188
+ `)}
189
+
190
+ ` + ` bunx dotenvx encrypt -f ${SECRETS_FILE}
191
+
192
+ ` + color.dim(` Committing a secret in the clear cannot be undone by a later commit;
193
+ the value stays in the history and must be treated as burned.
194
+ `));
195
+ return 1;
196
+ }
116
197
 
117
198
  // src/init.ts
118
199
  import * as p2 from "@clack/prompts";
@@ -504,6 +585,9 @@ var EXTENDED = {
504
585
  import: { run: importCommand, help: importHelp },
505
586
  deploy: { run: deployCommand, help: deployHelp },
506
587
  preflight: { run: preflightCommand, help: preflightHelp },
588
+ secrets: { run: secretsCliCommand, help: secretsHelp },
589
+ keys: { run: keysCliCommand, help: keysHelp },
590
+ guard: { run: guardCommand, help: async () => guardCommand() },
507
591
  dev: { run: appScript("dev"), help: appScriptHelp("dev") },
508
592
  build: { run: appScript("build"), help: appScriptHelp("build") },
509
593
  preview: { run: appScript("preview"), help: appScriptHelp("preview") }
@@ -16,7 +16,9 @@ import type { Project } from "../project";
16
16
  * secret-class key and every `unset` is removed from dist/ssr/wrangler.json
17
17
  * before it is uploaded; the worker reads those from `wrangler secret`.
18
18
  * 6. apply the committed migrations to the remote D1
19
- * 7. wrangler deploy, on exactly the config that was scrubbed
19
+ * 7. decrypt the repository's own secrets, if it keeps any
20
+ * 8. wrangler deploy, on exactly the config that was scrubbed, carrying
21
+ * those secrets up with the version
20
22
  */
21
23
  export interface CloudflareOptions {
22
24
  provision: boolean;
@@ -14,3 +14,15 @@ export declare function preflightCommand(args: string[]): Promise<number>;
14
14
  /** void's deploy help, then what vc adds. */
15
15
  export declare function deployHelp(): Promise<number>;
16
16
  export declare function preflightHelp(): Promise<number>;
17
+ /** `vc secrets` — what the repository declares, and whether it is encrypted. */
18
+ export declare function secretsCliCommand(args: string[]): Promise<number>;
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>;
@@ -0,0 +1,34 @@
1
+ import type { Manifest } from "../manifest";
2
+ /** Where the token comes from, in the order wrangler itself reads them. */
3
+ export declare function apiToken(): string | null;
4
+ /**
5
+ * The private key this token and account derive.
6
+ *
7
+ * The account id is the salt rather than a constant, so the same token used
8
+ * against two accounts does not produce one key — tokens are often scoped to
9
+ * several accounts, and a shared key across them would be a wider blast
10
+ * radius than anyone asked for.
11
+ */
12
+ export declare function derivePrivateKey(token: string, accountId: string): string;
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. */
16
+ export declare function committedPublicKey(root: string): string | null;
17
+ export type KeyState = {
18
+ ok: true;
19
+ privateKey: string;
20
+ publicKey: string;
21
+ fresh: boolean;
22
+ } | {
23
+ ok: false;
24
+ reason: string;
25
+ };
26
+ /**
27
+ * The key for this repository, or a refusal that says exactly what is wrong.
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>;
@@ -17,6 +17,8 @@ export interface Preflight {
17
17
  remote: Set<string> | null;
18
18
  missing: EnvKey[];
19
19
  routeProblem: string | null;
20
+ /** Names in `.env.secrets` whose value was committed unencrypted. */
21
+ bareSecrets: string[];
20
22
  ready: boolean;
21
23
  }
22
24
  export type SecretSource = "wrangler" | "void";
@@ -0,0 +1,74 @@
1
+ import type { Project } from "../project";
2
+ /**
3
+ * Secrets that live in the repository, encrypted.
4
+ *
5
+ * ── The problem this solves ──────────────────────────────────────────────
6
+ *
7
+ * A shop's secrets were set by hand, one `wrangler secret put` at a time,
8
+ * on a machine that happened to have them. Nothing in the repository said
9
+ * what the values WERE, only what they were called, so the shop could not
10
+ * be rebuilt from a checkout and a new deploy target needed somebody to
11
+ * remember. That is the last manual step between "push" and "live".
12
+ *
13
+ * dotenvx closes it. `.env.secrets` holds ciphertext and a public key, and
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 not stored at all — it is derived from the Cloudflare API
17
+ * token, so whoever can deploy the worker can read its secrets and nobody
18
+ * else can. See `./keys.ts`, which also spells out what that costs.
19
+ *
20
+ * ── Why the values still become worker SECRETS ───────────────────────────
21
+ *
22
+ * The lazy version of this would decrypt at build time and let the values
23
+ * land in the worker's `vars`. That undoes the thing we built the scrub
24
+ * step for: `vars` are plaintext in the deployed configuration and readable
25
+ * by anyone with dashboard access. So the plaintext exists only inside the
26
+ * build, in a temp file, and goes up through `wrangler deploy
27
+ * --secrets-file`, which stores them as real secrets and — importantly —
28
+ * applies ADDITIVELY, so a secret this file does not name is left alone
29
+ * rather than deleted.
30
+ *
31
+ * ── The tradeoffs, stated ────────────────────────────────────────────────
32
+ *
33
+ * Ciphertext in git is permanent. A key that ever leaks reads every secret
34
+ * in the history, including ones that were rotated — which is not true of
35
+ * `wrangler secret put`, where a rotation genuinely retires the old value.
36
+ *
37
+ * And because the key is derived, rotating the API token orphans the lot.
38
+ * `keys.ts` guards that with a public-key check, so the failure is a clear
39
+ * refusal rather than a decryption error nobody can place — but the guard
40
+ * cannot recover values, only explain them.
41
+ *
42
+ * Both are the price of a shop that rebuilds from a checkout, and both
43
+ * should be decisions rather than surprises.
44
+ */
45
+ export declare const SECRETS_FILE = ".env.secrets";
46
+ export declare const PRIVATE_KEY_VAR = "DOTENV_PRIVATE_KEY_SECRETS";
47
+ /** dotenvx, from the project's own install rather than a global one. */
48
+ export declare function findDotenvx(from: string): string | null;
49
+ /**
50
+ * The names a `.env.secrets` declares, read WITHOUT decrypting.
51
+ *
52
+ * dotenvx leaves keys in plaintext and encrypts only values, so preflight
53
+ * can say "all five are declared" on a machine that cannot read any of them.
54
+ */
55
+ export declare function declaredSecretNames(root: string): Set<string>;
56
+ /** Is a declared value actually encrypted, or was it committed in the clear? */
57
+ export declare function plaintextSecretNames(root: string): string[];
58
+ export interface DecryptedSecrets {
59
+ /** A temp file in `.env` format, for `wrangler deploy --secrets-file`. */
60
+ path: string;
61
+ names: string[];
62
+ cleanup: () => void;
63
+ }
64
+ /**
65
+ * Decrypt into a temp file the deploy can hand to wrangler.
66
+ *
67
+ * Written to disk rather than piped because that is what `--secrets-file`
68
+ * takes, and removed by `cleanup()` whatever happens next.
69
+ */
70
+ export declare function decryptSecrets(project: Project): Promise<DecryptedSecrets | {
71
+ error: string;
72
+ }>;
73
+ /** `vc secrets` — what the repository declares, and whether it is readable here. */
74
+ export declare function secretsCommand(project: Project, args: string[]): Promise<number>;
@@ -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 };
@@ -1,6 +1,3 @@
1
- import { createRequire } from "node:module";
2
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
-
4
1
  // src/catalog.ts
5
2
  var SIGN_IN = {
6
3
  id: "signIn",
@@ -586,4 +583,4 @@ var GROUPS = [
586
583
  ];
587
584
  var CHOICES = new Map(GROUPS.flatMap((group) => group.choices.map((choice) => [choice.id, choice])));
588
585
 
589
- export { __require, SIGN_IN, AUTH_PLUGINS, COMMERCE_CATALOG, COMMERCE_SALES, COMMERCE_OPERATIONS, PAYMENT, TAX, CARRIERS, ERP, EMAIL, DNS, CONTENT, UI, GROUPS, CHOICES };
586
+ export { SIGN_IN, AUTH_PLUGINS, COMMERCE_CATALOG, COMMERCE_SALES, COMMERCE_OPERATIONS, PAYMENT, TAX, CARRIERS, ERP, EMAIL, DNS, CONTENT, UI, GROUPS, CHOICES };