@coffre/vault 0.1.14 → 0.1.16
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-3O-ynZPg.js → vault-BexBMpZL.js} +72 -7
- package/package.json +3 -3
- package/src/store.ts +12 -1
- package/src/vault.ts +67 -10
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-1yVV2L0_.js";
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-BexBMpZL.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-1yVV2L0_.js";
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-BexBMpZL.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";
|
|
@@ -7,7 +7,7 @@ import { GENESIS_HASH, deriveLogKey, verifyEntries } from "@coffre/core/audit";
|
|
|
7
7
|
import { checkContext } from "@coffre/core/envelope";
|
|
8
8
|
import { SNAPSHOT, clockMillis, engineOf, forUpdate, isUniqueViolation } from "@coffre/db/dialect";
|
|
9
9
|
import { appendEntries, lockLogHead } from "@coffre/db/log";
|
|
10
|
-
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, sql } from "drizzle-orm";
|
|
10
|
+
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, lte, sql } from "drizzle-orm";
|
|
11
11
|
//#region src/accounting.ts
|
|
12
12
|
const KEY_ACTIONS = /* @__PURE__ */ new Set([
|
|
13
13
|
"secret.read",
|
|
@@ -356,6 +356,16 @@ async function hashesAt(db, seqs) {
|
|
|
356
356
|
}
|
|
357
357
|
/** How many entries' hashes one query asks for: a checkpoint every five minutes is tens of thousands of a year. */
|
|
358
358
|
const HASHES_AT_ONCE = 5e3;
|
|
359
|
+
/**
|
|
360
|
+
* Whether every entry from the first through `seq` is there. Seqs are the
|
|
361
|
+
* primary key, so the count is `seq + 1` exactly when none is missing: one
|
|
362
|
+
* pass over the key's index, never the entries.
|
|
363
|
+
*/
|
|
364
|
+
async function complete(db, seq) {
|
|
365
|
+
const { auditLog } = tablesOf(db);
|
|
366
|
+
const [{ n }] = await db.select({ n: count() }).from(auditLog).where(and(gte(auditLog.seq, 0n), lte(auditLog.seq, seq)));
|
|
367
|
+
return BigInt(n) === seq + 1n;
|
|
368
|
+
}
|
|
359
369
|
/** The entry at `seq`'s hash, or undefined when there is none. */
|
|
360
370
|
async function hashAt(db, seq) {
|
|
361
371
|
const { auditLog } = tablesOf(db);
|
|
@@ -856,6 +866,12 @@ const TAMPERED_SUBJECT = "this member's record failed the vault's integrity chec
|
|
|
856
866
|
const CHECKPOINT = "audit.checkpoint";
|
|
857
867
|
/** Who asks for checkpoints: the app's scheduled job. */
|
|
858
868
|
const SCHEDULER = "system:coffre-scheduler";
|
|
869
|
+
/**
|
|
870
|
+
* How often a checkpoint recomputes the chain from its first entry: the
|
|
871
|
+
* first of each hour, by the vault's clock. The others resume from the
|
|
872
|
+
* prefix the last one signed (`#recompute`).
|
|
873
|
+
*/
|
|
874
|
+
const FULL_RECOMPUTE_MS = 36e5;
|
|
859
875
|
/** A refusal and the entries that record it. */
|
|
860
876
|
var Refused = class {
|
|
861
877
|
refusal;
|
|
@@ -898,6 +914,10 @@ const MESSAGES = {
|
|
|
898
914
|
wrong_kek: "this vault's key does not open the data it holds",
|
|
899
915
|
tampered: "this member's record failed the vault's integrity check"
|
|
900
916
|
};
|
|
917
|
+
/** The credential a CI run's request came in on, as its entries record it: copied, never decided on. */
|
|
918
|
+
function traced(correlation) {
|
|
919
|
+
return correlation.credentialId == null ? {} : { credentialId: correlation.credentialId };
|
|
920
|
+
}
|
|
901
921
|
/**
|
|
902
922
|
* The one implementation of `Vault`. Every decision is one transaction on
|
|
903
923
|
* the shared database: it locks the rows of the members it is about, reads
|
|
@@ -1319,7 +1339,8 @@ var VaultService = class {
|
|
|
1319
1339
|
requestId: input.requestId,
|
|
1320
1340
|
metadata: JSON.stringify({
|
|
1321
1341
|
...detail,
|
|
1322
|
-
secretVersionId: id
|
|
1342
|
+
secretVersionId: id,
|
|
1343
|
+
...traced(input)
|
|
1323
1344
|
})
|
|
1324
1345
|
};
|
|
1325
1346
|
return {
|
|
@@ -1408,6 +1429,7 @@ var VaultService = class {
|
|
|
1408
1429
|
operation: action,
|
|
1409
1430
|
expiresAt: at + this.#prepared.options.keyBudgetMs + 2 * LOCK_TIMEOUT_MS,
|
|
1410
1431
|
...call.input.purpose === void 0 ? {} : { purpose: call.input.purpose },
|
|
1432
|
+
...traced(call.input),
|
|
1411
1433
|
keys: secrets.map((secret, item) => ({
|
|
1412
1434
|
item,
|
|
1413
1435
|
subject: secret.path,
|
|
@@ -2008,16 +2030,53 @@ var VaultService = class {
|
|
|
2008
2030
|
const row = await latestVaultEntry(db, [CHECKPOINT]);
|
|
2009
2031
|
return row === void 0 ? null : {
|
|
2010
2032
|
checkpoint: JSON.parse(row.metadata),
|
|
2011
|
-
seq: row.seq
|
|
2033
|
+
seq: row.seq,
|
|
2034
|
+
keyId: row.keyId
|
|
2012
2035
|
};
|
|
2013
2036
|
}
|
|
2037
|
+
/**
|
|
2038
|
+
* The chain recomputed to its last entry, for a checkpoint to sign: from
|
|
2039
|
+
* its first entry at the first checkpoint of each hour, and otherwise from
|
|
2040
|
+
* the prefix the last checkpoint signed, if that one was signed this hour.
|
|
2041
|
+
*
|
|
2042
|
+
* Resuming never signs what the vault did not recompute. The vault
|
|
2043
|
+
* recomputed the last checkpoint's prefix itself, from entry 0 at the
|
|
2044
|
+
* hour's first checkpoint and onward at each one since, and the entry
|
|
2045
|
+
* that holds it is the first one checked again, by its MAC. The next
|
|
2046
|
+
* signature extends that same chain.
|
|
2047
|
+
*
|
|
2048
|
+
* What resuming does not do is recompute the rows of that prefix again.
|
|
2049
|
+
* They can change while the hash stored at its end stays, so it first
|
|
2050
|
+
* checks the two things that are cheap: that hash is still there, which a
|
|
2051
|
+
* prefix rewritten and chained again changes, and every entry of the
|
|
2052
|
+
* prefix is, which a cut changes. Either failing, it recomputes from entry
|
|
2053
|
+
* 0, which names what broke. An entry edited in place, its hash left as
|
|
2054
|
+
* it was, is the one change left to the next full pass, within the hour.
|
|
2055
|
+
*
|
|
2056
|
+
* It resumes only from a checkpoint under the key the vault writes with
|
|
2057
|
+
* now: once the log reaches that key, no other writes again (`forward`).
|
|
2058
|
+
* After a rotation, the next pass is full.
|
|
2059
|
+
*/
|
|
2060
|
+
async #recompute(tx) {
|
|
2061
|
+
const { logKeys } = this.#prepared;
|
|
2062
|
+
const latest = await this.#latest(tx);
|
|
2063
|
+
const hour = (ms) => Math.floor(ms / FULL_RECOMPUTE_MS);
|
|
2064
|
+
if (!(latest !== null && latest.keyId === logKeys[0].keyId && latest.seq === BigInt(latest.checkpoint.seq) + 1n && hour(Date.parse(latest.checkpoint.signedAt)) === hour(await this.#now(tx)) && await carries(tx, latest.checkpoint) && await complete(tx, BigInt(latest.checkpoint.seq)))) return verifyChain(tx, logKeys, [], UNVERIFIED);
|
|
2065
|
+
const { seq, hash } = latest.checkpoint;
|
|
2066
|
+
return verifyChain(tx, logKeys, [], {
|
|
2067
|
+
nextSeq: BigInt(seq) + 1n,
|
|
2068
|
+
hash: Buffer.from(hash, "hex"),
|
|
2069
|
+
vaultEntries: 0,
|
|
2070
|
+
vaultKeys: [logKeys[0].keyId]
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2014
2073
|
async checkpoint() {
|
|
2015
2074
|
await this.#settled();
|
|
2016
2075
|
const { wrongKek } = this.#prepared;
|
|
2017
2076
|
const found = [];
|
|
2018
2077
|
const whole = await this.#db.transaction(async (tx) => {
|
|
2019
2078
|
await this.#sweep(tx, found);
|
|
2020
|
-
return
|
|
2079
|
+
return this.#recompute(tx);
|
|
2021
2080
|
}, SNAPSHOT);
|
|
2022
2081
|
return this.#decide([], async (d) => {
|
|
2023
2082
|
for (const entry of found) if (!d.reports.includes(entry)) d.reports.push(entry);
|
|
@@ -2339,6 +2398,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
|
2339
2398
|
function validateCorrelation(input) {
|
|
2340
2399
|
if (input.requestId != null) validateText(input.requestId);
|
|
2341
2400
|
if (input.operationId != null && (typeof input.operationId !== "string" || !UUID.test(input.operationId))) throw new Error(`operationId must be a lowercase UUID, got: ${String(input.operationId)}`);
|
|
2401
|
+
if (input.credentialId != null && (typeof input.credentialId !== "string" || !UUID.test(input.credentialId))) throw new Error(`credentialId must be a lowercase UUID, got: ${String(input.credentialId)}`);
|
|
2342
2402
|
}
|
|
2343
2403
|
/** Validate the entire batch before any provider sees a key. */
|
|
2344
2404
|
function validateItems(items, field) {
|
|
@@ -2399,7 +2459,8 @@ function keyEntry(action, principal, secret, decision, code, correlation, detail
|
|
|
2399
2459
|
subject: secret.path,
|
|
2400
2460
|
...action === "key.wrap" ? { secretId: secret.secretId } : {},
|
|
2401
2461
|
version: secret.version,
|
|
2402
|
-
...detail
|
|
2462
|
+
...detail,
|
|
2463
|
+
...traced(correlation)
|
|
2403
2464
|
})
|
|
2404
2465
|
};
|
|
2405
2466
|
}
|
|
@@ -2417,9 +2478,13 @@ function accessEntry(actor, action, principal, decision, correlation, detail, co
|
|
|
2417
2478
|
subjectPrincipal: PRINCIPAL.test(principal) ? principal : null,
|
|
2418
2479
|
operationId: correlation.operationId ?? null,
|
|
2419
2480
|
requestId: correlation.requestId ?? null,
|
|
2420
|
-
metadata: JSON.stringify(PRINCIPAL.test(principal) ?
|
|
2481
|
+
metadata: JSON.stringify(PRINCIPAL.test(principal) ? {
|
|
2482
|
+
...detail,
|
|
2483
|
+
...traced(correlation)
|
|
2484
|
+
} : {
|
|
2421
2485
|
subject: principal,
|
|
2422
|
-
...detail
|
|
2486
|
+
...detail,
|
|
2487
|
+
...traced(correlation)
|
|
2423
2488
|
})
|
|
2424
2489
|
};
|
|
2425
2490
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/vault",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
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.1.
|
|
40
|
-
"@coffre/db": "0.1.
|
|
39
|
+
"@coffre/core": "0.1.16",
|
|
40
|
+
"@coffre/db": "0.1.16"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "26.1.1",
|
package/src/store.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Author, StoredEntry } from '@coffre/core/audit';
|
|
|
2
2
|
import { ACCESS_ACTIONS, type SecretRef, type WrappedKey } from '@coffre/core/vault';
|
|
3
3
|
import { tablesOf, type Queryable, type Transaction } from '@coffre/db';
|
|
4
4
|
import { clockMillis, engineOf, forUpdate } from '@coffre/db/dialect';
|
|
5
|
-
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, sql } from 'drizzle-orm';
|
|
5
|
+
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, lte, sql } from 'drizzle-orm';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Every query the vault makes, and the only code in it that holds them.
|
|
@@ -316,6 +316,17 @@ export async function hashesAt(db: Queryable, seqs: readonly bigint[]): Promise<
|
|
|
316
316
|
/** How many entries' hashes one query asks for: a checkpoint every five minutes is tens of thousands of a year. */
|
|
317
317
|
const HASHES_AT_ONCE = 5000;
|
|
318
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Whether every entry from the first through `seq` is there. Seqs are the
|
|
321
|
+
* primary key, so the count is `seq + 1` exactly when none is missing: one
|
|
322
|
+
* pass over the key's index, never the entries.
|
|
323
|
+
*/
|
|
324
|
+
export async function complete(db: Queryable, seq: bigint): Promise<boolean> {
|
|
325
|
+
const { auditLog } = tablesOf(db);
|
|
326
|
+
const [{ n }] = await db.select({ n: count() }).from(auditLog).where(and(gte(auditLog.seq, 0n), lte(auditLog.seq, seq)));
|
|
327
|
+
return BigInt(n) === seq + 1n;
|
|
328
|
+
}
|
|
329
|
+
|
|
319
330
|
/** The entry at `seq`'s hash, or undefined when there is none. */
|
|
320
331
|
export async function hashAt(db: Queryable, seq: bigint): Promise<Buffer | undefined> {
|
|
321
332
|
const { auditLog } = tablesOf(db);
|
package/src/vault.ts
CHANGED
|
@@ -179,6 +179,13 @@ const CHECKPOINT = 'audit.checkpoint';
|
|
|
179
179
|
/** Who asks for checkpoints: the app's scheduled job. */
|
|
180
180
|
const SCHEDULER = 'system:coffre-scheduler';
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* How often a checkpoint recomputes the chain from its first entry: the
|
|
184
|
+
* first of each hour, by the vault's clock. The others resume from the
|
|
185
|
+
* prefix the last one signed (`#recompute`).
|
|
186
|
+
*/
|
|
187
|
+
const FULL_RECOMPUTE_MS = 60 * 60 * 1000;
|
|
188
|
+
|
|
182
189
|
/** A refusal and the entries that record it. */
|
|
183
190
|
class Refused {
|
|
184
191
|
readonly refusal: Refusal;
|
|
@@ -258,7 +265,12 @@ type Decision = {
|
|
|
258
265
|
type KeyAction = 'secret.read' | 'key.wrap' | 'key.rewrap';
|
|
259
266
|
|
|
260
267
|
/** What ties an entry to the app's request, and to the one action it is part of. */
|
|
261
|
-
type Correlation = { requestId?: string | null; operationId?: string | null };
|
|
268
|
+
type Correlation = { requestId?: string | null; operationId?: string | null; credentialId?: string | null };
|
|
269
|
+
|
|
270
|
+
/** The credential a CI run's request came in on, as its entries record it: copied, never decided on. */
|
|
271
|
+
function traced(correlation: Correlation): { credentialId?: string } {
|
|
272
|
+
return correlation.credentialId == null ? {} : { credentialId: correlation.credentialId };
|
|
273
|
+
}
|
|
262
274
|
|
|
263
275
|
/** Why a member's row is not the one the vault last wrote; rows.ts. */
|
|
264
276
|
type Fault = 'mac' | 'stale';
|
|
@@ -707,7 +719,7 @@ class VaultService implements Vault {
|
|
|
707
719
|
if (version === undefined) return {
|
|
708
720
|
actor: input.principal, action, decision: 'deny', code: 'bad_claim',
|
|
709
721
|
operationId: input.operationId, requestId: input.requestId,
|
|
710
|
-
metadata: JSON.stringify({ ...detail, secretVersionId: id }),
|
|
722
|
+
metadata: JSON.stringify({ ...detail, secretVersionId: id, ...traced(input) }),
|
|
711
723
|
};
|
|
712
724
|
return { ...keyEntry(action, input.principal, version.secret, 'deny', 'bad_claim', input, detail), secretVersionId: id };
|
|
713
725
|
}));
|
|
@@ -817,6 +829,7 @@ class VaultService implements Vault {
|
|
|
817
829
|
// Member and outcome locks can each wait before accounting is overdue.
|
|
818
830
|
expiresAt: at + this.#prepared.options.keyBudgetMs + 2 * LOCK_TIMEOUT_MS,
|
|
819
831
|
...(call.input.purpose === undefined ? {} : { purpose: call.input.purpose }),
|
|
832
|
+
...traced(call.input),
|
|
820
833
|
keys: secrets.map((secret, item) => ({ item, subject: secret.path, secretId: secret.secretId, version: secret.version })),
|
|
821
834
|
}),
|
|
822
835
|
}]);
|
|
@@ -1374,9 +1387,49 @@ class VaultService implements Vault {
|
|
|
1374
1387
|
// --- checkpoints and the log --------------------------------------------------
|
|
1375
1388
|
|
|
1376
1389
|
/** The last checkpoint the vault signed, and the entry that holds it: its newest allowed `audit.checkpoint`. */
|
|
1377
|
-
async #latest(db: Queryable): Promise<{ checkpoint: Checkpoint; seq: bigint } | null> {
|
|
1390
|
+
async #latest(db: Queryable): Promise<{ checkpoint: Checkpoint; seq: bigint; keyId: string } | null> {
|
|
1378
1391
|
const row = await store.latestVaultEntry(db, [CHECKPOINT]);
|
|
1379
|
-
return row === undefined ? null : { checkpoint: JSON.parse(row.metadata) as Checkpoint, seq: row.seq };
|
|
1392
|
+
return row === undefined ? null : { checkpoint: JSON.parse(row.metadata) as Checkpoint, seq: row.seq, keyId: row.keyId };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* The chain recomputed to its last entry, for a checkpoint to sign: from
|
|
1397
|
+
* its first entry at the first checkpoint of each hour, and otherwise from
|
|
1398
|
+
* the prefix the last checkpoint signed, if that one was signed this hour.
|
|
1399
|
+
*
|
|
1400
|
+
* Resuming never signs what the vault did not recompute. The vault
|
|
1401
|
+
* recomputed the last checkpoint's prefix itself, from entry 0 at the
|
|
1402
|
+
* hour's first checkpoint and onward at each one since, and the entry
|
|
1403
|
+
* that holds it is the first one checked again, by its MAC. The next
|
|
1404
|
+
* signature extends that same chain.
|
|
1405
|
+
*
|
|
1406
|
+
* What resuming does not do is recompute the rows of that prefix again.
|
|
1407
|
+
* They can change while the hash stored at its end stays, so it first
|
|
1408
|
+
* checks the two things that are cheap: that hash is still there, which a
|
|
1409
|
+
* prefix rewritten and chained again changes, and every entry of the
|
|
1410
|
+
* prefix is, which a cut changes. Either failing, it recomputes from entry
|
|
1411
|
+
* 0, which names what broke. An entry edited in place, its hash left as
|
|
1412
|
+
* it was, is the one change left to the next full pass, within the hour.
|
|
1413
|
+
*
|
|
1414
|
+
* It resumes only from a checkpoint under the key the vault writes with
|
|
1415
|
+
* now: once the log reaches that key, no other writes again (`forward`).
|
|
1416
|
+
* After a rotation, the next pass is full.
|
|
1417
|
+
*/
|
|
1418
|
+
async #recompute(tx: Queryable): Promise<{ verification: LogVerification; anchor: Anchor }> {
|
|
1419
|
+
const { logKeys } = this.#prepared;
|
|
1420
|
+
const latest = await this.#latest(tx);
|
|
1421
|
+
const hour = (ms: number) => Math.floor(ms / FULL_RECOMPUTE_MS);
|
|
1422
|
+
const resumable =
|
|
1423
|
+
latest !== null &&
|
|
1424
|
+
latest.keyId === logKeys[0].keyId &&
|
|
1425
|
+
latest.seq === BigInt(latest.checkpoint.seq) + 1n &&
|
|
1426
|
+
hour(Date.parse(latest.checkpoint.signedAt)) === hour(await this.#now(tx)) &&
|
|
1427
|
+
(await carries(tx, latest.checkpoint)) &&
|
|
1428
|
+
(await store.complete(tx, BigInt(latest.checkpoint.seq)));
|
|
1429
|
+
if (!resumable) return verifyChain(tx, logKeys, [], UNVERIFIED);
|
|
1430
|
+
const { seq, hash } = latest.checkpoint;
|
|
1431
|
+
const signed: Anchor = { nextSeq: BigInt(seq) + 1n, hash: Buffer.from(hash, 'hex'), vaultEntries: 0, vaultKeys: [logKeys[0].keyId] };
|
|
1432
|
+
return verifyChain(tx, logKeys, [], signed);
|
|
1380
1433
|
}
|
|
1381
1434
|
|
|
1382
1435
|
async checkpoint(): Promise<Outcome<{ checkpoint: Checkpoint }>> {
|
|
@@ -1386,14 +1439,14 @@ class VaultService implements Vault {
|
|
|
1386
1439
|
const { wrongKek } = this.#prepared;
|
|
1387
1440
|
// Then, in one snapshot and without the log's lock, so that no append
|
|
1388
1441
|
// waits on it: every member's row checked, since rows changed around the
|
|
1389
|
-
// vault write nothing to the log; and the
|
|
1390
|
-
//
|
|
1391
|
-
// since an entry cut from the middle leaves the hashes
|
|
1392
|
-
// checkpoint as they were.
|
|
1442
|
+
// vault write nothing to the log; and the chain recomputed, every hash
|
|
1443
|
+
// from content and every vault entry by its MAC, from the first entry
|
|
1444
|
+
// once an hour, since an entry cut from the middle leaves the hashes
|
|
1445
|
+
// around a later checkpoint as they were (`#recompute`).
|
|
1393
1446
|
const found: NewEntry[] = [];
|
|
1394
1447
|
const whole = await this.#db.transaction(async (tx) => {
|
|
1395
1448
|
await this.#sweep(tx, found);
|
|
1396
|
-
return
|
|
1449
|
+
return this.#recompute(tx);
|
|
1397
1450
|
}, SNAPSHOT);
|
|
1398
1451
|
return this.#decide([], async (d) => {
|
|
1399
1452
|
for (const entry of found) if (!d.reports.includes(entry)) d.reports.push(entry);
|
|
@@ -1665,6 +1718,9 @@ function validateCorrelation(input: Correlation): void {
|
|
|
1665
1718
|
if (input.operationId != null && (typeof input.operationId !== 'string' || !UUID.test(input.operationId))) {
|
|
1666
1719
|
throw new Error(`operationId must be a lowercase UUID, got: ${String(input.operationId)}`);
|
|
1667
1720
|
}
|
|
1721
|
+
if (input.credentialId != null && (typeof input.credentialId !== 'string' || !UUID.test(input.credentialId))) {
|
|
1722
|
+
throw new Error(`credentialId must be a lowercase UUID, got: ${String(input.credentialId)}`);
|
|
1723
|
+
}
|
|
1668
1724
|
}
|
|
1669
1725
|
|
|
1670
1726
|
/** Validate the entire batch before any provider sees a key. */
|
|
@@ -1733,6 +1789,7 @@ function keyEntry(
|
|
|
1733
1789
|
...(action === 'key.wrap' ? { secretId: secret.secretId } : {}),
|
|
1734
1790
|
version: secret.version,
|
|
1735
1791
|
...detail,
|
|
1792
|
+
...traced(correlation),
|
|
1736
1793
|
}),
|
|
1737
1794
|
};
|
|
1738
1795
|
}
|
|
@@ -1766,7 +1823,7 @@ function accessEntry(
|
|
|
1766
1823
|
subjectPrincipal: PRINCIPAL.test(principal) ? principal : null,
|
|
1767
1824
|
operationId: correlation.operationId ?? null,
|
|
1768
1825
|
requestId: correlation.requestId ?? null,
|
|
1769
|
-
metadata: JSON.stringify(PRINCIPAL.test(principal) ? detail : { subject: principal, ...detail }),
|
|
1826
|
+
metadata: JSON.stringify(PRINCIPAL.test(principal) ? { ...detail, ...traced(correlation) } : { subject: principal, ...detail, ...traced(correlation) }),
|
|
1770
1827
|
};
|
|
1771
1828
|
}
|
|
1772
1829
|
|