@coffre/vault 0.14.3 → 0.14.4
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 +1 -1
- package/dist/node.js +1 -1
- package/dist/{vault-B4IivgU8.js → vault-BBdXsny3.js} +50 -25
- package/package.json +3 -3
- package/src/snapshot.postgres.ts +10 -11
- package/src/snapshot.sqlite.ts +7 -10
- package/src/snapshot.ts +7 -3
- package/src/vault.ts +27 -4
package/dist/cloudflare.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-NM5NsYFs.js";
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-BBdXsny3.js";
|
|
3
3
|
import { createDatabase } from "@coffre/db";
|
|
4
4
|
import { HyperdrivePool, postgres } from "@coffre/db/hyperdrive";
|
|
5
5
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
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-NM5NsYFs.js";
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-BBdXsny3.js";
|
|
3
3
|
import { isUnavailable } from "@coffre/db/dialect";
|
|
4
4
|
import { chmodSync, existsSync, lstatSync, rmSync } from "node:fs";
|
|
5
5
|
import { Agent, createServer, request } from "node:http";
|
|
@@ -1216,17 +1216,16 @@ function snapshotQuery$1(asked) {
|
|
|
1216
1216
|
WHERE author = 'vault' AND action = 'reference.end' AND decision = 'allow' AND related_seq = ANY(${references})
|
|
1217
1217
|
),
|
|
1218
1218
|
'released', ${released === void 0 ? sql`0` : sql`(
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
)`}
|
|
1219
|
+
-- The keys released within the window, but those asked for, up to one past the cap: a row for each key the
|
|
1220
|
+
-- principal was ever released, however many reads of them the log holds (vault_releases, which the database
|
|
1221
|
+
-- keeps from it). The bound is a subquery, evaluated once: clock_timestamp() alone is volatile, and runs a row.
|
|
1222
|
+
SELECT count(*) FROM (
|
|
1223
|
+
SELECT 1 FROM vault_releases
|
|
1224
|
+
WHERE principal = ${released.principal} AND released_at > (SELECT ${NOW$1} + ${released.clockOffsetMs} - ${released.windowMs})
|
|
1225
|
+
AND key <> ALL(${sql.param([...released.keys])}::text[])
|
|
1226
|
+
LIMIT ${released.cap + 1}
|
|
1227
|
+
) AS before
|
|
1228
|
+
) + (SELECT count(DISTINCT key) FROM unnest(${sql.param([...released.keys])}::text[]) AS asked (key))`}
|
|
1230
1229
|
) AS snapshot`;
|
|
1231
1230
|
}
|
|
1232
1231
|
//#endregion
|
|
@@ -1307,17 +1306,14 @@ function snapshotQuery(asked) {
|
|
|
1307
1306
|
WHERE author = 'vault' AND action = 'reference.end' AND decision = 'allow' AND related_seq IN (SELECT value FROM json_each(${references}))
|
|
1308
1307
|
)),
|
|
1309
1308
|
'released', ${released === void 0 ? sql`0` : sql`(
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
WHERE e.author = 'vault' AND e.actor = ${released.principal} AND e.action = 'secret.read' AND e.decision = 'allow'
|
|
1317
|
-
AND e.occurred_at > ${NOW} + ${released.clockOffsetMs} - ${released.windowMs}
|
|
1318
|
-
UNION ALL SELECT value FROM json_each(${JSON.stringify(released.keys)})
|
|
1309
|
+
-- The keys released within the window, but those asked for, up to one past the cap (snapshot.postgres.ts).
|
|
1310
|
+
SELECT count(*) FROM (
|
|
1311
|
+
SELECT 1 FROM vault_releases
|
|
1312
|
+
WHERE principal = ${released.principal} AND released_at > ${NOW} + ${released.clockOffsetMs} - ${released.windowMs}
|
|
1313
|
+
AND key NOT IN (SELECT value FROM json_each(${JSON.stringify(released.keys)}))
|
|
1314
|
+
LIMIT ${released.cap + 1}
|
|
1319
1315
|
)
|
|
1320
|
-
)`}
|
|
1316
|
+
) + (SELECT count(DISTINCT value) FROM json_each(${JSON.stringify(released.keys)}))`}
|
|
1321
1317
|
) AS snapshot`;
|
|
1322
1318
|
}
|
|
1323
1319
|
//#endregion
|
|
@@ -2008,7 +2004,8 @@ var VaultService = class {
|
|
|
2008
2004
|
...released === void 0 ? {} : { released: {
|
|
2009
2005
|
...released,
|
|
2010
2006
|
windowMs: this.#config.bulkLimit.windowMs,
|
|
2011
|
-
clockOffsetMs: this.#prepared.options.clockOffset()
|
|
2007
|
+
clockOffsetMs: this.#prepared.options.clockOffset(),
|
|
2008
|
+
cap: this.#config.bulkLimit.count
|
|
2012
2009
|
} }
|
|
2013
2010
|
});
|
|
2014
2011
|
}
|
|
@@ -4057,9 +4054,9 @@ var VaultService = class {
|
|
|
4057
4054
|
const { wrongKek, logKeys } = this.#prepared;
|
|
4058
4055
|
const { walks } = this.#prepared.options;
|
|
4059
4056
|
const found = [];
|
|
4060
|
-
let seen
|
|
4057
|
+
let seen;
|
|
4061
4058
|
const resumed = await this.#db.transaction(async (tx) => {
|
|
4062
|
-
seen =
|
|
4059
|
+
seen = await newestCheckpointEntry(tx);
|
|
4063
4060
|
await this.#sweep(tx, found);
|
|
4064
4061
|
const from = await this.#resume(tx, "newest");
|
|
4065
4062
|
if (!from.ok) return from;
|
|
@@ -4111,10 +4108,14 @@ var VaultService = class {
|
|
|
4111
4108
|
reason: scrub.reason
|
|
4112
4109
|
});
|
|
4113
4110
|
const newest = latest === void 0 ? null : JSON.parse(latest.metadata);
|
|
4114
|
-
if (stalled !== null || ((await newestCheckpointEntry(d.tx))?.seq ?? null) !== seen) {
|
|
4111
|
+
if (stalled !== null || ((await newestCheckpointEntry(d.tx))?.seq ?? null) !== (seen?.seq ?? null)) {
|
|
4115
4112
|
const standing = await this.#standingRefusal(d.tx);
|
|
4116
4113
|
if (standing !== null) throw refused("log_broken", standing);
|
|
4117
4114
|
if (stalled === null && newest !== null) return { checkpoint: signedPart(newest) };
|
|
4115
|
+
} else if (seen?.decision === "deny" && walked.ok) {
|
|
4116
|
+
const standing = await this.#standingRefusal(d.tx);
|
|
4117
|
+
const named = typeof standing?.failedAtSeq === "number" ? BigInt(standing.failedAtSeq) : null;
|
|
4118
|
+
if (named !== null && named <= walked.from.at.seq && !await this.#entryHolds(d.tx, named)) throw refused("log_broken", standing);
|
|
4118
4119
|
}
|
|
4119
4120
|
if (newest !== null && latest.seq === head.nextSeq - 1n && latest.relatedSeq === latest.seq - 1n) return { checkpoint: signedPart(newest) };
|
|
4120
4121
|
if (newest !== null && !await carries(d.tx, newest)) throw refused("log_broken", { reason: `the log up to entry ${newest.seq} is not the prefix the last checkpoint signed` });
|
|
@@ -4252,6 +4253,30 @@ var VaultService = class {
|
|
|
4252
4253
|
};
|
|
4253
4254
|
}
|
|
4254
4255
|
/**
|
|
4256
|
+
* Whether entry `seq` is as the walk checks each: there, its hash its
|
|
4257
|
+
* own, linked to the entry before, and the vault's under its MAC; and the
|
|
4258
|
+
* entry after it, if any, linked to it, for a refusal can name either side
|
|
4259
|
+
* of a broken link (`forward`, at the end of a batch).
|
|
4260
|
+
*/
|
|
4261
|
+
async #entryHolds(db, seq) {
|
|
4262
|
+
const found = await entriesAt(db, seq === 0n ? [seq, seq + 1n] : [
|
|
4263
|
+
seq - 1n,
|
|
4264
|
+
seq,
|
|
4265
|
+
seq + 1n
|
|
4266
|
+
]);
|
|
4267
|
+
const entry = found.get(seq);
|
|
4268
|
+
const before = seq === 0n ? { hash: GENESIS_HASH } : found.get(seq - 1n);
|
|
4269
|
+
if (entry === void 0 || before === void 0) return false;
|
|
4270
|
+
const after = found.get(seq + 1n);
|
|
4271
|
+
if (after !== void 0 && !after.prevHash.equals(entry.hash)) return false;
|
|
4272
|
+
return verifyEntries([entry], {
|
|
4273
|
+
keys: this.#keysFor(entry),
|
|
4274
|
+
chainOnly: ["app"],
|
|
4275
|
+
startSeq: seq,
|
|
4276
|
+
startPrevHash: before.hash
|
|
4277
|
+
}).ok;
|
|
4278
|
+
}
|
|
4279
|
+
/**
|
|
4255
4280
|
* The newest refusal for a broken log since the newest checkpoint was
|
|
4256
4281
|
* signed, by the vault's MAC, as it said: it stands, for every
|
|
4257
4282
|
* verification and checkpoint, until a checkpoint is signed, which takes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/vault",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
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,8 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"drizzle-orm": "0.45.2",
|
|
39
|
-
"@coffre/core": "0.14.
|
|
40
|
-
"@coffre/db": "0.14.
|
|
39
|
+
"@coffre/core": "0.14.4",
|
|
40
|
+
"@coffre/db": "0.14.4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "26.1.1",
|
package/src/snapshot.postgres.ts
CHANGED
|
@@ -82,16 +82,15 @@ export function snapshotQuery(asked: Asked): SQL {
|
|
|
82
82
|
'released', ${released === undefined
|
|
83
83
|
? sql`0`
|
|
84
84
|
: sql`(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)`}
|
|
85
|
+
-- The keys released within the window, but those asked for, up to one past the cap: a row for each key the
|
|
86
|
+
-- principal was ever released, however many reads of them the log holds (vault_releases, which the database
|
|
87
|
+
-- keeps from it). The bound is a subquery, evaluated once: clock_timestamp() alone is volatile, and runs a row.
|
|
88
|
+
SELECT count(*) FROM (
|
|
89
|
+
SELECT 1 FROM vault_releases
|
|
90
|
+
WHERE principal = ${released.principal} AND released_at > (SELECT ${NOW} + ${released.clockOffsetMs} - ${released.windowMs})
|
|
91
|
+
AND key <> ALL(${sql.param([...released.keys])}::text[])
|
|
92
|
+
LIMIT ${released.cap + 1}
|
|
93
|
+
) AS before
|
|
94
|
+
) + (SELECT count(DISTINCT key) FROM unnest(${sql.param([...released.keys])}::text[]) AS asked (key))`}
|
|
96
95
|
) AS snapshot`;
|
|
97
96
|
}
|
package/src/snapshot.sqlite.ts
CHANGED
|
@@ -88,16 +88,13 @@ export function snapshotQuery(asked: Asked): SQL {
|
|
|
88
88
|
'released', ${released === undefined
|
|
89
89
|
? sql`0`
|
|
90
90
|
: sql`(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
WHERE e.author = 'vault' AND e.actor = ${released.principal} AND e.action = 'secret.read' AND e.decision = 'allow'
|
|
98
|
-
AND e.occurred_at > ${NOW} + ${released.clockOffsetMs} - ${released.windowMs}
|
|
99
|
-
UNION ALL SELECT value FROM json_each(${JSON.stringify(released.keys)})
|
|
91
|
+
-- The keys released within the window, but those asked for, up to one past the cap (snapshot.postgres.ts).
|
|
92
|
+
SELECT count(*) FROM (
|
|
93
|
+
SELECT 1 FROM vault_releases
|
|
94
|
+
WHERE principal = ${released.principal} AND released_at > ${NOW} + ${released.clockOffsetMs} - ${released.windowMs}
|
|
95
|
+
AND key NOT IN (SELECT value FROM json_each(${JSON.stringify(released.keys)}))
|
|
96
|
+
LIMIT ${released.cap + 1}
|
|
100
97
|
)
|
|
101
|
-
)`}
|
|
98
|
+
) + (SELECT count(DISTINCT value) FROM json_each(${JSON.stringify(released.keys)}))`}
|
|
102
99
|
) AS snapshot`;
|
|
103
100
|
}
|
package/src/snapshot.ts
CHANGED
|
@@ -49,7 +49,9 @@ export type Snapshot = {
|
|
|
49
49
|
/**
|
|
50
50
|
* How many distinct keys `asked.released.principal` holds released within
|
|
51
51
|
* its window once it is given `asked.released.keys` too: a key released
|
|
52
|
-
* again, or asked for again, counts once.
|
|
52
|
+
* again, or asked for again, counts once. Those released before are
|
|
53
|
+
* counted up to one past `asked.released.cap`, which is enough to say
|
|
54
|
+
* whether the read goes past it.
|
|
53
55
|
*/
|
|
54
56
|
released: number;
|
|
55
57
|
/** The log's head, locked, with the last entry and the newest `key.rotate`; null unless asked, or held by another. */
|
|
@@ -74,9 +76,11 @@ export type Asked = {
|
|
|
74
76
|
references?: readonly bigint[];
|
|
75
77
|
/**
|
|
76
78
|
* One principal's releases within the last `windowMs` of the clock, moved
|
|
77
|
-
* on by `clockOffsetMs` as the vault's is, with `keys`, secret ids, besides
|
|
79
|
+
* on by `clockOffsetMs` as the vault's is, with `keys`, secret ids, besides;
|
|
80
|
+
* read from `vault_releases`, which the database keeps from the log, up to
|
|
81
|
+
* `cap` + 1 of them.
|
|
78
82
|
*/
|
|
79
|
-
released?: { principal: string; keys: readonly string[]; windowMs: number; clockOffsetMs: number };
|
|
83
|
+
released?: { principal: string; keys: readonly string[]; windowMs: number; clockOffsetMs: number; cap: number };
|
|
80
84
|
/** Lock the log's head first. */
|
|
81
85
|
head?: boolean;
|
|
82
86
|
};
|
package/src/vault.ts
CHANGED
|
@@ -498,7 +498,7 @@ class VaultService implements Vault {
|
|
|
498
498
|
...rest,
|
|
499
499
|
principals,
|
|
500
500
|
head,
|
|
501
|
-
...(released === undefined ? {} : { released: { ...released, windowMs: this.#config.bulkLimit.windowMs, clockOffsetMs: this.#prepared.options.clockOffset() } }),
|
|
501
|
+
...(released === undefined ? {} : { released: { ...released, windowMs: this.#config.bulkLimit.windowMs, clockOffsetMs: this.#prepared.options.clockOffset(), cap: this.#config.bulkLimit.count } }),
|
|
502
502
|
});
|
|
503
503
|
}
|
|
504
504
|
|
|
@@ -2578,9 +2578,9 @@ class VaultService implements Vault {
|
|
|
2578
2578
|
const { walks } = this.#prepared.options;
|
|
2579
2579
|
const found: NewEntry[] = [];
|
|
2580
2580
|
// The newest checkpoint entry, signed or refused, as this call's snapshot found it.
|
|
2581
|
-
let seen:
|
|
2581
|
+
let seen: StoredEntry | undefined;
|
|
2582
2582
|
const resumed = await this.#db.transaction(async (tx) => {
|
|
2583
|
-
seen =
|
|
2583
|
+
seen = await store.newestCheckpointEntry(tx);
|
|
2584
2584
|
await this.#sweep(tx, found);
|
|
2585
2585
|
const from = await this.#resume(tx, 'newest');
|
|
2586
2586
|
if (!from.ok) return from;
|
|
@@ -2625,10 +2625,17 @@ class VaultService implements Vault {
|
|
|
2625
2625
|
// Another call checkpointed, or refused to, since this one's snapshot, or this one's rehash of the past
|
|
2626
2626
|
// could not run: what it would sign over is not what it checked. A refusal for a broken log stands,
|
|
2627
2627
|
// and a checkpoint signed meanwhile is the newest prefix; the next call starts from them.
|
|
2628
|
-
if (stalled !== null || ((await store.newestCheckpointEntry(d.tx))?.seq ?? null) !== seen) {
|
|
2628
|
+
if (stalled !== null || ((await store.newestCheckpointEntry(d.tx))?.seq ?? null) !== (seen?.seq ?? null)) {
|
|
2629
2629
|
const standing = await this.#standingRefusal(d.tx);
|
|
2630
2630
|
if (standing !== null) throw refused('log_broken', standing);
|
|
2631
2631
|
if (stalled === null && newest !== null) return { checkpoint: signedPart(newest) };
|
|
2632
|
+
} else if (seen?.decision === 'deny' && walked.ok) {
|
|
2633
|
+
// A refusal for a broken log stands since this call's snapshot, of an entry before where its walk began, which
|
|
2634
|
+
// it never read: that entry again, as the walk checks each, before anything is signed over it. Still broken,
|
|
2635
|
+
// the refusal stands; whole again, the signature clears it (T104).
|
|
2636
|
+
const standing = await this.#standingRefusal(d.tx);
|
|
2637
|
+
const named = typeof standing?.failedAtSeq === 'number' ? BigInt(standing.failedAtSeq) : null;
|
|
2638
|
+
if (named !== null && named <= walked.from.at.seq && !(await this.#entryHolds(d.tx, named))) throw refused('log_broken', standing!);
|
|
2632
2639
|
}
|
|
2633
2640
|
// Nothing since the last one, which signed every entry before its own: it is still the newest prefix.
|
|
2634
2641
|
if (newest !== null && latest!.seq === head.nextSeq - 1n && latest!.relatedSeq === latest!.seq - 1n) return { checkpoint: signedPart(newest) };
|
|
@@ -2722,6 +2729,22 @@ class VaultService implements Vault {
|
|
|
2722
2729
|
return { ok: true, next: null };
|
|
2723
2730
|
}
|
|
2724
2731
|
|
|
2732
|
+
/**
|
|
2733
|
+
* Whether entry `seq` is as the walk checks each: there, its hash its
|
|
2734
|
+
* own, linked to the entry before, and the vault's under its MAC; and the
|
|
2735
|
+
* entry after it, if any, linked to it, for a refusal can name either side
|
|
2736
|
+
* of a broken link (`forward`, at the end of a batch).
|
|
2737
|
+
*/
|
|
2738
|
+
async #entryHolds(db: Queryable, seq: bigint): Promise<boolean> {
|
|
2739
|
+
const found = await store.entriesAt(db, seq === 0n ? [seq, seq + 1n] : [seq - 1n, seq, seq + 1n]);
|
|
2740
|
+
const entry = found.get(seq);
|
|
2741
|
+
const before = seq === 0n ? { hash: GENESIS_HASH } : found.get(seq - 1n);
|
|
2742
|
+
if (entry === undefined || before === undefined) return false;
|
|
2743
|
+
const after = found.get(seq + 1n);
|
|
2744
|
+
if (after !== undefined && !after.prevHash.equals(entry.hash)) return false;
|
|
2745
|
+
return verifyEntries([entry], { keys: this.#keysFor(entry), chainOnly: ['app'], startSeq: seq, startPrevHash: before.hash }).ok;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2725
2748
|
/**
|
|
2726
2749
|
* The newest refusal for a broken log since the newest checkpoint was
|
|
2727
2750
|
* signed, by the vault's MAC, as it said: it stands, for every
|