@coffre/cli 0.0.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.
@@ -0,0 +1,108 @@
1
+ # coffre on Node
2
+
3
+ Two processes, configured in code:
4
+
5
+ - `src/server.ts`, the server: the API, sign-in, the pages and a job every
6
+ five minutes, on one port. Put a proxy that terminates TLS in front of it.
7
+ - `src/vault.ts`, the vault: the keys, and the members and grants, which it
8
+ keeps in the server's database through a login of its own. It answers
9
+ only the server, on a Unix socket.
10
+
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
13
+ in its own process instead; see the comment there. Everything below runs
14
+ from this directory, on Node 24 or later.
15
+
16
+ ## 1. Settings
17
+
18
+ ```sh
19
+ pnpm install
20
+ cp server.env.example server.env
21
+ cp vault.env.example vault.env
22
+ ```
23
+
24
+ Fill both in: `PUBLIC_URL`, a GitHub OAuth app whose callback is
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.
44
+
45
+ ## 2. The database
46
+
47
+ Use one Postgres database with three logins: its owner for migrations,
48
+ `coffre_runtime` for the server, and `coffre_vault_runtime` for the vault.
49
+ As an administrator, connect to the database with `psql` and create the
50
+ runtime logins. `\password` prompts for each password without putting it
51
+ in a SQL statement or shell history:
52
+
53
+ ```sql
54
+ CREATE ROLE coffre_runtime LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
55
+ CREATE ROLE coffre_vault_runtime LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
56
+ \password coffre_runtime
57
+ \password coffre_vault_runtime
58
+ ```
59
+
60
+ Migrate as the owner, who must also be able to create and grant roles:
61
+
62
+ ```sh
63
+ pnpm migrate "postgres://owner:…@db.example.com:5432/coffre"
64
+ ```
65
+
66
+ The migration creates the `coffre_app` and `coffre_vault` group roles and
67
+ grants each login only its group's rights. Only the vault writes members
68
+ and grants; each login appends to the audit log only as itself. Run the
69
+ migration again after every package upgrade, before starting either process.
70
+
71
+ Fill `DATABASE_URL` in each env file with the same host and database, using
72
+ `coffre_runtime` in `server.env` and `coffre_vault_runtime` in `vault.env`.
73
+ URL-encode special characters in passwords. Neither process gets the owner's
74
+ URL. Keep each env file readable only by its process's user (`chmod 600`).
75
+
76
+ For tests and local development only, both URLs may instead name the same
77
+ absolute SQLite file, e.g. `file:/tmp/coffre-local.db`; migrate that URL once.
78
+ SQLite has no database logins or separation of privileges.
79
+
80
+ ## 3. Run
81
+
82
+ ```sh
83
+ pnpm vault # first: the server connects to its socket
84
+ pnpm start
85
+ ```
86
+
87
+ Then sign in at `PUBLIC_URL` as a root admin, and from a terminal:
88
+
89
+ ```sh
90
+ coffre login https://secrets.example.com
91
+ ```
92
+
93
+ Point a monitor at `<PUBLIC_URL>/readyz`: it turns red when the audit log
94
+ stops taking writes or the vault stops checkpointing it.
95
+
96
+ Everything coffre keeps is in the database: secrets, members, grants and
97
+ the audit log. Back it up as one, keep the escrowed keys apart from it, and
98
+ follow the [restore runbook](https://github.com/erwinkn/coffre/blob/main/docs/restore.md) to bring it back.
99
+
100
+ `pnpm typecheck` checks the configuration against coffre's types.
101
+
102
+ ## Conformance
103
+
104
+ `pnpm conformance` runs the vault and the server on SQLite in a temporary
105
+ directory, signs people in through a stand-in GitHub, and checks what coffre
106
+ must never do: show a value to someone without access, act for another site
107
+ with someone's cookie, keep a removed member in, give a value it did not
108
+ log. Run it after changing this project, and before deploying the change.
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ *.env
3
+ *.db
4
+ *.db-*
5
+ *.sock
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "coffre-node",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=24"
7
+ },
8
+ "scripts": {
9
+ "vault": "node --env-file=vault.env src/vault.ts",
10
+ "start": "node --env-file=server.env src/server.ts",
11
+ "migrate": "coffre-server migrate",
12
+ "typecheck": "tsc --noEmit",
13
+ "conformance": "coffre-conformance node"
14
+ },
15
+ "dependencies": {
16
+ "@coffre/server": "0.1.1",
17
+ "@coffre/vault": "0.1.1"
18
+ },
19
+ "devDependencies": {
20
+ "@coffre/conformance": "0.1.1",
21
+ "@types/node": "26.1.1",
22
+ "typescript": "5.9.3"
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ # Do not install a version published in the last 7 days: most malicious
2
+ # releases are yanked within hours, and a service that holds every credential
3
+ # can wait a week. Coffre's own packages are exempt, so a fix to them lands
4
+ # the day it is published; they depend on nothing newer than this allows.
5
+ minimumReleaseAge: 10080
6
+ minimumReleaseAgeExclude:
7
+ - '@coffre/*'
@@ -0,0 +1,18 @@
1
+ # The server's settings, read by `pnpm start`. Copy to server.env.
2
+
3
+ PORT=3000
4
+ # Where people reach coffre, through the proxy that terminates TLS in front
5
+ # of it: an origin, no path.
6
+ PUBLIC_URL=https://secrets.example.com
7
+ # The vault uses this Postgres database through a different login.
8
+ DATABASE_URL=postgres://coffre_runtime:CHANGE_ME@127.0.0.1:5432/coffre
9
+ # The vault's socket, as in vault.env.
10
+ VAULT_SOCKET=vault.sock
11
+
12
+ # A GitHub OAuth app whose callback is <PUBLIC_URL>/auth/callback/github.
13
+ GITHUB_CLIENT_ID=
14
+ GITHUB_CLIENT_SECRET=
15
+
16
+ # From `coffre keys`, saved in your password manager first: signs the
17
+ # server's log entries, sessions and tokens.
18
+ AUDIT_CHAIN_KEY=
@@ -0,0 +1,37 @@
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
3
+ // (src/vault.ts), over a Unix socket. Settings come from server.env.
4
+ import { github, serve, signin } from '@coffre/server/node';
5
+ import { connectVault } from '@coffre/vault/node';
6
+
7
+ function env(name: string): string {
8
+ const value = process.env[name];
9
+ if (!value) throw new Error(`${name} is not set; see server.env.example`);
10
+ return value;
11
+ }
12
+
13
+ const server = await serve({
14
+ port: Number(env('PORT')),
15
+ publicUrl: env('PUBLIC_URL'),
16
+ database: env('DATABASE_URL'),
17
+ vault: connectVault(env('VAULT_SOCKET')),
18
+ // For tests or local SQLite development only, the vault in this process:
19
+ // vault: await localVault({ database: env('DATABASE_URL'), kek: …, rootAdmins: […], signingKey: … }),
20
+ auth: signin({
21
+ providers: [
22
+ github({
23
+ clientId: env('GITHUB_CLIENT_ID'),
24
+ clientSecret: env('GITHUB_CLIENT_SECRET'),
25
+ // Set for GitHub Enterprise Server; github.com otherwise.
26
+ webUrl: process.env.GITHUB_URL,
27
+ apiUrl: process.env.GITHUB_API_URL,
28
+ }),
29
+ ],
30
+ }),
31
+ auditChainKey: env('AUDIT_CHAIN_KEY'),
32
+ });
33
+ console.log(`coffre is listening on ${server.url}`);
34
+
35
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
36
+ process.once(signal, () => void server.close().then(() => process.exit(0)));
37
+ }
@@ -0,0 +1,27 @@
1
+ // The vault: the keys, and the members and grants, which it keeps in the
2
+ // server's database through a login of its own. It answers only on a Unix
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.
5
+ // Settings come from vault.env.
6
+ import { serveVault } from '@coffre/vault/node';
7
+
8
+ function env(name: string): string {
9
+ const value = process.env[name];
10
+ if (!value) throw new Error(`${name} is not set; see vault.env.example`);
11
+ return value;
12
+ }
13
+
14
+ const vault = await serveVault({
15
+ socket: env('VAULT_SOCKET'),
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
19
+ // still open: previousKeks: [{ id: 'kek-1', key: env('KEK_1') }],
20
+ rootAdmins: env('ROOT_ADMINS').split(',').map((email) => email.trim()),
21
+ signingKey: env('SIGNING_KEY'),
22
+ });
23
+ console.log(`the vault is listening on ${vault.socket}`);
24
+
25
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
26
+ process.once(signal, () => void vault.close().then(() => process.exit(0)));
27
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023"],
5
+ "types": ["node"],
6
+ "module": "nodenext",
7
+ "moduleResolution": "nodenext",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "erasableSyntaxOnly": true,
12
+ "verbatimModuleSyntax": true
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,16 @@
1
+ # The vault's settings, read by `pnpm vault`. Copy to vault.env, readable by
2
+ # the vault's user alone.
3
+
4
+ VAULT_SOCKET=vault.sock
5
+ # The server's Postgres database, as the vault's own login.
6
+ DATABASE_URL=postgres://coffre_vault_runtime:CHANGE_ME@127.0.0.1:5432/coffre
7
+
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
+ KEK_ID=kek-1
11
+ KEK=
12
+ # Signs the vault's log entries, checkpoints and member rows.
13
+ SIGNING_KEY=
14
+
15
+ # Comma-separated emails: the first people in, whom nobody can remove.
16
+ ROOT_ADMINS=you@example.com
@@ -0,0 +1,137 @@
1
+ # coffre on Cloudflare Workers
2
+
3
+ Two Workers, configured in code:
4
+
5
+ - `app/src/worker.ts`, the app: the API, sign-in, the pages and a Cron job
6
+ every five minutes. It reaches Postgres through Hyperdrive, and the vault
7
+ through a service binding.
8
+ - `vault/src/worker.ts`, the vault: the keys, and the members and grants,
9
+ which it keeps in the same database through a login of its own. It has no
10
+ URL of its own.
11
+
12
+ Settings that are not secret are `vars` in each `wrangler.jsonc`; secrets are
13
+ Worker secrets. Everything below runs from this directory.
14
+
15
+ ## 1. Settings
16
+
17
+ - `app/wrangler.jsonc`: `PUBLIC_URL`, and `GITHUB_CLIENT_ID` from a GitHub
18
+ OAuth app whose callback is `<PUBLIC_URL>/auth/callback/github`.
19
+ - `vault/wrangler.jsonc`: `ROOT_ADMINS`, the emails of the first people in,
20
+ and `KEK_ID`, which `coffre keys` gives you below.
21
+
22
+ ## 2. The database
23
+
24
+ coffre wants a Postgres database with three logins: its owner, for
25
+ migrations; `coffre_runtime`, which the app runs as; and
26
+ `coffre_vault_runtime`, which the vault runs as. Create the two as plain
27
+ logins. As the database's administrator, connect with `psql` and run:
28
+
29
+ ```sql
30
+ CREATE ROLE coffre_runtime LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
31
+ CREATE ROLE coffre_vault_runtime LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
32
+ \password coffre_runtime
33
+ \password coffre_vault_runtime
34
+ ```
35
+
36
+ Each `\password` prompts for a different generated password. The owner
37
+ must be able to create and grant roles: the migration creates `coffre_app`
38
+ and `coffre_vault`, and grants each login its own group. Only the vault may
39
+ write members and grants, and each appends to the log only as itself.
40
+ URL-encode special characters in the passwords in these URLs:
41
+
42
+ ```sh
43
+ pnpm install
44
+ pnpm migrate "postgres://owner:…@db.example.com:5432/coffre"
45
+ pnpm exec wrangler hyperdrive create coffre --caching-disabled \
46
+ --connection-string="postgres://coffre_runtime:…@db.example.com:5432/coffre"
47
+ pnpm exec wrangler hyperdrive create coffre-vault --caching-disabled \
48
+ --connection-string="postgres://coffre_vault_runtime:…@db.example.com:5432/coffre"
49
+ ```
50
+
51
+ Keep `--caching-disabled`: Hyperdrive otherwise caches reads for up to a
52
+ minute, and a revoked token or a signed-out session could keep working that
53
+ long. `wrangler.jsonc` cannot set it, so for a config made another way,
54
+ check `caching` in `wrangler hyperdrive get <id>`.
55
+
56
+ Put the ids they print under `hyperdrive`, the first in
57
+ `app/wrangler.jsonc` and the second in `vault/wrangler.jsonc`. Run
58
+ `pnpm migrate` again after every upgrade of `@coffre/server`, before
59
+ deploying it.
60
+
61
+ ## 3. Keys
62
+
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:
81
+
82
+ ```sh
83
+ pnpm exec wrangler secret put KEK -c vault/wrangler.jsonc
84
+ pnpm exec wrangler secret put SIGNING_KEY -c vault/wrangler.jsonc
85
+ pnpm exec wrangler secret put AUDIT_CHAIN_KEY -c app/wrangler.jsonc
86
+ pnpm exec wrangler secret put GITHUB_CLIENT_SECRET -c app/wrangler.jsonc
87
+ ```
88
+
89
+ Keep older KEKs after a rotation too: what they wrapped still needs them.
90
+
91
+ ## 4. Deploy
92
+
93
+ ```sh
94
+ pnpm run deploy
95
+ ```
96
+
97
+ deploys the vault, then the app, which binds to it. Route the app to
98
+ `PUBLIC_URL` in the dashboard, or with `routes` in `app/wrangler.jsonc`, then
99
+ sign in there as a root admin, and from a terminal:
100
+
101
+ ```sh
102
+ coffre login https://secrets.example.com
103
+ ```
104
+
105
+ Point a monitor at `<PUBLIC_URL>/readyz`: it turns red when the audit log
106
+ stops taking writes or the vault stops checkpointing it.
107
+
108
+ Everything coffre keeps is in the database: secrets, members, grants and
109
+ the audit log. Back it up as one, keep the escrowed keys apart from it, and
110
+ follow the [restore runbook](https://github.com/erwinkn/coffre/blob/main/docs/restore.md) to bring it back.
111
+
112
+ ## Locally
113
+
114
+ `pnpm dev` runs both Workers with `wrangler dev`. Put local secrets in
115
+ `app/.dev.vars` and `vault/.dev.vars`, and point Hyperdrive at a local
116
+ database with `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE`,
117
+ for the app's login, and
118
+ `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_VAULT_HYPERDRIVE`, for the
119
+ vault's.
120
+ `pnpm typecheck` checks the configuration against coffre's types, and
121
+ `pnpm build` bundles both Workers without deploying them.
122
+
123
+ ## Conformance
124
+
125
+ ```sh
126
+ pnpm conformance \
127
+ --postgres "postgres://owner:…@127.0.0.1:5432" \
128
+ --runtime "postgres://coffre_runtime:…@127.0.0.1:5432" \
129
+ --vault-runtime "postgres://coffre_vault_runtime:…@127.0.0.1:5432"
130
+ ```
131
+
132
+ runs both Workers under `wrangler dev`, on a database of their own that it
133
+ creates on that Postgres and drops after, signs people in through a
134
+ stand-in GitHub, and checks what coffre must never do: show a value to
135
+ someone without access, act for another site with someone's cookie, keep a
136
+ removed member in, give a value it did not log. Run it after changing this
137
+ project, and before deploying the change.
@@ -0,0 +1,33 @@
1
+ // The app Worker: coffre's API, sign-in, pages and scheduled job. It reaches
2
+ // the database through Hyperdrive and the keys only through the vault
3
+ // Worker's service binding.
4
+ import { coffre, github, postgres, signin, type Vault } from '@coffre/server/cloudflare';
5
+
6
+ type Env = {
7
+ HYPERDRIVE: Hyperdrive;
8
+ VAULT: Vault;
9
+ PUBLIC_URL: string;
10
+ GITHUB_CLIENT_ID: string;
11
+ GITHUB_CLIENT_SECRET: string;
12
+ /** Set for GitHub Enterprise Server; github.com otherwise. */
13
+ GITHUB_URL?: string;
14
+ GITHUB_API_URL?: string;
15
+ AUDIT_CHAIN_KEY: string;
16
+ };
17
+
18
+ export default coffre((env: Env) => ({
19
+ publicUrl: env.PUBLIC_URL,
20
+ database: postgres(env.HYPERDRIVE),
21
+ vault: env.VAULT,
22
+ auth: signin({
23
+ providers: [
24
+ github({
25
+ clientId: env.GITHUB_CLIENT_ID,
26
+ clientSecret: env.GITHUB_CLIENT_SECRET,
27
+ webUrl: env.GITHUB_URL,
28
+ apiUrl: env.GITHUB_API_URL,
29
+ }),
30
+ ],
31
+ }),
32
+ auditChainKey: env.AUDIT_CHAIN_KEY,
33
+ }));
@@ -0,0 +1,35 @@
1
+ // The app Worker. Settings that are not secret are `vars` below; the secrets
2
+ // go in with `wrangler secret put <NAME> -c app/wrangler.jsonc`, and locally
3
+ // in app/.dev.vars.
4
+ {
5
+ "$schema": "../node_modules/wrangler/config-schema.json",
6
+ "name": "coffre",
7
+ "main": "src/worker.ts",
8
+ "compatibility_date": "2026-08-06",
9
+ "compatibility_flags": ["nodejs_compat"],
10
+ "workers_dev": false,
11
+ "observability": { "enabled": true },
12
+ "vars": {
13
+ // Where people reach coffre: an origin, no path. Add it as a route or
14
+ // custom domain too.
15
+ "PUBLIC_URL": "https://secrets.example.com",
16
+ // A GitHub OAuth app whose callback is <PUBLIC_URL>/auth/callback/github.
17
+ "GITHUB_CLIENT_ID": "replace-with-your-client-id"
18
+ },
19
+ "secrets": {
20
+ // AUDIT_CHAIN_KEY is from `coffre keys`; the app never gets the vault's keys.
21
+ "required": ["GITHUB_CLIENT_SECRET", "AUDIT_CHAIN_KEY"]
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
25
+ // for about a minute, and this file cannot set it: see README.md. Locally,
26
+ // CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE stands in for
27
+ // the config.
28
+ "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "replace-with-your-hyperdrive-id" }],
29
+ // The vault Worker, vault/wrangler.jsonc. Deploy it first.
30
+ "services": [{ "binding": "VAULT", "service": "coffre-vault" }],
31
+ // The heartbeat, audit checkpoints and due syncs.
32
+ "triggers": { "crons": ["*/5 * * * *"] },
33
+ // The pages' scripts, styles and fonts, all under /_coffre/assets/.
34
+ "assets": { "directory": "../node_modules/@coffre/ui/dist/client" }
35
+ }
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ .wrangler
3
+ .dev.vars*
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "coffre-workers",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=24"
7
+ },
8
+ "scripts": {
9
+ "dev": "wrangler dev -c app/wrangler.jsonc -c vault/wrangler.jsonc",
10
+ "typecheck": "tsc --noEmit",
11
+ "build": "wrangler deploy --dry-run -c vault/wrangler.jsonc && wrangler deploy --dry-run -c app/wrangler.jsonc",
12
+ "migrate": "coffre-server migrate",
13
+ "deploy": "wrangler deploy -c vault/wrangler.jsonc && wrangler deploy -c app/wrangler.jsonc",
14
+ "conformance": "coffre-conformance workers"
15
+ },
16
+ "dependencies": {
17
+ "@coffre/server": "0.1.1",
18
+ "@coffre/ui": "0.1.1",
19
+ "@coffre/vault": "0.1.1"
20
+ },
21
+ "devDependencies": {
22
+ "@cloudflare/workers-types": "5.20260920.1",
23
+ "@coffre/conformance": "0.1.1",
24
+ "typescript": "5.9.3",
25
+ "wrangler": "4.118.0"
26
+ }
27
+ }
@@ -0,0 +1,14 @@
1
+ # wrangler's esbuild and workerd each come with an install script that checks
2
+ # the platform binary it already has as an optional dependency. Neither needs
3
+ # to run, and pnpm wants to be told.
4
+ allowBuilds:
5
+ esbuild: false
6
+ workerd: false
7
+
8
+ # Do not install a version published in the last 7 days: most malicious
9
+ # releases are yanked within hours, and a service that holds every credential
10
+ # can wait a week. Coffre's own packages are exempt, so a fix to them lands
11
+ # the day it is published; they depend on nothing newer than this allows.
12
+ minimumReleaseAge: 10080
13
+ minimumReleaseAgeExclude:
14
+ - '@coffre/*'
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023"],
5
+ "types": ["@cloudflare/workers-types"],
6
+ "module": "esnext",
7
+ "moduleResolution": "bundler",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true
12
+ },
13
+ "include": ["app/src", "vault/src"]
14
+ }
@@ -0,0 +1,21 @@
1
+ // The vault Worker: the keys, and the members and grants, which it decides
2
+ // on in the app's database through its own login. It has no route of its
3
+ // own; only the app Worker's service binding reaches it.
4
+ import { postgres, vault } from '@coffre/vault/cloudflare';
5
+
6
+ type Env = {
7
+ VAULT_HYPERDRIVE: Hyperdrive;
8
+ KEK_ID: string;
9
+ KEK: string;
10
+ SIGNING_KEY: string;
11
+ ROOT_ADMINS: string;
12
+ };
13
+
14
+ export default vault((env: Env) => ({
15
+ database: postgres(env.VAULT_HYPERDRIVE),
16
+ kek: { id: env.KEK_ID, key: env.KEK },
17
+ // After a rotation, the KEKs before it, so the data keys they wrapped
18
+ // still open: previousKeks: [{ id: 'kek-2026-09', key: env.KEK_2026_09 }],
19
+ rootAdmins: env.ROOT_ADMINS.split(',').map((email) => email.trim()),
20
+ signingKey: env.SIGNING_KEY,
21
+ }));
@@ -0,0 +1,29 @@
1
+ // The vault Worker. No route and no workers.dev address: only the app
2
+ // Worker's service binding reaches it. Its secrets go in with
3
+ // `wrangler secret put <NAME> -c vault/wrangler.jsonc`, and locally in
4
+ // vault/.dev.vars.
5
+ {
6
+ "$schema": "../node_modules/wrangler/config-schema.json",
7
+ "name": "coffre-vault",
8
+ "main": "src/worker.ts",
9
+ "compatibility_date": "2026-08-06",
10
+ "compatibility_flags": ["nodejs_compat"],
11
+ "workers_dev": false,
12
+ "observability": { "enabled": true },
13
+ "vars": {
14
+ // Names the KEK below: from `coffre keys`. A rotation gives the new one a new id.
15
+ "KEK_ID": "kek-1",
16
+ // Comma-separated emails: the first people in, whom nobody can remove.
17
+ "ROOT_ADMINS": "you@example.com"
18
+ },
19
+ "secrets": {
20
+ // From `coffre keys`: wrangler secret put KEK, then SIGNING_KEY.
21
+ "required": ["KEK", "SIGNING_KEY"]
22
+ },
23
+ // The app's database, as the vault's own login: `wrangler hyperdrive
24
+ // create coffre-vault --caching-disabled --connection-string=<vault login
25
+ // URL>` prints the id. Its own config, not the app's: the vault writes
26
+ // members and grants, which the app's login may only read. Locally,
27
+ // CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_VAULT_HYPERDRIVE stands in.
28
+ "hyperdrive": [{ "binding": "VAULT_HYPERDRIVE", "id": "replace-with-your-vault-hyperdrive-id" }]
29
+ }
package/package.json CHANGED
@@ -1,8 +1,36 @@
1
1
  {
2
2
  "name": "@coffre/cli",
3
- "version": "0.0.0",
4
- "description": "Reserved for coffre, a self-hosted secrets manager. The first release replaces this placeholder.",
3
+ "version": "0.1.1",
4
+ "description": "The coffre command line: secrets in your shell, and `coffre init` for a new deployment.",
5
5
  "license": "MIT",
6
- "homepage": "https://github.com/erwinkn/coffre",
7
- "repository": { "type": "git", "url": "git+https://github.com/erwinkn/coffre.git", "directory": "packages/cli" }
8
- }
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/erwinkn/coffre.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=24"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "bin": {
19
+ "coffre": "./dist/main.js"
20
+ },
21
+ "exports": {
22
+ "./package.json": "./package.json"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "26.1.1",
26
+ "tsdown": "0.23.0",
27
+ "typescript": "5.9.3",
28
+ "@coffre/core": "0.1.1",
29
+ "@coffre/client": "0.1.1"
30
+ },
31
+ "scripts": {
32
+ "build": "tsdown && node scripts/copy-templates.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "node --conditions=coffre:source --test"
35
+ }
36
+ }