@coffre/cli 0.0.0 → 0.1.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.
@@ -0,0 +1,93 @@
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 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.
29
+
30
+ ## 2. The database
31
+
32
+ Use one Postgres database with three logins: its owner for migrations,
33
+ `coffre_runtime` for the server, and `coffre_vault_runtime` for the vault.
34
+ As an administrator, connect to the database with `psql` and create the
35
+ runtime logins. `\password` prompts for each password without putting it
36
+ in a SQL statement or shell history:
37
+
38
+ ```sql
39
+ CREATE ROLE coffre_runtime LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
40
+ CREATE ROLE coffre_vault_runtime LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
41
+ \password coffre_runtime
42
+ \password coffre_vault_runtime
43
+ ```
44
+
45
+ Migrate as the owner, who must also be able to create and grant roles:
46
+
47
+ ```sh
48
+ pnpm migrate "postgres://owner:…@db.example.com:5432/coffre"
49
+ ```
50
+
51
+ The migration creates the `coffre_app` and `coffre_vault` group roles and
52
+ grants each login only its group's rights. Only the vault writes members
53
+ and grants; each login appends to the audit log only as itself. Run the
54
+ migration again after every package upgrade, before starting either process.
55
+
56
+ Fill `DATABASE_URL` in each env file with the same host and database, using
57
+ `coffre_runtime` in `server.env` and `coffre_vault_runtime` in `vault.env`.
58
+ URL-encode special characters in passwords. Neither process gets the owner's
59
+ URL. Keep each env file readable only by its process's user (`chmod 600`).
60
+
61
+ For tests and local development only, both URLs may instead name the same
62
+ absolute SQLite file, e.g. `file:/tmp/coffre-local.db`; migrate that URL once.
63
+ SQLite has no database logins or separation of privileges.
64
+
65
+ ## 3. Run
66
+
67
+ ```sh
68
+ pnpm vault # first: the server connects to its socket
69
+ pnpm start
70
+ ```
71
+
72
+ Then sign in at `PUBLIC_URL` as a root admin, and from a terminal:
73
+
74
+ ```sh
75
+ coffre login https://secrets.example.com
76
+ ```
77
+
78
+ Point a monitor at `<PUBLIC_URL>/readyz`: it turns red when the audit log
79
+ stops taking writes or the vault stops checkpointing it.
80
+
81
+ Everything coffre keeps is in the database: secrets, members, grants and
82
+ the audit log. Back it up as one, keep the escrowed keys apart from it, and
83
+ follow the [restore runbook](https://github.com/erwinkn/coffre/blob/main/docs/restore.md) to bring it back.
84
+
85
+ `pnpm typecheck` checks the configuration against coffre's types.
86
+
87
+ ## Conformance
88
+
89
+ `pnpm conformance` runs the vault and the server on SQLite in a temporary
90
+ directory, signs people in through a stand-in GitHub, and checks what coffre
91
+ must never do: show a value to someone without access, act for another site
92
+ with someone's cookie, keep a removed member in, give a value it did not
93
+ 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.0",
17
+ "@coffre/vault": "0.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@coffre/conformance": "0.1.0",
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
+ # 32 random bytes, base64 (`openssl rand -base64 32`): keys the audit log's
17
+ # hash chain.
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
+ # 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.
10
+ KEK_ID=kek-1
11
+ KEK=
12
+ # 32 random bytes, base64: signs the audit checkpoints.
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,124 @@
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`, a name for the key-encryption key 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. Secrets
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.
66
+
67
+ ```sh
68
+ pnpm exec wrangler secret put KEK -c vault/wrangler.jsonc
69
+ pnpm exec wrangler secret put SIGNING_KEY -c vault/wrangler.jsonc
70
+ pnpm exec wrangler secret put AUDIT_CHAIN_KEY -c app/wrangler.jsonc
71
+ pnpm exec wrangler secret put GITHUB_CLIENT_SECRET -c app/wrangler.jsonc
72
+ ```
73
+
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.
77
+
78
+ ## 4. Deploy
79
+
80
+ ```sh
81
+ pnpm run deploy
82
+ ```
83
+
84
+ deploys the vault, then the app, which binds to it. Route the app to
85
+ `PUBLIC_URL` in the dashboard, or with `routes` in `app/wrangler.jsonc`, then
86
+ sign in there as a root admin, and from a terminal:
87
+
88
+ ```sh
89
+ coffre login https://secrets.example.com
90
+ ```
91
+
92
+ Point a monitor at `<PUBLIC_URL>/readyz`: it turns red when the audit log
93
+ stops taking writes or the vault stops checkpointing it.
94
+
95
+ Everything coffre keeps is in the database: secrets, members, grants and
96
+ the audit log. Back it up as one, keep the escrowed keys apart from it, and
97
+ follow the [restore runbook](https://github.com/erwinkn/coffre/blob/main/docs/restore.md) to bring it back.
98
+
99
+ ## Locally
100
+
101
+ `pnpm dev` runs both Workers with `wrangler dev`. Put local secrets in
102
+ `app/.dev.vars` and `vault/.dev.vars`, and point Hyperdrive at a local
103
+ database with `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE`,
104
+ for the app's login, and
105
+ `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_VAULT_HYPERDRIVE`, for the
106
+ vault's.
107
+ `pnpm typecheck` checks the configuration against coffre's types, and
108
+ `pnpm build` bundles both Workers without deploying them.
109
+
110
+ ## Conformance
111
+
112
+ ```sh
113
+ pnpm conformance \
114
+ --postgres "postgres://owner:…@127.0.0.1:5432" \
115
+ --runtime "postgres://coffre_runtime:…@127.0.0.1:5432" \
116
+ --vault-runtime "postgres://coffre_vault_runtime:…@127.0.0.1:5432"
117
+ ```
118
+
119
+ runs both Workers under `wrangler dev`, on a database of their own that it
120
+ creates on that Postgres and drops after, signs people in through a
121
+ stand-in GitHub, and checks what coffre must never do: show a value to
122
+ someone without access, act for another site with someone's cookie, keep a
123
+ removed member in, give a value it did not log. Run it after changing this
124
+ 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,34 @@
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
+ "required": ["GITHUB_CLIENT_SECRET", "AUDIT_CHAIN_KEY"]
21
+ },
22
+ // `wrangler hyperdrive create coffre --caching-disabled --connection-string=<runtime login URL>`
23
+ // prints the id. Caching must stay off, or a revoked session keeps working
24
+ // for about a minute, and this file cannot set it: see README.md. Locally,
25
+ // CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE stands in for
26
+ // the config.
27
+ "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "replace-with-your-hyperdrive-id" }],
28
+ // The vault Worker, vault/wrangler.jsonc. Deploy it first.
29
+ "services": [{ "binding": "VAULT", "service": "coffre-vault" }],
30
+ // The heartbeat, audit checkpoints and due syncs.
31
+ "triggers": { "crons": ["*/5 * * * *"] },
32
+ // The pages' scripts, styles and fonts, all under /_coffre/assets/.
33
+ "assets": { "directory": "../node_modules/@coffre/ui/dist/client" }
34
+ }
@@ -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.0",
18
+ "@coffre/ui": "0.1.0",
19
+ "@coffre/vault": "0.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@cloudflare/workers-types": "5.20260920.1",
23
+ "@coffre/conformance": "0.1.0",
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; 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
+ // Each 32 random bytes, base64: `openssl rand -base64 32`.
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.0",
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/client": "0.1.0",
29
+ "@coffre/core": "0.1.0"
30
+ },
31
+ "scripts": {
32
+ "build": "tsdown && node scripts/copy-templates.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "node --conditions=coffre:source --test"
35
+ }
36
+ }