burnledger 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +15 -3
  2. package/dist/cjs/client.d.ts +14 -0
  3. package/dist/cjs/client.d.ts.map +1 -1
  4. package/dist/cjs/client.js +4 -0
  5. package/dist/cjs/client.js.map +1 -1
  6. package/dist/cjs/errors.d.ts +28 -1
  7. package/dist/cjs/errors.d.ts.map +1 -1
  8. package/dist/cjs/errors.js +30 -2
  9. package/dist/cjs/errors.js.map +1 -1
  10. package/dist/cjs/http.d.ts +21 -1
  11. package/dist/cjs/http.d.ts.map +1 -1
  12. package/dist/cjs/http.js +76 -12
  13. package/dist/cjs/http.js.map +1 -1
  14. package/dist/cjs/index.browser.d.ts +2 -4
  15. package/dist/cjs/index.browser.d.ts.map +1 -1
  16. package/dist/cjs/index.browser.js.map +1 -1
  17. package/dist/cjs/index.d.ts +14 -7
  18. package/dist/cjs/index.d.ts.map +1 -1
  19. package/dist/cjs/index.js +13 -2
  20. package/dist/cjs/index.js.map +1 -1
  21. package/dist/cjs/keys.d.ts +6 -0
  22. package/dist/cjs/keys.d.ts.map +1 -1
  23. package/dist/cjs/keys.js.map +1 -1
  24. package/dist/cjs/models.d.ts +24 -4
  25. package/dist/cjs/models.d.ts.map +1 -1
  26. package/dist/cjs/models.js +10 -4
  27. package/dist/cjs/models.js.map +1 -1
  28. package/dist/cjs/pagination.d.ts.map +1 -1
  29. package/dist/cjs/pagination.js.map +1 -1
  30. package/dist/cjs/verify.d.ts +83 -5
  31. package/dist/cjs/verify.d.ts.map +1 -1
  32. package/dist/cjs/verify.js +359 -28
  33. package/dist/cjs/verify.js.map +1 -1
  34. package/dist/cjs/web-verifier.d.ts +10 -5
  35. package/dist/cjs/web-verifier.d.ts.map +1 -1
  36. package/dist/cjs/web-verifier.js +7 -3
  37. package/dist/cjs/web-verifier.js.map +1 -1
  38. package/dist/cjs/webhooks.js.map +1 -1
  39. package/dist/esm/cli.d.ts +6 -0
  40. package/dist/esm/cli.d.ts.map +1 -1
  41. package/dist/esm/cli.js +116 -15
  42. package/dist/esm/cli.js.map +1 -1
  43. package/dist/esm/client.d.ts +14 -0
  44. package/dist/esm/client.d.ts.map +1 -1
  45. package/dist/esm/client.js +4 -0
  46. package/dist/esm/client.js.map +1 -1
  47. package/dist/esm/errors.d.ts +28 -1
  48. package/dist/esm/errors.d.ts.map +1 -1
  49. package/dist/esm/errors.js +29 -1
  50. package/dist/esm/errors.js.map +1 -1
  51. package/dist/esm/http.d.ts +21 -1
  52. package/dist/esm/http.d.ts.map +1 -1
  53. package/dist/esm/http.js +75 -12
  54. package/dist/esm/http.js.map +1 -1
  55. package/dist/esm/index.browser.d.ts +2 -4
  56. package/dist/esm/index.browser.d.ts.map +1 -1
  57. package/dist/esm/index.browser.js.map +1 -1
  58. package/dist/esm/index.d.ts +14 -7
  59. package/dist/esm/index.d.ts.map +1 -1
  60. package/dist/esm/index.js +11 -3
  61. package/dist/esm/index.js.map +1 -1
  62. package/dist/esm/keys.d.ts +6 -0
  63. package/dist/esm/keys.d.ts.map +1 -1
  64. package/dist/esm/keys.js.map +1 -1
  65. package/dist/esm/models.d.ts +24 -4
  66. package/dist/esm/models.d.ts.map +1 -1
  67. package/dist/esm/models.js +10 -4
  68. package/dist/esm/models.js.map +1 -1
  69. package/dist/esm/pagination.d.ts.map +1 -1
  70. package/dist/esm/verify.d.ts +83 -5
  71. package/dist/esm/verify.d.ts.map +1 -1
  72. package/dist/esm/verify.js +357 -28
  73. package/dist/esm/verify.js.map +1 -1
  74. package/dist/esm/web-verifier.d.ts +10 -5
  75. package/dist/esm/web-verifier.d.ts.map +1 -1
  76. package/dist/esm/web-verifier.js +11 -3
  77. package/dist/esm/web-verifier.js.map +1 -1
  78. package/dist/esm/webhooks.js.map +1 -1
  79. package/package.json +5 -3
  80. package/src/cli.ts +123 -13
  81. package/src/client.ts +16 -0
  82. package/src/errors.ts +32 -2
  83. package/src/http.ts +79 -12
  84. package/src/index.browser.ts +2 -2
  85. package/src/index.ts +16 -4
  86. package/src/keys.ts +11 -1
  87. package/src/models.ts +34 -8
  88. package/src/verify.ts +428 -28
  89. package/src/web-verifier.ts +14 -5
