@zincapp/znvault-cli 4.22.1 → 4.24.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/dist/commands/lmk-ceremony.d.ts +3 -0
- package/dist/commands/lmk-ceremony.d.ts.map +1 -0
- package/dist/commands/lmk-ceremony.js +597 -0
- package/dist/commands/lmk-ceremony.js.map +1 -0
- package/dist/commands/lmk-escrow.d.ts.map +1 -1
- package/dist/commands/lmk-escrow.js +73 -5
- package/dist/commands/lmk-escrow.js.map +1 -1
- package/dist/commands/lmk-preflight.d.ts +3 -0
- package/dist/commands/lmk-preflight.d.ts.map +1 -0
- package/dist/commands/lmk-preflight.js +117 -0
- package/dist/commands/lmk-preflight.js.map +1 -0
- package/dist/commands/lmk-restore-drill.d.ts +3 -0
- package/dist/commands/lmk-restore-drill.d.ts.map +1 -0
- package/dist/commands/lmk-restore-drill.js +160 -0
- package/dist/commands/lmk-restore-drill.js.map +1 -0
- package/dist/commands/lmk.d.ts.map +1 -1
- package/dist/commands/lmk.js +4 -0
- package/dist/commands/lmk.js.map +1 -1
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/bsk-source.d.ts +21 -0
- package/dist/lib/bsk-source.d.ts.map +1 -0
- package/dist/lib/bsk-source.js +89 -0
- package/dist/lib/bsk-source.js.map +1 -0
- package/dist/lib/ceremony/gates.d.ts +126 -0
- package/dist/lib/ceremony/gates.d.ts.map +1 -0
- package/dist/lib/ceremony/gates.js +270 -0
- package/dist/lib/ceremony/gates.js.map +1 -0
- package/dist/lib/ceremony/system.d.ts +36 -0
- package/dist/lib/ceremony/system.d.ts.map +1 -0
- package/dist/lib/ceremony/system.js +176 -0
- package/dist/lib/ceremony/system.js.map +1 -0
- package/dist/lib/ceremony/workspace.d.ts +54 -0
- package/dist/lib/ceremony/workspace.d.ts.map +1 -0
- package/dist/lib/ceremony/workspace.js +98 -0
- package/dist/lib/ceremony/workspace.js.map +1 -0
- package/dist/lib/config/index.d.ts +1 -1
- package/dist/lib/config/index.d.ts.map +1 -1
- package/dist/lib/config/index.js +1 -1
- package/dist/lib/config/index.js.map +1 -1
- package/dist/lib/config/profile.d.ts +14 -0
- package/dist/lib/config/profile.d.ts.map +1 -1
- package/dist/lib/config/profile.js +22 -0
- package/dist/lib/config/profile.js.map +1 -1
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/db/index.d.ts +9 -1
- package/dist/lib/db/index.d.ts.map +1 -1
- package/dist/lib/db/index.js +9 -0
- package/dist/lib/db/index.js.map +1 -1
- package/dist/lib/db/key-lifecycle.d.ts +86 -0
- package/dist/lib/db/key-lifecycle.d.ts.map +1 -0
- package/dist/lib/db/key-lifecycle.js +182 -0
- package/dist/lib/db/key-lifecycle.js.map +1 -0
- package/dist/lib/db/lmk-escrow.d.ts +18 -0
- package/dist/lib/db/lmk-escrow.d.ts.map +1 -1
- package/dist/lib/db/lmk-escrow.js +25 -0
- package/dist/lib/db/lmk-escrow.js.map +1 -1
- package/dist/lib/db/preflight.d.ts +42 -0
- package/dist/lib/db/preflight.d.ts.map +1 -0
- package/dist/lib/db/preflight.js +165 -0
- package/dist/lib/db/preflight.js.map +1 -0
- package/dist/lib/kcv.d.ts +34 -0
- package/dist/lib/kcv.d.ts.map +1 -0
- package/dist/lib/kcv.js +89 -0
- package/dist/lib/kcv.js.map +1 -0
- package/dist/lib/lmk-escrow-restore.d.ts +6 -2
- package/dist/lib/lmk-escrow-restore.d.ts.map +1 -1
- package/dist/lib/lmk-escrow-restore.js +1 -1
- package/dist/lib/lmk-escrow-restore.js.map +1 -1
- package/dist/lib/lmk-escrow.d.ts +14 -1
- package/dist/lib/lmk-escrow.d.ts.map +1 -1
- package/dist/lib/lmk-escrow.js +5 -1
- package/dist/lib/lmk-escrow.js.map +1 -1
- package/dist/lib/preflight.d.ts +93 -0
- package/dist/lib/preflight.d.ts.map +1 -0
- package/dist/lib/preflight.js +129 -0
- package/dist/lib/preflight.js.map +1 -0
- package/dist/lib/restore-drill.d.ts +73 -0
- package/dist/lib/restore-drill.d.ts.map +1 -0
- package/dist/lib/restore-drill.js +185 -0
- package/dist/lib/restore-drill.js.map +1 -0
- package/dist/lib/sentinel-client.d.ts +26 -0
- package/dist/lib/sentinel-client.d.ts.map +1 -0
- package/dist/lib/sentinel-client.js +165 -0
- package/dist/lib/sentinel-client.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BaseDBClient } from './client.js';
|
|
2
|
+
export type CeremonyPhase = 'preflight' | 'workspace' | 'material' | 'write-a' | 'write-b' | 'verify' | 'teardown';
|
|
3
|
+
export interface PhaseEvent {
|
|
4
|
+
seq: number;
|
|
5
|
+
phase: string;
|
|
6
|
+
state: string;
|
|
7
|
+
at: string;
|
|
8
|
+
detail: Record<string, unknown> | null;
|
|
9
|
+
}
|
|
10
|
+
export interface LifecycleOperation {
|
|
11
|
+
operationId: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
state: string;
|
|
14
|
+
phase: string;
|
|
15
|
+
epoch: number;
|
|
16
|
+
ownerNodeId: string;
|
|
17
|
+
ownerPrincipal: string;
|
|
18
|
+
minRelease: string;
|
|
19
|
+
startedAt: string;
|
|
20
|
+
finishedAt: string | null;
|
|
21
|
+
lastError: string | null;
|
|
22
|
+
}
|
|
23
|
+
export declare class KeyLifecycleOperations extends BaseDBClient {
|
|
24
|
+
/**
|
|
25
|
+
* Is the schema present?
|
|
26
|
+
*
|
|
27
|
+
* The CLI ships independently of the server, so it can meet a deployment that
|
|
28
|
+
* predates migration 093. Saying so plainly beats a `relation does not exist`
|
|
29
|
+
* in the middle of a ceremony.
|
|
30
|
+
*/
|
|
31
|
+
schemaPresent(): Promise<boolean>;
|
|
32
|
+
/** The operation currently holding the exclusion slot, if any. */
|
|
33
|
+
active(): Promise<LifecycleOperation | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Start a ceremony, or find out who already holds the slot.
|
|
36
|
+
*
|
|
37
|
+
* Does NOT look before it leaps: a "is anything running?" SELECT followed by
|
|
38
|
+
* an INSERT is exactly the race the index exists to close. The INSERT is the
|
|
39
|
+
* check, and 23505 is the answer.
|
|
40
|
+
*/
|
|
41
|
+
claim(input: {
|
|
42
|
+
phase: CeremonyPhase;
|
|
43
|
+
ownerNodeId: string;
|
|
44
|
+
ownerPrincipal: string;
|
|
45
|
+
minRelease: string;
|
|
46
|
+
detail?: Record<string, unknown>;
|
|
47
|
+
}): Promise<{
|
|
48
|
+
ok: true;
|
|
49
|
+
operation: LifecycleOperation;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
active: LifecycleOperation | null;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Move to the next phase, recording it in the same transaction.
|
|
56
|
+
*
|
|
57
|
+
* Compare-and-swap on `epoch`: a caller holding a stale read is refused
|
|
58
|
+
* rather than silently overwriting whoever moved first.
|
|
59
|
+
*/
|
|
60
|
+
advance(input: {
|
|
61
|
+
operationId: string;
|
|
62
|
+
expectedEpoch: number;
|
|
63
|
+
phase: CeremonyPhase;
|
|
64
|
+
nodeId: string;
|
|
65
|
+
detail?: Record<string, unknown>;
|
|
66
|
+
}): Promise<LifecycleOperation>;
|
|
67
|
+
/**
|
|
68
|
+
* Close the operation, releasing the slot.
|
|
69
|
+
*
|
|
70
|
+
* `ABANDONED` is a real transition with a reason rather than a DELETE: why a
|
|
71
|
+
* ceremony was abandoned is exactly what the next person needs to read, and a
|
|
72
|
+
* global exclusion with no escape hatch is a deadlock waiting for its first
|
|
73
|
+
* interrupted ceremony.
|
|
74
|
+
*/
|
|
75
|
+
finish(input: {
|
|
76
|
+
operationId: string;
|
|
77
|
+
expectedEpoch: number;
|
|
78
|
+
outcome: 'COMPLETED' | 'FAILED' | 'ABANDONED';
|
|
79
|
+
nodeId: string;
|
|
80
|
+
error?: string;
|
|
81
|
+
detail?: Record<string, unknown>;
|
|
82
|
+
}): Promise<LifecycleOperation>;
|
|
83
|
+
/** The full route travelled, oldest first. */
|
|
84
|
+
history(operationId: string): Promise<PhaseEvent[]>;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=key-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-lifecycle.d.ts","sourceRoot":"","sources":["../../../src/lib/db/key-lifecycle.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,UAAU,GACV,SAAS,GACT,SAAS,GACT,QAAQ,GACR,UAAU,CAAC;AAEf,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAsCD,qBAAa,sBAAuB,SAAQ,YAAY;IACtD;;;;;;OAMG;IACG,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC;IAQvC,kEAAkE;IAC5D,MAAM,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IASlD;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE;QACjB,KAAK,EAAE,aAAa,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,kBAAkB,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAAA;KAAE,CAAC;IAgC3G;;;;;OAKG;IACG,OAAO,CAAC,KAAK,EAAE;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,aAAa,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAqC/B;;;;;;;OAOG;IACG,MAAM,CAAC,KAAK,EAAE;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC;QACtB,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;QAC9C,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAClC,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAqC/B,8CAA8C;IACxC,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CAU1D"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Path: src/lib/db/key-lifecycle.ts
|
|
2
|
+
//
|
|
3
|
+
// CLI-side access to the durable key-lifecycle state (zn-vault migration 093).
|
|
4
|
+
//
|
|
5
|
+
// WHY THE CEREMONY WRITES HERE AND NOT TO A LOG FILE. The owner's requirement
|
|
6
|
+
// was that a ceremony leave a record of who ran it, when, and how far it got —
|
|
7
|
+
// the thing a shell script cannot give you, because a script that dies leaves
|
|
8
|
+
// nothing and a script that is edited leaves no trace of the edit.
|
|
9
|
+
//
|
|
10
|
+
// `key_lifecycle_operations` gives all three, and two more that matter:
|
|
11
|
+
//
|
|
12
|
+
// EXCLUSION IS IN THE DATABASE. A partial unique index means a second
|
|
13
|
+
// ceremony — or a ceremony during an LMK rotation — is refused by PostgreSQL,
|
|
14
|
+
// not by an `if` that both processes evaluate to false at the same instant.
|
|
15
|
+
//
|
|
16
|
+
// THE HISTORY CANNOT BE REWRITTEN. `key_lifecycle_phase_events` is
|
|
17
|
+
// append-only by trigger and is written in the SAME transaction as the state
|
|
18
|
+
// change, so a ceremony that dies at phase 5 still shows how it reached
|
|
19
|
+
// phase 5.
|
|
20
|
+
//
|
|
21
|
+
// PORT NOTICE. The server has a richer repository for these tables
|
|
22
|
+
// (`zn-vault/src/db/repo.key-lifecycle.ts`) with compare-and-swap on `epoch`.
|
|
23
|
+
// This package ships to npm on its own and cannot import it. The contract is
|
|
24
|
+
// the SCHEMA, pinned by the migration — not this file and not that one. What is
|
|
25
|
+
// duplicated here is deliberately the minimum: claim, advance, finish, read.
|
|
26
|
+
import { BaseDBClient } from './client.js';
|
|
27
|
+
const COLUMNS = `operation_id, kind, state, phase, epoch, owner_node_id,
|
|
28
|
+
owner_principal, min_release, started_at, finished_at, last_error`;
|
|
29
|
+
const PG_UNIQUE_VIOLATION = '23505';
|
|
30
|
+
function toOperation(r) {
|
|
31
|
+
return {
|
|
32
|
+
operationId: r.operation_id,
|
|
33
|
+
kind: r.kind,
|
|
34
|
+
state: r.state,
|
|
35
|
+
phase: r.phase,
|
|
36
|
+
// BIGINT arrives as a string from some drivers, a number from others.
|
|
37
|
+
epoch: Number(r.epoch),
|
|
38
|
+
ownerNodeId: r.owner_node_id,
|
|
39
|
+
ownerPrincipal: r.owner_principal,
|
|
40
|
+
minRelease: r.min_release,
|
|
41
|
+
startedAt: r.started_at,
|
|
42
|
+
finishedAt: r.finished_at,
|
|
43
|
+
lastError: r.last_error,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export class KeyLifecycleOperations extends BaseDBClient {
|
|
47
|
+
/**
|
|
48
|
+
* Is the schema present?
|
|
49
|
+
*
|
|
50
|
+
* The CLI ships independently of the server, so it can meet a deployment that
|
|
51
|
+
* predates migration 093. Saying so plainly beats a `relation does not exist`
|
|
52
|
+
* in the middle of a ceremony.
|
|
53
|
+
*/
|
|
54
|
+
async schemaPresent() {
|
|
55
|
+
await this.connect();
|
|
56
|
+
const r = await this.getRawClient().query(`SELECT to_regclass('public.key_lifecycle_operations')::text AS present`);
|
|
57
|
+
return (r.rows[0]?.present ?? null) !== null;
|
|
58
|
+
}
|
|
59
|
+
/** The operation currently holding the exclusion slot, if any. */
|
|
60
|
+
async active() {
|
|
61
|
+
await this.connect();
|
|
62
|
+
const r = await this.getRawClient().query(`SELECT ${COLUMNS} FROM key_lifecycle_operations WHERE state = 'IN_PROGRESS' LIMIT 1`);
|
|
63
|
+
const row = r.rows.at(0);
|
|
64
|
+
return row === undefined ? null : toOperation(row);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Start a ceremony, or find out who already holds the slot.
|
|
68
|
+
*
|
|
69
|
+
* Does NOT look before it leaps: a "is anything running?" SELECT followed by
|
|
70
|
+
* an INSERT is exactly the race the index exists to close. The INSERT is the
|
|
71
|
+
* check, and 23505 is the answer.
|
|
72
|
+
*/
|
|
73
|
+
async claim(input) {
|
|
74
|
+
await this.connect();
|
|
75
|
+
const c = this.getRawClient();
|
|
76
|
+
try {
|
|
77
|
+
await c.query('BEGIN');
|
|
78
|
+
const r = await c.query(`INSERT INTO key_lifecycle_operations
|
|
79
|
+
(kind, state, phase, epoch, owner_node_id, owner_principal, min_release)
|
|
80
|
+
VALUES ('ESCROW_SNAPSHOT', 'IN_PROGRESS', $1, 0, $2, $3, $4)
|
|
81
|
+
RETURNING ${COLUMNS}`, [input.phase, input.ownerNodeId, input.ownerPrincipal, input.minRelease]);
|
|
82
|
+
const row = r.rows.at(0);
|
|
83
|
+
if (row === undefined)
|
|
84
|
+
throw new Error('claim: INSERT ... RETURNING produced no row');
|
|
85
|
+
// Same transaction: an operation whose history starts one commit late is
|
|
86
|
+
// an operation whose first phase can vanish.
|
|
87
|
+
await c.query(`INSERT INTO key_lifecycle_phase_events
|
|
88
|
+
(operation_id, seq, phase, state, node_id, detail)
|
|
89
|
+
VALUES ($1, 0, $2, 'IN_PROGRESS', $3, $4)`, [row.operation_id, input.phase, input.ownerNodeId,
|
|
90
|
+
input.detail ? JSON.stringify(input.detail) : null]);
|
|
91
|
+
await c.query('COMMIT');
|
|
92
|
+
return { ok: true, operation: toOperation(row) };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
await c.query('ROLLBACK').catch(() => undefined);
|
|
96
|
+
if (error.code !== PG_UNIQUE_VIOLATION)
|
|
97
|
+
throw error;
|
|
98
|
+
return { ok: false, active: await this.active() };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Move to the next phase, recording it in the same transaction.
|
|
103
|
+
*
|
|
104
|
+
* Compare-and-swap on `epoch`: a caller holding a stale read is refused
|
|
105
|
+
* rather than silently overwriting whoever moved first.
|
|
106
|
+
*/
|
|
107
|
+
async advance(input) {
|
|
108
|
+
await this.connect();
|
|
109
|
+
const c = this.getRawClient();
|
|
110
|
+
const next = input.expectedEpoch + 1;
|
|
111
|
+
try {
|
|
112
|
+
await c.query('BEGIN');
|
|
113
|
+
const r = await c.query(`UPDATE key_lifecycle_operations
|
|
114
|
+
SET phase = $1, epoch = $2, updated_at = now()
|
|
115
|
+
WHERE operation_id = $3 AND epoch = $4 AND state = 'IN_PROGRESS'
|
|
116
|
+
RETURNING ${COLUMNS}`, [input.phase, next, input.operationId, input.expectedEpoch]);
|
|
117
|
+
const row = r.rows.at(0);
|
|
118
|
+
if (row === undefined) {
|
|
119
|
+
await c.query('ROLLBACK');
|
|
120
|
+
throw new Error(`Could not advance operation ${input.operationId} to '${input.phase}': it is ` +
|
|
121
|
+
'no longer IN_PROGRESS at the expected epoch. Someone or something else ' +
|
|
122
|
+
"moved it. Run 'ceremony status' before deciding what to do.");
|
|
123
|
+
}
|
|
124
|
+
await c.query(`INSERT INTO key_lifecycle_phase_events
|
|
125
|
+
(operation_id, seq, phase, state, node_id, detail)
|
|
126
|
+
VALUES ($1, $2, $3, 'IN_PROGRESS', $4, $5)`, [input.operationId, next, input.phase, input.nodeId,
|
|
127
|
+
input.detail ? JSON.stringify(input.detail) : null]);
|
|
128
|
+
await c.query('COMMIT');
|
|
129
|
+
return toOperation(row);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await c.query('ROLLBACK').catch(() => undefined);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Close the operation, releasing the slot.
|
|
138
|
+
*
|
|
139
|
+
* `ABANDONED` is a real transition with a reason rather than a DELETE: why a
|
|
140
|
+
* ceremony was abandoned is exactly what the next person needs to read, and a
|
|
141
|
+
* global exclusion with no escape hatch is a deadlock waiting for its first
|
|
142
|
+
* interrupted ceremony.
|
|
143
|
+
*/
|
|
144
|
+
async finish(input) {
|
|
145
|
+
await this.connect();
|
|
146
|
+
const c = this.getRawClient();
|
|
147
|
+
const next = input.expectedEpoch + 1;
|
|
148
|
+
try {
|
|
149
|
+
await c.query('BEGIN');
|
|
150
|
+
const r = await c.query(`UPDATE key_lifecycle_operations
|
|
151
|
+
SET state = $1, epoch = $2, last_error = COALESCE($3, last_error),
|
|
152
|
+
updated_at = now(), finished_at = now()
|
|
153
|
+
WHERE operation_id = $4 AND epoch = $5 AND state = 'IN_PROGRESS'
|
|
154
|
+
RETURNING ${COLUMNS}`, [input.outcome, next, input.error ?? null, input.operationId, input.expectedEpoch]);
|
|
155
|
+
const row = r.rows.at(0);
|
|
156
|
+
if (row === undefined) {
|
|
157
|
+
await c.query('ROLLBACK');
|
|
158
|
+
throw new Error(`Could not close operation ${input.operationId}: it is no longer IN_PROGRESS ` +
|
|
159
|
+
'at the expected epoch.');
|
|
160
|
+
}
|
|
161
|
+
await c.query(`INSERT INTO key_lifecycle_phase_events
|
|
162
|
+
(operation_id, seq, phase, state, node_id, detail)
|
|
163
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [input.operationId, next, row.phase, input.outcome, input.nodeId,
|
|
164
|
+
input.detail ? JSON.stringify(input.detail) : null]);
|
|
165
|
+
await c.query('COMMIT');
|
|
166
|
+
return toOperation(row);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
await c.query('ROLLBACK').catch(() => undefined);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** The full route travelled, oldest first. */
|
|
174
|
+
async history(operationId) {
|
|
175
|
+
await this.connect();
|
|
176
|
+
const r = await this.getRawClient().query(`SELECT seq, phase, state, at::text AS at, detail
|
|
177
|
+
FROM key_lifecycle_phase_events
|
|
178
|
+
WHERE operation_id = $1 ORDER BY seq ASC`, [operationId]);
|
|
179
|
+
return r.rows;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=key-lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-lifecycle.js","sourceRoot":"","sources":["../../../src/lib/db/key-lifecycle.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,6 +37,13 @@ export interface LmkEscrowActiveRotation {
|
|
|
37
37
|
status: string;
|
|
38
38
|
startedAt: Date | null;
|
|
39
39
|
}
|
|
40
|
+
export interface RootKeyEnvelopeRow {
|
|
41
|
+
providerId: string;
|
|
42
|
+
providerType: string;
|
|
43
|
+
keyId: string | null;
|
|
44
|
+
ciphertext: Buffer;
|
|
45
|
+
kcv: string;
|
|
46
|
+
}
|
|
40
47
|
export interface LmkEscrowDatabaseSnapshot {
|
|
41
48
|
capturedAt: Date;
|
|
42
49
|
databaseName: string;
|
|
@@ -54,6 +61,17 @@ export interface LmkEscrowDatabaseSnapshot {
|
|
|
54
61
|
* This class never unwraps LMKs and never writes to PostgreSQL.
|
|
55
62
|
*/
|
|
56
63
|
export declare class LmkEscrowOperations extends BaseDBClient {
|
|
64
|
+
/**
|
|
65
|
+
* Read one root-key envelope, for sourcing the BSK from the hardware root
|
|
66
|
+
* instead of a cleartext file.
|
|
67
|
+
*
|
|
68
|
+
* `kcv` is the load-bearing column: it is what the returned key is checked
|
|
69
|
+
* against, and the appliance neither supplies it nor can influence it at read
|
|
70
|
+
* time. See `resolveBskFromProvider`.
|
|
71
|
+
*/
|
|
72
|
+
getRootKeyEnvelope(providerId: string): Promise<RootKeyEnvelopeRow | null>;
|
|
73
|
+
/** Provider ids that have an envelope, for a useful "did you mean" on error. */
|
|
74
|
+
listRootKeyEnvelopeProviders(): Promise<string[]>;
|
|
57
75
|
capture(backupId?: string): Promise<LmkEscrowDatabaseSnapshot>;
|
|
58
76
|
}
|
|
59
77
|
//# sourceMappingURL=lmk-escrow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lmk-escrow.d.ts","sourceRoot":"","sources":["../../../src/lib/db/lmk-escrow.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,IAAI,CAAC;IAClB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,IAAI,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACrC,cAAc,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC/C,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAAC;CACvC;AAqBD;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,YAAY;
|
|
1
|
+
{"version":3,"file":"lmk-escrow.d.ts","sourceRoot":"","sources":["../../../src/lib/db/lmk-escrow.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,IAAI,CAAC;IAClB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,IAAI,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACrC,cAAc,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC/C,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAAC;CACvC;AAqBD;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,YAAY;IACnD;;;;;;;OAOG;IACG,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAehF,gFAAgF;IAC1E,4BAA4B,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAQjD,OAAO,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC;CAqHrE"}
|
|
@@ -5,6 +5,31 @@ import { BaseDBClient } from './client.js';
|
|
|
5
5
|
* This class never unwraps LMKs and never writes to PostgreSQL.
|
|
6
6
|
*/
|
|
7
7
|
export class LmkEscrowOperations extends BaseDBClient {
|
|
8
|
+
/**
|
|
9
|
+
* Read one root-key envelope, for sourcing the BSK from the hardware root
|
|
10
|
+
* instead of a cleartext file.
|
|
11
|
+
*
|
|
12
|
+
* `kcv` is the load-bearing column: it is what the returned key is checked
|
|
13
|
+
* against, and the appliance neither supplies it nor can influence it at read
|
|
14
|
+
* time. See `resolveBskFromProvider`.
|
|
15
|
+
*/
|
|
16
|
+
async getRootKeyEnvelope(providerId) {
|
|
17
|
+
await this.connect();
|
|
18
|
+
const result = await this.getRawClient().query(`SELECT provider_id AS "providerId",
|
|
19
|
+
provider_type AS "providerType",
|
|
20
|
+
key_id AS "keyId",
|
|
21
|
+
ciphertext,
|
|
22
|
+
kcv
|
|
23
|
+
FROM root_key_envelopes
|
|
24
|
+
WHERE provider_id = $1`, [providerId]);
|
|
25
|
+
return result.rows[0] ?? null;
|
|
26
|
+
}
|
|
27
|
+
/** Provider ids that have an envelope, for a useful "did you mean" on error. */
|
|
28
|
+
async listRootKeyEnvelopeProviders() {
|
|
29
|
+
await this.connect();
|
|
30
|
+
const result = await this.getRawClient().query(`SELECT provider_id AS "providerId" FROM root_key_envelopes ORDER BY provider_id`);
|
|
31
|
+
return result.rows.map((r) => r.providerId);
|
|
32
|
+
}
|
|
8
33
|
async capture(backupId) {
|
|
9
34
|
await this.connect();
|
|
10
35
|
const client = this.getRawClient();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lmk-escrow.js","sourceRoot":"","sources":["../../../src/lib/db/lmk-escrow.ts"],"names":[],"mappings":"AAAA,iCAAiC;AAEjC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"lmk-escrow.js","sourceRoot":"","sources":["../../../src/lib/db/lmk-escrow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BaseDBClient } from './client.js';
|
|
2
|
+
import type { PreflightAuditHead, PreflightBackup, PreflightEnvelope, PreflightLmkVersion, PreflightRotation } from '../preflight.js';
|
|
3
|
+
export interface PreflightDatabaseSnapshot {
|
|
4
|
+
capturedAt: Date;
|
|
5
|
+
databaseName: string;
|
|
6
|
+
postgresVersion: string;
|
|
7
|
+
walLsn: string;
|
|
8
|
+
transactionSnapshot: string;
|
|
9
|
+
latestMigration: string | null;
|
|
10
|
+
lmkVersions: PreflightLmkVersion[];
|
|
11
|
+
rootKeyEnvelopes: PreflightEnvelope[];
|
|
12
|
+
rootKeyEnvelopesTablePresent: boolean;
|
|
13
|
+
activeRotations: PreflightRotation[];
|
|
14
|
+
auditHead: PreflightAuditHead | null;
|
|
15
|
+
latestVerifiedBackup: PreflightBackup | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Capture the key-hierarchy state a BSK rotation depends on.
|
|
19
|
+
*
|
|
20
|
+
* READ ONLY, AND SAID SO TO POSTGRESQL. The transaction is opened
|
|
21
|
+
* `REPEATABLE READ READ ONLY`, which makes any write on this connection fail
|
|
22
|
+
* with SQLSTATE 25006 rather than relying on nobody having added one. Test 1.1
|
|
23
|
+
* of the milestone is "preflight over production, and ZERO writes"; a promise
|
|
24
|
+
* kept only by inspection is not evidence.
|
|
25
|
+
*
|
|
26
|
+
* WHY NOT THE API. `/v1/superadmin/rootkey/status` and `.../verify` each WRITE
|
|
27
|
+
* AN AUDIT ROW. Reading the state through them would break the zero-writes
|
|
28
|
+
* property invisibly — the preflight would be the thing that falsified its own
|
|
29
|
+
* result. This reads PostgreSQL directly, exactly as the escrow snapshot does.
|
|
30
|
+
*
|
|
31
|
+
* REPEATABLE READ, not READ COMMITTED: every query below must see one instant.
|
|
32
|
+
* A version list read before a rotation commits and an envelope list read after
|
|
33
|
+
* would produce an inventory that never existed, and the gates would be
|
|
34
|
+
* evaluated over that.
|
|
35
|
+
*
|
|
36
|
+
* Nothing here selects key material. `wrapped_lmk` is read as `octet_length`
|
|
37
|
+
* only, `root_key_envelopes.ciphertext` is not read at all.
|
|
38
|
+
*/
|
|
39
|
+
export declare class PreflightOperations extends BaseDBClient {
|
|
40
|
+
capture(): Promise<PreflightDatabaseSnapshot>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../../../src/lib/db/preflight.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,IAAI,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;IACtC,4BAA4B,EAAE,OAAO,CAAC;IACtC,eAAe,EAAE,iBAAiB,EAAE,CAAC;IACrC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACrC,oBAAoB,EAAE,eAAe,GAAG,IAAI,CAAC;CAC9C;AAgDD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,mBAAoB,SAAQ,YAAY;IAC7C,OAAO,IAAI,OAAO,CAAC,yBAAyB,CAAC;CAkJpD"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Path: src/lib/db/preflight.ts
|
|
2
|
+
import { BaseDBClient } from './client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Capture the key-hierarchy state a BSK rotation depends on.
|
|
5
|
+
*
|
|
6
|
+
* READ ONLY, AND SAID SO TO POSTGRESQL. The transaction is opened
|
|
7
|
+
* `REPEATABLE READ READ ONLY`, which makes any write on this connection fail
|
|
8
|
+
* with SQLSTATE 25006 rather than relying on nobody having added one. Test 1.1
|
|
9
|
+
* of the milestone is "preflight over production, and ZERO writes"; a promise
|
|
10
|
+
* kept only by inspection is not evidence.
|
|
11
|
+
*
|
|
12
|
+
* WHY NOT THE API. `/v1/superadmin/rootkey/status` and `.../verify` each WRITE
|
|
13
|
+
* AN AUDIT ROW. Reading the state through them would break the zero-writes
|
|
14
|
+
* property invisibly — the preflight would be the thing that falsified its own
|
|
15
|
+
* result. This reads PostgreSQL directly, exactly as the escrow snapshot does.
|
|
16
|
+
*
|
|
17
|
+
* REPEATABLE READ, not READ COMMITTED: every query below must see one instant.
|
|
18
|
+
* A version list read before a rotation commits and an envelope list read after
|
|
19
|
+
* would produce an inventory that never existed, and the gates would be
|
|
20
|
+
* evaluated over that.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here selects key material. `wrapped_lmk` is read as `octet_length`
|
|
23
|
+
* only, `root_key_envelopes.ciphertext` is not read at all.
|
|
24
|
+
*/
|
|
25
|
+
export class PreflightOperations extends BaseDBClient {
|
|
26
|
+
async capture() {
|
|
27
|
+
await this.connect();
|
|
28
|
+
const client = this.getRawClient();
|
|
29
|
+
await client.query('BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
30
|
+
try {
|
|
31
|
+
const identityResult = await client.query(`
|
|
32
|
+
SELECT
|
|
33
|
+
clock_timestamp() AS "capturedAt",
|
|
34
|
+
current_database() AS "databaseName",
|
|
35
|
+
current_setting('server_version') AS "postgresVersion",
|
|
36
|
+
pg_current_wal_lsn()::text AS "walLsn",
|
|
37
|
+
txid_current_snapshot()::text AS "transactionSnapshot",
|
|
38
|
+
(SELECT name FROM _migrations ORDER BY name DESC LIMIT 1) AS "latestMigration",
|
|
39
|
+
to_regclass('public.root_key_envelopes')::text AS "rootKeyEnvelopesTable"
|
|
40
|
+
`);
|
|
41
|
+
// pg's types claim `rows[0]` is always present. It is not, and a
|
|
42
|
+
// preflight that reads `undefined.databaseName` reports a crash where it
|
|
43
|
+
// should report an unreadable database.
|
|
44
|
+
const identity = identityResult.rows.at(0);
|
|
45
|
+
if (identity === undefined) {
|
|
46
|
+
throw new Error('Could not read PostgreSQL snapshot identity');
|
|
47
|
+
}
|
|
48
|
+
// octet_length, never the bytes. A preflight that pulled wrapped key
|
|
49
|
+
// material into a CLI process would put it in a core dump, a swap file
|
|
50
|
+
// and an error report, for no gain: every gate here is about presence.
|
|
51
|
+
const versionsResult = await client.query(`
|
|
52
|
+
SELECT
|
|
53
|
+
version,
|
|
54
|
+
status::text AS status,
|
|
55
|
+
octet_length(wrapped_lmk) AS "wrappedBytes"
|
|
56
|
+
FROM lmk_versions
|
|
57
|
+
ORDER BY version
|
|
58
|
+
`);
|
|
59
|
+
const rootKeyEnvelopesTablePresent = identity.rootKeyEnvelopesTable !== null;
|
|
60
|
+
// A node predating migration 092 has no such table, and `to_regclass`
|
|
61
|
+
// answering NULL is the only safe way to ask — a failing query would
|
|
62
|
+
// abort the whole transaction, losing the reads already taken.
|
|
63
|
+
const envelopesResult = rootKeyEnvelopesTablePresent
|
|
64
|
+
? await client.query(`
|
|
65
|
+
SELECT
|
|
66
|
+
provider_id AS "providerId",
|
|
67
|
+
provider_type AS "providerType",
|
|
68
|
+
key_id AS "keyId",
|
|
69
|
+
kcv
|
|
70
|
+
FROM root_key_envelopes
|
|
71
|
+
ORDER BY provider_id
|
|
72
|
+
`)
|
|
73
|
+
: { rows: [] };
|
|
74
|
+
const rotationResult = await client.query(`
|
|
75
|
+
SELECT
|
|
76
|
+
rotation_id AS "rotationId",
|
|
77
|
+
old_lmk_version AS "oldLmkVersion",
|
|
78
|
+
new_lmk_version AS "newLmkVersion",
|
|
79
|
+
status::text AS status,
|
|
80
|
+
started_at AS "startedAt"
|
|
81
|
+
FROM lmk_rotation_progress
|
|
82
|
+
WHERE status IN ('IN_PROGRESS', 'FAILED')
|
|
83
|
+
ORDER BY started_at DESC
|
|
84
|
+
`);
|
|
85
|
+
const auditResult = await client.query(`
|
|
86
|
+
SELECT
|
|
87
|
+
id::text AS id,
|
|
88
|
+
ts AS timestamp,
|
|
89
|
+
curr_hmac AS "currentHmac",
|
|
90
|
+
lmk_version AS "lmkVersion",
|
|
91
|
+
hmac_format_version AS "hmacFormatVersion"
|
|
92
|
+
FROM audit_log
|
|
93
|
+
WHERE curr_hmac IS NOT NULL
|
|
94
|
+
ORDER BY id DESC
|
|
95
|
+
LIMIT 1
|
|
96
|
+
`);
|
|
97
|
+
const backupResult = await client.query(`
|
|
98
|
+
SELECT
|
|
99
|
+
id,
|
|
100
|
+
filename,
|
|
101
|
+
completed_at AS "completedAt",
|
|
102
|
+
verified_at AS "verifiedAt"
|
|
103
|
+
FROM backups
|
|
104
|
+
WHERE deleted_at IS NULL
|
|
105
|
+
AND status::text = 'VERIFIED'
|
|
106
|
+
AND encrypted = true
|
|
107
|
+
AND verified_at IS NOT NULL
|
|
108
|
+
AND completed_at IS NOT NULL
|
|
109
|
+
ORDER BY verified_at DESC
|
|
110
|
+
LIMIT 1
|
|
111
|
+
`);
|
|
112
|
+
await client.query('COMMIT');
|
|
113
|
+
const auditHeadRow = auditResult.rows.at(0);
|
|
114
|
+
const backupRow = backupResult.rows.at(0);
|
|
115
|
+
return {
|
|
116
|
+
capturedAt: identity.capturedAt,
|
|
117
|
+
databaseName: identity.databaseName,
|
|
118
|
+
postgresVersion: identity.postgresVersion,
|
|
119
|
+
walLsn: identity.walLsn,
|
|
120
|
+
transactionSnapshot: identity.transactionSnapshot,
|
|
121
|
+
latestMigration: identity.latestMigration,
|
|
122
|
+
lmkVersions: versionsResult.rows.map((row) => {
|
|
123
|
+
const bytes = row.wrappedBytes === null ? 0 : Number(row.wrappedBytes);
|
|
124
|
+
return {
|
|
125
|
+
version: row.version,
|
|
126
|
+
status: row.status,
|
|
127
|
+
hasWrappedLmk: bytes > 0,
|
|
128
|
+
wrappedBytes: bytes,
|
|
129
|
+
};
|
|
130
|
+
}),
|
|
131
|
+
rootKeyEnvelopes: envelopesResult.rows,
|
|
132
|
+
rootKeyEnvelopesTablePresent,
|
|
133
|
+
activeRotations: rotationResult.rows.map((row) => ({
|
|
134
|
+
rotationId: row.rotationId,
|
|
135
|
+
oldLmkVersion: row.oldLmkVersion,
|
|
136
|
+
newLmkVersion: row.newLmkVersion,
|
|
137
|
+
status: row.status,
|
|
138
|
+
startedAt: row.startedAt?.toISOString() ?? null,
|
|
139
|
+
})),
|
|
140
|
+
auditHead: auditHeadRow !== undefined
|
|
141
|
+
? {
|
|
142
|
+
id: auditHeadRow.id,
|
|
143
|
+
timestamp: auditHeadRow.timestamp?.toISOString() ?? null,
|
|
144
|
+
currentHmacBase64: Buffer.from(auditHeadRow.currentHmac).toString('base64'),
|
|
145
|
+
lmkVersion: auditHeadRow.lmkVersion,
|
|
146
|
+
hmacFormatVersion: auditHeadRow.hmacFormatVersion,
|
|
147
|
+
}
|
|
148
|
+
: null,
|
|
149
|
+
latestVerifiedBackup: backupRow !== undefined
|
|
150
|
+
? {
|
|
151
|
+
id: backupRow.id,
|
|
152
|
+
filename: backupRow.filename,
|
|
153
|
+
completedAt: backupRow.completedAt.toISOString(),
|
|
154
|
+
verifiedAt: backupRow.verifiedAt.toISOString(),
|
|
155
|
+
}
|
|
156
|
+
: null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
await client.query('ROLLBACK');
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.js","sourceRoot":"","sources":["../../../src/lib/db/preflight.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Domain-separation label. Versioned; changing it is a new KCV format. */
|
|
2
|
+
export declare const BSK_KCV_CONTEXT = "zn-vault-bsk-kcv-v1";
|
|
3
|
+
/**
|
|
4
|
+
* Compute the publishable Key Check Value of a bootstrap key.
|
|
5
|
+
*
|
|
6
|
+
* Deterministic: same key, same KCV — across providers, nodes and sites.
|
|
7
|
+
* Safe to log and display; never reveals or accelerates recovery of the key.
|
|
8
|
+
*
|
|
9
|
+
* @throws if the key is not exactly 32 bytes. A short buffer would still
|
|
10
|
+
* produce a well-formed fingerprint — of the wrong thing — and the escrow
|
|
11
|
+
* gate would report it as "the two custodies hold different keys".
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeBskKcv(key: Buffer): string;
|
|
14
|
+
/**
|
|
15
|
+
* Is this string a KCV of the construction above?
|
|
16
|
+
*
|
|
17
|
+
* On this side of the port the values being gated arrive from PostgreSQL
|
|
18
|
+
* (`root_key_envelopes.kcv`, a TEXT column with no CHECK constraint) and from
|
|
19
|
+
* an unauthenticated `/v1/health` body. They are not merely compared: they are
|
|
20
|
+
* republished into signed preflight evidence that leaves the CPD. A
|
|
21
|
+
* "looks-plausible" check would let arbitrary text — including something
|
|
22
|
+
* shaped like key material — into that artefact.
|
|
23
|
+
*
|
|
24
|
+
* Pinning the ACTUAL construction closes it for free: anything a KCV
|
|
25
|
+
* comparison could ever legitimately succeed against matches this pattern, so
|
|
26
|
+
* refusing everything else costs nothing. A malformed value is a protocol
|
|
27
|
+
* violation, distinct from — and checked before — a well-formed KCV that
|
|
28
|
+
* disagrees.
|
|
29
|
+
*
|
|
30
|
+
* Non-strings return false rather than throwing: the caller is reading a
|
|
31
|
+
* nullable column.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isBskKcv(value: string): boolean;
|
|
34
|
+
//# sourceMappingURL=kcv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kcv.d.ts","sourceRoot":"","sources":["../../src/lib/kcv.ts"],"names":[],"mappings":"AAuCA,2EAA2E;AAC3E,eAAO,MAAM,eAAe,wBAAwB,CAAC;AAKrD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQjD;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAG/C"}
|