@coffre/cli 0.1.2 → 0.1.3

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.
@@ -9,7 +9,7 @@ Two processes, configured in code:
9
9
  only the server, on a Unix socket.
10
10
 
11
11
  Run them as two users that share a group, and the process facing the network
12
- never holds the KEK. For local development, `server.ts` can run the vault
12
+ never holds the vault key. For local development, `server.ts` can run the vault
13
13
  in its own process instead; see the comment there. Everything below runs
14
14
  from this directory, on Node 24 or later.
15
15
 
@@ -35,23 +35,26 @@ npx @coffre/cli setup
35
35
  ```
36
36
 
37
37
  Run it with the CLI you ran `coffre init` with. It asks for the database
38
- administrator's connection string at a hidden prompt (a script can pipe it
39
- in, or set `COFFRE_SETUP_DATABASE_URL`; never pass it as an argument). It
38
+ administrator's connection string at a hidden prompt. A script can pipe it
39
+ in, or set `COFFRE_SETUP_DATABASE_URL`; never pass it as an argument. It
40
40
  makes the two logins coffre runs as, `coffre_runtime` for the server and
41
- `coffre_vault_runtime` for the vault, migrates the database, checks that
42
- each login holds only its rights, and prints every value at once, as one
43
- block for each process. It keeps no copy and writes no file. Save its
44
- output in your password manager, with the OAuth client secret, before
45
- anything else. Then the app's block goes in `server.env` and the vault's in
46
- `vault.env`: one key for each process, so that the server, which faces the
47
- network, never holds what decrypts a value.
48
-
49
- - `KEK`, the vault's, decrypts every value, and the vault derives from it the
50
- key it signs its records with. Lose it, and every value is lost.
51
- - `AUDIT_CHAIN_KEY`, the server's, signs the server's log entries, sessions
52
- and tokens. Lose it, and everyone is signed out and the log stops
53
- verifying.
54
- - Each `DATABASE_URL` is the same database through that process's own login.
41
+ `coffre_vault_runtime` for the vault, migrates the database, and checks that
42
+ each login holds only its rights.
43
+
44
+ Then it shows five values on a screen of their own, which leaves nothing
45
+ behind in your scrollback. Copy each with `c` into your password manager,
46
+ beside the OAuth client secret, then into its file; `w` shows where each
47
+ goes. Nothing keeps a copy. There is one key for each process, so that the
48
+ server, which faces the network, never holds what decrypts a value.
49
+
50
+ - `server.env` takes the app key, `APP_KEY`, and the app's database URL, as
51
+ `DATABASE_URL`. The app key signs the server's log entries, sessions and
52
+ tokens. Lose it, and everyone is signed out and the log stops verifying.
53
+ - `vault.env` takes the vault ID, `VAULT_KEY_ID`, the vault key, `VAULT_KEY`,
54
+ and the vault's database URL, as `DATABASE_URL`. The vault key decrypts
55
+ every value, and the vault derives from it the key it signs its records
56
+ with. Lose it, and every value is lost. The vault ID only names it.
57
+ - Both URLs are the same database, each through its process's own login.
55
58
  Neither process gets the administrator's URL.
56
59
 
57
60
  With AWS KMS instead of a key of your own, the vault also needs a
@@ -13,11 +13,11 @@
13
13
  "conformance": "coffre-conformance node"
14
14
  },
15
15
  "dependencies": {
16
- "@coffre/server": "0.1.2",
17
- "@coffre/vault": "0.1.2"
16
+ "@coffre/server": "0.1.3",
17
+ "@coffre/vault": "0.1.3"
18
18
  },
19
19
  "devDependencies": {
20
- "@coffre/conformance": "0.1.2",
20
+ "@coffre/conformance": "0.1.3",
21
21
  "@types/node": "26.1.1",
22
22
  "typescript": "5.9.3"
23
23
  }
@@ -14,6 +14,6 @@ VAULT_SOCKET=vault.sock
14
14
  GITHUB_CLIENT_ID=
15
15
  GITHUB_CLIENT_SECRET=
16
16
 
17
- # From `coffre setup`, saved in your password manager first: signs the
18
- # server's log entries, sessions and tokens.
19
- AUDIT_CHAIN_KEY=
17
+ # From `coffre setup`, saved in your password manager first: the app key,
18
+ # which signs the server's log entries, sessions and tokens.
19
+ APP_KEY=
@@ -1,5 +1,5 @@
1
1
  // coffre's server: the API, sign-in, pages and scheduled job, in one
2
- // process. It holds no KEK: it asks the vault, a process of its own
2
+ // process. It holds no vault key: it asks the vault, a process of its own
3
3
  // (src/vault.ts), over a Unix socket. Settings come from server.env.
4
4
  import { github, serve, signin } from '@coffre/server/node';
5
5
  import { connectVault } from '@coffre/vault/node';
@@ -28,7 +28,7 @@ const server = await serve({
28
28
  }),
29
29
  ],
30
30
  }),
31
- auditChainKey: env('AUDIT_CHAIN_KEY'),
31
+ auditChainKey: env('APP_KEY'),
32
32
  });
33
33
  console.log(`coffre is listening on ${server.url}`);
34
34
 
@@ -1,7 +1,7 @@
1
1
  // The vault: the keys, and the members and grants, which it keeps in the
2
2
  // server's database through a login of its own. It answers only on a Unix
3
3
  // socket, which it makes 0660: run it as its own user, sharing a group with
4
- // the server's, and nothing that faces the network can read the KEK.
4
+ // the server's, and nothing that faces the network can read the vault key.
5
5
  // Settings come from vault.env.
6
6
  import { serveVault } from '@coffre/vault/node';
7
7
 
@@ -14,13 +14,13 @@ function env(name: string): string {
14
14
  const vault = await serveVault({
15
15
  socket: env('VAULT_SOCKET'),
16
16
  database: env('DATABASE_URL'),
17
- kek: { id: env('KEK_ID'), key: env('KEK') },
18
- // After a rotation, the KEKs before it, so the data keys they wrapped still
19
- // open and the records signed under them before it still verify:
20
- // previousKeks: [{ id: 'kek-1', key: env('KEK_1') }],
17
+ kek: { id: env('VAULT_KEY_ID'), key: env('VAULT_KEY') },
18
+ // After a rotation, the vault keys before it, so the data keys they wrapped
19
+ // still open and the records signed under them before it still verify:
20
+ // previousKeks: [{ id: 'vault-2026-04-01-k7q2xm', key: env('OLD_VAULT_KEY') }],
21
21
  rootAdmins: env('ROOT_ADMINS').split(',').map((email) => email.trim()),
22
- // The vault derives its signing key from the KEK. With a KEK a key service
23
- // holds, such as awsKms(…), it needs one of its own: signingKey: env('SIGNING_KEY').
22
+ // The vault derives its signing key from the vault key. With a key a key
23
+ // service holds, such as awsKms(…), it needs one of its own: signingKey: env('SIGNING_KEY').
24
24
  });
25
25
  console.log(`the vault is listening on ${vault.socket}`);
26
26
 
@@ -5,11 +5,12 @@ VAULT_SOCKET=vault.sock
5
5
  # From `coffre setup`: the server's 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
- # From `coffre setup`, saved in your password manager first: the
9
- # key-encryption key, and its name. A rotation gives the new one a new name.
10
- # The vault derives the key it signs its records with from it.
11
- KEK_ID=kek-1
12
- KEK=
8
+ # From `coffre setup`, saved in your password manager first: the vault key,
9
+ # which decrypts every value, and its ID, which is not secret. A rotation
10
+ # gives the new key a new ID. The vault derives the key it signs its
11
+ # records with from it.
12
+ VAULT_KEY_ID=vault-1
13
+ VAULT_KEY=
13
14
 
14
15
  # Comma-separated emails: the first people in, whom nobody can remove.
15
16
  ROOT_ADMINS=you@example.com
@@ -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`, which `coffre setup` gives you below.
20
+ and `VAULT_KEY_ID`, which `coffre setup` gives you below.
21
21
 
