@coffre/vault 0.1.3 → 0.1.5
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/dist/cloudflare.js +10 -6
- package/dist/node.js +1 -1
- package/dist/{vault-pexzy7Z-.js → vault-PQvFopA6.js} +38 -33
- package/package.json +5 -4
- package/src/cloudflare.ts +10 -5
- package/src/vault.ts +43 -38
package/dist/cloudflare.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-DHuqw-IB.js";
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-PQvFopA6.js";
|
|
3
3
|
import { createDatabase } from "@coffre/db";
|
|
4
4
|
import { HyperdrivePool, postgres } from "@coffre/db/hyperdrive";
|
|
5
5
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
6
6
|
//#region src/cloudflare.ts
|
|
7
7
|
/** Set when the Worker's module runs `vault(…)`, before any call comes in. */
|
|
8
8
|
let configure = null;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* The configuration, checked and made ready once per `env`, which the
|
|
11
|
+
* isolate keeps: the ready value itself, never the work of making it, which
|
|
12
|
+
* would belong to the call that started it (vault.ts, `PreparedVault`).
|
|
13
|
+
*/
|
|
10
14
|
const ready = /* @__PURE__ */ new WeakMap();
|
|
11
15
|
/**
|
|
12
16
|
* What the app's `VAULT` service binding calls. Each call gets a database of
|
|
@@ -19,14 +23,14 @@ var VaultEntrypoint = class extends WorkerEntrypoint {
|
|
|
19
23
|
if (entry === void 0) {
|
|
20
24
|
if (configure === null) throw new Error("the vault Worker must export default vault(…)");
|
|
21
25
|
const config = configure(env);
|
|
22
|
-
|
|
26
|
+
const prepared = await prepareVault(resolveVaultConfig(config));
|
|
27
|
+
entry = ready.get(env) ?? {
|
|
23
28
|
database: config.database,
|
|
24
29
|
prepared
|
|
25
|
-
}
|
|
30
|
+
};
|
|
26
31
|
ready.set(env, entry);
|
|
27
|
-
entry.catch(() => ready.delete(env));
|
|
28
32
|
}
|
|
29
|
-
const { database, prepared } =
|
|
33
|
+
const { database, prepared } = entry;
|
|
30
34
|
return openVault(createDatabase(new HyperdrivePool(database.hyperdrive.connectionString)), prepared);
|
|
31
35
|
}
|
|
32
36
|
async unwrap(input) {
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-DHuqw-IB.js";
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-PQvFopA6.js";
|
|
3
3
|
import { chmodSync, existsSync, lstatSync, rmSync } from "node:fs";
|
|
4
4
|
import { Agent, createServer, request } from "node:http";
|
|
5
5
|
import { openDatabase } from "@coffre/db/connect";
|
|
@@ -792,8 +792,7 @@ async function prepareVault(config, options = {}) {
|
|
|
792
792
|
reported: /* @__PURE__ */ new Set(),
|
|
793
793
|
since: null,
|
|
794
794
|
superseded: null,
|
|
795
|
-
|
|
796
|
-
kekCheck: null,
|
|
795
|
+
kekChecked: false,
|
|
797
796
|
wrongKek: null
|
|
798
797
|
};
|
|
799
798
|
}
|
|
@@ -1100,12 +1099,7 @@ var VaultService = class {
|
|
|
1100
1099
|
*/
|
|
1101
1100
|
async #settled() {
|
|
1102
1101
|
const prepared = this.#prepared;
|
|
1103
|
-
if (prepared.since === null && prepared.superseded === null)
|
|
1104
|
-
prepared.settling ??= this.#settle().finally(() => {
|
|
1105
|
-
prepared.settling = null;
|
|
1106
|
-
});
|
|
1107
|
-
await prepared.settling;
|
|
1108
|
-
}
|
|
1102
|
+
if (prepared.since === null && prepared.superseded === null) await this.#settle();
|
|
1109
1103
|
return prepared.superseded;
|
|
1110
1104
|
}
|
|
1111
1105
|
/**
|
|
@@ -1910,19 +1904,21 @@ var VaultService = class {
|
|
|
1910
1904
|
/**
|
|
1911
1905
|
* Null when every KEK the vault is given opens what it wrapped; otherwise
|
|
1912
1906
|
* why not, naming the KEK, never its key. Decided once per process, before
|
|
1913
|
-
* its first key operation or checkpoint
|
|
1914
|
-
*
|
|
1907
|
+
* its first key operation or checkpoint: until then, each call checks for
|
|
1908
|
+
* itself, over its own database, and the first verdict is kept. A key
|
|
1909
|
+
* service that cannot answer leaves it undecided: the error is thrown,
|
|
1910
|
+
* and the next call asks again.
|
|
1915
1911
|
*/
|
|
1916
|
-
#kekMismatch() {
|
|
1912
|
+
async #kekMismatch() {
|
|
1917
1913
|
const prepared = this.#prepared;
|
|
1918
|
-
if (prepared.
|
|
1919
|
-
const
|
|
1920
|
-
prepared.
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
}
|
|
1914
|
+
if (!prepared.kekChecked) {
|
|
1915
|
+
const wrong = await this.#checkKeks();
|
|
1916
|
+
if (!prepared.kekChecked) {
|
|
1917
|
+
prepared.wrongKek = wrong;
|
|
1918
|
+
prepared.kekChecked = true;
|
|
1919
|
+
}
|
|
1924
1920
|
}
|
|
1925
|
-
return prepared.
|
|
1921
|
+
return prepared.wrongKek;
|
|
1926
1922
|
}
|
|
1927
1923
|
/**
|
|
1928
1924
|
* Each KEK opens its check value, or, with none recorded yet (a fresh
|
|
@@ -1933,12 +1929,7 @@ var VaultService = class {
|
|
|
1933
1929
|
async #checkKeks() {
|
|
1934
1930
|
const superseded = await this.#settled();
|
|
1935
1931
|
if (superseded !== null) return superseded;
|
|
1936
|
-
const checks =
|
|
1937
|
-
for (const entry of await vaultEntriesOf(this.#db, [KEY_CHECK], -1n, VERIFY_BATCH)) {
|
|
1938
|
-
if (!this.#authentic(entry)) continue;
|
|
1939
|
-
const wrapped = JSON.parse(entry.metadata);
|
|
1940
|
-
checks.set(`${wrapped.kekProvider}:${wrapped.kekId}`, unwrappable(wrapped));
|
|
1941
|
-
}
|
|
1932
|
+
const checks = await this.#checkValues(this.#db);
|
|
1942
1933
|
const budget = this.#prepared.options.keyBudgetMs;
|
|
1943
1934
|
for (const kek of this.#config.keks.all) {
|
|
1944
1935
|
const operation = {
|
|
@@ -1977,18 +1968,32 @@ var VaultService = class {
|
|
|
1977
1968
|
}
|
|
1978
1969
|
if (samples.length > 0 && proof === null) return mismatch;
|
|
1979
1970
|
const wrapped = await kek.wrap(Buffer.from(KEY_CHECK_VALUE), KEY_CHECK_CONTEXT, operation);
|
|
1980
|
-
await this.#db.transaction((tx) =>
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1971
|
+
await this.#db.transaction(async (tx) => {
|
|
1972
|
+
await lockLogHead(tx);
|
|
1973
|
+
if ((await this.#checkValues(tx)).has(`${kek.provider}:${kek.keyId}`)) return;
|
|
1974
|
+
await this.#append(tx, [{
|
|
1975
|
+
actor: VAULT_ACTOR,
|
|
1976
|
+
action: KEY_CHECK,
|
|
1977
|
+
decision: "allow",
|
|
1978
|
+
metadata: JSON.stringify({
|
|
1979
|
+
...serialisable(wrapped),
|
|
1980
|
+
proof
|
|
1981
|
+
})
|
|
1982
|
+
}]);
|
|
1983
|
+
});
|
|
1989
1984
|
}
|
|
1990
1985
|
return null;
|
|
1991
1986
|
}
|
|
1987
|
+
/** Each KEK's recorded check value, by `provider:keyId`. */
|
|
1988
|
+
async #checkValues(db) {
|
|
1989
|
+
const checks = /* @__PURE__ */ new Map();
|
|
1990
|
+
for (const entry of await vaultEntriesOf(db, [KEY_CHECK], -1n, VERIFY_BATCH)) {
|
|
1991
|
+
if (!this.#authentic(entry)) continue;
|
|
1992
|
+
const wrapped = JSON.parse(entry.metadata);
|
|
1993
|
+
checks.set(`${wrapped.kekProvider}:${wrapped.kekId}`, unwrappable(wrapped));
|
|
1994
|
+
}
|
|
1995
|
+
return checks;
|
|
1996
|
+
}
|
|
1992
1997
|
/** The last checkpoint the vault signed, and the entry that holds it: its newest allowed `audit.checkpoint`. */
|
|
1993
1998
|
async #latest(db) {
|
|
1994
1999
|
const row = await latestVaultEntry(db, [CHECKPOINT]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/vault",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "coffre's vault: keys, grants and members, in the database the app uses, as a Cloudflare Worker or on Node.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,15 +36,16 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"drizzle-orm": "0.45.2",
|
|
39
|
-
"@coffre/core": "0.1.
|
|
40
|
-
"@coffre/db": "0.1.
|
|
39
|
+
"@coffre/core": "0.1.5",
|
|
40
|
+
"@coffre/db": "0.1.5"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "26.1.1",
|
|
44
44
|
"@types/pg": "8.20.0",
|
|
45
45
|
"pg": "8.16.3",
|
|
46
46
|
"tsdown": "0.23.0",
|
|
47
|
-
"typescript": "5.9.3"
|
|
47
|
+
"typescript": "5.9.3",
|
|
48
|
+
"wrangler": "4.118.0"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "tsdown",
|
package/src/cloudflare.ts
CHANGED
|
@@ -42,8 +42,12 @@ export type WorkersVaultConfig = VaultConfig & { database: PostgresDatabase };
|
|
|
42
42
|
/** Set when the Worker's module runs `vault(…)`, before any call comes in. */
|
|
43
43
|
let configure: ((env: never) => WorkersVaultConfig) | null = null;
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The configuration, checked and made ready once per `env`, which the
|
|
47
|
+
* isolate keeps: the ready value itself, never the work of making it, which
|
|
48
|
+
* would belong to the call that started it (vault.ts, `PreparedVault`).
|
|
49
|
+
*/
|
|
50
|
+
const ready = new WeakMap<object, { database: PostgresDatabase; prepared: PreparedVault }>();
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
53
|
* What the app's `VAULT` service binding calls. Each call gets a database of
|
|
@@ -56,11 +60,12 @@ export class VaultEntrypoint extends WorkerEntrypoint implements Vault {
|
|
|
56
60
|
if (entry === undefined) {
|
|
57
61
|
if (configure === null) throw new Error('the vault Worker must export default vault(…)');
|
|
58
62
|
const config = configure(env as never);
|
|
59
|
-
|
|
63
|
+
const prepared = await prepareVault(resolveVaultConfig(config));
|
|
64
|
+
// Calls that raced here each made one; the first kept serves them all from now on.
|
|
65
|
+
entry = ready.get(env) ?? { database: config.database, prepared };
|
|
60
66
|
ready.set(env, entry);
|
|
61
|
-
entry.catch(() => ready.delete(env));
|
|
62
67
|
}
|
|
63
|
-
const { database, prepared } =
|
|
68
|
+
const { database, prepared } = entry;
|
|
64
69
|
return openVault(createDatabase(new HyperdrivePool(database.hyperdrive.connectionString)), prepared);
|
|
65
70
|
}
|
|
66
71
|
|
package/src/vault.ts
CHANGED
|
@@ -75,7 +75,9 @@ export type VaultOptions = {
|
|
|
75
75
|
/**
|
|
76
76
|
* A vault's configuration made ready to use, once per process, or once per
|
|
77
77
|
* isolate on Workers: its signer, its log key, and how far it has verified
|
|
78
|
-
* the log.
|
|
78
|
+
* the log. It holds settled values only, never work under way: on Workers,
|
|
79
|
+
* a call's I/O is that call's, and a call that waited on another's would be
|
|
80
|
+
* cancelled with it, as hung (isolate.test.ts). Everything else is in the database, so any number of instances
|
|
79
81
|
* share one set of members, one log and one bulk count.
|
|
80
82
|
*/
|
|
81
83
|
export type PreparedVault = {
|
|
@@ -101,12 +103,10 @@ export type PreparedVault = {
|
|
|
101
103
|
since: bigint | null;
|
|
102
104
|
/** Why it writes nothing, once settled that it may not. */
|
|
103
105
|
superseded: string | null;
|
|
104
|
-
/** The settling under way. */
|
|
105
|
-
settling: Promise<void> | null;
|
|
106
106
|
/** Tampering this process has logged already, so that a forged row is one entry, not one per request. */
|
|
107
107
|
reported: Set<string>;
|
|
108
|
-
/** Whether its KEKs open what they wrapped,
|
|
109
|
-
|
|
108
|
+
/** Whether its KEKs are decided to open what they wrapped, or not: `#kekMismatch`. */
|
|
109
|
+
kekChecked: boolean;
|
|
110
110
|
/** Why not, once decided that they do not. */
|
|
111
111
|
wrongKek: string | null;
|
|
112
112
|
};
|
|
@@ -121,8 +121,7 @@ export async function prepareVault(config: ResolvedVaultConfig, options: VaultOp
|
|
|
121
121
|
reported: new Set(),
|
|
122
122
|
since: null,
|
|
123
123
|
superseded: null,
|
|
124
|
-
|
|
125
|
-
kekCheck: null,
|
|
124
|
+
kekChecked: false,
|
|
126
125
|
wrongKek: null,
|
|
127
126
|
};
|
|
128
127
|
}
|
|
@@ -485,12 +484,10 @@ class VaultService implements Vault {
|
|
|
485
484
|
*/
|
|
486
485
|
async #settled(): Promise<string | null> {
|
|
487
486
|
const prepared = this.#prepared;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
await prepared.settling;
|
|
493
|
-
}
|
|
487
|
+
// Each call that finds it unsettled reads for itself, over its own
|
|
488
|
+
// database; calls that race settle it alike, and a rotation they race
|
|
489
|
+
// to write goes in once (`#rotate`).
|
|
490
|
+
if (prepared.since === null && prepared.superseded === null) await this.#settle();
|
|
494
491
|
return prepared.superseded;
|
|
495
492
|
}
|
|
496
493
|
|
|
@@ -1310,22 +1307,21 @@ class VaultService implements Vault {
|
|
|
1310
1307
|
/**
|
|
1311
1308
|
* Null when every KEK the vault is given opens what it wrapped; otherwise
|
|
1312
1309
|
* why not, naming the KEK, never its key. Decided once per process, before
|
|
1313
|
-
* its first key operation or checkpoint
|
|
1314
|
-
*
|
|
1310
|
+
* its first key operation or checkpoint: until then, each call checks for
|
|
1311
|
+
* itself, over its own database, and the first verdict is kept. A key
|
|
1312
|
+
* service that cannot answer leaves it undecided: the error is thrown,
|
|
1313
|
+
* and the next call asks again.
|
|
1315
1314
|
*/
|
|
1316
|
-
#kekMismatch(): Promise<string | null> {
|
|
1315
|
+
async #kekMismatch(): Promise<string | null> {
|
|
1317
1316
|
const prepared = this.#prepared;
|
|
1318
|
-
if (prepared.
|
|
1319
|
-
const
|
|
1320
|
-
prepared.
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
if (prepared.kekCheck === check) prepared.kekCheck = null;
|
|
1325
|
-
},
|
|
1326
|
-
);
|
|
1317
|
+
if (!prepared.kekChecked) {
|
|
1318
|
+
const wrong = await this.#checkKeks();
|
|
1319
|
+
if (!prepared.kekChecked) {
|
|
1320
|
+
prepared.wrongKek = wrong;
|
|
1321
|
+
prepared.kekChecked = true;
|
|
1322
|
+
}
|
|
1327
1323
|
}
|
|
1328
|
-
return prepared.
|
|
1324
|
+
return prepared.wrongKek;
|
|
1329
1325
|
}
|
|
1330
1326
|
|
|
1331
1327
|
/**
|
|
@@ -1338,13 +1334,7 @@ class VaultService implements Vault {
|
|
|
1338
1334
|
// A vault that may not write cannot record a check either; why it may not is the answer.
|
|
1339
1335
|
const superseded = await this.#settled();
|
|
1340
1336
|
if (superseded !== null) return superseded;
|
|
1341
|
-
const checks =
|
|
1342
|
-
for (const entry of await store.vaultEntriesOf(this.#db, [KEY_CHECK], -1n, VERIFY_BATCH)) {
|
|
1343
|
-
// A check in the vault's name that the vault did not write proves nothing either way.
|
|
1344
|
-
if (!this.#authentic(entry)) continue;
|
|
1345
|
-
const wrapped = JSON.parse(entry.metadata) as WrappedKey;
|
|
1346
|
-
checks.set(`${wrapped.kekProvider}:${wrapped.kekId}`, unwrappable(wrapped));
|
|
1347
|
-
}
|
|
1337
|
+
const checks = await this.#checkValues(this.#db);
|
|
1348
1338
|
const budget = this.#prepared.options.keyBudgetMs;
|
|
1349
1339
|
for (const kek of this.#config.keks.all) {
|
|
1350
1340
|
const operation = { deadline: Date.now() + budget, signal: AbortSignal.timeout(budget) };
|
|
@@ -1375,16 +1365,31 @@ class VaultService implements Vault {
|
|
|
1375
1365
|
}
|
|
1376
1366
|
if (samples.length > 0 && proof === null) return mismatch;
|
|
1377
1367
|
const wrapped = await kek.wrap(Buffer.from(KEY_CHECK_VALUE), KEY_CHECK_CONTEXT, operation);
|
|
1378
|
-
// The key it opened, if any, is in the log, as every key the vault opens is.
|
|
1379
|
-
|
|
1380
|
-
|
|
1368
|
+
// The key it opened, if any, is in the log, as every key the vault opens is. Another call that
|
|
1369
|
+
// checked at the same time may have recorded one first: under the log's lock, the first stays.
|
|
1370
|
+
await this.#db.transaction(async (tx) => {
|
|
1371
|
+
await lockLogHead(tx);
|
|
1372
|
+
if ((await this.#checkValues(tx)).has(`${kek.provider}:${kek.keyId}`)) return;
|
|
1373
|
+
await this.#append(tx, [
|
|
1381
1374
|
{ actor: VAULT_ACTOR, action: KEY_CHECK, decision: 'allow', metadata: JSON.stringify({ ...serialisable(wrapped), proof }) },
|
|
1382
|
-
])
|
|
1383
|
-
);
|
|
1375
|
+
]);
|
|
1376
|
+
});
|
|
1384
1377
|
}
|
|
1385
1378
|
return null;
|
|
1386
1379
|
}
|
|
1387
1380
|
|
|
1381
|
+
/** Each KEK's recorded check value, by `provider:keyId`. */
|
|
1382
|
+
async #checkValues(db: Queryable): Promise<Map<string, WrappedDek>> {
|
|
1383
|
+
const checks = new Map<string, WrappedDek>();
|
|
1384
|
+
for (const entry of await store.vaultEntriesOf(db, [KEY_CHECK], -1n, VERIFY_BATCH)) {
|
|
1385
|
+
// A check in the vault's name that the vault did not write proves nothing either way.
|
|
1386
|
+
if (!this.#authentic(entry)) continue;
|
|
1387
|
+
const wrapped = JSON.parse(entry.metadata) as WrappedKey;
|
|
1388
|
+
checks.set(`${wrapped.kekProvider}:${wrapped.kekId}`, unwrappable(wrapped));
|
|
1389
|
+
}
|
|
1390
|
+
return checks;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1388
1393
|
// --- checkpoints and the log --------------------------------------------------
|
|
1389
1394
|
|
|
1390
1395
|
/** The last checkpoint the vault signed, and the entry that holds it: its newest allowed `audit.checkpoint`. */
|