burnledger 0.4.0 → 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 (76) hide show
  1. package/README.md +3 -3
  2. package/dist/cjs/client.d.ts +10 -0
  3. package/dist/cjs/client.d.ts.map +1 -1
  4. package/dist/cjs/client.js +3 -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 +4 -9
  18. package/dist/cjs/index.d.ts.map +1 -1
  19. package/dist/cjs/index.js +3 -1
  20. package/dist/cjs/index.js.map +1 -1
  21. package/dist/cjs/models.d.ts +22 -4
  22. package/dist/cjs/models.d.ts.map +1 -1
  23. package/dist/cjs/models.js +9 -4
  24. package/dist/cjs/models.js.map +1 -1
  25. package/dist/cjs/verify.d.ts +41 -6
  26. package/dist/cjs/verify.d.ts.map +1 -1
  27. package/dist/cjs/verify.js +213 -33
  28. package/dist/cjs/verify.js.map +1 -1
  29. package/dist/cjs/web-verifier.d.ts +10 -5
  30. package/dist/cjs/web-verifier.d.ts.map +1 -1
  31. package/dist/cjs/web-verifier.js +7 -3
  32. package/dist/cjs/web-verifier.js.map +1 -1
  33. package/dist/esm/cli.d.ts.map +1 -1
  34. package/dist/esm/cli.js +76 -10
  35. package/dist/esm/cli.js.map +1 -1
  36. package/dist/esm/client.d.ts +10 -0
  37. package/dist/esm/client.d.ts.map +1 -1
  38. package/dist/esm/client.js +3 -0
  39. package/dist/esm/client.js.map +1 -1
  40. package/dist/esm/errors.d.ts +28 -1
  41. package/dist/esm/errors.d.ts.map +1 -1
  42. package/dist/esm/errors.js +29 -1
  43. package/dist/esm/errors.js.map +1 -1
  44. package/dist/esm/http.d.ts +21 -1
  45. package/dist/esm/http.d.ts.map +1 -1
  46. package/dist/esm/http.js +75 -12
  47. package/dist/esm/http.js.map +1 -1
  48. package/dist/esm/index.browser.d.ts +2 -4
  49. package/dist/esm/index.browser.d.ts.map +1 -1
  50. package/dist/esm/index.browser.js.map +1 -1
  51. package/dist/esm/index.d.ts +4 -9
  52. package/dist/esm/index.d.ts.map +1 -1
  53. package/dist/esm/index.js +1 -1
  54. package/dist/esm/index.js.map +1 -1
  55. package/dist/esm/models.d.ts +22 -4
  56. package/dist/esm/models.d.ts.map +1 -1
  57. package/dist/esm/models.js +9 -4
  58. package/dist/esm/models.js.map +1 -1
  59. package/dist/esm/verify.d.ts +41 -6
  60. package/dist/esm/verify.d.ts.map +1 -1
  61. package/dist/esm/verify.js +213 -33
  62. package/dist/esm/verify.js.map +1 -1
  63. package/dist/esm/web-verifier.d.ts +10 -5
  64. package/dist/esm/web-verifier.d.ts.map +1 -1
  65. package/dist/esm/web-verifier.js +11 -3
  66. package/dist/esm/web-verifier.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/cli.ts +82 -9
  69. package/src/client.ts +11 -0
  70. package/src/errors.ts +32 -2
  71. package/src/http.ts +79 -12
  72. package/src/index.browser.ts +2 -2
  73. package/src/index.ts +6 -8
  74. package/src/models.ts +31 -8
  75. package/src/verify.ts +263 -41
  76. package/src/web-verifier.ts +14 -5
package/src/cli.ts CHANGED
@@ -119,6 +119,45 @@ export function certVerdict(
119
119
  );
120
120
  }
121
121
 
