@coffre/cli 0.1.0 → 0.1.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/main.js CHANGED
@@ -5,6 +5,7 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync,
5
5
  import { homedir, hostname } from "node:os";
6
6
  import { basename, dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { randomBytes } from "node:crypto";
8
9
  //#region src/init.ts
9
10
  const KINDS = ["workers", "node"];
10
11
  /** Where a template lives: in the package when published, else in the repository. */
@@ -68,6 +69,67 @@ function init(kind, target, version) {
68
69
  return written;
69
70
  }
70
71
  //#endregion
72
+ //#region src/keys.ts
73
+ /**
74
+ * Three fresh keys, each 32 random bytes in base64, and an id for the KEK
75
+ * dated to the day, so that a rotation, even one the same month, gets a new
76
+ * one: the vault refuses two KEKs with the same id.
77
+ */
78
+ function generateKeys(now = /* @__PURE__ */ new Date()) {
79
+ const key = () => randomBytes(32).toString("base64");
80
+ return {
81
+ KEK_ID: `kek-${now.toISOString().slice(0, 10)}`,
82
+ KEK: key(),
83
+ SIGNING_KEY: key(),
84
+ AUDIT_CHAIN_KEY: key()
85
+ };
86
+ }
87
+ /** The keys as a dotenv block, then, as comments, what each is for and where it goes. */
88
+ function formatKeys(keys) {
89
+ return `KEK_ID=${keys.KEK_ID}
90
+ KEK=${keys.KEK}
91
+ SIGNING_KEY=${keys.SIGNING_KEY}
92
+ AUDIT_CHAIN_KEY=${keys.AUDIT_CHAIN_KEY}
93
+
94
+ # Save all four in your password manager now. They are shown once, and
95
+ # coffre keeps no copy.
96
+ #
97
+ # The vault gets KEK_ID, KEK and SIGNING_KEY; the app gets AUDIT_CHAIN_KEY.
98
+ # They are kept apart so that the app, which faces the network, never holds
99
+ # what decrypts a value: whoever has the KEK and a copy of the database has
100
+ # every value.
101
+ #
102
+ # KEK decrypts every value. Lose it, and every value is lost.
103
+ # SIGNING_KEY signs the vault's log entries, checkpoints and member
104
+ # rows. Lose it, and the log stops verifying and every
105
+ # member is refused.
106
+ # AUDIT_CHAIN_KEY signs the app's log entries, sessions and tokens. Lose
107
+ # it, and the log stops verifying and everyone is signed
108
+ # out.
109
+ # KEK_ID names the KEK; not secret.
110
+ #
111
+ # On Workers, KEK_ID is a var in vault/wrangler.jsonc, and the other three are
112
+ # Worker secrets (wrangler secret put). On Node, they go in vault.env and
113
+ # server.env.
114
+ #
115
+ # These are for a new deployment. To rotate a deployment's KEK, take only
116
+ # KEK_ID and KEK, and keep the old pair in previousKeks: its SIGNING_KEY and
117
+ # AUDIT_CHAIN_KEY cannot be changed.
118
+ `;
119
+ }
120
+ function keys(args) {
121
+ const { values } = parseArgs({
122
+ args,
123
+ options: { json: {
124
+ type: "boolean",
125
+ default: false
126
+ } },
127
+ allowPositionals: false
128
+ });
129
+ const fresh = generateKeys();
130
+ process.stdout.write(values.json ? `${JSON.stringify(fresh)}\n` : formatKeys(fresh));
131
+ }
132
+ //#endregion
71
133
  //#region src/instance.ts
72
134
  const MODES = ["signin", "cloudflare"];
73
135
  function emptyStore() {
@@ -1531,6 +1593,7 @@ const USAGE = `coffre - secrets, with an audit log
1531
1593
  New deployment
1532
1594
  coffre init --workers [<dir>] two Cloudflare Workers: the app and its vault
1533
1595
  coffre init --node [<dir>] a Node server, and its vault beside it
1596
+ coffre keys [--json] its three keys and the KEK's id, made here and shown once
1534
1597
 
1535
1598
  Session
1536
1599
  coffre login [<url>] [--no-browser] sign in, and make <url> the current instance
@@ -1579,6 +1642,9 @@ switch (command) {
1579
1642
  case "init":
1580
1643
  initProject(rest);
1581
1644
  break;
1645
+ case "keys":
1646
+ keys(rest);
1647
+ break;
1582
1648
  case "login":
1583
1649
  await login(rest);
1584
1650
  break;
@@ -22,10 +22,25 @@ cp vault.env.example vault.env
22
22
  ```
23
23
 
24
24
  Fill both in: `PUBLIC_URL`, a GitHub OAuth app whose callback is
25
- `<PUBLIC_URL>/auth/callback/github`, `ROOT_ADMINS`, and three keys from
26
- `openssl rand -base64 32`. Escrow `KEK` and its `KEK_ID`, `SIGNING_KEY`, `AUDIT_CHAIN_KEY` and the OAuth
27
- client secret in a password manager. Without the KEK, stored values cannot
28
- be read; without the other keys, the existing log cannot be verified.
25
+ `<PUBLIC_URL>/auth/callback/github`, `ROOT_ADMINS`, and the keys from
26
+
27
+ ```sh
28
+ coffre keys
29
+ ```
30
+
31
+ Run it with the CLI you ran `coffre init` with, or as
32
+ `npx @coffre/cli keys`. It prints three keys and the KEK's id, once, and
33
+ keeps no copy. Save its output in your password manager, with the OAuth
34
+ client secret, before anything else. `KEK_ID`, `KEK` and `SIGNING_KEY` go in
35
+ `vault.env`, and `AUDIT_CHAIN_KEY` in `server.env`, so that the server,
36
+ which faces the network, never holds what decrypts a value:
37
+
38
+ - `KEK` decrypts every value. Lose it, and every value is lost.
39
+ - `SIGNING_KEY` signs the vault's log entries and member rows.
40
+ - `AUDIT_CHAIN_KEY` signs the server's log entries, sessions and tokens.
41
+
42
+ Lose either of the last two, and the log stops verifying and everyone is
43
+ locked out.
29
44
 
30
45
  ## 2. The database
31
46
 
@@ -13,11 +13,11 @@
13
13
  "conformance": "coffre-conformance node"
14
14
  },
15
15
  "dependencies": {
16
- "@coffre/server": "0.1.0",
17
- "@coffre/vault": "0.1.0"
16
+ "@coffre/server": "0.1.1",
17
+ "@coffre/vault": "0.1.1"
18
18
  },
19
19
  "devDependencies": {
20
- "@coffre/conformance": "0.1.0",
20
+ "@coffre/conformance": "0.1.1",
21
21
  "@types/node": "26.1.1",
22
22
  "typescript": "5.9.3"
23
23
  }
@@ -13,6 +13,6 @@ VAULT_SOCKET=vault.sock
13
13
  GITHUB_CLIENT_ID=
14
14
  GITHUB_CLIENT_SECRET=
15
15
 
16
- # 32 random bytes, base64 (`openssl rand -base64 32`): keys the audit log's
17
- # hash chain.
16
+ # From `coffre keys`, saved in your password manager first: signs the
17
+ # server's log entries, sessions and tokens.
18
18
  AUDIT_CHAIN_KEY=
@@ -5,11 +5,11 @@ VAULT_SOCKET=vault.sock
5
5
  # The server's Postgres database, as the vault's own login.
6
6
  DATABASE_URL=postgres://coffre_vault_runtime:CHANGE_ME@127.0.0.1:5432/coffre
7
7
 
8
- # The key-encryption key: 32 random bytes, base64 (`openssl rand -base64 32`),
9
- # and a name for it. A rotation gives the new one a new name.
8
+ # From `coffre keys`, saved in your password manager first.
9
+ # The key-encryption key, and its name. A rotation gives the new one a new name.
10
10
  KEK_ID=kek-1
11
11
  KEK=
12
- # 32 random bytes, base64: signs the audit checkpoints.
12
+ # Signs the vault's log entries, checkpoints and member rows.
13
13
  SIGNING_KEY=
14
14
 
15
15
  # Comma-separated emails: the first people in, whom nobody can remove.
@@ -17,7 +17,7 @@ Worker secrets. Everything below runs from this directory.
17
17
  - `app/wrangler.jsonc`: `PUBLIC_URL`, and `GITHUB_CLIENT_ID` from a GitHub
18
18
  OAuth app whose callback is `<PUBLIC_URL>/auth/callback/github`.
19
19
  - `vault/wrangler.jsonc`: `ROOT_ADMINS`, the emails of the first people in,
20
- and `KEK_ID`, a name for the key-encryption key below.
20
+ and `KEK_ID`, which `coffre keys` gives you below.
21
21
 
22
22
  ## 2. The database
23
23
 
@@ -58,11 +58,26 @@ Put the ids they print under `hyperdrive`, the first in
58
58
  `pnpm migrate` again after every upgrade of `@coffre/server`, before
59
59
  deploying it.
60
60
 
61
- ## 3. Secrets
61
+ ## 3. Keys
62
62
 
63
- Generate three separate keys with `openssl rand -base64 32` and save them
64
- in your password manager first. The commands below prompt for the saved
65
- values; save the GitHub client secret there too.
63
+ ```sh
64
+ coffre keys
65
+ ```
66
+
67
+ Run it with the CLI you ran `coffre init` with, or as
68
+ `npx @coffre/cli keys`. It prints three keys and the KEK's id, once, and
69
+ keeps no copy. Save its output in your password manager, with the GitHub
70
+ client secret, before anything else. The vault gets `KEK` and `SIGNING_KEY`,
71
+ and the app `AUDIT_CHAIN_KEY`, so that the app, which faces the network,
72
+ never holds what decrypts a value:
73
+
74
+ - `KEK` decrypts every value. Lose it, and every value is lost.
75
+ - `SIGNING_KEY` signs the vault's log entries and member rows.
76
+ - `AUDIT_CHAIN_KEY` signs the app's log entries, sessions and tokens.
77
+
78
+ Lose either of the last two, and the log stops verifying and everyone is
79
+ locked out. Put `KEK_ID` in `vault/wrangler.jsonc`, then set the secrets;
80
+ each command prompts for the saved value:
66
81
 
67
82
  ```sh
68
83
  pnpm exec wrangler secret put KEK -c vault/wrangler.jsonc
@@ -71,9 +86,7 @@ pnpm exec wrangler secret put AUDIT_CHAIN_KEY -c app/wrangler.jsonc
71
86
  pnpm exec wrangler secret put GITHUB_CLIENT_SECRET -c app/wrangler.jsonc
72
87
  ```
73
88
 
74
- Escrow `KEK` with its `KEK_ID`, `SIGNING_KEY` and `AUDIT_CHAIN_KEY`.
75
- Without the KEK, stored values cannot be read; without the other keys, the
76
- existing log cannot be verified. Keep older KEKs after rotation too.
89
+ Keep older KEKs after a rotation too: what they wrapped still needs them.
77
90
 
78
91
  ## 4. Deploy
79
92
 
@@ -17,6 +17,7 @@
17
17
  "GITHUB_CLIENT_ID": "replace-with-your-client-id"
18
18
  },
