@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 +66 -0
- package/dist/templates/node/README.md +19 -4
- package/dist/templates/node/package.json +3 -3
- package/dist/templates/node/server.env.example +2 -2
- package/dist/templates/node/vault.env.example +3 -3
- package/dist/templates/workers/README.md +21 -8
- package/dist/templates/workers/app/wrangler.jsonc +1 -0
- package/dist/templates/workers/package.json +4 -4
- package/dist/templates/workers/vault/wrangler.jsonc +2 -2
- package/package.json +3 -3
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
17
|
-
"@coffre/vault": "0.1.
|
|
16
|
+
"@coffre/server": "0.1.1",
|
|
17
|
+
"@coffre/vault": "0.1.1"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@coffre/conformance": "0.1.
|
|
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
|
-
#
|
|
17
|
-
#
|
|
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
|
-
#
|
|
9
|
-
# and
|
|
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
|
-
#
|
|
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`,
|
|
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.
|
|
61
|
+
## 3. Keys
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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.
|
|
18
|
-
"@coffre/ui": "0.1.
|
|
19
|
-
"@coffre/vault": "0.1.
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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/
|
|
29
|
-
"@coffre/
|
|
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",
|