22
22
  ## 2. The database and keys
23
23
 
@@ -28,48 +28,53 @@ npx @coffre/cli setup
28
28
  ```
29
29
 
30
30
  Run it with the CLI you ran `coffre init` with. It asks for the database
31
- administrator's connection string at a hidden prompt (a script can pipe it
32
- in, or set `COFFRE_SETUP_DATABASE_URL`; never pass it as an argument). It
31
+ administrator's connection string at a hidden prompt. A script can pipe it
32
+ in, or set `COFFRE_SETUP_DATABASE_URL`; never pass it as an argument. It
33
33
  makes the two logins coffre runs as, `coffre_runtime` for the app and
34
- `coffre_vault_runtime` for the vault, migrates the database, checks that
35
- each login holds only its rights, and prints every value at once: the two
36
- keys, the KEK's id, and each login's connection string. It keeps no copy and
37
- writes no file. Save its output in your password manager, with the GitHub
38
- client secret, before anything else. There is one key for each Worker, so
34
+ `coffre_vault_runtime` for the vault, migrates the database, and checks that
35
+ each login holds only its rights.
36
+
37
+ Then it shows five values on a screen of their own, which leaves nothing
38
+ behind in your scrollback: the app key and the app's database URL, and the
39
+ vault ID, the vault key and the vault's database URL. Copy each into your
40
+ password manager with `c`, beside the GitHub client secret; `w` shows where
41
+ each one goes. Nothing keeps a copy. There is one key for each Worker, so
39
42
  that the app, which faces the network, never holds what decrypts a value:
40
43
 
41
- - `KEK`, the vault's, decrypts every value, and the vault derives from it the
42
- key it signs its records with. Lose it, and every value is lost.
43
- - `AUDIT_CHAIN_KEY`, the app's, signs the app's log entries, sessions and
44
- tokens. Lose it, and everyone is signed out and the log stops verifying.
44
+ - `VAULT_KEY`, the vault key, decrypts every value, and the vault derives
45
+ from it the key it signs its records with. Lose it, and every value is
46
+ lost.
47
+ - `APP_KEY`, the app key, signs the app's log entries, sessions and tokens.
48
+ Lose it, and everyone is signed out and the log stops verifying.
49
+ - `VAULT_KEY_ID`, the vault ID, names the vault key. It is not secret.
45
50
 
46
51
  To do the same by hand, see
47
52
  [deploy.md](https://github.com/erwinkn/coffre/blob/main/docs/deploy.md#appendix-the-database-by-hand).
48
53
 
49
54
  ## 3. Hyperdrive and secrets
50
55
 
51
- Run the two `wrangler hyperdrive create` commands setup printed: one config
52
- per login, each `--caching-disabled`. Hyperdrive otherwise caches reads for
53
- up to a minute, and a revoked token or a signed-out session could keep
54
- working that long. `wrangler.jsonc` cannot set it, so for a config made
56
+ Run the two `wrangler hyperdrive create` commands from setup's screen: one
57
+ config per login, each `--caching-disabled`. Hyperdrive otherwise caches
58
+ reads for up to a minute, and a revoked token or a signed-out session could
59
+ keep working that long. `wrangler.jsonc` cannot set it, so for a config made
55
60
  another way, check `caching` in `wrangler hyperdrive get <id>`.
56
61
 
57
62
  Put the ids they print under `hyperdrive`, the first in
58
- `app/wrangler.jsonc` and the second in `vault/wrangler.jsonc`, and `KEK_ID`
59
- in `vault/wrangler.jsonc`. Then set the secrets; each command prompts for
60
- the saved value:
63
+ `app/wrangler.jsonc` and the second in `vault/wrangler.jsonc`, and
64
+ `VAULT_KEY_ID` in `vault/wrangler.jsonc`. Then set the secrets; each command
65
+ prompts for the saved value:
61
66
 
62
67
  ```sh
