burnledger 0.9.0 → 0.10.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/README.md +9 -4
- package/dist/cjs/audit-pack.d.ts +201 -0
- package/dist/cjs/audit-pack.d.ts.map +1 -0
- package/dist/cjs/audit-pack.js +867 -0
- package/dist/cjs/audit-pack.js.map +1 -0
- package/dist/cjs/client.d.ts +41 -1
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +107 -59
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/enclave-registration.d.ts +14 -2
- package/dist/cjs/enclave-registration.d.ts.map +1 -1
- package/dist/cjs/enclave-registration.js +14 -2
- package/dist/cjs/enclave-registration.js.map +1 -1
- package/dist/cjs/index.d.ts +46 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +114 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/models.d.ts +28 -2
- package/dist/cjs/models.d.ts.map +1 -1
- package/dist/cjs/models.js +19 -1
- 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/run-record.d.ts +130 -0
- package/dist/cjs/run-record.d.ts.map +1 -0
- package/dist/cjs/run-record.js +272 -0
- package/dist/cjs/run-record.js.map +1 -0
- package/dist/cjs/verify.d.ts +58 -0
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +277 -38
- package/dist/cjs/verify.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/audit-pack.d.ts +201 -0
- package/dist/esm/audit-pack.d.ts.map +1 -0
- package/dist/esm/audit-pack.js +858 -0
- package/dist/esm/audit-pack.js.map +1 -0
- package/dist/esm/cli.d.ts +52 -0
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +283 -11
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/client.d.ts +41 -1
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +108 -27
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/enclave-registration.d.ts +14 -2
- package/dist/esm/enclave-registration.d.ts.map +1 -1
- package/dist/esm/enclave-registration.js +14 -2
- package/dist/esm/enclave-registration.js.map +1 -1
- package/dist/esm/index.d.ts +46 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +64 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/models.d.ts +28 -2
- package/dist/esm/models.d.ts.map +1 -1
- package/dist/esm/models.js +18 -1
- 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/run-record.d.ts +130 -0
- package/dist/esm/run-record.d.ts.map +1 -0
- package/dist/esm/run-record.js +262 -0
- package/dist/esm/run-record.js.map +1 -0
- package/dist/esm/verify.d.ts +58 -0
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +270 -41
- package/dist/esm/verify.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/audit-pack.ts +1067 -0
- package/src/cli.ts +289 -10
- package/src/client.ts +132 -27
- package/src/enclave-registration.ts +14 -2
- package/src/index.ts +130 -1
- package/src/models.ts +47 -3
- package/src/node-runtime.ts +57 -0
- package/src/run-record.ts +371 -0
- package/src/verify.ts +301 -41
- package/src/webhooks.ts +28 -11
package/src/client.ts
CHANGED
|
@@ -19,6 +19,8 @@ import type {
|
|
|
19
19
|
LogEntry,
|
|
20
20
|
Profile,
|
|
21
21
|
RevocationStatus,
|
|
22
|
+
Run,
|
|
23
|
+
RunKind,
|
|
22
24
|
SignedTreeHead,
|
|
23
25
|
System,
|
|
24
26
|
SystemHealth,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
parseLogEntry,
|
|
41
43
|
parseProfile,
|
|
42
44
|
parseRevocationStatus,
|
|
45
|
+
parseRun,
|
|
43
46
|
parseSignedTreeHead,
|
|
44
47
|
parseSystem,
|
|
45
48
|
parseSystemHealth,
|
|
@@ -50,6 +53,10 @@ import {
|
|
|
50
53
|
} from "./models.js";
|
|
51
54
|
import type { KeyEnrollmentResult, SystemRegistrationCertificate } from "./models.js";
|
|
52
55
|
import { Paginator } from "./pagination.js";
|
|
56
|
+
// Node-only work (enrolment, registration, savePdf) is reached through here, not
|
|
57
|
+
// by naming the Node modules — so the browser entry that shares this class never
|
|
58
|
+
// pulls node:crypto or node:fs into a bundle. See node-runtime.ts.
|
|
59
|
+
import { loadNodeRuntime } from "./node-runtime.js";
|
|
53
60
|
// Types only: erased at build, so the browser entry that shares this module
|
|
54
61
|
// never loads node:crypto. The code behind them is imported lazily, on use.
|
|
55
62
|
import type { CustomerKeyGroup } from "./customer-keys.js";
|
|
@@ -155,7 +162,7 @@ export class BurnLedger {
|
|
|
155
162
|
async getSystem(systemId: string): Promise<System> {
|
|
156
163
|
const data = await this.transport.request(
|
|
157
164
|
"GET",
|
|
158
|
-
`/v1/systems/${systemId}`,
|
|
165
|
+
`/v1/systems/${encodeId(systemId)}`,
|
|
159
166
|
);
|
|
160
167
|
return parseSystem(data as Raw);
|
|
161
168
|
}
|
|
@@ -167,13 +174,13 @@ export class BurnLedger {
|
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
async deregisterSystem(systemId: string): Promise<void> {
|
|
170
|
-
await this.transport.request("DELETE", `/v1/systems/${systemId}`);
|
|
177
|
+
await this.transport.request("DELETE", `/v1/systems/${encodeId(systemId)}`);
|
|
171
178
|
}
|
|
172
179
|
|
|
173
180
|
async healthCheck(systemId: string): Promise<System> {
|
|
174
181
|
const data = await this.transport.request(
|
|
175
182
|
"POST",
|
|
176
|
-
`/v1/systems/${systemId}/health-check`,
|
|
183
|
+
`/v1/systems/${encodeId(systemId)}/health-check`,
|
|
177
184
|
);
|
|
178
185
|
return parseSystem(data as Raw);
|
|
179
186
|
}
|
|
@@ -187,6 +194,9 @@ export class BurnLedger {
|
|
|
187
194
|
proofMode?: string;
|
|
188
195
|
expiresIn?: string;
|
|
189
196
|
webhookUrl?: string;
|
|
197
|
+
/** Issue the record under this OPEN run (ADR-031). The run must be this
|
|
198
|
+
* team's and its declared systems must be exactly systemIds. */
|
|
199
|
+
runId?: string;
|
|
190
200
|
},
|
|
191
201
|
): Promise<Attestation> {
|
|
192
202
|
const body: Record<string, unknown> = {
|
|
@@ -196,6 +206,7 @@ export class BurnLedger {
|
|
|
196
206
|
};
|
|
197
207
|
if (opts?.systemIds !== undefined) body.system_ids = opts.systemIds;
|
|
198
208
|
if (opts?.webhookUrl !== undefined) body.webhook_url = opts.webhookUrl;
|
|
209
|
+
if (opts?.runId !== undefined) body.run_id = opts.runId;
|
|
199
210
|
|
|
200
211
|
const data = await this.transport.request("POST", "/v1/attestations", {
|
|
201
212
|
json: body,
|
|
@@ -206,7 +217,7 @@ export class BurnLedger {
|
|
|
206
217
|
async getAttestation(attestationId: string): Promise<Attestation> {
|
|
207
218
|
const data = await this.transport.request(
|
|
208
219
|
"GET",
|
|
209
|
-
`/v1/attestations/${attestationId}`,
|
|
220
|
+
`/v1/attestations/${encodeId(attestationId)}`,
|
|
210
221
|
);
|
|
211
222
|
return parseAttestation(data as Raw);
|
|
212
223
|
}
|
|
@@ -232,7 +243,7 @@ export class BurnLedger {
|
|
|
232
243
|
const body = { subject_identifier: subjectIdentifier };
|
|
233
244
|
const data = await this.transport.request(
|
|
234
245
|
"POST",
|
|
235
|
-
`/v1/attestations/${attestationId}/verify`,
|
|
246
|
+
`/v1/attestations/${encodeId(attestationId)}/verify`,
|
|
236
247
|
{ json: body },
|
|
237
248
|
);
|
|
238
249
|
const result = parseVerifyResult(data as Raw);
|
|
@@ -246,7 +257,7 @@ export class BurnLedger {
|
|
|
246
257
|
fetch: async () => {
|
|
247
258
|
const d = await this.transport.request(
|
|
248
259
|
"POST",
|
|
249
|
-
`/v1/attestations/${attestationId}/verify`,
|
|
260
|
+
`/v1/attestations/${encodeId(attestationId)}/verify`,
|
|
250
261
|
{ json: body },
|
|
251
262
|
);
|
|
252
263
|
return parseVerifyResult(d as Raw);
|
|
@@ -263,13 +274,16 @@ export class BurnLedger {
|
|
|
263
274
|
systemIds: string[];
|
|
264
275
|
proofMode?: string;
|
|
265
276
|
expiresIn?: string;
|
|
277
|
+
/** Issue every record under this OPEN run (ADR-031). */
|
|
278
|
+
runId?: string;
|
|
266
279
|
}): Promise<BatchAttestationResponse> {
|
|
267
|
-
const body = {
|
|
280
|
+
const body: Record<string, unknown> = {
|
|
268
281
|
subject_identifiers: opts.subjectIdentifiers,
|
|
269
282
|
system_ids: opts.systemIds,
|
|
270
283
|
proof_mode: opts.proofMode ?? "count",
|
|
271
284
|
expires_in: opts.expiresIn ?? "72h",
|
|
272
285
|
};
|
|
286
|
+
if (opts.runId !== undefined) body.run_id = opts.runId;
|
|
273
287
|
const data = await this.transport.request("POST", "/v1/attestations/batch", {
|
|
274
288
|
json: body,
|
|
275
289
|
});
|
|
@@ -281,7 +295,7 @@ export class BurnLedger {
|
|
|
281
295
|
async getCertificate(certificateId: string): Promise<CertificateResponse> {
|
|
282
296
|
const data = await this.transport.request(
|
|
283
297
|
"GET",
|
|
284
|
-
`/v1/certificates/${certificateId}`,
|
|
298
|
+
`/v1/certificates/${encodeId(certificateId)}`,
|
|
285
299
|
);
|
|
286
300
|
return parseCertificateResponse(data as Raw);
|
|
287
301
|
}
|
|
@@ -300,20 +314,20 @@ export class BurnLedger {
|
|
|
300
314
|
async downloadPdf(certificateId: string): Promise<Uint8Array> {
|
|
301
315
|
return this.transport.requestBytes(
|
|
302
316
|
"GET",
|
|
303
|
-
`/v1/certificates/${certificateId}/pdf`,
|
|
317
|
+
`/v1/certificates/${encodeId(certificateId)}/pdf`,
|
|
304
318
|
);
|
|
305
319
|
}
|
|
306
320
|
|
|
307
321
|
async savePdf(certificateId: string, path: string): Promise<void> {
|
|
308
322
|
const pdf = await this.downloadPdf(certificateId);
|
|
309
|
-
const { writeFile } = await
|
|
323
|
+
const { writeFile } = await loadNodeRuntime();
|
|
310
324
|
await writeFile(path, pdf);
|
|
311
325
|
}
|
|
312
326
|
|
|
313
327
|
async getRevocationStatus(certificateId: string): Promise<RevocationStatus> {
|
|
314
328
|
const data = await this.transport.request(
|
|
315
329
|
"GET",
|
|
316
|
-
`/v1/certificates/${certificateId}/revocation-status`,
|
|
330
|
+
`/v1/certificates/${encodeId(certificateId)}/revocation-status`,
|
|
317
331
|
);
|
|
318
332
|
return parseRevocationStatus(data as Raw);
|
|
319
333
|
}
|
|
@@ -324,7 +338,7 @@ export class BurnLedger {
|
|
|
324
338
|
): Promise<CertificateResponse> {
|
|
325
339
|
const data = await this.transport.request(
|
|
326
340
|
"POST",
|
|
327
|
-
`/v1/certificates/${certificateId}/revoke`,
|
|
341
|
+
`/v1/certificates/${encodeId(certificateId)}/revoke`,
|
|
328
342
|
{ json: { reason: opts.reason } },
|
|
329
343
|
);
|
|
330
344
|
return parseCertificateResponse(data as Raw);
|
|
@@ -373,6 +387,77 @@ export class BurnLedger {
|
|
|
373
387
|
return this.transport.requestBytes("GET", path);
|
|
374
388
|
}
|
|
375
389
|
|
|
390
|
+
/** One page of an audit pack (docs/audit-pack.md): the export's records as
|
|
391
|
+
* signed documents, with the issuer's keys and log head, framed by a header
|
|
392
|
+
* and a footer. The footer's next_cursor names the next page; pass it back
|
|
393
|
+
* as `cursor` and keep each page as its own file — pages chain by their
|
|
394
|
+
* cursors, not by their bytes. */
|
|
395
|
+
async exportAuditPack(opts?: {
|
|
396
|
+
status?: "ACTIVE" | "REVOKED";
|
|
397
|
+
issuedAfter?: string;
|
|
398
|
+
issuedBefore?: string;
|
|
399
|
+
/** Only the records issued under this run (ADR-031); a closed run's pack
|
|
400
|
+
* carries the signed run record in its header. */
|
|
401
|
+
runId?: string;
|
|
402
|
+
cursor?: string;
|
|
403
|
+
}): Promise<Uint8Array> {
|
|
404
|
+
const params: Record<string, string> = {};
|
|
405
|
+
if (opts?.status !== undefined) params.status = opts.status;
|
|
406
|
+
if (opts?.issuedAfter !== undefined) params.issued_after = opts.issuedAfter;
|
|
407
|
+
if (opts?.issuedBefore !== undefined) params.issued_before = opts.issuedBefore;
|
|
408
|
+
if (opts?.runId !== undefined) params.run_id = opts.runId;
|
|
409
|
+
if (opts?.cursor !== undefined) params.cursor = opts.cursor;
|
|
410
|
+
|
|
411
|
+
const qs = new URLSearchParams(params).toString();
|
|
412
|
+
const path = qs ? `/v1/certificates/audit-pack?${qs}` : "/v1/certificates/audit-pack";
|
|
413
|
+
return this.transport.requestBytes("GET", path);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// --- Runs (ADR-031) ---
|
|
417
|
+
|
|
418
|
+
/** Open a bulk run with the customer's declaration: what the input list was
|
|
419
|
+
* (its SHA-256 over the canonical form and entry count), the deadline, and
|
|
420
|
+
* the systems every record in the run will cover. The enclave never sees the
|
|
421
|
+
* list; the digest and count are signed into the run record as declared. */
|
|
422
|
+
async openRun(opts: {
|
|
423
|
+
runKind: RunKind;
|
|
424
|
+
/** 64 hex characters. */
|
|
425
|
+
listDigest: string;
|
|
426
|
+
listCount: number;
|
|
427
|
+
notAfter: Date | string;
|
|
428
|
+
systemIds: string[];
|
|
429
|
+
}): Promise<Run> {
|
|
430
|
+
const body = {
|
|
431
|
+
run_kind: opts.runKind,
|
|
432
|
+
list_digest: opts.listDigest,
|
|
433
|
+
list_count: opts.listCount,
|
|
434
|
+
not_after: opts.notAfter instanceof Date ? opts.notAfter.toISOString() : opts.notAfter,
|
|
435
|
+
system_ids: opts.systemIds,
|
|
436
|
+
};
|
|
437
|
+
const data = await this.transport.request("POST", "/v1/runs", { json: body });
|
|
438
|
+
return parseRun(data as Raw);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Close a run: the enclave signs the run record over what it issued under
|
|
442
|
+
* it and the record is appended to the transparency log. 409 (ConflictError)
|
|
443
|
+
* when the run is not OPEN — including a run an enclave restart forgot,
|
|
444
|
+
* which is marked LOST and can never be closed. */
|
|
445
|
+
async closeRun(runId: string): Promise<Run> {
|
|
446
|
+
const data = await this.transport.request("POST", `/v1/runs/${runId}/close`);
|
|
447
|
+
return parseRun(data as Raw);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async getRun(runId: string): Promise<Run> {
|
|
451
|
+
const data = await this.transport.request("GET", `/v1/runs/${runId}`);
|
|
452
|
+
return parseRun(data as Raw);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
listRuns(opts?: { limit?: number }): Paginator<Run> {
|
|
456
|
+
return new Paginator(this.transport, "/v1/runs", parseRun, {
|
|
457
|
+
params: { limit: opts?.limit ?? 25 },
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
376
461
|
// --- Webhooks ---
|
|
377
462
|
|
|
378
463
|
async registerWebhook(opts: { url: string }): Promise<Webhook> {
|
|
@@ -389,13 +474,13 @@ export class BurnLedger {
|
|
|
389
474
|
}
|
|
390
475
|
|
|
391
476
|
async deleteWebhook(webhookId: string): Promise<void> {
|
|
392
|
-
await this.transport.request("DELETE", `/v1/webhooks/${webhookId}`);
|
|
477
|
+
await this.transport.request("DELETE", `/v1/webhooks/${encodeId(webhookId)}`);
|
|
393
478
|
}
|
|
394
479
|
|
|
395
480
|
async rotateWebhookSecret(webhookId: string): Promise<WebhookRotateResponse> {
|
|
396
481
|
const data = await this.transport.request(
|
|
397
482
|
"POST",
|
|
398
|
-
`/v1/webhooks/${webhookId}/rotate-secret`,
|
|
483
|
+
`/v1/webhooks/${encodeId(webhookId)}/rotate-secret`,
|
|
399
484
|
);
|
|
400
485
|
return parseWebhookRotateResponse(data as Raw);
|
|
401
486
|
}
|
|
@@ -403,7 +488,7 @@ export class BurnLedger {
|
|
|
403
488
|
async commitWebhookRotation(webhookId: string): Promise<void> {
|
|
404
489
|
await this.transport.request(
|
|
405
490
|
"POST",
|
|
406
|
-
`/v1/webhooks/${webhookId}/commit-rotation`,
|
|
491
|
+
`/v1/webhooks/${encodeId(webhookId)}/commit-rotation`,
|
|
407
492
|
);
|
|
408
493
|
}
|
|
409
494
|
|
|
@@ -419,14 +504,14 @@ export class BurnLedger {
|
|
|
419
504
|
async retryDelivery(deliveryId: string): Promise<void> {
|
|
420
505
|
await this.transport.request(
|
|
421
506
|
"POST",
|
|
422
|
-
`/v1/webhooks/deliveries/${deliveryId}/retry`,
|
|
507
|
+
`/v1/webhooks/deliveries/${encodeId(deliveryId)}/retry`,
|
|
423
508
|
);
|
|
424
509
|
}
|
|
425
510
|
|
|
426
511
|
async resolveDelivery(deliveryId: string): Promise<void> {
|
|
427
512
|
await this.transport.request(
|
|
428
513
|
"DELETE",
|
|
429
|
-
`/v1/webhooks/deliveries/${deliveryId}`,
|
|
514
|
+
`/v1/webhooks/deliveries/${encodeId(deliveryId)}`,
|
|
430
515
|
);
|
|
431
516
|
}
|
|
432
517
|
|
|
@@ -453,7 +538,7 @@ export class BurnLedger {
|
|
|
453
538
|
}
|
|
454
539
|
|
|
455
540
|
async revokeApiKey(keyId: string): Promise<void> {
|
|
456
|
-
await this.transport.request("DELETE", `/v1/api-keys/${keyId}`);
|
|
541
|
+
await this.transport.request("DELETE", `/v1/api-keys/${encodeId(keyId)}`);
|
|
457
542
|
}
|
|
458
543
|
|
|
459
544
|
// --- Profile ---
|
|
@@ -494,7 +579,7 @@ export class BurnLedger {
|
|
|
494
579
|
* and every document they return must verify under its signing key.
|
|
495
580
|
*/
|
|
496
581
|
async attestEnclaveIdentity(pin: EnclavePinOptions): Promise<EnclaveIdentity> {
|
|
497
|
-
const flow = await
|
|
582
|
+
const { registration: flow } = await loadNodeRuntime();
|
|
498
583
|
const { nonce, params } = flow.attestationParams(pin.nonce);
|
|
499
584
|
const data = await this.transport.request("GET", "/v1/enclave/attestation", {
|
|
500
585
|
params,
|
|
@@ -521,7 +606,7 @@ export class BurnLedger {
|
|
|
521
606
|
/** The time to judge the answer's not_before against. Defaults to now. */
|
|
522
607
|
now?: Date;
|
|
523
608
|
}): Promise<KeyEnrollmentResult> {
|
|
524
|
-
const flow = await
|
|
609
|
+
const { registration: flow } = await loadNodeRuntime();
|
|
525
610
|
const { body, keyId, notAfter } = await flow.enrollBody(opts);
|
|
526
611
|
const data = await this.transport.request("POST", "/v1/enclave/keys", { json: body });
|
|
527
612
|
return flow.checkEnrollment(data, {
|
|
@@ -552,7 +637,7 @@ export class BurnLedger {
|
|
|
552
637
|
/** The time to judge the answer's not_before against. Defaults to now. */
|
|
553
638
|
now?: Date;
|
|
554
639
|
}): Promise<KeyEnrollmentResult> {
|
|
555
|
-
const flow = await
|
|
640
|
+
const { registration: flow } = await loadNodeRuntime();
|
|
556
641
|
const { body, prevKeyId, nextKeyId, notAfter } = await flow.rotateBody(opts);
|
|
557
642
|
const data = await this.transport.request("POST", "/v1/enclave/keys/rotate", { json: body });
|
|
558
643
|
return flow.checkEnrollment(data, {
|
|
@@ -581,7 +666,7 @@ export class BurnLedger {
|
|
|
581
666
|
queryTemplate: string;
|
|
582
667
|
connectorType: string;
|
|
583
668
|
}): Promise<SystemRegistrationCertificate> {
|
|
584
|
-
const flow = await
|
|
669
|
+
const { registration: flow } = await loadNodeRuntime();
|
|
585
670
|
const { body, keyId, configDigest } = await flow.registrationBody(opts);
|
|
586
671
|
const data = await this.transport.request("POST", "/v1/enclave/registrations", { json: body });
|
|
587
672
|
return flow.checkRegistration(data, {
|
|
@@ -615,8 +700,7 @@ export class BurnLedger {
|
|
|
615
700
|
maxBytes?: number;
|
|
616
701
|
queryTimeout?: string;
|
|
617
702
|
}): Promise<RegisteredSystem> {
|
|
618
|
-
const flow = await
|
|
619
|
-
const { sealToKey } = await import("./enclave-seal.js");
|
|
703
|
+
const { registration: flow, seal } = await loadNodeRuntime();
|
|
620
704
|
const config = flow.configBytes(resolveConnectionConfig(opts.connectionConfig, opts.dsn, opts.uri));
|
|
621
705
|
const system = await this.registerSystem({
|
|
622
706
|
name: opts.name,
|
|
@@ -627,7 +711,7 @@ export class BurnLedger {
|
|
|
627
711
|
maxRecords: opts.maxRecords,
|
|
628
712
|
maxBytes: opts.maxBytes,
|
|
629
713
|
queryTimeout: opts.queryTimeout,
|
|
630
|
-
sealedConnectionConfig: await sealToKey(opts.enclave.configSealKey, config),
|
|
714
|
+
sealedConnectionConfig: await seal.sealToKey(opts.enclave.configSealKey, config),
|
|
631
715
|
});
|
|
632
716
|
const registration = await this.registerSystemWithKey({
|
|
633
717
|
teamId: opts.teamId,
|
|
@@ -647,7 +731,7 @@ export class BurnLedger {
|
|
|
647
731
|
async getSystemHealth(systemId: string): Promise<SystemHealth> {
|
|
648
732
|
const data = await this.transport.request(
|
|
649
733
|
"GET",
|
|
650
|
-
`/v1/systems/${systemId}/health`,
|
|
734
|
+
`/v1/systems/${encodeId(systemId)}/health`,
|
|
651
735
|
);
|
|
652
736
|
return parseSystemHealth(data as Raw);
|
|
653
737
|
}
|
|
@@ -716,6 +800,20 @@ export class BurnLedger {
|
|
|
716
800
|
// Connection config resolution
|
|
717
801
|
// ---------------------------------------------------------------------------
|
|
718
802
|
|
|
803
|
+
/** Percent-encode an id before it goes into a URL path, and refuse `.`/`..`.
|
|
804
|
+
*
|
|
805
|
+
* A raw id lets untrusted input escape its segment: `deregisterSystem("x/../../
|
|
806
|
+
* api-keys/k1")` would otherwise resolve to `DELETE /v1/api-keys/k1`.
|
|
807
|
+
* encodeURIComponent escapes the `/`, but it leaves `.` and `..` untouched, and
|
|
808
|
+
* a segment that IS `.` or `..` is still traversal — so those are rejected
|
|
809
|
+
* outright. An empty id is rejected too: it collapses two path segments into one. */
|
|
810
|
+
function encodeId(id: string): string {
|
|
811
|
+
if (id === "" || id === "." || id === "..") {
|
|
812
|
+
throw new Error(`invalid id path segment: ${JSON.stringify(id)}`);
|
|
813
|
+
}
|
|
814
|
+
return encodeURIComponent(id);
|
|
815
|
+
}
|
|
816
|
+
|
|
719
817
|
/** Standard base64, as Go decodes a []byte field. No Buffer: this module is
|
|
720
818
|
* also the browser entry's. */
|
|
721
819
|
function bytesToBase64(bytes: Uint8Array): string {
|
|
@@ -769,7 +867,14 @@ async function poll<T>(opts: {
|
|
|
769
867
|
result = await opts.fetch();
|
|
770
868
|
} catch (err) {
|
|
771
869
|
if (err instanceof RateLimitError && err.retryAfter !== undefined) {
|
|
772
|
-
|
|
870
|
+
const remaining = deadline - Date.now();
|
|
871
|
+
if (remaining <= 0) {
|
|
872
|
+
throw new TimeoutError(opts.operation, elapsed);
|
|
873
|
+
}
|
|
874
|
+
// Bound the wait to the caller's remaining budget: a server asking for a
|
|
875
|
+
// day, or an Infinity Retry-After, must not block past the timeout the
|
|
876
|
+
// caller set (Math.min(Infinity, remaining) === remaining).
|
|
877
|
+
await sleep(Math.min(err.retryAfter * 1000, remaining));
|
|
773
878
|
continue;
|
|
774
879
|
}
|
|
775
880
|
throw err;
|
|
@@ -353,8 +353,20 @@ export async function registrationBody(opts: {
|
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
/** Verify the registration under the attested enclave's signing key, then
|
|
357
|
-
*
|
|
356
|
+
/** Verify the registration under the attested enclave's signing key, then refuse
|
|
357
|
+
* it unless it binds the key id, system id, config digest and connector this
|
|
358
|
+
* caller signed.
|
|
359
|
+
*
|
|
360
|
+
* Two signed fields are deliberately NOT compared here, so the binding is
|
|
361
|
+
* narrower than "everything you signed". `query_template_hash` in the answer is
|
|
362
|
+
* HashQuery over the NORMALIZED subject query, while the request signs sha256 of
|
|
363
|
+
* the RAW template; the two are not byte-equal even for an honest registration,
|
|
364
|
+
* so comparing them would reject legitimate answers (see the README's
|
|
365
|
+
* "query-template normalization" note). `registered_at` is not checked for
|
|
366
|
+
* freshness either: there is no signed bound to hold it to. A relay could
|
|
367
|
+
* therefore substitute an older genuine registration for the same system, config,
|
|
368
|
+
* connector and key but a different query template — the customer path calls this
|
|
369
|
+
* immediately after its own POST, which narrows that window but does not close it. */
|
|
358
370
|
export async function checkRegistration(
|
|
359
371
|
data: unknown,
|
|
360
372
|
opts: {
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
/** BurnLedger TypeScript SDK — public API (Node.js entry point). */
|
|
2
2
|
|
|
3
|
+
// Wire the Node-only client capabilities (enrolment, registration, savePdf). The
|
|
4
|
+
// browser entry never does this, so those modules — and node:crypto/node:fs with
|
|
5
|
+
// them — stay out of any browser bundle. See node-runtime.ts.
|
|
6
|
+
import { setNodeRuntimeLoader } from "./node-runtime.js";
|
|
7
|
+
setNodeRuntimeLoader(async () => {
|
|
8
|
+
const [registration, seal, fs] = await Promise.all([
|
|
9
|
+
import("./enclave-registration.js"),
|
|
10
|
+
import("./enclave-seal.js"),
|
|
11
|
+
import("node:fs/promises"),
|
|
12
|
+
]);
|
|
13
|
+
return { registration, seal, writeFile: (path, data) => fs.writeFile(path, data) };
|
|
14
|
+
});
|
|
15
|
+
|
|
3
16
|
export { BurnLedger, MAX_BATCH_REVOKE } from "./client.js";
|
|
4
17
|
export type { BurnLedgerOptions } from "./client.js";
|
|
5
18
|
|
|
@@ -35,6 +48,9 @@ export type {
|
|
|
35
48
|
ConnectorType,
|
|
36
49
|
HashScope,
|
|
37
50
|
LogEntryType,
|
|
51
|
+
Run,
|
|
52
|
+
RunKind,
|
|
53
|
+
RunStatus,
|
|
38
54
|
VerificationResult,
|
|
39
55
|
TransparencyResult,
|
|
40
56
|
ApiKeyRole,
|
|
@@ -71,7 +87,7 @@ export type {
|
|
|
71
87
|
|
|
72
88
|
// Raw-JSON → model parsers, for consumers that fetch API responses outside
|
|
73
89
|
// the client (the dashboard's paginated list hooks) but render SDK types.
|
|
74
|
-
export { parseSystem, parseCertificateResponse, parseWebhook } from "./models.js";
|
|
90
|
+
export { parseSystem, parseCertificateResponse, parseRun, parseWebhook } from "./models.js";
|
|
75
91
|
|
|
76
92
|
export { Paginator } from "./pagination.js";
|
|
77
93
|
|
|
@@ -157,6 +173,119 @@ export function verifyConsistency(
|
|
|
157
173
|
|
|
158
174
|
export { verifyWebhookSignature, isTimestampFresh } from "./webhooks.js";
|
|
159
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Audit pack verification (docs/audit-pack.md). Node only: a pack is a file an
|
|
178
|
+
* auditor holds, and the browser verifier reads one record at a time.
|
|
179
|
+
*/
|
|
180
|
+
export {
|
|
181
|
+
AUDIT_PACK_FORMAT,
|
|
182
|
+
PAYLOAD_TYPE_AUDIT_PACK_FOOTER,
|
|
183
|
+
auditPackFooterStatement,
|
|
184
|
+
buildAuditPackFooterPayload,
|
|
185
|
+
chainAuditPackPages,
|
|
186
|
+
} from "./audit-pack.js";
|
|
187
|
+
export type {
|
|
188
|
+
AuditPackFilters,
|
|
189
|
+
AuditPackFooter,
|
|
190
|
+
AuditPackFooterResult,
|
|
191
|
+
AuditPackFooterSigned,
|
|
192
|
+
AuditPackFooterStatement,
|
|
193
|
+
AuditPackHeader,
|
|
194
|
+
AuditPackLogHead,
|
|
195
|
+
AuditPackOptions,
|
|
196
|
+
AuditPackPage,
|
|
197
|
+
AuditPackRecordFailure,
|
|
198
|
+
AuditPackResult,
|
|
199
|
+
AuditPackVerdict,
|
|
200
|
+
SignedTreeHeadDocument,
|
|
201
|
+
} from "./audit-pack.js";
|
|
202
|
+
import {
|
|
203
|
+
verifyAuditPack as _verifyAuditPack,
|
|
204
|
+
verifyAuditPackFooter as _verifyAuditPackFooter,
|
|
205
|
+
} from "./audit-pack.js";
|
|
206
|
+
import type {
|
|
207
|
+
AuditPackFooterResult,
|
|
208
|
+
AuditPackFooterStatement,
|
|
209
|
+
AuditPackOptions,
|
|
210
|
+
AuditPackResult,
|
|
211
|
+
} from "./audit-pack.js";
|
|
212
|
+
|
|
213
|
+
/** Verify one audit pack page offline, as `burnledger check --pack` does.
|
|
214
|
+
* `keys` null uses the pack's own issuer_keys (trust on first use, and the
|
|
215
|
+
* result's keysFromPack says so); a supplied key set outranks them. */
|
|
216
|
+
export function verifyAuditPack(
|
|
217
|
+
source: string | Uint8Array,
|
|
218
|
+
keys: Map<string, PublicKeyInfo> | null,
|
|
219
|
+
now?: Date,
|
|
220
|
+
options?: AuditPackOptions,
|
|
221
|
+
): Promise<AuditPackResult> {
|
|
222
|
+
return _verifyAuditPack(nodeCrypto, source, keys, now ?? new Date(), options);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Verify a footer statement — rebuilt from a page's header and footer by
|
|
226
|
+
* auditPackFooterStatement — under a key set. verifyAuditPack already does
|
|
227
|
+
* this for every signed footer it reads; this is for a reader holding the
|
|
228
|
+
* statement on its own. */
|
|
229
|
+
export function verifyAuditPackFooter(
|
|
230
|
+
statement: AuditPackFooterStatement,
|
|
231
|
+
keys: Map<string, PublicKeyInfo>,
|
|
232
|
+
): Promise<AuditPackFooterResult> {
|
|
233
|
+
return _verifyAuditPackFooter(nodeCrypto, statement, keys);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The run record (docs/run-record.md, ADR-031): the enclave's signed word on
|
|
238
|
+
* one bulk run, checked beside the run's records. Node only, as the audit pack
|
|
239
|
+
* verifier that carries it is.
|
|
240
|
+
*/
|
|
241
|
+
export {
|
|
242
|
+
PAYLOAD_TYPE_RUN_RECORD,
|
|
243
|
+
buildRunRecordPayload,
|
|
244
|
+
parseRunRecordDocument,
|
|
245
|
+
} from "./run-record.js";
|
|
246
|
+
export type { RunCheck, RunCheckRecord, RunRecord, RunRecordResult } from "./run-record.js";
|
|
247
|
+
import {
|
|
248
|
+
checkRunAgainstRecords as _checkRunAgainstRecords,
|
|
249
|
+
runRecordHash as _runRecordHash,
|
|
250
|
+
runRoots as _runRoots,
|
|
251
|
+
verifyRunRecord as _verifyRunRecord,
|
|
252
|
+
verifyRunRecordInclusion as _verifyRunRecordInclusion,
|
|
253
|
+
} from "./run-record.js";
|
|
254
|
+
import type { RunCheck, RunCheckRecord, RunRecord, RunRecordResult } from "./run-record.js";
|
|
255
|
+
|
|
256
|
+
/** Verify a run record's signature under a key set: the shape, the key's
|
|
257
|
+
* authority at the record's close instant, then the signature. */
|
|
258
|
+
export function verifyRunRecord(record: RunRecord, keys: Map<string, PublicKeyInfo>): Promise<RunRecordResult> {
|
|
259
|
+
return _verifyRunRecord(nodeCrypto, record, keys);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** What the run record's transparency leaf commits to: SHA-256 of its payload. */
|
|
263
|
+
export function runRecordHash(record: RunRecord): Promise<Uint8Array> {
|
|
264
|
+
return _runRecordHash(nodeCrypto, record);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Verify the run record's own inclusion proof under the key that signed it,
|
|
268
|
+
* as core.TransparencyResult names the outcome; NOT_INCLUDED without one. */
|
|
269
|
+
export function verifyRunRecordInclusion(record: RunRecord, key: PublicKeyInfo): Promise<string> {
|
|
270
|
+
return _verifyRunRecordInclusion(nodeCrypto, record, key);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The roots a run record commits to, reproduced from subject hashes and
|
|
274
|
+
* certificate ids in any order: subjects de-duplicated, both leaf sets sorted. */
|
|
275
|
+
export function runRoots(
|
|
276
|
+
subjects: Uint8Array[],
|
|
277
|
+
certificateIds: string[],
|
|
278
|
+
): Promise<{ subjectsRoot: Uint8Array; subjectsCount: number; recordsRoot: Uint8Array; recordsCount: number }> {
|
|
279
|
+
return _runRoots(nodeCrypto, subjects, certificateIds);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Hold a run record beside every record of the run: whether the roots
|
|
283
|
+
* reproduce, the shortfall against the declared list, whether it closed in
|
|
284
|
+
* time, and how many records are labelled with another run. */
|
|
285
|
+
export function checkRunAgainstRecords(record: RunRecord, records: RunCheckRecord[]): Promise<RunCheck> {
|
|
286
|
+
return _checkRunAgainstRecords(nodeCrypto, record, records);
|
|
287
|
+
}
|
|
288
|
+
|
|
160
289
|
/**
|
|
161
290
|
* Client-side connection-config sealing.
|
|
162
291
|
*
|
package/src/models.ts
CHANGED
|
@@ -73,7 +73,9 @@ export type ConnectorType =
|
|
|
73
73
|
| "bigquery"
|
|
74
74
|
| "azure_blob";
|
|
75
75
|
export type HashScope = "full" | "existence";
|
|
76
|
-
export type LogEntryType = "CERTIFICATE" | "REVOCATION";
|
|
76
|
+
export type LogEntryType = "CERTIFICATE" | "REVOCATION" | "RUN_RECORD";
|
|
77
|
+
export type RunKind = "drop_cycle" | "bulk_erasure";
|
|
78
|
+
export type RunStatus = "OPEN" | "CLOSED" | "LOST";
|
|
77
79
|
|
|
78
80
|
/** Result of offline certificate signature verification. */
|
|
79
81
|
export type VerificationResult = "VALID" | "INVALID";
|
|
@@ -190,11 +192,35 @@ export interface SignedTreeHead {
|
|
|
190
192
|
export interface LogEntry {
|
|
191
193
|
readonly index: number;
|
|
192
194
|
readonly entryType: LogEntryType;
|
|
193
|
-
|
|
195
|
+
/** Present on CERTIFICATE and REVOCATION leaves. */
|
|
196
|
+
readonly certificateId: string | undefined;
|
|
197
|
+
/** Present on RUN_RECORD leaves (ADR-031). */
|
|
198
|
+
readonly runId: string | undefined;
|
|
194
199
|
readonly hash: string;
|
|
195
200
|
readonly appendedAt: Date;
|
|
196
201
|
}
|
|
197
202
|
|
|
203
|
+
/** One bulk run (ADR-031, docs/run-record.md): the customer's declaration as
|
|
204
|
+
* opened, and once CLOSED the enclave-signed run record under `record`, with
|
|
205
|
+
* its transparency proof merged in when a signed tree head covers its leaf.
|
|
206
|
+
* The record is kept raw so it can be handed straight to verifyRunRecord. */
|
|
207
|
+
export interface Run {
|
|
208
|
+
readonly id: string;
|
|
209
|
+
readonly teamId: string;
|
|
210
|
+
readonly runKind: RunKind;
|
|
211
|
+
/** SHA-256 of the input list's canonical form, 64 hex characters. */
|
|
212
|
+
readonly listDigest: string;
|
|
213
|
+
readonly listCount: number;
|
|
214
|
+
readonly notAfter: Date;
|
|
215
|
+
readonly systemIds: readonly string[];
|
|
216
|
+
readonly status: RunStatus;
|
|
217
|
+
readonly openedAt: Date;
|
|
218
|
+
readonly closedAt: Date | undefined;
|
|
219
|
+
/** The run record's leaf in the transparency log, once appended. */
|
|
220
|
+
readonly logIndex: number | undefined;
|
|
221
|
+
readonly record: Record<string, unknown> | undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
198
224
|
export interface InclusionProof {
|
|
199
225
|
readonly index: number;
|
|
200
226
|
readonly treeSize: number;
|
|
@@ -486,12 +512,30 @@ export function parseLogEntry(d: Raw): LogEntry {
|
|
|
486
512
|
return {
|
|
487
513
|
index: d.index,
|
|
488
514
|
entryType: d.entry_type as LogEntryType,
|
|
489
|
-
certificateId: d.certificate_id,
|
|
515
|
+
certificateId: d.certificate_id ?? undefined,
|
|
516
|
+
runId: d.run_id ?? undefined,
|
|
490
517
|
hash: d.hash,
|
|
491
518
|
appendedAt: parseDt(d.appended_at),
|
|
492
519
|
};
|
|
493
520
|
}
|
|
494
521
|
|
|
522
|
+
export function parseRun(d: Raw): Run {
|
|
523
|
+
return {
|
|
524
|
+
id: d.id,
|
|
525
|
+
teamId: d.team_id,
|
|
526
|
+
runKind: d.run_kind as RunKind,
|
|
527
|
+
listDigest: d.list_digest,
|
|
528
|
+
listCount: d.list_count,
|
|
529
|
+
notAfter: parseDt(d.not_after),
|
|
530
|
+
systemIds: d.system_ids ?? [],
|
|
531
|
+
status: d.status as RunStatus,
|
|
532
|
+
openedAt: parseDt(d.opened_at),
|
|
533
|
+
closedAt: parseDtOpt(d.closed_at),
|
|
534
|
+
logIndex: d.log_index ?? undefined,
|
|
535
|
+
record: d.record ?? undefined,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
495
539
|
export function parseInclusionProof(d: Raw): InclusionProof {
|
|
496
540
|
return {
|
|
497
541
|
index: d.index,
|