@coffre/vault 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.
- package/LICENSE +21 -0
- package/README.md +25 -3
- package/dist/cloudflare.d.ts +55 -0
- package/dist/cloudflare.js +73 -0
- package/dist/index-qXbB_vlp.d.ts +40 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/node.d.ts +48 -0
- package/dist/node.js +142 -0
- package/dist/vault-DvTsSAOX.js +2225 -0
- package/package.json +51 -5
- package/src/accounting.ts +110 -0
- package/src/checkpoint.ts +25 -0
- package/src/cloudflare-workers.d.ts +14 -0
- package/src/cloudflare.ts +89 -0
- package/src/config.ts +146 -0
- package/src/index.ts +6 -0
- package/src/local.ts +48 -0
- package/src/log.ts +140 -0
- package/src/node.ts +141 -0
- package/src/replay.ts +150 -0
- package/src/rows.ts +67 -0
- package/src/store.ts +420 -0
- package/src/vault.ts +1564 -0
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,5 +1,27 @@
|
|
|
1
1
|
# @coffre/vault
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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
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 };
|