19
19
  "secrets": {
20
+ // AUDIT_CHAIN_KEY is from `coffre keys`; the app never gets the vault's keys.
20
21
  "required": ["GITHUB_CLIENT_SECRET", "AUDIT_CHAIN_KEY"]
21
22
  },
22
23
  // `wrangler hyperdrive create coffre --caching-disabled --connection-string=<runtime login URL>`
@@ -14,13 +14,13 @@
14
14
  "conformance": "coffre-conformance workers"
15
15
  },
16
16
  "dependencies": {
17
- "@coffre/server": "0.1.0",
18
- "@coffre/ui": "0.1.0",
19
- "@coffre/vault": "0.1.0"
17
+ "@coffre/server": "0.1.1",
18
+ "@coffre/ui": "0.1.1",
19
+ "@coffre/vault": "0.1.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@cloudflare/workers-types": "5.20260920.1",
23
- "@coffre/conformance": "0.1.0",
23
+ "@coffre/conformance": "0.1.1",
24
24
  "typescript": "5.9.3",
25
25
  "wrangler": "4.118.0"
26
26
  }
@@ -11,13 +11,13 @@
11
11
  "workers_dev": false,
12
12
  "observability": { "enabled": true },
13
13
  "vars": {
14
- // Names the KEK below; a rotation gives the new one a new id.
14
+ // Names the KEK below: from `coffre keys`. A rotation gives the new one a new id.
15
15
  "KEK_ID": "kek-1",
16
16
  // Comma-separated emails: the first people in, whom nobody can remove.
17
17
  "ROOT_ADMINS": "you@example.com"
18
18
  },
19
19
  "secrets": {
20
- // Each 32 random bytes, base64: `openssl rand -base64 32`.
20
+ // From `coffre keys`: wrangler secret put KEK, then SIGNING_KEY.
21
21
  "required": ["KEK", "SIGNING_KEY"]
22
22
  },
23
23
  // The app's database, as the vault's own login: `wrangler hyperdrive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffre/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The coffre command line: secrets in your shell, and `coffre init` for a new deployment.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,8 +25,8 @@
25
25
  "@types/node": "26.1.1",
26
26
  "tsdown": "0.23.0",
27
27
  "typescript": "5.9.3",
28
- "@coffre/client": "0.1.0",
29
- "@coffre/core": "0.1.0"
28
+ "@coffre/core": "0.1.1",
29
+ "@coffre/client": "0.1.1"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsdown && node scripts/copy-templates.ts",