package/src/http.ts CHANGED
@@ -1,16 +1,29 @@
1
1
  /** HTTP transport layer wrapping native fetch.
2
2
  *
3
3
  * Handles: Bearer auth, response envelope unwrapping, retry on 5xx/transport
4
- * errors, Retry-After respect on 429, and raw byte streaming for PDFs.
4
+ * errors and on 429, and raw byte streaming for PDFs.
5
+ *
6
+ * Retry delay: a `Retry-After` header (delta-seconds or HTTP-date) is honoured
7
+ * when the server sends one and it is at most `MAX_RETRY_AFTER`; otherwise
8
+ * exponential backoff with jitter. A 429 with no usable `Retry-After`, one
9
+ * beyond the cap, or one on the last attempt throws `RateLimitError` with
10
+ * `retryAfter` populated so the caller can decide.
5
11
  */
6
12
 
7
13
  import { raiseForError } from "./errors.js";
8
14
 
15
+ /** Longest Retry-After, in seconds, the transport will wait on before
16
+ * throwing instead: a server asking for minutes gets the error and the
17
+ * number rather than a promise nobody can cancel. */
18
+ export const MAX_RETRY_AFTER = 30;
19
+
9
20
  export interface TransportOptions {
10
21
  baseUrl: string;
11
22
  apiKey: string;
12
23
  timeout: number;
13
24
  maxRetries: number;
25
+ /** Team to act as. Omit to let the API use the caller's default team. */
26
+ teamId?: string;
14
27
  }
15
28
 
