@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.
Files changed (89) hide show
  1. package/dist/commands/lmk-ceremony.d.ts +3 -0
  2. package/dist/commands/lmk-ceremony.d.ts.map +1 -0
  3. package/dist/commands/lmk-ceremony.js +597 -0
  4. package/dist/commands/lmk-ceremony.js.map +1 -0
  5. package/dist/commands/lmk-escrow.d.ts.map +1 -1
  6. package/dist/commands/lmk-escrow.js +73 -5
  7. package/dist/commands/lmk-escrow.js.map +1 -1
  8. package/dist/commands/lmk-preflight.d.ts +3 -0
  9. package/dist/commands/lmk-preflight.d.ts.map +1 -0
  10. package/dist/commands/lmk-preflight.js +117 -0
  11. package/dist/commands/lmk-preflight.js.map +1 -0
  12. package/dist/commands/lmk-restore-drill.d.ts +3 -0
  13. package/dist/commands/lmk-restore-drill.d.ts.map +1 -0
  14. package/dist/commands/lmk-restore-drill.js +160 -0
  15. package/dist/commands/lmk-restore-drill.js.map +1 -0
  16. package/dist/commands/lmk.d.ts.map +1 -1
  17. package/dist/commands/lmk.js +4 -0
  18. package/dist/commands/lmk.js.map +1 -1
  19. package/dist/index.js +35 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/lib/bsk-source.d.ts +21 -0
  22. package/dist/lib/bsk-source.d.ts.map +1 -0
  23. package/dist/lib/bsk-source.js +89 -0
  24. package/dist/lib/bsk-source.js.map +1 -0
  25. package/dist/lib/ceremony/gates.d.ts +126 -0
  26. package/dist/lib/ceremony/gates.d.ts.map +1 -0
  27. package/dist/lib/ceremony/gates.js +270 -0
  28. package/dist/lib/ceremony/gates.js.map +1 -0
  29. package/dist/lib/ceremony/system.d.ts +36 -0
  30. package/dist/lib/ceremony/system.d.ts.map +1 -0
  31. package/dist/lib/ceremony/system.js +176 -0
  32. package/dist/lib/ceremony/system.js.map +1 -0
  33. package/dist/lib/ceremony/workspace.d.ts +54 -0
  34. package/dist/lib/ceremony/workspace.d.ts.map +1 -0
  35. package/dist/lib/ceremony/workspace.js +98 -0
  36. package/dist/lib/ceremony/workspace.js.map +1 -0
  37. package/dist/lib/config/index.d.ts +1 -1
  38. package/dist/lib/config/index.d.ts.map +1 -1
  39. package/dist/lib/config/index.js +1 -1
  40. package/dist/lib/config/index.js.map +1 -1
  41. package/dist/lib/config/profile.d.ts +14 -0
  42. package/dist/lib/config/profile.d.ts.map +1 -1
  43. package/dist/lib/config/profile.js +22 -0
  44. package/dist/lib/config/profile.js.map +1 -1
  45. package/dist/lib/config.d.ts +1 -1
  46. package/dist/lib/config.d.ts.map +1 -1
  47. package/dist/lib/config.js +1 -1
  48. package/dist/lib/config.js.map +1 -1
  49. package/dist/lib/db/index.d.ts +9 -1
  50. package/dist/lib/db/index.d.ts.map +1 -1
  51. package/dist/lib/db/index.js +9 -0
  52. package/dist/lib/db/index.js.map +1 -1
  53. package/dist/lib/db/key-lifecycle.d.ts +86 -0
  54. package/dist/lib/db/key-lifecycle.d.ts.map +1 -0
  55. package/dist/lib/db/key-lifecycle.js +182 -0
  56. package/dist/lib/db/key-lifecycle.js.map +1 -0
  57. package/dist/lib/db/lmk-escrow.d.ts +18 -0
  58. package/dist/lib/db/lmk-escrow.d.ts.map +1 -1
  59. package/dist/lib/db/lmk-escrow.js +25 -0
  60. package/dist/lib/db/lmk-escrow.js.map +1 -1
  61. package/dist/lib/db/preflight.d.ts +42 -0
  62. package/dist/lib/db/preflight.d.ts.map +1 -0
  63. package/dist/lib/db/preflight.js +165 -0
  64. package/dist/lib/db/preflight.js.map +1 -0
  65. package/dist/lib/kcv.d.ts +34 -0
  66. package/dist/lib/kcv.d.ts.map +1 -0
  67. package/dist/lib/kcv.js +89 -0
  68. package/dist/lib/kcv.js.map +1 -0
  69. package/dist/lib/lmk-escrow-restore.d.ts +6 -2
  70. package/dist/lib/lmk-escrow-restore.d.ts.map +1 -1
  71. package/dist/lib/lmk-escrow-restore.js +1 -1
  72. package/dist/lib/lmk-escrow-restore.js.map +1 -1
  73. package/dist/lib/lmk-escrow.d.ts +14 -1
  74. package/dist/lib/lmk-escrow.d.ts.map +1 -1
  75. package/dist/lib/lmk-escrow.js +5 -1
  76. package/dist/lib/lmk-escrow.js.map +1 -1
  77. package/dist/lib/preflight.d.ts +93 -0
  78. package/dist/lib/preflight.d.ts.map +1 -0
  79. package/dist/lib/preflight.js +129 -0
  80. package/dist/lib/preflight.js.map +1 -0
  81. package/dist/lib/restore-drill.d.ts +73 -0
  82. package/dist/lib/restore-drill.d.ts.map +1 -0
  83. package/dist/lib/restore-drill.js +185 -0
  84. package/dist/lib/restore-drill.js.map +1 -0
  85. package/dist/lib/sentinel-client.d.ts +26 -0
  86. package/dist/lib/sentinel-client.d.ts.map +1 -0
  87. package/dist/lib/sentinel-client.js +165 -0
  88. package/dist/lib/sentinel-client.js.map +1 -0
  89. 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;IAC7C,OAAO,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC;CAqHrE"}
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;AA4E3C;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,YAAY;IACnD,KAAK,CAAC,OAAO,CAAC,QAAiB;QAC7B,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACnC,MAAM,MAAM,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;QAElF,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,KAAK,CAAsB;;;;;;;;OAQ9D,CAAC,CAAC;YACH,IAAI,cAAc,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;YACjE,CAAC;YACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAExC,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,KAAK,CAAsB;;;;;;;;;;;;;;;;;;OAkB9D,CAAC,CAAC;YAEH,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,KAAK,CAAqB;;;;;;;;;;;OAW1D,CAAC,CAAC;YAEH,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,KAAK,CAA0B;;;;;;;;;;;OAWlE,CAAC,CAAC;YAEH,IAAI,MAAM,GAAkC,IAAI,CAAC;YACjD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,KAAK,CAAqB;;;;;;;;;;;;SAY3D,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACf,IAAI,YAAY,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACnC,MAAM,IAAI,KAAK,CAAC,UAAU,QAAQ,oCAAoC,CAAC,CAAC;gBAC1E,CAAC;gBACD,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACvC,IACE,SAAS,CAAC,MAAM,KAAK,UAAU;oBAC/B,CAAC,SAAS,CAAC,SAAS;oBACpB,SAAS,CAAC,QAAQ,KAAK,IAAI;oBAC3B,SAAS,CAAC,QAAQ,KAAK,EAAE;oBACzB,SAAS,CAAC,WAAW,KAAK,IAAI;oBAC9B,SAAS,CAAC,UAAU,KAAK,IAAI,EAC7B,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,UAAU,QAAQ,kDAAkD,CAAC,CAAC;gBACxF,CAAC;gBACD,MAAM,GAAG;oBACP,GAAG,SAAS;oBACZ,QAAQ,EAAE,SAAS,CAAC,QAAQ;oBAC5B,SAAS,EAAE,IAAI;oBACf,WAAW,EAAE,SAAS,CAAC,WAAW;oBAClC,UAAU,EAAE,SAAS,CAAC,UAAU;iBACjC,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC7B,OAAO;gBACL,GAAG,QAAQ;gBACX,QAAQ,EAAE,cAAc,CAAC,IAAI;gBAC7B,SAAS,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI;gBACtC,cAAc,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI;gBAC9C,MAAM;aACP,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC/B,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
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":"AAAA,gCAAgC;AAEhC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAsE3C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,mBAAoB,SAAQ,YAAY;IACnD,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACnC,MAAM,MAAM,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;QAElF,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,KAAK,CAAc;;;;;;;;;OAStD,CAAC,CAAC;YACH,iEAAiE;YACjE,yEAAyE;YACzE,wCAAwC;YACxC,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC3C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;YACjE,CAAC;YAED,qEAAqE;YACrE,uEAAuE;YACvE,uEAAuE;YACvE,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,KAAK,CAAa;;;;;;;OAOrD,CAAC,CAAC;YAEH,MAAM,4BAA4B,GAAG,QAAQ,CAAC,qBAAqB,KAAK,IAAI,CAAC;YAC7E,sEAAsE;YACtE,qEAAqE;YACrE,+DAA+D;YAC/D,MAAM,eAAe,GAAG,4BAA4B;gBAClD,CAAC,CAAC,MAAM,MAAM,CAAC,KAAK,CAAc;;;;;;;;WAQ/B,CAAC;gBACJ,CAAC,CAAC,EAAE,IAAI,EAAE,EAAmB,EAAE,CAAC;YAElC,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,KAAK,CAAc;;;;;;;;;;OAUtD,CAAC,CAAC;YAEH,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,KAAK,CAAe;;;;;;;;;;;OAWpD,CAAC,CAAC;YAEH,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,KAAK,CAAY;;;;;;;;;;;;;;OAclD,CAAC,CAAC;YAEH,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAE7B,MAAM,YAAY,GAAG,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC5C,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAE1C,OAAO;gBACL,UAAU,EAAE,QAAQ,CAAC,UAAU;gBAC/B,YAAY,EAAE,QAAQ,CAAC,YAAY;gBACnC,eAAe,EAAE,QAAQ,CAAC,eAAe;gBACzC,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,mBAAmB,EAAE,QAAQ,CAAC,mBAAmB;gBACjD,eAAe,EAAE,QAAQ,CAAC,eAAe;gBACzC,WAAW,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;oBAC3C,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;oBACvE,OAAO;wBACL,OAAO,EAAE,GAAG,CAAC,OAAO;wBACpB,MAAM,EAAE,GAAG,CAAC,MAAM;wBAClB,aAAa,EAAE,KAAK,GAAG,CAAC;wBACxB,YAAY,EAAE,KAAK;qBACpB,CAAC;gBACJ,CAAC,CAAC;gBACF,gBAAgB,EAAE,eAAe,CAAC,IAAI;gBACtC,4BAA4B;gBAC5B,eAAe,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;oBACjD,UAAU,EAAE,GAAG,CAAC,UAAU;oBAC1B,aAAa,EAAE,GAAG,CAAC,aAAa;oBAChC,aAAa,EAAE,GAAG,CAAC,aAAa;oBAChC,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,IAAI;iBAChD,CAAC,CAAC;gBACH,SAAS,EAAE,YAAY,KAAK,SAAS;oBACnC,CAAC,CAAC;wBACE,EAAE,EAAE,YAAY,CAAC,EAAE;wBACnB,SAAS,EAAE,YAAY,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,IAAI;wBACxD,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;wBAC3E,UAAU,EAAE,YAAY,CAAC,UAAU;wBACnC,iBAAiB,EAAE,YAAY,CAAC,iBAAiB;qBAClD;oBACH,CAAC,CAAC,IAAI;gBACR,oBAAoB,EAAE,SAAS,KAAK,SAAS;oBAC3C,CAAC,CAAC;wBACE,EAAE,EAAE,SAAS,CAAC,EAAE;wBAChB,QAAQ,EAAE,SAAS,CAAC,QAAQ;wBAC5B,WAAW,EAAE,SAAS,CAAC,WAAW,CAAC,WAAW,EAAE;wBAChD,UAAU,EAAE,SAAS,CAAC,UAAU,CAAC,WAAW,EAAE;qBAC/C;oBACH,CAAC,CAAC,IAAI;aACT,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC/B,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
@@ -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"}