63
- pnpm exec wrangler secret put KEK -c vault/wrangler.jsonc
64
- pnpm exec wrangler secret put AUDIT_CHAIN_KEY -c app/wrangler.jsonc
68
+ pnpm exec wrangler secret put VAULT_KEY -c vault/wrangler.jsonc
69
+ pnpm exec wrangler secret put APP_KEY -c app/wrangler.jsonc
65
70
  pnpm exec wrangler secret put GITHUB_CLIENT_SECRET -c app/wrangler.jsonc
66
71
  ```
67
72
 
68
73
  Run `pnpm migrate`, with the administrator's URL in `DATABASE_URL`, after
69
74
  every upgrade of `@coffre/server`, before deploying it.
70
75
 
71
- Keep older KEKs after a rotation, for good: what they wrapped still needs
72
- them, and so does what the vault signed under them before it. With AWS KMS
76
+ Keep older vault keys after a rotation, for good: what they wrapped still
77
+ needs them, and so does what the vault signed under them before it. With AWS KMS
73
78
  instead of a key of your own, the vault also needs a `SIGNING_KEY`
74
79
  ([keys](https://github.com/erwinkn/coffre/blob/main/docs/keys.md#aws-kms)).
75
80
 
@@ -12,7 +12,7 @@ type Env = {
12
12
  /** Set for GitHub Enterprise Server; github.com otherwise. */
13
13
  GITHUB_URL?: string;
14
14
  GITHUB_API_URL?: string;
15
- AUDIT_CHAIN_KEY: string;
15
+ APP_KEY: string;
16
16
  };
17
17
 
18
18
  export default coffre((env: Env) => ({
@@ -29,5 +29,5 @@ export default coffre((env: Env) => ({
29
29
  }),
30
30
  ],
31
31
  }),
32
- auditChainKey: env.AUDIT_CHAIN_KEY,
32
+ auditChainKey: env.APP_KEY,
33
33
  }));
@@ -17,11 +17,11 @@
17
17
  "GITHUB_CLIENT_ID": "replace-with-your-client-id"
18
18
  },
19
19
  "secrets": {
20
- // AUDIT_CHAIN_KEY is from `coffre setup`; the app never gets the vault's keys.
21
- "required": ["GITHUB_CLIENT_SECRET", "AUDIT_CHAIN_KEY"]
20
+ // APP_KEY is from `coffre setup`; the app never gets the vault's keys.
21
+ "required": ["GITHUB_CLIENT_SECRET", "APP_KEY"]
22
22
  },
23
- // `wrangler hyperdrive create coffre --caching-disabled --connection-string=<runtime login URL>`
24
- // prints the id. Caching must stay off, or a revoked session keeps working
23
+ // `wrangler hyperdrive create coffre --caching-disabled`, as `coffre setup`
24
+ // shows it, prints the id. Caching must stay off, or a revoked session keeps working
25
25
  // for about a minute, and this file cannot set it: see README.md. Locally,
26
26
  // CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE stands in for
27
27
  // the config.
@@ -14,13 +14,13 @@
14
14
  "conformance": "coffre-conformance workers"
15
15
  },
16
16
  "dependencies": {
17
- "@coffre/server": "0.1.2",
18
- "@coffre/ui": "0.1.2",
19
- "@coffre/vault": "0.1.2"
17
+ "@coffre/server": "0.1.3",
18
+ "@coffre/ui": "0.1.3",
19
+ "@coffre/vault": "0.1.3"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@cloudflare/workers-types": "5.20260920.1",
23
- "@coffre/conformance": "0.1.2",
23
+ "@coffre/conformance": "0.1.3",
24
24
  "typescript": "5.9.3",
25
25
  "wrangler": "4.118.0"
26
26
  }
@@ -5,18 +5,18 @@ import { postgres, vault } from '@coffre/vault/cloudflare';
5
5
 
6
6
  type Env = {
7
7
  VAULT_HYPERDRIVE: Hyperdrive;
8
- KEK_ID: string;
9
- KEK: string;
8
+ VAULT_KEY_ID: string;
9
+ VAULT_KEY: string;
10
10
  ROOT_ADMINS: string;
11
11
  };
12
12
 
13
13
  export default vault((env: Env) => ({
14
14
  database: postgres(env.VAULT_HYPERDRIVE),
15
- kek: { id: env.KEK_ID, key: env.KEK },
16
- // After a rotation, the KEKs before it, so the data keys they wrapped still
17
- // open and the records signed under them before it still verify:
18
- // previousKeks: [{ id: 'kek-2026-09', key: env.KEK_2026_09 }],
15
+ kek: { id: env.VAULT_KEY_ID, key: env.VAULT_KEY },
16
+ // After a rotation, the vault keys before it, so the data keys they wrapped
17
+ // still open and the records signed under them before it still verify:
18
+ // previousKeks: [{ id: 'vault-2026-04-01-k7q2xm', key: env.OLD_VAULT_KEY }],
19
19
  rootAdmins: env.ROOT_ADMINS.split(',').map((email) => email.trim()),
20
- // The vault derives its signing key from the KEK. With a KEK a key service
21
- // holds, such as awsKms(…), it needs one of its own: signingKey: env.SIGNING_KEY.
20
+ // The vault derives its signing key from the vault key. With a key a key
21
+ // service holds, such as awsKms(…), it needs one of its own: signingKey: env.SIGNING_KEY.
22
22
  }));
@@ -11,19 +11,20 @@
11
11
  "workers_dev": false,
12
12
  "observability": { "enabled": true },
13
13
  "vars": {
14
- // Names the KEK below: from `coffre setup`. A rotation gives the new one a new id.
15
- "KEK_ID": "kek-1",
14
+ // Names the vault key below, and is not secret: from `coffre setup`. A
15
+ // rotation gives the new key a new ID.
16
+ "VAULT_KEY_ID": "vault-1",
16
17
  // Comma-separated emails: the first people in, whom nobody can remove.
17
18
  "ROOT_ADMINS": "you@example.com"
18
19
  },
19
20
  "secrets": {
20
- // From `coffre setup`: wrangler secret put KEK. The vault derives the key
21
- // it signs its records with from it.
22
- "required": ["KEK"]
21
+ // The vault key, from `coffre setup`: wrangler secret put VAULT_KEY. The
22
+ // vault derives the key it signs its records with from it.
23
+ "required": ["VAULT_KEY"]
23
24
  },
24
25
  // The app's database, as the vault's own login: `wrangler hyperdrive
