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.
Files changed (88) hide show
  1. package/README.md +9 -4
  2. package/dist/cjs/audit-pack.d.ts +201 -0
  3. package/dist/cjs/audit-pack.d.ts.map +1 -0
  4. package/dist/cjs/audit-pack.js +867 -0
  5. package/dist/cjs/audit-pack.js.map +1 -0
  6. package/dist/cjs/client.d.ts +41 -1
  7. package/dist/cjs/client.d.ts.map +1 -1
  8. package/dist/cjs/client.js +107 -59
  9. package/dist/cjs/client.js.map +1 -1
  10. package/dist/cjs/enclave-registration.d.ts +14 -2
  11. package/dist/cjs/enclave-registration.d.ts.map +1 -1
  12. package/dist/cjs/enclave-registration.js +14 -2
  13. package/dist/cjs/enclave-registration.js.map +1 -1
  14. package/dist/cjs/index.d.ts +46 -2
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js +114 -2
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/models.d.ts +28 -2
  19. package/dist/cjs/models.d.ts.map +1 -1
  20. package/dist/cjs/models.js +19 -1
  21. package/dist/cjs/models.js.map +1 -1
  22. package/dist/cjs/node-runtime.d.ts +40 -0
  23. package/dist/cjs/node-runtime.d.ts.map +1 -0
  24. package/dist/cjs/node-runtime.js +42 -0
  25. package/dist/cjs/node-runtime.js.map +1 -0
  26. package/dist/cjs/run-record.d.ts +130 -0
  27. package/dist/cjs/run-record.d.ts.map +1 -0
  28. package/dist/cjs/run-record.js +272 -0
  29. package/dist/cjs/run-record.js.map +1 -0
  30. package/dist/cjs/verify.d.ts +58 -0
  31. package/dist/cjs/verify.d.ts.map +1 -1
  32. package/dist/cjs/verify.js +277 -38
  33. package/dist/cjs/verify.js.map +1 -1
  34. package/dist/cjs/webhooks.d.ts +9 -2
  35. package/dist/cjs/webhooks.d.ts.map +1 -1
  36. package/dist/cjs/webhooks.js +29 -11
  37. package/dist/cjs/webhooks.js.map +1 -1
  38. package/dist/esm/audit-pack.d.ts +201 -0
  39. package/dist/esm/audit-pack.d.ts.map +1 -0
  40. package/dist/esm/audit-pack.js +858 -0
  41. package/dist/esm/audit-pack.js.map +1 -0
  42. package/dist/esm/cli.d.ts +52 -0
  43. package/dist/esm/cli.d.ts.map +1 -1
  44. package/dist/esm/cli.js +283 -11
  45. package/dist/esm/cli.js.map +1 -1
  46. package/dist/esm/client.d.ts +41 -1
  47. package/dist/esm/client.d.ts.map +1 -1
  48. package/dist/esm/client.js +108 -27
  49. package/dist/esm/client.js.map +1 -1
  50. package/dist/esm/enclave-registration.d.ts +14 -2
  51. package/dist/esm/enclave-registration.d.ts.map +1 -1
  52. package/dist/esm/enclave-registration.js +14 -2
  53. package/dist/esm/enclave-registration.js.map +1 -1
  54. package/dist/esm/index.d.ts +46 -2
  55. package/dist/esm/index.d.ts.map +1 -1
  56. package/dist/esm/index.js +64 -1
  57. package/dist/esm/index.js.map +1 -1
  58. package/dist/esm/models.d.ts +28 -2
  59. package/dist/esm/models.d.ts.map +1 -1
  60. package/dist/esm/models.js +18 -1
  61. package/dist/esm/models.js.map +1 -1
  62. package/dist/esm/node-runtime.d.ts +40 -0
  63. package/dist/esm/node-runtime.d.ts.map +1 -0
  64. package/dist/esm/node-runtime.js +38 -0
  65. package/dist/esm/node-runtime.js.map +1 -0
  66. package/dist/esm/run-record.d.ts +130 -0
  67. package/dist/esm/run-record.d.ts.map +1 -0
  68. package/dist/esm/run-record.js +262 -0
  69. package/dist/esm/run-record.js.map +1 -0
  70. package/dist/esm/verify.d.ts +58 -0
  71. package/dist/esm/verify.d.ts.map +1 -1
  72. package/dist/esm/verify.js +270 -41
  73. package/dist/esm/verify.js.map +1 -1
  74. package/dist/esm/webhooks.d.ts +9 -2
  75. package/dist/esm/webhooks.d.ts.map +1 -1
  76. package/dist/esm/webhooks.js +29 -11
  77. package/dist/esm/webhooks.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/audit-pack.ts +1067 -0
  80. package/src/cli.ts +289 -10
  81. package/src/client.ts +132 -27
  82. package/src/enclave-registration.ts +14 -2
  83. package/src/index.ts +130 -1
  84. package/src/models.ts +47 -3
  85. package/src/node-runtime.ts +57 -0
  86. package/src/run-record.ts +371 -0
  87. package/src/verify.ts +301 -41
  88. 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 import("node:fs/promises");
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 import("./enclave-registration.js");
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 import("./enclave-registration.js");
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 import("./enclave-registration.js");
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 import("./enclave-registration.js");
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 import("./enclave-registration.js");
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
- await sleep(err.retryAfter * 1000);
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
- * refuse it unless it binds what this caller signed. */
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
- readonly certificateId: string;
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,