burnledger 0.8.1 → 0.9.1
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/README.md +144 -2
- package/dist/cjs/anchor.d.ts +5 -3
- package/dist/cjs/anchor.d.ts.map +1 -1
- package/dist/cjs/anchor.js +10 -4
- package/dist/cjs/anchor.js.map +1 -1
- package/dist/cjs/client.d.ts +95 -0
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +175 -54
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/customer-keys.d.ts +73 -1
- package/dist/cjs/customer-keys.d.ts.map +1 -1
- package/dist/cjs/customer-keys.js +329 -3
- package/dist/cjs/customer-keys.js.map +1 -1
- package/dist/cjs/enclave-registration.d.ts +165 -0
- package/dist/cjs/enclave-registration.d.ts.map +1 -0
- package/dist/cjs/enclave-registration.js +287 -0
- package/dist/cjs/enclave-registration.js.map +1 -0
- package/dist/cjs/enclave-seal.d.ts +43 -0
- package/dist/cjs/enclave-seal.d.ts.map +1 -1
- package/dist/cjs/enclave-seal.js +62 -1
- package/dist/cjs/enclave-seal.js.map +1 -1
- package/dist/cjs/errors.d.ts +10 -0
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +11 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.browser.d.ts +7 -3
- package/dist/cjs/index.browser.d.ts.map +1 -1
- package/dist/cjs/index.browser.js +6 -3
- package/dist/cjs/index.browser.js.map +1 -1
- package/dist/cjs/index.d.ts +22 -10
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +75 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/key-group.d.ts +80 -0
- package/dist/cjs/key-group.d.ts.map +1 -0
- package/dist/cjs/key-group.js +136 -0
- package/dist/cjs/key-group.js.map +1 -0
- package/dist/cjs/models.d.ts +60 -0
- package/dist/cjs/models.d.ts.map +1 -1
- package/dist/cjs/models.js +63 -0
- package/dist/cjs/models.js.map +1 -1
- package/dist/cjs/node-runtime.d.ts +40 -0
- package/dist/cjs/node-runtime.d.ts.map +1 -0
- package/dist/cjs/node-runtime.js +42 -0
- package/dist/cjs/node-runtime.js.map +1 -0
- package/dist/cjs/status-document.d.ts +25 -0
- package/dist/cjs/status-document.d.ts.map +1 -0
- package/dist/cjs/status-document.js +62 -0
- package/dist/cjs/status-document.js.map +1 -0
- package/dist/cjs/verify.d.ts +108 -3
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +451 -171
- package/dist/cjs/verify.js.map +1 -1
- package/dist/cjs/web-verifier.d.ts +23 -3
- package/dist/cjs/web-verifier.d.ts.map +1 -1
- package/dist/cjs/web-verifier.js +31 -5
- package/dist/cjs/web-verifier.js.map +1 -1
- package/dist/cjs/webhooks.d.ts +9 -2
- package/dist/cjs/webhooks.d.ts.map +1 -1
- package/dist/cjs/webhooks.js +29 -11
- package/dist/cjs/webhooks.js.map +1 -1
- package/dist/esm/anchor.d.ts +5 -3
- package/dist/esm/anchor.d.ts.map +1 -1
- package/dist/esm/anchor.js +10 -4
- package/dist/esm/anchor.js.map +1 -1
- package/dist/esm/cli.d.ts +30 -17
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +142 -83
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/client.d.ts +95 -0
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +175 -21
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/customer-keys.d.ts +73 -1
- package/dist/esm/customer-keys.d.ts.map +1 -1
- package/dist/esm/customer-keys.js +323 -3
- package/dist/esm/customer-keys.js.map +1 -1
- package/dist/esm/enclave-registration.d.ts +165 -0
- package/dist/esm/enclave-registration.d.ts.map +1 -0
- package/dist/esm/enclave-registration.js +277 -0
- package/dist/esm/enclave-registration.js.map +1 -0
- package/dist/esm/enclave-seal.d.ts +43 -0
- package/dist/esm/enclave-seal.d.ts.map +1 -1
- package/dist/esm/enclave-seal.js +61 -1
- package/dist/esm/enclave-seal.js.map +1 -1
- package/dist/esm/errors.d.ts +10 -0
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.browser.d.ts +7 -3
- package/dist/esm/index.browser.d.ts.map +1 -1
- package/dist/esm/index.browser.js +6 -3
- package/dist/esm/index.browser.js.map +1 -1
- package/dist/esm/index.d.ts +22 -10
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +30 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/key-group.d.ts +80 -0
- package/dist/esm/key-group.d.ts.map +1 -0
- package/dist/esm/key-group.js +130 -0
- package/dist/esm/key-group.js.map +1 -0
- package/dist/esm/models.d.ts +60 -0
- package/dist/esm/models.d.ts.map +1 -1
- package/dist/esm/models.js +59 -0
- package/dist/esm/models.js.map +1 -1
- package/dist/esm/node-runtime.d.ts +40 -0
- package/dist/esm/node-runtime.d.ts.map +1 -0
- package/dist/esm/node-runtime.js +38 -0
- package/dist/esm/node-runtime.js.map +1 -0
- package/dist/esm/status-document.d.ts +25 -0
- package/dist/esm/status-document.d.ts.map +1 -0
- package/dist/esm/status-document.js +59 -0
- package/dist/esm/status-document.js.map +1 -0
- package/dist/esm/verify.d.ts +108 -3
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +448 -171
- package/dist/esm/verify.js.map +1 -1
- package/dist/esm/web-verifier.d.ts +23 -3
- package/dist/esm/web-verifier.d.ts.map +1 -1
- package/dist/esm/web-verifier.js +27 -5
- package/dist/esm/web-verifier.js.map +1 -1
- package/dist/esm/webhooks.d.ts +9 -2
- package/dist/esm/webhooks.d.ts.map +1 -1
- package/dist/esm/webhooks.js +29 -11
- package/dist/esm/webhooks.js.map +1 -1
- package/package.json +1 -1
- package/src/anchor.ts +10 -4
- package/src/cli.ts +145 -78
- package/src/client.ts +241 -25
- package/src/customer-keys.ts +373 -3
- package/src/enclave-registration.ts +402 -0
- package/src/enclave-seal.ts +108 -1
- package/src/errors.ts +11 -0
- package/src/index.browser.ts +9 -3
- package/src/index.ts +48 -6
- package/src/key-group.ts +181 -0
- package/src/models.ts +131 -0
- package/src/node-runtime.ts +57 -0
- package/src/status-document.ts +59 -0
- package/src/verify.ts +565 -177
- package/src/web-verifier.ts +36 -3
- package/src/webhooks.ts +28 -11
package/src/client.ts
CHANGED
|
@@ -48,7 +48,18 @@ import {
|
|
|
48
48
|
parseWebhookDelivery,
|
|
49
49
|
parseWebhookRotateResponse,
|
|
50
50
|
} from "./models.js";
|
|
51
|
+
import type { KeyEnrollmentResult, SystemRegistrationCertificate } from "./models.js";
|
|
51
52
|
import { Paginator } from "./pagination.js";
|
|
53
|
+
// Node-only work (enrolment, registration, savePdf) is reached through here, not
|
|
54
|
+
// by naming the Node modules — so the browser entry that shares this class never
|
|
55
|
+
// pulls node:crypto or node:fs into a bundle. See node-runtime.ts.
|
|
56
|
+
import { loadNodeRuntime } from "./node-runtime.js";
|
|
57
|
+
// Types only: erased at build, so the browser entry that shares this module
|
|
58
|
+
// never loads node:crypto. The code behind them is imported lazily, on use.
|
|
59
|
+
import type { CustomerKeyGroup } from "./customer-keys.js";
|
|
60
|
+
import type { EnclavePinOptions, RegisteredSystem } from "./enclave-registration.js";
|
|
61
|
+
import type { EnclaveIdentity } from "./enclave-seal.js";
|
|
62
|
+
import type { KeyGroupSigner } from "./key-group.js";
|
|
52
63
|
|
|
53
64
|
type Raw = Record<string, unknown>;
|
|
54
65
|
|
|
@@ -102,6 +113,13 @@ export class BurnLedger {
|
|
|
102
113
|
connectionConfig?: Record<string, unknown>;
|
|
103
114
|
dsn?: string;
|
|
104
115
|
uri?: string;
|
|
116
|
+
/**
|
|
117
|
+
* The connection config already sealed to the enclave — by
|
|
118
|
+
* sealConnectionConfig, or by createRegisteredSystem, which does it for you.
|
|
119
|
+
* The API relays these bytes and cannot open them. Instead of
|
|
120
|
+
* connectionConfig/dsn/uri, never beside them.
|
|
121
|
+
*/
|
|
122
|
+
sealedConnectionConfig?: Uint8Array;
|
|
105
123
|
subjectQuery: string;
|
|
106
124
|
hashScope?: string;
|
|
107
125
|
/** The system holds data you treat as PHI. Makes the default proof mode
|
|
@@ -112,15 +130,19 @@ export class BurnLedger {
|
|
|
112
130
|
maxBytes?: number;
|
|
113
131
|
queryTimeout?: string;
|
|
114
132
|
}): Promise<System> {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
opts.uri
|
|
119
|
-
)
|
|
133
|
+
const sealed = opts.sealedConnectionConfig;
|
|
134
|
+
if (
|
|
135
|
+
sealed !== undefined &&
|
|
136
|
+
(opts.connectionConfig !== undefined || opts.dsn !== undefined || opts.uri !== undefined)
|
|
137
|
+
) {
|
|
138
|
+
throw new Error("Cannot provide both sealedConnectionConfig and connectionConfig/dsn/uri.");
|
|
139
|
+
}
|
|
120
140
|
const body = {
|
|
121
141
|
name: opts.name,
|
|
122
142
|
connector_type: opts.connectorType,
|
|
123
|
-
|
|
143
|
+
...(sealed === undefined
|
|
144
|
+
? { connection_config: resolveConnectionConfig(opts.connectionConfig, opts.dsn, opts.uri) }
|
|
145
|
+
: { sealed_connection_config: bytesToBase64(sealed) }),
|
|
124
146
|
subject_query: opts.subjectQuery,
|
|
125
147
|
hash_scope: opts.hashScope ?? "existence",
|
|
126
148
|
phi_in_scope: opts.phiInScope ?? false,
|
|
@@ -137,7 +159,7 @@ export class BurnLedger {
|
|
|
137
159
|
async getSystem(systemId: string): Promise<System> {
|
|
138
160
|
const data = await this.transport.request(
|
|
139
161
|
"GET",
|
|
140
|
-
`/v1/systems/${systemId}`,
|
|
162
|
+
`/v1/systems/${encodeId(systemId)}`,
|
|
141
163
|
);
|
|
142
164
|
return parseSystem(data as Raw);
|
|
143
165
|
}
|
|
@@ -149,13 +171,13 @@ export class BurnLedger {
|
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
async deregisterSystem(systemId: string): Promise<void> {
|
|
152
|
-
await this.transport.request("DELETE", `/v1/systems/${systemId}`);
|
|
174
|
+
await this.transport.request("DELETE", `/v1/systems/${encodeId(systemId)}`);
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
async healthCheck(systemId: string): Promise<System> {
|
|
156
178
|
const data = await this.transport.request(
|
|
157
179
|
"POST",
|
|
158
|
-
`/v1/systems/${systemId}/health-check`,
|
|
180
|
+
`/v1/systems/${encodeId(systemId)}/health-check`,
|
|
159
181
|
);
|
|
160
182
|
return parseSystem(data as Raw);
|
|
161
183
|
}
|
|
@@ -188,7 +210,7 @@ export class BurnLedger {
|
|
|
188
210
|
async getAttestation(attestationId: string): Promise<Attestation> {
|
|
189
211
|
const data = await this.transport.request(
|
|
190
212
|
"GET",
|
|
191
|
-
`/v1/attestations/${attestationId}`,
|
|
213
|
+
`/v1/attestations/${encodeId(attestationId)}`,
|
|
192
214
|
);
|
|
193
215
|
return parseAttestation(data as Raw);
|
|
194
216
|
}
|
|
@@ -214,7 +236,7 @@ export class BurnLedger {
|
|
|
214
236
|
const body = { subject_identifier: subjectIdentifier };
|
|
215
237
|
const data = await this.transport.request(
|
|
216
238
|
"POST",
|
|
217
|
-
`/v1/attestations/${attestationId}/verify`,
|
|
239
|
+
`/v1/attestations/${encodeId(attestationId)}/verify`,
|
|
218
240
|
{ json: body },
|
|
219
241
|
);
|
|
220
242
|
const result = parseVerifyResult(data as Raw);
|
|
@@ -228,7 +250,7 @@ export class BurnLedger {
|
|
|
228
250
|
fetch: async () => {
|
|
229
251
|
const d = await this.transport.request(
|
|
230
252
|
"POST",
|
|
231
|
-
`/v1/attestations/${attestationId}/verify`,
|
|
253
|
+
`/v1/attestations/${encodeId(attestationId)}/verify`,
|
|
232
254
|
{ json: body },
|
|
233
255
|
);
|
|
234
256
|
return parseVerifyResult(d as Raw);
|
|
@@ -263,7 +285,7 @@ export class BurnLedger {
|
|
|
263
285
|
async getCertificate(certificateId: string): Promise<CertificateResponse> {
|
|
264
286
|
const data = await this.transport.request(
|
|
265
287
|
"GET",
|
|
266
|
-
`/v1/certificates/${certificateId}`,
|
|
288
|
+
`/v1/certificates/${encodeId(certificateId)}`,
|
|
267
289
|
);
|
|
268
290
|
return parseCertificateResponse(data as Raw);
|
|
269
291
|
}
|
|
@@ -282,20 +304,20 @@ export class BurnLedger {
|
|
|
282
304
|
async downloadPdf(certificateId: string): Promise<Uint8Array> {
|
|
283
305
|
return this.transport.requestBytes(
|
|
284
306
|
"GET",
|
|
285
|
-
`/v1/certificates/${certificateId}/pdf`,
|
|
307
|
+
`/v1/certificates/${encodeId(certificateId)}/pdf`,
|
|
286
308
|
);
|
|
287
309
|
}
|
|
288
310
|
|
|
289
311
|
async savePdf(certificateId: string, path: string): Promise<void> {
|
|
290
312
|
const pdf = await this.downloadPdf(certificateId);
|
|
291
|
-
const { writeFile } = await
|
|
313
|
+
const { writeFile } = await loadNodeRuntime();
|
|
292
314
|
await writeFile(path, pdf);
|
|
293
315
|
}
|
|
294
316
|
|
|
295
317
|
async getRevocationStatus(certificateId: string): Promise<RevocationStatus> {
|
|
296
318
|
const data = await this.transport.request(
|
|
297
319
|
"GET",
|
|
298
|
-
`/v1/certificates/${certificateId}/revocation-status`,
|
|
320
|
+
`/v1/certificates/${encodeId(certificateId)}/revocation-status`,
|
|
299
321
|
);
|
|
300
322
|
return parseRevocationStatus(data as Raw);
|
|
301
323
|
}
|
|
@@ -306,7 +328,7 @@ export class BurnLedger {
|
|
|
306
328
|
): Promise<CertificateResponse> {
|
|
307
329
|
const data = await this.transport.request(
|
|
308
330
|
"POST",
|
|
309
|
-
`/v1/certificates/${certificateId}/revoke`,
|
|
331
|
+
`/v1/certificates/${encodeId(certificateId)}/revoke`,
|
|
310
332
|
{ json: { reason: opts.reason } },
|
|
311
333
|
);
|
|
312
334
|
return parseCertificateResponse(data as Raw);
|
|
@@ -371,13 +393,13 @@ export class BurnLedger {
|
|
|
371
393
|
}
|
|
372
394
|
|
|
373
395
|
async deleteWebhook(webhookId: string): Promise<void> {
|
|
374
|
-
await this.transport.request("DELETE", `/v1/webhooks/${webhookId}`);
|
|
396
|
+
await this.transport.request("DELETE", `/v1/webhooks/${encodeId(webhookId)}`);
|
|
375
397
|
}
|
|
376
398
|
|
|
377
399
|
async rotateWebhookSecret(webhookId: string): Promise<WebhookRotateResponse> {
|
|
378
400
|
const data = await this.transport.request(
|
|
379
401
|
"POST",
|
|
380
|
-
`/v1/webhooks/${webhookId}/rotate-secret`,
|
|
402
|
+
`/v1/webhooks/${encodeId(webhookId)}/rotate-secret`,
|
|
381
403
|
);
|
|
382
404
|
return parseWebhookRotateResponse(data as Raw);
|
|
383
405
|
}
|
|
@@ -385,7 +407,7 @@ export class BurnLedger {
|
|
|
385
407
|
async commitWebhookRotation(webhookId: string): Promise<void> {
|
|
386
408
|
await this.transport.request(
|
|
387
409
|
"POST",
|
|
388
|
-
`/v1/webhooks/${webhookId}/commit-rotation`,
|
|
410
|
+
`/v1/webhooks/${encodeId(webhookId)}/commit-rotation`,
|
|
389
411
|
);
|
|
390
412
|
}
|
|
391
413
|
|
|
@@ -401,14 +423,14 @@ export class BurnLedger {
|
|
|
401
423
|
async retryDelivery(deliveryId: string): Promise<void> {
|
|
402
424
|
await this.transport.request(
|
|
403
425
|
"POST",
|
|
404
|
-
`/v1/webhooks/deliveries/${deliveryId}/retry`,
|
|
426
|
+
`/v1/webhooks/deliveries/${encodeId(deliveryId)}/retry`,
|
|
405
427
|
);
|
|
406
428
|
}
|
|
407
429
|
|
|
408
430
|
async resolveDelivery(deliveryId: string): Promise<void> {
|
|
409
431
|
await this.transport.request(
|
|
410
432
|
"DELETE",
|
|
411
|
-
`/v1/webhooks/deliveries/${deliveryId}`,
|
|
433
|
+
`/v1/webhooks/deliveries/${encodeId(deliveryId)}`,
|
|
412
434
|
);
|
|
413
435
|
}
|
|
414
436
|
|
|
@@ -435,7 +457,7 @@ export class BurnLedger {
|
|
|
435
457
|
}
|
|
436
458
|
|
|
437
459
|
async revokeApiKey(keyId: string): Promise<void> {
|
|
438
|
-
await this.transport.request("DELETE", `/v1/api-keys/${keyId}`);
|
|
460
|
+
await this.transport.request("DELETE", `/v1/api-keys/${encodeId(keyId)}`);
|
|
439
461
|
}
|
|
440
462
|
|
|
441
463
|
// --- Profile ---
|
|
@@ -458,12 +480,177 @@ export class BurnLedger {
|
|
|
458
480
|
await this.transport.request("DELETE", "/v1/me");
|
|
459
481
|
}
|
|
460
482
|
|
|
483
|
+
// --- Customer key groups and enclave registration (ADR-025; Node only) ---
|
|
484
|
+
//
|
|
485
|
+
// These load node:crypto on first use, lazily, because this class is also the
|
|
486
|
+
// browser entry's. ADR-025 §2 makes a client the customer runs locally the
|
|
487
|
+
// only place enrolment can happen, so a browser has no business here anyway.
|
|
488
|
+
// `teamId` is the team every signed payload binds; it is also sent as
|
|
489
|
+
// asserted_team_id, so naming the wrong team is a 403 that says so rather than
|
|
490
|
+
// an enclave 401 that cannot.
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Verify the enclave: `GET /v1/enclave/attestation` over a 32-byte challenge
|
|
494
|
+
* drawn here (or `pin.nonce`), checked against the PCR0 you pinned out of band.
|
|
495
|
+
* Returns the keys the document binds. Throws EnclaveAttestationError on
|
|
496
|
+
* anything short of a document from the pinned image answering this challenge.
|
|
497
|
+
* Pass the result as `enclave` to the calls below: configs are sealed to it,
|
|
498
|
+
* and every document they return must verify under its signing key.
|
|
499
|
+
*/
|
|
500
|
+
async attestEnclaveIdentity(pin: EnclavePinOptions): Promise<EnclaveIdentity> {
|
|
501
|
+
const { registration: flow } = await loadNodeRuntime();
|
|
502
|
+
const { nonce, params } = flow.attestationParams(pin.nonce);
|
|
503
|
+
const data = await this.transport.request("GET", "/v1/enclave/attestation", {
|
|
504
|
+
params,
|
|
505
|
+
authenticated: false,
|
|
506
|
+
});
|
|
507
|
+
return flow.identityFromResponse(data, { ...pin, nonce });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Enrol a key group for your team (ENROLL_KEY). Every member signs; the private
|
|
512
|
+
* keys stay with the signers. `notAfter` is the latest the authorization may
|
|
513
|
+
* end, signed by every member so a relayed copy of this request cannot renew
|
|
514
|
+
* it; it defaults to the enclave's 90-day maximum. Throws VerificationError
|
|
515
|
+
* unless both documents that come back verify under `enclave`'s signing key,
|
|
516
|
+
* name exactly this group — ADR-025 §2's one comparison — and your team, end
|
|
517
|
+
* exactly at `notAfter` and do not start after `now` (this machine's clock by default).
|
|
518
|
+
*/
|
|
519
|
+
async enrollKeyGroup(opts: {
|
|
520
|
+
teamId: string;
|
|
521
|
+
group: CustomerKeyGroup;
|
|
522
|
+
signers: readonly KeyGroupSigner[];
|
|
523
|
+
enclave: EnclaveIdentity;
|
|
524
|
+
notAfter?: Date;
|
|
525
|
+
/** The time to judge the answer's not_before against. Defaults to now. */
|
|
526
|
+
now?: Date;
|
|
527
|
+
}): Promise<KeyEnrollmentResult> {
|
|
528
|
+
const { registration: flow } = await loadNodeRuntime();
|
|
529
|
+
const { body, keyId, notAfter } = await flow.enrollBody(opts);
|
|
530
|
+
const data = await this.transport.request("POST", "/v1/enclave/keys", { json: body });
|
|
531
|
+
return flow.checkEnrollment(data, {
|
|
532
|
+
enclave: opts.enclave,
|
|
533
|
+
keyId,
|
|
534
|
+
prevKeyId: undefined,
|
|
535
|
+
teamId: opts.teamId,
|
|
536
|
+
notAfter,
|
|
537
|
+
now: opts.now,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Replace an enrolled group with another (ROTATE_KEY). The OUTGOING group
|
|
543
|
+
* signs at its threshold and EVERY incoming member signs, over the same
|
|
544
|
+
* request and `notAfter`. Systems registered under the old group keep working
|
|
545
|
+
* through the link this issues; nothing is re-registered. The answer is checked
|
|
546
|
+
* as `enrollKeyGroup`'s is, and must also link to the outgoing group.
|
|
547
|
+
*/
|
|
548
|
+
async rotateKeyGroup(opts: {
|
|
549
|
+
teamId: string;
|
|
550
|
+
previousGroup: CustomerKeyGroup;
|
|
551
|
+
previousSigners: readonly KeyGroupSigner[];
|
|
552
|
+
nextGroup: CustomerKeyGroup;
|
|
553
|
+
nextSigners: readonly KeyGroupSigner[];
|
|
554
|
+
enclave: EnclaveIdentity;
|
|
555
|
+
notAfter?: Date;
|
|
556
|
+
/** The time to judge the answer's not_before against. Defaults to now. */
|
|
557
|
+
now?: Date;
|
|
558
|
+
}): Promise<KeyEnrollmentResult> {
|
|
559
|
+
const { registration: flow } = await loadNodeRuntime();
|
|
560
|
+
const { body, prevKeyId, nextKeyId, notAfter } = await flow.rotateBody(opts);
|
|
561
|
+
const data = await this.transport.request("POST", "/v1/enclave/keys/rotate", { json: body });
|
|
562
|
+
return flow.checkEnrollment(data, {
|
|
563
|
+
enclave: opts.enclave,
|
|
564
|
+
keyId: nextKeyId,
|
|
565
|
+
prevKeyId,
|
|
566
|
+
teamId: opts.teamId,
|
|
567
|
+
notAfter,
|
|
568
|
+
now: opts.now,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Register an existing system under your key group (REGISTER_SYSTEM), signed at
|
|
574
|
+
* the group's threshold. `config` must be the exact bytes submitted when the
|
|
575
|
+
* system was created, and `queryTemplate` the system's subject query exactly.
|
|
576
|
+
* The registration that comes back must verify under `enclave`'s signing key.
|
|
577
|
+
*/
|
|
578
|
+
async registerSystemWithKey(opts: {
|
|
579
|
+
teamId: string;
|
|
580
|
+
group: CustomerKeyGroup;
|
|
581
|
+
signers: readonly KeyGroupSigner[];
|
|
582
|
+
enclave: EnclaveIdentity;
|
|
583
|
+
systemId: string;
|
|
584
|
+
config: Uint8Array | string;
|
|
585
|
+
queryTemplate: string;
|
|
586
|
+
connectorType: string;
|
|
587
|
+
}): Promise<SystemRegistrationCertificate> {
|
|
588
|
+
const { registration: flow } = await loadNodeRuntime();
|
|
589
|
+
const { body, keyId, configDigest } = await flow.registrationBody(opts);
|
|
590
|
+
const data = await this.transport.request("POST", "/v1/enclave/registrations", { json: body });
|
|
591
|
+
return flow.checkRegistration(data, {
|
|
592
|
+
enclave: opts.enclave,
|
|
593
|
+
keyId,
|
|
594
|
+
systemId: opts.systemId,
|
|
595
|
+
configDigest,
|
|
596
|
+
connectorType: opts.connectorType,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* ADR-025's whole customer path for a new system: seal the config to `enclave`
|
|
602
|
+
* (from {@link attestEnclaveIdentity}), create the system with only the sealed
|
|
603
|
+
* bytes, and register it under your key group.
|
|
604
|
+
*/
|
|
605
|
+
async createRegisteredSystem(opts: {
|
|
606
|
+
teamId: string;
|
|
607
|
+
group: CustomerKeyGroup;
|
|
608
|
+
signers: readonly KeyGroupSigner[];
|
|
609
|
+
enclave: EnclaveIdentity;
|
|
610
|
+
name: string;
|
|
611
|
+
connectorType: string;
|
|
612
|
+
connectionConfig?: Record<string, unknown>;
|
|
613
|
+
dsn?: string;
|
|
614
|
+
uri?: string;
|
|
615
|
+
subjectQuery: string;
|
|
616
|
+
hashScope?: string;
|
|
617
|
+
phiInScope?: boolean;
|
|
618
|
+
maxRecords?: number;
|
|
619
|
+
maxBytes?: number;
|
|
620
|
+
queryTimeout?: string;
|
|
621
|
+
}): Promise<RegisteredSystem> {
|
|
622
|
+
const { registration: flow, seal } = await loadNodeRuntime();
|
|
623
|
+
const config = flow.configBytes(resolveConnectionConfig(opts.connectionConfig, opts.dsn, opts.uri));
|
|
624
|
+
const system = await this.registerSystem({
|
|
625
|
+
name: opts.name,
|
|
626
|
+
connectorType: opts.connectorType,
|
|
627
|
+
subjectQuery: opts.subjectQuery,
|
|
628
|
+
hashScope: opts.hashScope,
|
|
629
|
+
phiInScope: opts.phiInScope,
|
|
630
|
+
maxRecords: opts.maxRecords,
|
|
631
|
+
maxBytes: opts.maxBytes,
|
|
632
|
+
queryTimeout: opts.queryTimeout,
|
|
633
|
+
sealedConnectionConfig: await seal.sealToKey(opts.enclave.configSealKey, config),
|
|
634
|
+
});
|
|
635
|
+
const registration = await this.registerSystemWithKey({
|
|
636
|
+
teamId: opts.teamId,
|
|
637
|
+
group: opts.group,
|
|
638
|
+
signers: opts.signers,
|
|
639
|
+
enclave: opts.enclave,
|
|
640
|
+
systemId: system.id,
|
|
641
|
+
config,
|
|
642
|
+
queryTemplate: opts.subjectQuery,
|
|
643
|
+
connectorType: opts.connectorType,
|
|
644
|
+
});
|
|
645
|
+
return { system, registration };
|
|
646
|
+
}
|
|
647
|
+
|
|
461
648
|
// --- System Health ---
|
|
462
649
|
|
|
463
650
|
async getSystemHealth(systemId: string): Promise<SystemHealth> {
|
|
464
651
|
const data = await this.transport.request(
|
|
465
652
|
"GET",
|
|
466
|
-
`/v1/systems/${systemId}/health`,
|
|
653
|
+
`/v1/systems/${encodeId(systemId)}/health`,
|
|
467
654
|
);
|
|
468
655
|
return parseSystemHealth(data as Raw);
|
|
469
656
|
}
|
|
@@ -532,6 +719,28 @@ export class BurnLedger {
|
|
|
532
719
|
// Connection config resolution
|
|
533
720
|
// ---------------------------------------------------------------------------
|
|
534
721
|
|
|
722
|
+
/** Percent-encode an id before it goes into a URL path, and refuse `.`/`..`.
|
|
723
|
+
*
|
|
724
|
+
* A raw id lets untrusted input escape its segment: `deregisterSystem("x/../../
|
|
725
|
+
* api-keys/k1")` would otherwise resolve to `DELETE /v1/api-keys/k1`.
|
|
726
|
+
* encodeURIComponent escapes the `/`, but it leaves `.` and `..` untouched, and
|
|
727
|
+
* a segment that IS `.` or `..` is still traversal — so those are rejected
|
|
728
|
+
* outright. An empty id is rejected too: it collapses two path segments into one. */
|
|
729
|
+
function encodeId(id: string): string {
|
|
730
|
+
if (id === "" || id === "." || id === "..") {
|
|
731
|
+
throw new Error(`invalid id path segment: ${JSON.stringify(id)}`);
|
|
732
|
+
}
|
|
733
|
+
return encodeURIComponent(id);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Standard base64, as Go decodes a []byte field. No Buffer: this module is
|
|
737
|
+
* also the browser entry's. */
|
|
738
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
739
|
+
let binary = "";
|
|
740
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
741
|
+
return btoa(binary);
|
|
742
|
+
}
|
|
743
|
+
|
|
535
744
|
function resolveConnectionConfig(
|
|
536
745
|
connectionConfig: Record<string, unknown> | undefined,
|
|
537
746
|
dsn: string | undefined,
|
|
@@ -577,7 +786,14 @@ async function poll<T>(opts: {
|
|
|
577
786
|
result = await opts.fetch();
|
|
578
787
|
} catch (err) {
|
|
579
788
|
if (err instanceof RateLimitError && err.retryAfter !== undefined) {
|
|
580
|
-
|
|
789
|
+
const remaining = deadline - Date.now();
|
|
790
|
+
if (remaining <= 0) {
|
|
791
|
+
throw new TimeoutError(opts.operation, elapsed);
|
|
792
|
+
}
|
|
793
|
+
// Bound the wait to the caller's remaining budget: a server asking for a
|
|
794
|
+
// day, or an Infinity Retry-After, must not block past the timeout the
|
|
795
|
+
// caller set (Math.min(Infinity, remaining) === remaining).
|
|
796
|
+
await sleep(Math.min(err.retryAfter * 1000, remaining));
|
|
581
797
|
continue;
|
|
582
798
|
}
|
|
583
799
|
throw err;
|