@coffre/vault 0.0.0-stage → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erwin Kuhn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @coffre/vault
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ coffre's vault: the keys and who holds what. It decides every read and
4
+ write of a key and appends to the shared audit log as `vault`. It runs as a
5
+ Worker beside the app's Worker, or as its own process on Node. Both use
6
+ one Postgres database, each through its own restricted login:
7
+
8
+ ```ts
9
+ import { postgres, vault } from '@coffre/vault/cloudflare';
10
+
11
+ export default vault((env) => ({
12
+ database: postgres(env.VAULT_HYPERDRIVE), // coffre_vault_runtime, caching disabled
13
+ kek: { id: env.KEK_ID, key: env.KEK }, // or awsKms({ keyArn, credentials })
14
+ rootAdmins: env.ROOT_ADMINS.split(','),
15
+ signingKey: env.SIGNING_KEY,
16
+ }));
17
+ ```
18
+
19
+ On Node, `serveVault({ socket, database, … })` from `@coffre/vault/node`,
20
+ with the vault's Postgres URL. Only `coffre_vault_runtime` may write members
21
+ and grants; the app uses `coffre_runtime`. See the
22
+ [deploy guide](https://github.com/erwinkn/coffre/blob/main/docs/deploy.md)
23
+ for the logins and both Hyperdrive configs.
24
+
25
+ Part of [coffre](https://github.com/erwinkn/coffre), a secrets manager you
26
+ deploy as a small project of your own. Its eight `@coffre/*` packages are
27
+ released together, at one version. MIT licensed.
@@ -0,0 +1,55 @@
1
+ import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-qXbB_vlp.js";
2
+ import { AdmitInput, RemoveInput, RewrapInput, SetAccessInput, UnwrapInput, Vault, VerifyLogInput, WrapInput } from "@coffre/core/vault";
3
+ import { PostgresDatabase, PostgresDatabase as PostgresDatabase$1, postgres } from "@coffre/db/hyperdrive";
4
+ import { WorkerEntrypoint } from "cloudflare:workers";
5
+ export * from "@coffre/core/vault";
6
+ //#region src/cloudflare.d.ts
7
+ /** What the vault Worker's `vault(env => …)` returns. */
8
+ export type WorkersVaultConfig = VaultConfig & {
9
+ database: PostgresDatabase$1;
10
+ };
11
+ /**
12
+ * What the app's `VAULT` service binding calls. Each call gets a database of
13
+ * its own, since a Worker's connections belong to the call that made them.
14
+ */
15
+ export declare class VaultEntrypoint extends WorkerEntrypoint implements Vault {
16
+ #private;
17
+ unwrap(input: UnwrapInput): Promise<import("@coffre/core/vault").Outcome<{
18
+ keys: string[];
19
+ }>>;
20
+ wrap(input: WrapInput): Promise<import("@coffre/core/vault").Outcome<{
21
+ wrapped: import("@coffre/core/vault").WrappedKey[];
22
+ seqs: number[];
23
+ }>>;
24
+ rewrap(input: RewrapInput): Promise<import("@coffre/core/vault").Outcome<{
25
+ wrapped: import("@coffre/core/vault").WrappedKey[];
26
+ seqs: number[];
27
+ }>>;
28
+ access(principal: string): Promise<import("@coffre/core/vault").Access>;
29
+ setAccess(input: SetAccessInput): Promise<import("@coffre/core/vault").Outcome<{
30
+ changes: import("@coffre/core/vault").AccessChange[];
31
+ }>>;
32
+ admit(input: AdmitInput): Promise<import("@coffre/core/vault").Outcome<{
33
+ created: boolean;
34
+ owner: boolean;
35
+ generation: number;
36
+ }>>;
37
+ remove(input: RemoveInput): Promise<import("@coffre/core/vault").Outcome<{
38
+ revoked: import("@coffre/core/vault").Grant[];
39
+ generation: number;
40
+ }>>;
41
+ checkpoint(): Promise<import("@coffre/core/vault").Outcome<{
42
+ checkpoint: import("@coffre/core/vault").Checkpoint;
43
+ }>>;
44
+ about(): Promise<{
45
+ publicKey: string;
46
+ rootAdmins: string[];
47
+ }>;
48
+ verifyLog(input: VerifyLogInput): Promise<import("@coffre/core/vault").LogVerification>;
49
+ /** No HTTP surface: only the app's service binding reaches the vault. */
50
+ fetch(): Response;
51
+ }
52
+ /** The vault Worker's default export, `VaultEntrypoint`, configured from its `env`. */
53
+ export declare function vault<Env>(configure_: (env: Env) => WorkersVaultConfig): typeof VaultEntrypoint;
54
+ //#endregion
55
+ export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type PostgresDatabase, type SecretContext, type Vault, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, postgres, verifyCheckpoint };
@@ -0,0 +1,73 @@
1
+ import { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, verifyCheckpoint } from "./index.js";
2
+ import { n as prepareVault, r as resolveVaultConfig, t as openVault } from "./vault-DvTsSAOX.js";
3
+ import { createDatabase } from "@coffre/db";
4
+ import { HyperdrivePool, postgres } from "@coffre/db/hyperdrive";
5
+ import { WorkerEntrypoint } from "cloudflare:workers";
6
+ //#region src/cloudflare.ts
7
+ /** Set when the Worker's module runs `vault(…)`, before any call comes in. */
8
+ let configure = null;
9
+ /** The configuration, checked and made ready once per `env`, which the isolate keeps. */
10
+ const ready = /* @__PURE__ */ new WeakMap();
11
+ /**
12
+ * What the app's `VAULT` service binding calls. Each call gets a database of
13
+ * its own, since a Worker's connections belong to the call that made them.
14
+ */
15
+ var VaultEntrypoint = class extends WorkerEntrypoint {
16
+ async #vault() {
17
+ const env = this.env;
18
+ let entry = ready.get(env);
19
+ if (entry === void 0) {
20
+ if (configure === null) throw new Error("the vault Worker must export default vault(…)");
21
+ const config = configure(env);
22
+ entry = prepareVault(resolveVaultConfig(config)).then((prepared) => ({
23
+ database: config.database,
24
+ prepared
25
+ }));
26
+ ready.set(env, entry);
27
+ entry.catch(() => ready.delete(env));
28
+ }
29
+ const { database, prepared } = await entry;
30
+ return openVault(createDatabase(new HyperdrivePool(database.hyperdrive.connectionString)), prepared);
31
+ }
32
+ async unwrap(input) {
33
+ return (await this.#vault()).unwrap(input);
34
+ }
35
+ async wrap(input) {
36
+ return (await this.#vault()).wrap(input);
37
+ }
38
+ async rewrap(input) {
39
+ return (await this.#vault()).rewrap(input);
40
+ }
41
+ async access(principal) {
42
+ return (await this.#vault()).access(principal);
43
+ }
44
+ async setAccess(input) {
45
+ return (await this.#vault()).setAccess(input);
46
+ }
47
+ async admit(input) {
48
+ return (await this.#vault()).admit(input);
49
+ }
50
+ async remove(input) {
51
+ return (await this.#vault()).remove(input);
52
+ }
53
+ async checkpoint() {
54
+ return (await this.#vault()).checkpoint();
55
+ }
56
+ async about() {
57
+ return (await this.#vault()).about();
58
+ }
59
+ async verifyLog(input) {
60
+ return (await this.#vault()).verifyLog(input);
61
+ }
62
+ /** No HTTP surface: only the app's service binding reaches the vault. */
63
+ fetch() {
64
+ return new Response("not found", { status: 404 });
65
+ }
66
+ };
67
+ /** The vault Worker's default export, `VaultEntrypoint`, configured from its `env`. */
68
+ function vault(configure_) {
69
+ configure = configure_;
70
+ return VaultEntrypoint;
71
+ }
72
+ //#endregion
73
+ export { KekBadClaimError, KekUnavailableError, VaultEntrypoint, awsKms, checkpointMessage, postgres, vault, verifyCheckpoint };
@@ -0,0 +1,40 @@
1
+ import { checkpointMessage as checkpointMessage$1, verifyCheckpoint as verifyCheckpoint$1 } from "@coffre/core/vault";
2
+ import { AwsCredentials, AwsKmsOptions, KekBadClaimError as KekBadClaimError$1, KekProvider, KekProvider as KekProvider$1, KekRegistry, KekUnavailableError as KekUnavailableError$1, WrappedDek, awsKms } from "@coffre/core/kek";
3
+ import { SecretContext } from "@coffre/core/envelope";
4
+ //#region src/config.d.ts
5
+ /**
6
+ * A key-encryption key: 32 random bytes, base64, and the id envelopes record
7
+ * it under; or one a key service holds, such as `awsKms({ keyArn, … })`.
8
+ */
9
+ type Kek = {
10
+ id: string;
11
+ key: string;
12
+ } | KekProvider;
13
+ /**
14
+ * What a deployment writes: every key coffre has, and who may always get
15
+ * in. The app holds none of it.
16
+ *
17
+ * {
18
+ * kek: awsKms({ keyArn: env.KMS_KEY_ARN, credentials: { … } }),
19
+ * previousKeks: [{ id: 'kek-2025-01', key: env.KEK_2025_01 }],
20
+ * rootAdmins: ['admin@acme.example'],
21
+ * signingKey: env.SIGNING_KEY,
22
+ * }
23
+ */
24
+ type VaultConfig = {
25
+ /** Wraps every new data key. */
26
+ kek: Kek;
27
+ /** Older KEKs, still unwrapping what they wrapped until a rewrap moves it on. */
28
+ previousKeks?: readonly Kek[];
29
+ /** At least one email: the only way into a fresh instance, and the only members nobody can remove. */
30
+ rootAdmins: readonly string[];
31
+ /** 32 random bytes, base64: the Ed25519 seed audit checkpoints are signed with. */
32
+ signingKey: string;
33
+ /** At most `count` data keys unwrapped per principal in any `windowMinutes`; 1000 in 15 unless set. */
34
+ bulkLimit?: {
35
+ count: number;
36
+ windowMinutes: number;
37
+ };
38
+ };
39
+ //#endregion
40
+ export { KekUnavailableError$1 as a, awsKms as c, Kek as d, VaultConfig as f, KekProvider$1 as i, checkpointMessage$1 as l, AwsKmsOptions as n, SecretContext as o, KekBadClaimError$1 as r, WrappedDek as s, AwsCredentials as t, verifyCheckpoint$1 as u };
@@ -0,0 +1,3 @@
1
+ import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-qXbB_vlp.js";
2
+ export type * from "@coffre/core/vault";
3
+ export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type SecretContext, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, verifyCheckpoint };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { checkpointMessage, verifyCheckpoint } from "@coffre/core/vault";
2
+ import { KekBadClaimError, KekUnavailableError, awsKms } from "@coffre/core/kek";
3
+ export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, verifyCheckpoint };
package/dist/node.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-qXbB_vlp.js";
2
+ import { Vault, Vault as Vault$1 } from "@coffre/core/vault";
3
+ import { Database } from "@coffre/db";
4
+ import "@coffre/core/audit";
5
+ export * from "@coffre/core/vault";
6
+ //#region src/vault.d.ts
7
+ type VaultOptions = {
8
+ /**
9
+ * How long every key operation of one call may take, in milliseconds;
10
+ * 5 seconds unless set. Past it, a call fails as an outage.
11
+ */
12
+ keyBudgetMs?: number;
13
+ /** Milliseconds added to the database's clock where the vault decides by it. Tests move time with it. */
14
+ clockOffset?: () => number;
15
+ };
16
+ //#endregion
17
+ //#region src/local.d.ts
18
+ type LocalVault = Vault$1 & {
19
+ close(): Promise<void>;
20
+ };
21
+ //#endregion
22
+ //#region src/node.d.ts
23
+ export type NodeVaultConfig = VaultConfig & {
24
+ /**
25
+ * The database the server uses, as the vault's own login:
26
+ * `postgres://coffre_vault_runtime:…@…/coffre`, or a `file:` URL for local
27
+ * development. Or one already open, which the vault leaves open.
28
+ */
29
+ database: string | Database;
30
+ };
31
+ /** The vault in this process. */
32
+ export declare function localVault(config: NodeVaultConfig, options?: VaultOptions): Promise<LocalVault>;
33
+ export type VaultServer = {
34
+ socket: string;
35
+ close(): Promise<void>;
36
+ };
37
+ /**
38
+ * The vault as a process of its own, answering on a Unix socket. The socket
39
+ * is made `0660`: put it in a directory the server's user can reach through
40
+ * a shared group, e.g. `/run/coffre/vault.sock`.
41
+ */
42
+ export declare function serveVault(config: NodeVaultConfig & {
43
+ socket: string;
44
+ }): Promise<VaultServer>;
45
+ /** The vault that `serveVault` runs, from the server's side of its socket. */
46
+ export declare function connectVault(socket: string): Vault;
47
+ //#endregion
48
+ export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type LocalVault, type SecretContext, type Vault, type VaultConfig, type VaultOptions, type WrappedDek, awsKms, checkpointMessage, verifyCheckpoint };
package/dist/node.js ADDED
@@ -0,0 +1,142 @@
1
+ import { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, verifyCheckpoint } from "./index.js";
2
+ import { n as prepareVault, r as resolveVaultConfig, t as openVault } from "./vault-DvTsSAOX.js";
3
+ import { chmodSync, existsSync, lstatSync, rmSync } from "node:fs";
4
+ import { Agent, createServer, request } from "node:http";
5
+ import { openDatabase } from "@coffre/db/connect";
6
+ //#region src/local.ts
7
+ /** Every call the vault answers, in the order of the `Vault` interface. */
8
+ const METHODS = [
9
+ "unwrap",
10
+ "wrap",
11
+ "rewrap",
12
+ "access",
13
+ "setAccess",
14
+ "admit",
15
+ "remove",
16
+ "checkpoint",
17
+ "about",
18
+ "verifyLog"
19
+ ];
20
+ /**
21
+ * The vault in this process, over `db`. Every argument and result goes
22
+ * through JSON on the way, as it would over RPC or a socket, so what works
23
+ * here does not rely on sharing objects with the caller. `close` is
24
+ * whatever lets go of the database, when the vault opened it.
25
+ */
26
+ async function openLocalVault(db, config, options = {}, close = async () => {}) {
27
+ const vault = openVault(db, await prepareVault(config, options));
28
+ const local = Object.fromEntries(METHODS.map((name) => [name, async (...args) => json(await vault[name](...json(args)))]));
29
+ return Object.assign(local, { close });
30
+ }
31
+ function json(value) {
32
+ return value === void 0 ? void 0 : JSON.parse(JSON.stringify(value));
33
+ }
34
+ //#endregion
35
+ //#region src/node.ts
36
+ /**
37
+ * The vault on Node, one of two ways, each over the database the server
38
+ * uses, through the vault's own login:
39
+ *
40
+ * - in the server's own process: `vault: await localVault({ database, ...keys })`;
41
+ * - as a process of its own, which the server reaches over a Unix socket:
42
+ * `serveVault({ socket, database, ...keys })` there, and
43
+ * `vault: connectVault(socket)` in the server.
44
+ *
45
+ * The second keeps every key out of the process that faces the network. The
46
+ * socket is the whole of its authentication: a file only the vault's user
47
+ * and the group it shares with the server may open, so no port to reach and
48
+ * no shared secret to leak. Each call is one HTTP POST over it, `/<method>`
49
+ * with the arguments as a JSON array, and refusals come back as values, as
50
+ * from any vault; only a failure is an error.
51
+ */
52
+ /** The vault in this process. */
53
+ async function localVault(config, options = {}) {
54
+ const resolved = resolveVaultConfig(config);
55
+ if (typeof config.database !== "string") return openLocalVault(config.database, resolved, options);
56
+ const { db, close } = await openDatabase(config.database);
57
+ return openLocalVault(db, resolved, options, close).catch(async (error) => {
58
+ await close();
59
+ throw error;
60
+ });
61
+ }
62
+ const METHOD_NAMES = new Set(METHODS);
63
+ const MAX_BODY = 8388608;
64
+ /**
65
+ * The vault as a process of its own, answering on a Unix socket. The socket
66
+ * is made `0660`: put it in a directory the server's user can reach through
67
+ * a shared group, e.g. `/run/coffre/vault.sock`.
68
+ */
69
+ async function serveVault(config) {
70
+ const vault = await localVault(config);
71
+ const server = createServer(async (req, res) => {
72
+ const name = (req.url ?? "").slice(1);
73
+ const reply = (status, body) => {
74
+ res.writeHead(status, { "content-type": "application/json" });
75
+ res.end(JSON.stringify(body));
76
+ };
77
+ if (req.method !== "POST" || !METHOD_NAMES.has(name)) return reply(404, { error: "not a vault call" });
78
+ try {
79
+ const args = JSON.parse(await readBody(req));
80
+ if (!Array.isArray(args)) return reply(400, { error: "the arguments must be a JSON array" });
81
+ const result = await vault[name](...args);
82
+ reply(200, result === void 0 ? {} : { result });
83
+ } catch (error) {
84
+ console.error(`vault ${name} failed`, error);
85
+ reply(500, { error: error instanceof Error ? error.message : "the vault failed" });
86
+ }
87
+ });
88
+ if (existsSync(config.socket)) {
89
+ if (!lstatSync(config.socket).isSocket()) throw new Error(`${config.socket} exists and is not a socket`);
90
+ rmSync(config.socket);
91
+ }
92
+ await new Promise((resolve, reject) => {
93
+ server.once("error", reject);
94
+ server.listen(config.socket, () => resolve());
95
+ });
96
+ chmodSync(config.socket, 432);
97
+ return {
98
+ socket: config.socket,
99
+ close: () => new Promise((resolve) => {
100
+ server.close(() => void vault.close().then(resolve));
101
+ server.closeAllConnections();
102
+ })
103
+ };
104
+ }
105
+ /** The vault that `serveVault` runs, from the server's side of its socket. */
106
+ function connectVault(socket) {
107
+ const agent = new Agent({ keepAlive: true });
108
+ const call = (name) => (...args) => new Promise((resolve, reject) => {
109
+ const body = JSON.stringify(args);
110
+ const req = request({
111
+ socketPath: socket,
112
+ agent,
113
+ method: "POST",
114
+ path: `/${name}`,
115
+ headers: {
116
+ "content-type": "application/json",
117
+ "content-length": Buffer.byteLength(body)
118
+ }
119
+ }, (res) => {
120
+ readBody(res).then((text) => {
121
+ const payload = JSON.parse(text);
122
+ if (res.statusCode === 200) resolve(payload.result);
123
+ else reject(/* @__PURE__ */ new Error(`vault ${name}: ${payload.error ?? `status ${res.statusCode}`}`));
124
+ }, reject);
125
+ });
126
+ req.on("error", (error) => reject(/* @__PURE__ */ new Error(`the vault at ${socket} is unreachable: ${error.message}`)));
127
+ req.end(body);
128
+ });
129
+ return Object.fromEntries(METHODS.map((name) => [name, call(name)]));
130
+ }
131
+ async function readBody(stream) {
132
+ const chunks = [];
133
+ let size = 0;
134
+ for await (const chunk of stream) {
135
+ size += chunk.length;
136
+ if (size > MAX_BODY) throw new Error("the message is too large");
137
+ chunks.push(chunk);
138
+ }
139
+ return Buffer.concat(chunks).toString("utf8");
140
+ }
141
+ //#endregion
142
+ export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, connectVault, localVault, serveVault, verifyCheckpoint };