@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,185 @@
1
+ // Path: src/lib/restore-drill.ts
2
+ //
3
+ // The two gates of the isolated-restore drill.
4
+ //
5
+ // THE DRILL'S FAILURE MODE IS SUCCESS. If the restored database ends up with no
6
+ // LMK version above zero, `initializeFromPG()` reads "no ACTIVE version" and
7
+ // takes the legitimate first-boot path: `bootstrapInitialLMKVersion()` mints a
8
+ // brand-new LMK, wraps it under the bootstrap key on disk, inserts version 1,
9
+ // and the vault comes up healthy. `/v1/health` answers 200 with `status: 'ok'`
10
+ // and a root key whose KCV even MATCHES the escrow bundle — the BSK really was
11
+ // restored from it. Every signal an operator would look at says the drill
12
+ // passed. What actually happened is that a new key hierarchy was created over
13
+ // an empty database, and the only thing the drill exists to demonstrate — that
14
+ // this bundle brings back THIS deployment's keys — was never exercised.
15
+ //
16
+ // So neither gate may read an exit code or an HTTP status. Degraded root-key
17
+ // resolution returns 200 with the degradation reported inside the body, and a
18
+ // null resolution state omits the `rootKey` block entirely while still
19
+ // returning 200 and `status: 'ok'`. Both gates read state and compare it
20
+ // against what the bundle says, and both explain themselves: a failed drill is
21
+ // investigated cold, hours later, by someone who was not there.
22
+ //
23
+ // Pure functions on purpose. The orchestration around them drives docker from a
24
+ // shell script and cannot be unit-tested, so everything that DECIDES anything
25
+ // lives here where it can be.
26
+ /**
27
+ * Versions at or below zero are not real key generations.
28
+ *
29
+ * `models.postgres.sql` seeds `version=0, status='ACTIVE',
30
+ * key_id='ZK_MODE_PLACEHOLDER'` on every boot. Counting it would make a
31
+ * correctly restored database look like it has two ACTIVE versions.
32
+ */
33
+ function positives(versions) {
34
+ return versions.filter((v) => v.version > 0);
35
+ }
36
+ function describe(versions) {
37
+ const rows = positives(versions);
38
+ if (rows.length === 0)
39
+ return '(no versions above zero)';
40
+ return rows
41
+ .map((v) => `v${String(v.version)} ${v.status}${v.hasWrappedLmk ? '' : ' [no material]'}`)
42
+ .join(', ');
43
+ }
44
+ /**
45
+ * Gate 1, run BEFORE the vault is started against the restored database.
46
+ *
47
+ * Before, not after, because once the vault mints a replacement LMK the
48
+ * evidence of what went wrong is overwritten: the table then holds a perfectly
49
+ * ordinary version 1 with material, and nothing distinguishes it from a
50
+ * successful restore except that it is the wrong key.
51
+ *
52
+ * @throws with a diagnosis whenever the database could not have come from the
53
+ * deployment the bundle describes.
54
+ */
55
+ export function assertRestoredDatabaseIsRecoverable(versions, expectedActiveVersion) {
56
+ const rows = positives(versions);
57
+ if (rows.length === 0) {
58
+ throw new Error('The restored database holds no LMK version above zero. Starting a vault ' +
59
+ 'against it would NOT fail: it would take the first-boot path, MINT a new ' +
60
+ 'LMK, and report a completely healthy start — the drill would pass having ' +
61
+ 'restored nothing. Check that the database dump was actually applied and ' +
62
+ 'that it came from the same snapshot as the escrow bundle.');
63
+ }
64
+ const recoverable = rows.filter((v) => v.hasWrappedLmk);
65
+ if (recoverable.length === 0) {
66
+ throw new Error(`The restored database holds ${String(rows.length)} LMK version(s) above zero ` +
67
+ `but none has wrapped material (${describe(versions)}). The inventory came ` +
68
+ 'back and the key material did not, which reaches the same mint-a-new-key ' +
69
+ 'path as an empty table.');
70
+ }
71
+ const actives = rows.filter((v) => v.status === 'ACTIVE');
72
+ if (actives.length !== 1) {
73
+ throw new Error(`The restored database has ${String(actives.length)} ACTIVE LMK version(s) ` +
74
+ `above zero (${describe(versions)}); exactly one is required. A drill run ` +
75
+ 'against an ambiguous key state proves nothing about either version.');
76
+ }
77
+ const active = actives.at(0);
78
+ if (active === undefined)
79
+ throw new Error('unreachable: one ACTIVE row expected');
80
+ if (!active.hasWrappedLmk) {
81
+ throw new Error(`The ACTIVE LMK version ${String(active.version)} has no wrapped material. ` +
82
+ 'The vault cannot unwrap the current key from this database.');
83
+ }
84
+ if (active.version !== expectedActiveVersion) {
85
+ throw new Error(`The restored database is ACTIVE on LMK version ${String(active.version)}, but ` +
86
+ `the escrow bundle was taken when version ${String(expectedActiveVersion)} was ` +
87
+ 'active. The bundle and the dump are from different moments, so a successful ' +
88
+ 'boot would say nothing about either of them.');
89
+ }
90
+ }
91
+ /**
92
+ * Gate 2, run AFTER the vault has started.
93
+ *
94
+ * Confirms three separate things, because each can be true while the others are
95
+ * not: the node resolved the key the bundle carries, it did so cleanly, and it
96
+ * did NOT create anything new to do it.
97
+ *
98
+ * WHAT THIS GATE CANNOT DO, measured on a bench rather than assumed. A vault
99
+ * started against an empty database mints LMK version 1 and reports
100
+ * `status: ok` with a root-key KCV that MATCHES the bundle — the bootstrap key
101
+ * really did come from it. If the bundle's own active version happens to be 1,
102
+ * every field checked below is correct and the mint passes. Production is on
103
+ * version 4 today, so this gate would catch it there; that is luck, not design.
104
+ * `assertRestoredDatabaseIsRecoverable` is what actually closes it, which is
105
+ * why the pre-boot gate is never "the one the post gate makes redundant".
106
+ *
107
+ * @throws with both values named whenever a comparison fails.
108
+ */
109
+ export function assertBootedOnRestoredKeys(input) {
110
+ const { health, versionsAfterBoot, expectedBskKcv, expectedActiveVersion } = input;
111
+ // A null resolution state omits the block and still answers 200 with
112
+ // status 'ok'. Reading `health.rootKey?.kcv` into a comparison would compare
113
+ // `undefined` and quietly skip the only check that matters.
114
+ const rootKey = health.rootKey;
115
+ if (rootKey === undefined || typeof rootKey.kcv !== 'string' || rootKey.kcv === '') {
116
+ throw new Error('The health body carries no rootKey block, so there is nothing to compare ' +
117
+ 'against the escrow bundle. This is what a null root-key resolution state ' +
118
+ 'looks like from outside — HTTP 200, status "ok", and no answer to the only ' +
119
+ 'question the drill asked.');
120
+ }
121
+ if (rootKey.degraded === true) {
122
+ throw new Error(`The node resolved a root key (${rootKey.kcv}) but reports DEGRADED: at least ` +
123
+ 'one configured provider failed. On a drill whose whole point is "the ' +
124
+ 'escrowed key opens this database", a partial answer is not an answer.');
125
+ }
126
+ if (rootKey.kcv !== expectedBskKcv) {
127
+ throw new Error('The node booted on a DIFFERENT bootstrap key than the escrow bundle ' +
128
+ `carries. Bundle: ${expectedBskKcv}. Running node: ${rootKey.kcv}. ` +
129
+ 'Either the key file was not restored from this bundle, or another ' +
130
+ 'root-key provider answered first.');
131
+ }
132
+ const actives = positives(versionsAfterBoot).filter((v) => v.status === 'ACTIVE');
133
+ const active = actives.at(0);
134
+ if (actives.length !== 1 || active === undefined) {
135
+ throw new Error(`After boot the database has ${String(actives.length)} ACTIVE LMK version(s) ` +
136
+ `above zero (${describe(versionsAfterBoot)}); exactly one is required.`);
137
+ }
138
+ if (active.version !== expectedActiveVersion) {
139
+ throw new Error(`After boot the vault is ACTIVE on LMK version ${String(active.version)}, not ` +
140
+ `the version ${String(expectedActiveVersion)} the escrow bundle was taken at. ` +
141
+ 'The most likely cause is that the vault MINTED a new LMK because it found ' +
142
+ 'nothing to load — which produces a healthy-looking node running on a key ' +
143
+ 'that was created seconds ago and exists nowhere else. ' +
144
+ `Versions now: ${describe(versionsAfterBoot)}.`);
145
+ }
146
+ }
147
+ /**
148
+ * Refuse to run the drill against anything that is not on this machine.
149
+ *
150
+ * The drill reads a database and then asserts things about a vault; both
151
+ * halves are harmless. What is not harmless is running it with a production
152
+ * URL by accident — the pre-boot gate would report a green production, the
153
+ * post-boot gate would compare production's KCV against an escrow bundle, and
154
+ * the whole exercise would be recorded as a successful restore drill without a
155
+ * single byte having been restored. That is the same false green the gates
156
+ * exist to remove, arriving through the front door.
157
+ *
158
+ * Hostname only. Anything that resolves elsewhere at runtime (a name pointing
159
+ * at a remote address, an SSH tunnel) is out of scope for a lab guard, and
160
+ * pretending otherwise would be worse than saying so.
161
+ *
162
+ * @throws when the target host is not loopback.
163
+ */
164
+ export function assertIsolatedTarget(rawUrl, what) {
165
+ let host;
166
+ try {
167
+ host = new URL(rawUrl).hostname;
168
+ }
169
+ catch {
170
+ throw new Error(`The ${what} is not a valid URL: ${rawUrl}`);
171
+ }
172
+ // `new URL().hostname` KEEPS the brackets on an IPv6 literal — `[::1]`, not
173
+ // `::1`. Comparing against the bare form silently rejects a legitimate
174
+ // loopback bench, which is the kind of guard people disable rather than fix.
175
+ const bare = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
176
+ const loopback = bare === 'localhost' || bare === '::1' || bare.startsWith('127.');
177
+ if (!loopback) {
178
+ throw new Error(`Refusing to run the restore drill against ${what} host "${host}". The drill ` +
179
+ 'is only meaningful on an isolated bench: pointed at a live deployment it ' +
180
+ 'would compare that deployment against the escrow bundle and record a ' +
181
+ 'successful "restore" in which nothing was restored. Use a loopback ' +
182
+ 'address on the bench host.');
183
+ }
184
+ }
185
+ //# sourceMappingURL=restore-drill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore-drill.js","sourceRoot":"","sources":["../../src/lib/restore-drill.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export interface SentinelClientConfig {
2
+ /** Base URL of the appliance. https:// only. */
3
+ url: string;
4
+ /** CA bundle that must issue the appliance's certificate. */
5
+ caPath: string;
6
+ /** Client certificate presented to the appliance. */
7
+ certPath: string;
8
+ /** Its private key. */
9
+ keyPath: string;
10
+ timeoutMs?: number;
11
+ }
12
+ export interface SentinelClient {
13
+ /** Open an envelope. Returns a fresh 32-byte buffer the caller must wipe. */
14
+ unwrap(envelope: Buffer): Promise<Buffer>;
15
+ /** Liveness plus a KCV claim. No plaintext crosses the wire. */
16
+ probe(envelope: Buffer): Promise<string>;
17
+ }
18
+ /**
19
+ * Build the client. Reads the certificate material eagerly so a missing or
20
+ * unreadable file fails now — at the top of a ceremony, with everything still
21
+ * locked — rather than at the moment the key is expected to arrive.
22
+ *
23
+ * @throws on a non-https URL or unreadable certificate material.
24
+ */
25
+ export declare function createSentinelClient(config: SentinelClientConfig): SentinelClient;
26
+ //# sourceMappingURL=sentinel-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinel-client.d.ts","sourceRoot":"","sources":["../../src/lib/sentinel-client.ts"],"names":[],"mappings":"AAuDA,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,gEAAgE;IAChE,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAuHjF"}
@@ -0,0 +1,165 @@
1
+ // Path: src/lib/sentinel-client.ts
2
+ //
3
+ // Minimal mTLS JSON client for the Archon Sentinel appliance, so an escrow
4
+ // ceremony can source the bootstrap key from the hardware root instead of from
5
+ // a cleartext file on a production node.
6
+ //
7
+ // PORT NOTICE. This is a SECOND implementation of a protocol the server already
8
+ // speaks in zn-vault/src/vault-crypto/root-key/sentinel-adapter.ts. This package
9
+ // ships to npm on its own and cannot import from the server, so the protocol
10
+ // necessarily exists twice — normally the way a contract dies quietly, with both
11
+ // suites green and the two sides agreeing on nothing.
12
+ //
13
+ // The defence is the one used for `kcv1:`: neither side is checked against the
14
+ // other. Both are pinned to frozen literals — paths, field names, encoding —
15
+ // in test/lib/sentinel-client.test.ts here and in the adapter's own suite there.
16
+ // Rename a field on either side and both go red.
17
+ //
18
+ // The wire, copied verbatim from the server adapter's header:
19
+ //
20
+ // POST /v1/root/unwrap {"envelope":"<b64>"} -> 200 {"plaintext":"<b64>"}
21
+ // POST /v1/root/probe {"envelope":"<b64>"} -> 200 {"kcv":"kcv1:..."}
22
+ //
23
+ // DELIBERATELY NOT IMPLEMENTED: `wrap`. Its request body carries the bootstrap
24
+ // key, and provisioning a new root is a server-side operation with its own
25
+ // audit trail (`znvault superadmin rootkey wrap`). A ceremony host has no
26
+ // business minting envelopes, so the capability is simply absent rather than
27
+ // present-and-guarded.
28
+ import { request } from 'node:https';
29
+ import { readFileSync } from 'node:fs';
30
+ /** Frozen wire paths. Pinned as literals in the test suite. */
31
+ const OP_PATHS = {
32
+ unwrap: '/v1/root/unwrap',
33
+ probe: '/v1/root/probe',
34
+ };
35
+ /**
36
+ * The purpose this client asks for, sent EXPLICITLY.
37
+ *
38
+ * Protocol v1.1 binds a purpose into the envelope's cryptography so a KMIP KEK
39
+ * envelope can never be opened as if it were the vault's bootstrap key. The
40
+ * field is optional and defaults to this same value — but the daemon's own
41
+ * comment calls that default a "staged rollout" affordance, there so the
42
+ * pre-purpose vault keeps booting during the migration.
43
+ *
44
+ * New code written today has no business depending on a compatibility default
45
+ * that exists to be removed. Sending it also makes the ask readable on the
46
+ * wire, which matters when the wire is being watched during a key ceremony.
47
+ */
48
+ const BSK_PURPOSE = 'zn-vault-bsk-v1';
49
+ const BSK_LEN = 32;
50
+ const MAX_RESPONSE_BYTES = 64 * 1024;
51
+ /**
52
+ * Build the client. Reads the certificate material eagerly so a missing or
53
+ * unreadable file fails now — at the top of a ceremony, with everything still
54
+ * locked — rather than at the moment the key is expected to arrive.
55
+ *
56
+ * @throws on a non-https URL or unreadable certificate material.
57
+ */
58
+ export function createSentinelClient(config) {
59
+ const target = new URL(config.url);
60
+ if (target.protocol !== 'https:') {
61
+ // The unwrap response carries the bootstrap key. There is no version of
62
+ // this that may travel in clear, and a typo in a runbook must not be what
63
+ // decides it.
64
+ throw new Error(`The Sentinel URL must be https://, got ${JSON.stringify(config.url)}. ` +
65
+ 'The unwrap response carries the bootstrap key.');
66
+ }
67
+ const tls = {
68
+ ca: readFileSync(config.caPath),
69
+ cert: readFileSync(config.certPath),
70
+ key: readFileSync(config.keyPath),
71
+ };
72
+ const basePath = target.pathname.replace(/\/+$/, '');
73
+ const timeoutMs = config.timeoutMs ?? 5000;
74
+ async function call(op, payload) {
75
+ const body = Buffer.from(JSON.stringify(payload), 'utf-8');
76
+ return await new Promise((resolve, reject) => {
77
+ const req = request({
78
+ method: 'POST',
79
+ protocol: target.protocol,
80
+ hostname: target.hostname,
81
+ port: target.port,
82
+ path: `${basePath}${OP_PATHS[op]}`,
83
+ headers: { 'content-type': 'application/json', 'content-length': body.length },
84
+ ca: tls.ca,
85
+ cert: tls.cert,
86
+ key: tls.key,
87
+ // Left at its default (true) on purpose. The CA above is the pin:
88
+ // without it Node would validate against the public root store and
89
+ // accept a certificate this deployment never issued — the wrong
90
+ // check, not a weaker one.
91
+ timeout: timeoutMs,
92
+ }, (res) => {
93
+ const chunks = [];
94
+ let total = 0;
95
+ res.on('data', (chunk) => {
96
+ total += chunk.length;
97
+ if (total > MAX_RESPONSE_BYTES) {
98
+ req.destroy(new Error(`Sentinel ${op} response exceeded ${String(MAX_RESPONSE_BYTES)} bytes`));
99
+ return;
100
+ }
101
+ chunks.push(chunk);
102
+ });
103
+ res.on('end', () => {
104
+ const status = res.statusCode ?? 0;
105
+ const raw = Buffer.concat(chunks).toString('utf-8');
106
+ if (status < 200 || status >= 300) {
107
+ reject(new Error(`Sentinel ${op} failed: HTTP ${String(status)}`));
108
+ return;
109
+ }
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(raw);
113
+ }
114
+ catch {
115
+ reject(new Error(`Sentinel ${op} returned HTTP ${String(status)} with a malformed JSON body`));
116
+ return;
117
+ }
118
+ if (typeof parsed !== 'object' || parsed === null) {
119
+ reject(new Error(`Sentinel ${op} returned a JSON value that is not an object`));
120
+ return;
121
+ }
122
+ resolve(parsed);
123
+ });
124
+ });
125
+ req.on('timeout', () => {
126
+ req.destroy(new Error(`Sentinel ${op} timed out after ${String(timeoutMs)}ms`));
127
+ });
128
+ req.on('error', reject);
129
+ req.end(body);
130
+ });
131
+ }
132
+ return {
133
+ async unwrap(envelope) {
134
+ const response = await call('unwrap', {
135
+ envelope: envelope.toString('base64'),
136
+ purpose: BSK_PURPOSE,
137
+ });
138
+ const plaintext = response.plaintext;
139
+ if (typeof plaintext !== 'string') {
140
+ throw new Error('Sentinel unwrap returned 200 without a string "plaintext" field. ' +
141
+ 'Something answered that is not the appliance protocol — a proxy, a ' +
142
+ 'captive portal, or a mismatched appliance version.');
143
+ }
144
+ const key = Buffer.from(plaintext, 'base64');
145
+ if (key.length !== BSK_LEN) {
146
+ key.fill(0);
147
+ throw new Error(`Sentinel unwrap returned ${String(key.length)} bytes; a bootstrap key is ` +
148
+ `exactly ${String(BSK_LEN)} bytes.`);
149
+ }
150
+ return key;
151
+ },
152
+ async probe(envelope) {
153
+ const response = await call('probe', {
154
+ envelope: envelope.toString('base64'),
155
+ purpose: BSK_PURPOSE,
156
+ });
157
+ const kcv = response.kcv;
158
+ if (typeof kcv !== 'string') {
159
+ throw new Error('Sentinel probe returned 200 without a string "kcv" field.');
160
+ }
161
+ return kcv;
162
+ },
163
+ };
164
+ }
165
+ //# sourceMappingURL=sentinel-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinel-client.js","sourceRoot":"","sources":["../../src/lib/sentinel-client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zincapp/znvault-cli",
3
- "version": "4.22.1",
3
+ "version": "4.24.0",
4
4
  "description": "Official CLI for ZnVault administration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",