122
+ /** Verdicts that are neither a pass nor a refusal, and what each one leaves
123
+ * unestablished. ADR-017 §2 calls them results a reader must be SHOWN; the
124
+ * verdict string alone leaves them to guess which half of the check is
125
+ * missing. Mirrors softKeyVerdictNote in cmd/cli/format.go. */
126
+ const SOFT_KEY_VERDICTS: Record<string, string> = {
127
+ VALID_KEY_WINDOW_UNKNOWN:
128
+ "signatures verify; the signing key states a validity interval and this " +
129
+ "certificate carries no transparency proof to date it against",
130
+ VALID_KEY_COMPROMISED_LATER:
131
+ "signatures verify and appear to predate the key's compromise; " +
132
+ "confirm against the witness's record",
133
+ };
134
+
135
+ /** Qualifies everything this CLI says from
136
+ * GET /v1/certificates/{id}/revocation-status. That response is a plain JSON
137
+ * envelope with no signature over it, so it is the API's word rather than
138
+ * something the reader can re-check. The signed form exists — a
139
+ * certificate-status statement, which verifyCertificateWithStatus verifies —
140
+ * and this CLI does not fetch one; until it does, saying whose word this is
141
+ * beats printing the field as an established fact. Mirrors
142
+ * unsignedRevocationNote in cmd/cli/format.go. */
143
+ const UNSIGNED_REVOCATION_NOTE = "unsigned API response, not a signed status statement";
144
+
145
+ /**
146
+ * The certificate formats whose signing payload covers enclave_pcr0.
147
+ * Mirrors core.SignatureCoversEnclavePCR0 (core/payload.go).
148
+ *
149
+ * A SET, not a single version, and a predicate rather than an equality test.
150
+ * It was `=== "5.0"`, which silently stopped being true the moment v6 was
151
+ * added: v6 signs the measurement exactly as v5 does, and this CLI told the
152
+ * reader the field was under no signature while the Go CLI said the opposite
153
+ * about the same file.
154
+ */
155
+ const ENCLAVE_PCR0_SIGNED_IN: readonly string[] = Object.freeze(["5.0", "6.0"]);
156
+
157
+ function signatureCoversEnclavePcr0(version: string): boolean {
158
+ return ENCLAVE_PCR0_SIGNED_IN.includes(version);
159
+ }
160
+
122
161
  /** Byte fields arrive as number arrays (Go [N]byte) or strings; show hex. */