16
29
  export class Transport {
@@ -18,12 +31,18 @@ export class Transport {
18
31
  private readonly apiKey: string;
19
32
  private readonly timeout: number;
20
33
  private readonly maxRetries: number;
34
+ // Which team the caller is acting as. The API resolves scope from this
35
+ // header and otherwise falls back to the caller's default team, so a member
36
+ // of more than one team who does not send it silently acts as whichever they
37
+ // joined first.
38
+ private readonly teamId?: string;
21
39
 
22
40
  constructor(opts: TransportOptions) {
23
41
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
24
42
  this.apiKey = opts.apiKey;
25
43
  this.timeout = opts.timeout;
26
44
  this.maxRetries = opts.maxRetries;
45
+ this.teamId = opts.teamId;
27
46
  }
28
47
 
29
48
  /** Make a request and return the unwrapped `data` field. */
@@ -56,6 +75,12 @@ export class Transport {
56
75
  };
57
76
  if (authenticated) {
58
77
  headers["Authorization"] = `Bearer ${this.apiKey}`;
78
+ // Alongside the key, never without it: the header names the scope the
79
+ // caller is acting in, which is meaningless for an unauthenticated
80
+ // request and is not a second credential.
81
+ if (this.teamId) {
82
+ headers["X-Team-ID"] = this.teamId;
83
+ }
59
84
  }
60
85
 
61
86
  let url = `${this.baseUrl}${path}`;
@@ -80,14 +105,14 @@ export class Transport {
80
105
  }
81
106
 
82
107
  let lastError: Error | undefined;
108
+ // Delay before the next attempt when the server named one; undefined
109
+ // means fall back to backoff.
110
+ let retryAfter: number | undefined;
83
111
 
84
112
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
85
113
  if (attempt > 0) {
86
- // Backoff with ±25% jitter so coordinated 5xx events don't produce
87
- // a synchronized retry storm from every client at the same instant.
88
- const base = 500 * 2 ** (attempt - 1);
89
- const jitter = base * 0.25 * (Math.random() * 2 - 1);
90
- await sleep(base + jitter);
114
+ await sleep(retryDelayMs(attempt, retryAfter));
115
+ retryAfter = undefined;
91
116
  }
92
117
 
93
118
  const controller = new AbortController();
@@ -116,8 +141,19 @@ export class Transport {
116
141
  return {};
117
142
  }
118
143
 
144
+ retryAfter = parseRetryAfter(resp);
119
145
  if (resp.status === 429) {
120
- const retryAfter = parseRetryAfter(resp);
146
+ // Only on the server's say-so: without Retry-After there is no delay
147
+ // known to be enough, and beyond the cap the caller gets the number
148
+ // (RateLimitError.retryAfter) rather than a hung promise.
149
+ if (
150
+ retryAfter !== undefined &&
151
+ retryAfter <= MAX_RETRY_AFTER &&
152
+ attempt < this.maxRetries
153
+ ) {
154
+ lastError = new Error(`Server error ${resp.status}`);
155
+ continue;
156
+ }
121
157
  const errorBody = await safeJson(resp);
122
158
  raiseForError(resp.status, errorBody, retryAfter);
123
159
  }
@@ -150,6 +186,11 @@ export class Transport {
150
186
  const headers: Record<string, string> = {};
151
187
  if (authenticated) {
152
188
  headers["Authorization"] = `Bearer ${this.apiKey}`;
189
+ // A downloaded certificate is as team-scoped as the JSON one; omitting
190
+ // the header here would hand a member their default team's PDF.
191
+ if (this.teamId) {
192
+ headers["X-Team-ID"] = this.teamId;
193
+ }
153
194
  }
154
195
 
155
196
  const url = `${this.baseUrl}${path}`;
@@ -172,11 +213,37 @@ export class Transport {
172
213
  }
173
214
  }
174
215
 
175
- function parseRetryAfter(resp: Response): number | undefined {
176
- const raw = resp.headers.get("Retry-After");
177
- if (raw == null) return undefined;
178
- const n = Number(raw);
179
- return Number.isFinite(n) ? n : undefined;
216
+ /** Seconds to wait per the Retry-After header (RFC 9110 §10.2.3).
217
+ *
218
+ * Accepts both forms the header allows: non-negative delta-seconds and an
219
+ * HTTP-date, converted to seconds from now (a date already past reads as 0).
220
+ * Anything else is treated as absent.
221
+ */
222
+ export function parseRetryAfter(resp: Response): number | undefined {
223
+ const raw = resp.headers.get("Retry-After")?.trim();
224
+ if (!raw) return undefined;
225
+ if (/^\d+(\.\d+)?$/.test(raw)) {
226
+ return Number(raw);
227
+ }
228
+ // Every HTTP-date form (IMF-fixdate, RFC 850, asctime) opens with a weekday
229
+ // name; Date.parse alone would also read "-3" or "2099" as a year.
230
+ if (!/^[A-Za-z]{3}/.test(raw)) return undefined;
231
+ const when = Date.parse(raw);
232
+ if (Number.isNaN(when)) return undefined;
233
+ return Math.max(0, (when - Date.now()) / 1000);
234
+ }
235
+
236
+ /** Milliseconds to sleep before retry `attempt` (1-based). A server-supplied
237
+ * delay within the cap wins; otherwise exponential backoff with ±25% jitter so
238
+ * coordinated 5xx events don't produce a synchronized retry storm from every
239
+ * client at the same instant. */
240
+ function retryDelayMs(attempt: number, retryAfter: number | undefined): number {
241
+ if (retryAfter !== undefined && retryAfter <= MAX_RETRY_AFTER) {
242
+ return retryAfter * 1000;
243
+ }
244
+ const base = Math.min(500 * 2 ** (attempt - 1), MAX_RETRY_AFTER * 1000);
245
+ const jitter = base * 0.25 * (Math.random() * 2 - 1);
246
+ return base + jitter;
180
247
  }
181
248
 
182
249
  async function safeJson(resp: Response): Promise<Record<string, unknown>> {
@@ -69,7 +69,7 @@ import {
69
69
  verifyConsistency as _verifyConsistency,
70
70
  publicKeyFromHex as _publicKeyFromHex,
71
71
  } from "./verify.js";
72
- import type { PublicKeyInfo } from "./verify.js";
72
+ import type { PublicKeyInfo, PublicKeyOptions } from "./verify.js";
73
73
  import type { VerificationResult, TransparencyResult } from "./models.js";
74
74
 
75
75
  type Cert = Record<string, unknown>;
@@ -93,7 +93,7 @@ export function verifyTransparency(
93
93
  /** Construct a PublicKeyInfo from a hex-encoded Ed25519 public key. */
94
94
  export function publicKeyFromHex(
95
95
  hexKey: string,
96
- opts?: { revoked?: boolean },
96
+ opts?: PublicKeyOptions,
97
97
  ): Promise<PublicKeyInfo> {
98
98
  return _publicKeyFromHex(browserCrypto, hexKey, opts);
99
99
  }
package/src/index.ts CHANGED
@@ -14,11 +14,15 @@ export {
14
14
  ServerError,
15
15
  TimeoutError,
16
16
  VerificationError,
17
+ CERTIFICATE_REVOKED,
18
+ UNSUPPORTED_FORMAT_VERSION,
17
19
  } from "./errors.js";
18
20
 
19
21
  export { Transport } from "./http.js";
20
22
  export type { TransportOptions } from "./http.js";
21
23
 
24
+ export type { PublicKeyOptions } from "./verify.js";
25
+
22
26
  export type {
23
27
  ProofMode,
24
28
  AttestationStatus,
@@ -76,7 +80,7 @@ import {
76
80
  verifyConsistency as _verifyConsistency,
77
81
  publicKeyFromHex as _publicKeyFromHex,
78
82
  } from "./verify.js";
79
- import type { PublicKeyInfo } from "./verify.js";
83
+ import type { PublicKeyInfo, PublicKeyOptions } from "./verify.js";
80
84
  import type { VerificationResult, TransparencyResult } from "./models.js";
81
85
 
82
86
  type Cert = Record<string, unknown>;
@@ -112,10 +116,18 @@ export function verifyTransparency(
112
116
  return _verifyTransparency(nodeCrypto, certificate, publicKeys);
113
117
  }
114
118
 
115
- /** Construct a PublicKeyInfo from a hex-encoded Ed25519 public key. */
119
+ /** Construct a PublicKeyInfo from a hex-encoded Ed25519 public key.
120
+ *
121
+ * The options mirror a published key entry from `/.well-known/burnledger-keys`.
122
+ * They were previously narrowed here to `{ revoked?: boolean }` while the
123
+ * implementation already accepted the ADR-017 fields — so a consumer could not
124
+ * express a compromised key or a validity interval through the public API at
125
+ * all, and every key they built looked unconditionally active. Widening only;
126
+ * `revoked` still works and still means "unusable".
127
+ */
116
128
  export function publicKeyFromHex(
117
129
  hexKey: string,
118
- opts?: { revoked?: boolean },
130
+ opts?: PublicKeyOptions,
119
131
  ): Promise<PublicKeyInfo> {
120
132
  return _publicKeyFromHex(nodeCrypto, hexKey, opts);
121
133
  }
@@ -131,4 +143,4 @@ export function verifyConsistency(
131
143
  return _verifyConsistency(nodeCrypto, oldSize, newSize, oldRoot, newRoot, proof);
132
144
  }
133
145
 
134
- export { verifyWebhookSignature } from "./webhooks.js";
146
+ export { verifyWebhookSignature, isTimestampFresh } from "./webhooks.js";
package/src/keys.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  // Public-key file parsing shared by the CLI (and unit-testable in isolation).
2
2
 
3
- export type KeyEntry = { key_id: string; public_key: string; key_status: string };
3
+ export type KeyEntry = {
4
+ key_id: string;
5
+ public_key: string;
6
+ key_status: string;
7
+ /** ADR-017 §1. Optional because key sets published before it carry none, and
8
+ * "no interval stated" is nothing to check rather than something the verifier
9
+ * failed to check. */
10
+ not_before?: string;
11
+ not_after?: string | null;
12
+ compromised_from?: string | null;
13
+ };
4
14
 
5
15
  /**
6
16
  * Normalize a parsed keys file to an array of key entries. Accepts a bare
package/src/models.ts CHANGED
@@ -91,6 +91,8 @@ export interface System {
91
91
  readonly connectorType: ConnectorType;
92
92
  readonly subjectQuery: string;
93
93
  readonly hashScope: HashScope;
94
+ /** Marked as holding PHI; changes the default proof mode to merkle. */
95
+ readonly phiInScope: boolean;
94
96
  readonly maxRecords: number;
95
97
  readonly maxBytes: number;
96
98
  readonly queryTimeout: string;
@@ -144,6 +146,13 @@ export interface CertificateResponse {
144
146
  readonly revokedAt: Date | undefined;
145
147
  readonly revocationReason: string | undefined;
146
148
  readonly certificate: Record<string, unknown>;
149
+ /**
150
+ * Raw enclave-signed status statement stapled by the server. Best-effort on
151
+ * the server side, so undefined when it could not be produced; GET
152
+ * /v1/certificates/{id}/status is the call that errors instead. Kept raw so
153
+ * it can be handed straight to verifyCertificateWithStatus().
154
+ */
155
+ readonly statusStatement: Record<string, unknown> | undefined;
147
156
  }
148
157
 
149
158
  export interface RevocationStatus {
@@ -204,6 +213,9 @@ export interface Profile {
204
213
  readonly id: string;
205
214
  readonly name: string;
206
215
  readonly email: string;
216
+ /** A new address waiting for the code mailed to it; `email` stays the
217
+ * current one until the change is confirmed from the dashboard. */
218
+ readonly pendingEmail: string | undefined;
207
219
  readonly plan: string;
208
220
  readonly planStartedAt: string | undefined;
209
221
  readonly createdAt: string;
@@ -216,9 +228,9 @@ export interface Profile {
216
228
 
217
229
  export interface SystemHealth {
218
230
  readonly systemId: string;
219
- readonly status: HealthStatus;
220
- readonly lastCheckAt: string | undefined;
221
- readonly error: string | undefined;
231
+ readonly healthStatus: HealthStatus;
232
+ readonly healthCheckedAt: Date | undefined;
233
+ readonly healthError: string | undefined;
222
234
  }
223
235
 
224
236
  // ---------------------------------------------------------------------------
@@ -231,8 +243,14 @@ export interface BatchAttestationResponse {
231
243
  }
232
244
 
233
245
  export interface BatchAttestationError {
234
- readonly subjectIdentifier: string;
246
+ /** Position in the submitted subjectIdentifiers list. index is the durable
247
+ * key: subjectIdentifier is verbatim on the live response, but a replayed
248
+ * (Idempotency-Key) response carries a "sha256:<hex>" placeholder instead,
249
+ * because the cached copy never holds a raw identifier (PRIV-2). Do not key
250
+ * a retry on it after a replay. */
251
+ readonly index: number;
235
252
  readonly error: string;
253
+ readonly subjectIdentifier: string;
236
254
  }
237
255
 
238
256
  // ---------------------------------------------------------------------------
@@ -264,6 +282,7 @@ export interface WebhookRotateResponse {
264
282
  readonly id: string;
265
283
  readonly url: string;
266
284
  readonly newSecret: string;
285
+ readonly rotationExpiresAt: Date | undefined;
267
286
  }
268
287
 
269
288
  export interface WebhookDelivery {
@@ -272,6 +291,7 @@ export interface WebhookDelivery {
272
291
  readonly eventType: string;
273
292
  readonly errorMessage: string;
274
293
  readonly attempts: number;
294
+ readonly nextAttemptAt: Date | undefined;
275
295
  readonly createdAt: Date;
276
296
  }
277
297
 
@@ -336,6 +356,7 @@ export function parseSystem(d: Raw): System {
336
356
  // A server too old to record how the read-only guarantee was established
337
357
  // has established nothing, so the absent field is "unknown" and never
338
358
  // "verified_by_introspection".
359
+ phiInScope: d.phi_in_scope === true,
339
360
  readOnlyEnforcement: (d.read_only_enforcement ?? "unknown") as ReadOnlyEnforcement,
340
361
  };
341
362
  }
@@ -393,6 +414,7 @@ export function parseCertificateResponse(d: Raw): CertificateResponse {
393
414
  revokedAt: parseDtOpt(d.revoked_at),
394
415
  revocationReason: d.revocation_reason ?? undefined,
395
416
  certificate: d.certificate,
417
+ statusStatement: d.status_statement ?? undefined,
396
418
  };
397
419
  }
398
420
 
@@ -452,8 +474,9 @@ export function parseConsistencyProof(d: Raw): ConsistencyProof {
452
474
 
453
475
  function parseBatchAttestationError(d: Raw): BatchAttestationError {
454
476
  return {
455
- subjectIdentifier: d.subject_identifier,
477
+ index: d.index,
456
478
  error: d.error,
479
+ subjectIdentifier: d.subject_identifier ?? "",
457
480
  };
458
481
  }
459
482
 
@@ -489,6 +512,7 @@ export function parseWebhookRotateResponse(d: Raw): WebhookRotateResponse {
489
512
  id: d.id,
490
513
  url: d.url,
491
514
  newSecret: d.new_secret,
515
+ rotationExpiresAt: parseDtOpt(d.rotation_expires_at),
492
516
  };
493
517
  }
494
518
 
@@ -499,6 +523,7 @@ export function parseWebhookDelivery(d: Raw): WebhookDelivery {
499
523
  eventType: d.event_type,
500
524
  errorMessage: d.error_message,
501
525
  attempts: d.attempts,
526
+ nextAttemptAt: parseDtOpt(d.next_attempt_at),
502
527
  createdAt: parseDt(d.created_at),
503
528
  };
504
529
  }
@@ -541,6 +566,7 @@ export function parseProfile(d: Raw): Profile {
541
566
  id: d.id,
542
567
  name: d.name,
543
568
  email: d.email,
569
+ pendingEmail: d.pending_email ?? undefined,
544
570
  plan: d.plan,
545
571
  planStartedAt: d.plan_started_at ?? undefined,
546
572
  createdAt: d.created_at,
@@ -551,8 +577,8 @@ export function parseProfile(d: Raw): Profile {
551
577
  export function parseSystemHealth(d: Raw): SystemHealth {
552
578
  return {
553
579
  systemId: d.system_id,
554
- status: d.status as HealthStatus,
555
- lastCheckAt: d.last_check_at ?? undefined,
556
- error: d.error ?? undefined,
580
+ healthStatus: (d.health_status ?? "UNKNOWN") as HealthStatus,
581
+ healthCheckedAt: parseDtOpt(d.health_checked_at),
582
+ healthError: d.health_error ?? undefined,
557
583
  };
558
584
  }