25
- // create coffre-vault --caching-disabled --connection-string=<vault login
26
- // URL>` prints the id. Its own config, not the app's: the vault writes
26
+ // create coffre-vault --caching-disabled`, as `coffre setup` shows it,
27
+ // prints the id. Its own config, not the app's: the vault writes
27
28
  // members and grants, which the app's login may only read. Locally,
28
29
  // CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_VAULT_HYPERDRIVE stands in.
29
30
  "hyperdrive": [{ "binding": "VAULT_HYPERDRIVE", "id": "replace-with-your-vault-hyperdrive-id" }]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffre/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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": {
@@ -27,9 +27,9 @@
27
27
  "pg": "8.16.3",
28
28
  "tsdown": "0.23.0",
29
29
  "typescript": "5.9.3",
30
- "@coffre/client": "0.1.2",
31
- "@coffre/core": "0.1.2",
32
- "@coffre/db": "0.1.2"
30
+ "@coffre/client": "0.1.3",
31
+ "@coffre/core": "0.1.3",
32
+ "@coffre/db": "0.1.3"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsdown && node scripts/copy-templates.ts && node scripts/copy-migrations.ts",
@@ -1,64 +0,0 @@
1
- import { parseArgs } from "node:util";
2
- import { randomBytes } from "node:crypto";
3
- //#region src/keys.ts
4
- /**
5
- * Two fresh keys, each 32 random bytes in base64, and an id for the KEK
6
- * dated to the day, so that a rotation, even one the same month, gets a new
7
- * one: the vault refuses two KEKs with the same id.
8
- */
9
- function generateKeys(now = /* @__PURE__ */ new Date()) {
10
- const key = () => randomBytes(32).toString("base64");
11
- return {
12
- KEK_ID: `kek-${now.toISOString().slice(0, 10)}`,
13
- KEK: key(),
14
- AUDIT_CHAIN_KEY: key()
15
- };
16
- }
17
- /** What each key is for, as comments: `coffre keys` and `coffre setup` both say it. */
18
- const KEYS_EXPLAINED = `# Two keys, one for each component, so that the app, which faces the
19
- # network, never holds what decrypts a value:
20
- #
21
- # KEK the vault's. Decrypts every value, and signs the vault's
22
- # log entries, member rows and checkpoints. Lose it, and
23
- # every value is lost for good.
24
- # AUDIT_CHAIN_KEY the app's. Signs the app's log entries, sessions and
25
- # tokens. Lose it, and everyone is signed out and the log
26
- # stops verifying.
27
- # KEK_ID names the KEK; not secret.
28
- #
29
- # Whoever has the KEK and a copy of the database has every value: keep it
30
- # apart from the backups.
31
- `;
32
- /** The keys as a dotenv block, then, as comments, what each is for and where it goes. */
33
- function formatKeys(keys) {
34
- return `KEK_ID=${keys.KEK_ID}
35
- KEK=${keys.KEK}
36
- AUDIT_CHAIN_KEY=${keys.AUDIT_CHAIN_KEY}
37
-
38
- # Save all three in your password manager now. They are shown once, and
39
- # coffre keeps no copy.
40
- #
41
- ${KEYS_EXPLAINED}#
42
- # On Workers, KEK_ID is a var in vault/wrangler.jsonc, and KEK and
43
- # AUDIT_CHAIN_KEY are Worker secrets (wrangler secret put). On Node, KEK_ID
44
- # and KEK go in vault.env, and AUDIT_CHAIN_KEY in server.env.
45
- #
46
- # These are for a new deployment. To rotate a deployment's KEK, take only
47
- # KEK_ID and KEK, and keep the old pair in previousKeks, for what it wrapped
48
- # and signed. AUDIT_CHAIN_KEY cannot be changed.
49
- `;
50
- }
51
- function keys(args) {
52
- const { values } = parseArgs({
53
- args,
54
- options: { json: {
55
- type: "boolean",
56
- default: false
57
- } },
58
- allowPositionals: false
59
- });
60
- const fresh = generateKeys();
61
- process.stdout.write(values.json ? `${JSON.stringify(fresh)}\n` : formatKeys(fresh));
62
- }
63
- //#endregion
64
- export { generateKeys as n, keys as r, KEYS_EXPLAINED as t };