123
162
  function displayHash(v: unknown): string {
124
163
  if (Array.isArray(v)) {
@@ -136,18 +175,32 @@ export function formatOutput(
136
175
  const att = cert.attestation as Record<string, unknown>;
137
176
  const L: string[] = [];
138
177
 
139
- if (sigResult !== "VALID") return `Certificate dc_${id}: INVALID (${sigResult})\n`;
140
- if (transparencyFailed) return `Certificate dc_${id}: INVALID (transparency: ${transparency})\n`;
178
+ // A soft key verdict is not a forgery: every signature verified, and what
179
+ // could not be established is the key's authority. "INVALID" would send an
180
+ // auditor hunting for tampering that is not there, and "VALID" — which this
181
+ // CLI printed — claims a check that did not happen.
182
+ const note = SOFT_KEY_VERDICTS[sigResult];
183
+ if (note !== undefined) return `Verification record dc_${id}: ${sigResult} (${note})\n`;
184
+ if (sigResult !== "VALID") return `Verification record dc_${id}: INVALID (${sigResult})\n`;
185
+ if (transparencyFailed) return `Verification record dc_${id}: INVALID (transparency: ${transparency})\n`;
141
186
  if (online && revocation?.status === "REVOKED") {
142
- L.push(`Certificate dc_${id}: INVALID (REVOKED)`);
187
+ L.push(`Verification record dc_${id}: INVALID (REVOKED)`);
143
188
  if (revocation.reason) L.push(` Revocation reason: ${revocation.reason}`);
189
+ // Fail closed on the API's word — an unbelieved revocation is the worse
190
+ // error — but name the source: this verdict rests on the same unsigned
191
+ // field as the one below.
192
+ L.push(` Revocation source: ${UNSIGNED_REVOCATION_NOTE}`);
144
193
  return L.join("\n") + "\n";
145
194
  }
146
195
 
147
- const tag = online ? "" : " (offline revocation status not checked)";
148
- L.push(`Certificate dc_${id}: VALID${tag}`);
196
+ // An --online answer is not a stronger answer, only a different one, so the
197
+ // qualifier is replaced rather than dropped.
198
+ const tag = online
199
+ ? ` (the API reports it is not revoked; ${UNSIGNED_REVOCATION_NOTE})`
200
+ : " (offline — revocation status not checked)";
201
+ L.push(`Verification record dc_${id}: VALID${tag}`);
149
202
  L.push(` Subject: sha256:${sub.slice(0, 6)}...`);
150
- L.push(` Canonical version: ${cert.certificate_format_version as string}`);
203
+ L.push(` Format version: ${cert.certificate_format_version as string}`);
151
204
 
152
205
  // v3 carries ONE system list at the top level, with both counts on each
153
206
  // record. Reading att.systems here is what made this CLI die on every real
@@ -167,10 +220,24 @@ export function formatOutput(
167
220
  // Its own line, never appended to a verdict (ADR-020), and worded so it
168
221
  // cannot be read as a checked fact: nothing offline can verify a measurement.
169
222
  // Proof is the NSM attestation document.
223
+ //
224
+ // The attribution differs by version. From v5 the field is inside the signing
225
+ // payload, so it is the signer's claim about itself. On v4 — the format that
226
+ // is actually issued, and into which the enclave writes its real measurement —
227
+ // no signature covers it, so calling it the signer's claim names an author
228
+ // nothing established: it is whatever the last holder of the file wrote.
170
229
  const issuer = cert.issuer as Record<string, unknown> | undefined;
171
230
  const pcr0 = issuer?.enclave_pcr0;
172
231
  if (typeof pcr0 === "string" && pcr0 !== "") {
173
- L.push(` Enclave image (claimed by signer, not verified here): ${pcr0}`);
232
+ const version = cert.certificate_format_version as string;
233
+ if (signatureCoversEnclavePcr0(version)) {
234
+ L.push(` Enclave image (claimed by signer, not verified here): ${pcr0}`);
235
+ } else {
236
+ L.push(
237
+ ` Enclave image (format ${version} puts this field under no signature — ` +
238
+ `unattributable, not verified here): ${pcr0}`,
239
+ );
240
+ }
174
241
  }
175
242
 
176
243
  if (transparency === "INCLUDED") {
@@ -179,7 +246,9 @@ export function formatOutput(
179
246
  } else {
180
247
  L.push(` Transparency log: ${transparency === "NOT_AVAILABLE" ? "not included" : transparency}`);
181
248
  }
182
- if (online && revocation) L.push(` Revocation status: ${revocation.status}`);
249
+ if (online && revocation) {
250
+ L.push(` Revocation status: ${revocation.status} (${UNSIGNED_REVOCATION_NOTE})`);
251
+ }
183
252
  return L.join("\n") + "\n";
184
253
  }
185
254
 
@@ -202,8 +271,12 @@ async function main(): Promise<void> {
202
271
  const cert = unwrapCertificate(JSON.parse(await readFile(args.certPath, "utf-8")) as Cert);
203
272
  const keys = await loadKeys(args.keysPath);
204
273
 
274
+ // The verifier's own verdict, never a hardcoded word. VALID_KEY_WINDOW_UNKNOWN
275
+ // and VALID_KEY_COMPROMISED_LATER are cases verifyCertificate deliberately
276
+ // refused to call VALID, and this is the tool an auditor runs in a script —
277
+ // the one whose exit code gets trusted.
205
278
  let sigResult = "VALID";
206
- try { await verifyCertificate(nodeCrypto, cert, keys); }
279
+ try { sigResult = await verifyCertificate(nodeCrypto, cert, keys); }
207
280
  catch (e) { sigResult = e instanceof Error ? e.message : "unknown error"; }
208
281
 
209
282
  // NOT_AVAILABLE (no proof) is a legitimate result; only a thrown error is a
package/src/client.ts CHANGED
@@ -55,6 +55,14 @@ export interface BurnLedgerOptions {
55
55
  baseUrl?: string;
56
56
  timeout?: number;
57
57
  maxRetries?: number;
58
+ /**
59
+ * Team to act as, sent as X-Team-ID beside the key.
60
+ *
61
+ * Omit it and the API uses the caller's default team — the first one they
62
+ * joined — which is the right answer for a single-team account and the wrong
63
+ * one for anybody else.
64
+ */
65
+ teamId?: string;
58
66
  }
59
67
 
60
68
  export class BurnLedger {
@@ -66,6 +74,7 @@ export class BurnLedger {
66
74
  apiKey: opts.apiKey,
67
75
  timeout: opts.timeout ?? 30,
68
76
  maxRetries: opts.maxRetries ?? 2,
77
+ teamId: opts.teamId,
69
78
  });
70
79
  }
71
80
 
@@ -405,6 +414,8 @@ export class BurnLedger {
405
414
  return parseProfile(data as Raw);
406
415
  }
407
416
 
417
+ /** Rename the account. `email` must be the current address: changing it
418
+ * needs a dashboard session, and an API key is refused with 403. */
408
419
  async updateMe(params: { name: string; email: string }): Promise<Profile> {
409
420
  const data = await this.transport.request("PATCH", "/v1/me", {
410
421
  json: { name: params.name, email: params.email },
package/src/errors.ts CHANGED
@@ -101,14 +101,44 @@ export class TimeoutError extends BurnLedgerError {
101
101
 
102
102
  export class VerificationError extends BurnLedgerError {
103
103
  readonly reason: string;
104
-
105
- constructor(reason: string) {
104
+ /**
105
+ * A stable discriminator for the one rejection a caller must be able to tell
106
+ * apart from the rest: an authentic status statement that says REVOKED.
107
+ *
108
+ * Every other rejection on the status path — unknown key, revoked key, bad
109
+ * signature, a statement about a different certificate — means the statement
110
+ * could not be authenticated, which is not the same claim and must not be
111
+ * rendered as one. `reason` is prose and may be reworded; this is not.
112
+ *
113
+ * Undefined on every rejection that is not specifically coded.
114
+ */
115
+ readonly code?: string;
116
+
117
+ constructor(reason: string, code?: string) {
106
118
  super(reason);
107
119
  this.name = "VerificationError";
108
120
  this.reason = reason;
121
+ this.code = code;
109
122
  }
110
123
  }
111
124
 
125
+ /** {@link VerificationError.code} for a verified statement that says REVOKED. */
126
+ export const CERTIFICATE_REVOKED = "CERTIFICATE_REVOKED";
127
+
128
+ /**
129
+ * The record is in a format this SDK does not know how to read.
130
+ *
131
+ * This is the second rejection a caller must be able to tell apart, for the
132
+ * same reason as the first: it is not a claim about the document. Before this
133
+ * code existed, a record in a newer format fell through to the oldest payload
134
+ * shape, the rebuilt bytes did not match, and the SDK threw "certificate
135
+ * signature is invalid" - telling a holder their genuine evidence had been
136
+ * tampered with when the answer was "upgrade this library".
137
+ *
138
+ * A UI must render it as "cannot check", never as a failure.
139
+ */
140
+ export const UNSUPPORTED_FORMAT_VERSION = "UNSUPPORTED_FORMAT_VERSION";
141
+
112
142
  const STATUS_TO_CLASS: Record<
113
143
  number,
114
144
  new (opts: {
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>;
@@ -123,13 +127,7 @@ export function verifyTransparency(
123
127
  */
124
128
  export function publicKeyFromHex(
125
129
  hexKey: string,
126
- opts?: {
127
- revoked?: boolean;
128
- keyStatus?: string;
129
- notBefore?: string;
130
- notAfter?: string;
131
- compromisedFrom?: string;
132
- },
130
+ opts?: PublicKeyOptions,
133
131
  ): Promise<PublicKeyInfo> {
134
132
  return _publicKeyFromHex(nodeCrypto, hexKey, opts);
135
133
  }
package/src/models.ts CHANGED
@@ -146,6 +146,13 @@ export interface CertificateResponse {
146
146
  readonly revokedAt: Date | undefined;
147
147
  readonly revocationReason: string | undefined;
148
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;
149
156
  }
150
157
 
151
158
  export interface RevocationStatus {
@@ -206,6 +213,9 @@ export interface Profile {
206
213
  readonly id: string;
207
214
  readonly name: string;
208
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;
209
219
  readonly plan: string;
210
220
  readonly planStartedAt: string | undefined;
211
221
  readonly createdAt: string;
@@ -218,9 +228,9 @@ export interface Profile {
218
228
 
219
229
  export interface SystemHealth {
220
230
  readonly systemId: string;
221
- readonly status: HealthStatus;
222
- readonly lastCheckAt: string | undefined;
223
- readonly error: string | undefined;
231
+ readonly healthStatus: HealthStatus;
232
+ readonly healthCheckedAt: Date | undefined;
233
+ readonly healthError: string | undefined;
224
234
  }
225
235
 
226
236
  // ---------------------------------------------------------------------------
@@ -233,8 +243,14 @@ export interface BatchAttestationResponse {
233
243
  }
234
244
 
235
245
  export interface BatchAttestationError {
236
- 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;
237
252
  readonly error: string;
253
+ readonly subjectIdentifier: string;
238
254
  }
239
255
 
240
256
  // ---------------------------------------------------------------------------
@@ -266,6 +282,7 @@ export interface WebhookRotateResponse {
266
282
  readonly id: string;
267
283
  readonly url: string;
268
284
  readonly newSecret: string;
285
+ readonly rotationExpiresAt: Date | undefined;
269
286
  }
270
287
 
271
288
  export interface WebhookDelivery {
@@ -274,6 +291,7 @@ export interface WebhookDelivery {
274
291
  readonly eventType: string;
275
292
  readonly errorMessage: string;
276
293
  readonly attempts: number;
294
+ readonly nextAttemptAt: Date | undefined;
277
295
  readonly createdAt: Date;
278
296
  }
279
297
 
@@ -396,6 +414,7 @@ export function parseCertificateResponse(d: Raw): CertificateResponse {
396
414
  revokedAt: parseDtOpt(d.revoked_at),
397
415
  revocationReason: d.revocation_reason ?? undefined,
398
416
  certificate: d.certificate,
417
+ statusStatement: d.status_statement ?? undefined,
399
418
  };
400
419
  }
401
420
 
@@ -455,8 +474,9 @@ export function parseConsistencyProof(d: Raw): ConsistencyProof {
455
474
 
456
475
  function parseBatchAttestationError(d: Raw): BatchAttestationError {
457
476
  return {
458
- subjectIdentifier: d.subject_identifier,
477
+ index: d.index,
459
478
  error: d.error,
479
+ subjectIdentifier: d.subject_identifier ?? "",
460
480
  };
461
481
  }
462
482
 
@@ -492,6 +512,7 @@ export function parseWebhookRotateResponse(d: Raw): WebhookRotateResponse {
492
512
  id: d.id,
493
513
  url: d.url,
494
514
  newSecret: d.new_secret,
515
+ rotationExpiresAt: parseDtOpt(d.rotation_expires_at),
495
516
  };
496
517
  }
497
518
 
@@ -502,6 +523,7 @@ export function parseWebhookDelivery(d: Raw): WebhookDelivery {
502
523
  eventType: d.event_type,
503
524
  errorMessage: d.error_message,
504
525
  attempts: d.attempts,
526
+ nextAttemptAt: parseDtOpt(d.next_attempt_at),
505
527
  createdAt: parseDt(d.created_at),
506
528
  };
507
529
  }
@@ -544,6 +566,7 @@ export function parseProfile(d: Raw): Profile {
544
566
  id: d.id,
545
567
  name: d.name,
546
568
  email: d.email,
569
+ pendingEmail: d.pending_email ?? undefined,
547
570
  plan: d.plan,
548
571
  planStartedAt: d.plan_started_at ?? undefined,
549
572
  createdAt: d.created_at,
@@ -554,8 +577,8 @@ export function parseProfile(d: Raw): Profile {
554
577
  export function parseSystemHealth(d: Raw): SystemHealth {
555
578
  return {
556
579
  systemId: d.system_id,
557
- status: d.status as HealthStatus,
558
- lastCheckAt: d.last_check_at ?? undefined,
559
- 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,
560
583
  };
561
584
  }