verdifax 0.3.0 → 1.1.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/dist/index.mjs CHANGED
@@ -23,15 +23,23 @@ var NotFoundError = class extends VerdifaxError {
23
23
  name = "VerdifaxNotFoundError";
24
24
  };
25
25
  var APIError = class extends VerdifaxError {
26
- constructor(status, path, message) {
26
+ constructor(status, path, message, recordHash, signedRequest) {
27
27
  super(`verdifax: API error at ${path} (HTTP ${status}): ${message}`);
28
28
  this.status = status;
29
29
  this.path = path;
30
30
  this.message = message;
31
+ this.recordHash = recordHash;
32
+ if (signedRequest !== void 0) {
33
+ Object.defineProperty(this, "signedRequest", {
34
+ value: signedRequest,
35
+ enumerable: false
36
+ });
37
+ }
31
38
  }
32
39
  status;
33
40
  path;
34
41
  message;
42
+ recordHash;
35
43
  name = "VerdifaxAPIError";
36
44
  };
37
45
  var APIKeyRequiredError = class extends VerdifaxError {
@@ -51,8 +59,137 @@ import {
51
59
  randomBytes,
52
60
  sign
53
61
  } from "crypto";
62
+
63
+ // src/validation.ts
64
+ function validateHex64(value, fieldName) {
65
+ if (typeof value !== "string" || value.length !== 64) {
66
+ throw new VerdifaxError(
67
+ `verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
68
+ );
69
+ }
70
+ if (!/^[0-9a-f]{64}$/.test(value)) {
71
+ throw new VerdifaxError(
72
+ `verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
73
+ );
74
+ }
75
+ }
76
+ function validateRouteId(value) {
77
+ if (!value || typeof value !== "string") {
78
+ throw new VerdifaxError("verdifax: route_id is required");
79
+ }
80
+ if (value.length > 256) {
81
+ throw new VerdifaxError(
82
+ `verdifax: route_id is too long (${value.length} > 256)`
83
+ );
84
+ }
85
+ if (!/^[\x20-\x7E]+$/.test(value)) {
86
+ throw new VerdifaxError(
87
+ "verdifax: route_id contains non-printable-ASCII character"
88
+ );
89
+ }
90
+ }
91
+ function validateJobName(value, fieldName) {
92
+ if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
93
+ throw new VerdifaxError(
94
+ `verdifax: ${fieldName} must be 1 to 128 letters, digits, dots, underscores, colons or dashes`
95
+ );
96
+ }
97
+ }
98
+
99
+ // src/helpers.ts
100
+ function formatProviderPayload(provider, prompt, response) {
101
+ if ([provider, prompt, response].some((text) => /[\uD800-\uDFFF]/u.test(text))) {
102
+ throw new VerdifaxError(
103
+ "verdifax: provider payload text cannot be encoded as UTF-8"
104
+ );
105
+ }
106
+ return JSON.stringify({ prompt, provider, response });
107
+ }
108
+ function isProvided(value) {
109
+ return value !== null && value !== void 0 && value !== "" && value !== false;
110
+ }
111
+ function withAttestedFlag(context) {
112
+ const provided = Object.entries(context).some(
113
+ ([name, value]) => name !== "attested" && isProvided(value)
114
+ );
115
+ return { ...context, attested: provided };
116
+ }
117
+ function withDeclaredFlag(context) {
118
+ const declared = Object.entries(context).some(
119
+ ([name, value]) => name !== "declared" && isProvided(value) && !(Array.isArray(value) && value.length === 0)
120
+ );
121
+ return { ...context, declared };
122
+ }
123
+ function refuseCallerOriginHashes(substance) {
124
+ const origin = substance?.origin;
125
+ if (origin?.source_ip_hash || origin?.user_agent_hash) {
126
+ throw new VerdifaxError(
127
+ "verdifax: request_substance.origin.source_ip_hash and user_agent_hash are computed by the server and cannot be supplied"
128
+ );
129
+ }
130
+ }
131
+ var JOB_FIELDS = {
132
+ job_id: true,
133
+ step_id: true,
134
+ previous_record_hashes: true,
135
+ external_previous: true,
136
+ final: true
137
+ };
138
+ function refuseUnknownFields(value, known, label) {
139
+ for (const name of Object.keys(value)) {
140
+ if (!Object.hasOwn(known, name)) {
141
+ throw new VerdifaxError(`verdifax: ${label} has an unknown field: ${name}`);
142
+ }
143
+ }
144
+ }
145
+ function sortedUnique(hashes, fieldName) {
146
+ if (hashes === void 0) return [];
147
+ if (!Array.isArray(hashes) || hashes.some((hash) => typeof hash !== "string")) {
148
+ throw new VerdifaxError(`verdifax: ${fieldName} must be a list of strings`);
149
+ }
150
+ return [...new Set(hashes)].sort();
151
+ }
152
+ function normalizedJob(job) {
153
+ if (typeof job !== "object" || job === null || Array.isArray(job)) {
154
+ throw new VerdifaxError("verdifax: job must be an object");
155
+ }
156
+ refuseUnknownFields(job, JOB_FIELDS, "job");
157
+ validateJobName(job.job_id, "job_id");
158
+ validateJobName(job.step_id, "step_id");
159
+ if (job.final !== void 0 && typeof job.final !== "boolean") {
160
+ throw new VerdifaxError("verdifax: job.final must be true or false");
161
+ }
162
+ const out = { job_id: job.job_id, step_id: job.step_id };
163
+ const previous = sortedUnique(job.previous_record_hashes, "previous_record_hashes");
164
+ if (previous.length > 0) out.previous_record_hashes = previous;
165
+ const external = sortedUnique(job.external_previous, "external_previous");
166
+ if (external.length > 0) out.external_previous = external;
167
+ if (job.final) out.final = true;
168
+ return out;
169
+ }
170
+ function withExecuteFlags(req) {
171
+ const body = { ...req };
172
+ if (req.job === void 0 || req.job === null) {
173
+ delete body.job;
174
+ } else {
175
+ body.job = normalizedJob(req.job);
176
+ }
177
+ if (req.attested_context) {
178
+ body.attested_context = withAttestedFlag(req.attested_context);
179
+ }
180
+ if (req.reproducibility_context) {
181
+ body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
182
+ }
183
+ return body;
184
+ }
185
+
186
+ // src/signing.ts
54
187
  var REQUEST_SIGNATURE_FORM = "verdifax.request.v1";
188
+ var REQUEST_SIGNATURE_FORM_V2 = "verdifax.request.v2";
189
+ var REQUEST_SIGNATURE_VERSION_2 = "2";
190
+ var JOB_FORM = "verdifax.job.v1";
55
191
  var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
192
+ var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
56
193
  var SIGNING_KEY_BYTES = 32;
57
194
  var PKCS8_ED25519_HEADER = Buffer.from(
58
195
  "302e020100300506032b657004220420",
@@ -83,6 +220,12 @@ function loadSigningKey(key) {
83
220
  type: "pkcs8"
84
221
  });
85
222
  }
223
+ function sha256Hex(text) {
224
+ return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
225
+ }
226
+ function signedAtNow() {
227
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
228
+ }
86
229
  function requestSignaturePreimage(fields) {
87
230
  return Buffer.from(
88
231
  [
@@ -97,13 +240,62 @@ function requestSignaturePreimage(fields) {
97
240
  "utf8"
98
241
  );
99
242
  }
243
+ function requestSignaturePreimageV2(fields) {
244
+ const { job } = fields;
245
+ return Buffer.from(
246
+ [
247
+ REQUEST_SIGNATURE_FORM_V2,
248
+ fields.body_hash,
249
+ fields.payload_hash,
250
+ fields.program_id,
251
+ fields.route_id,
252
+ fields.registry_record_hash,
253
+ fields.ai_output_hash,
254
+ JOB_FORM,
255
+ job?.job_id ?? "",
256
+ job?.step_id ?? "",
257
+ (job?.previous_record_hashes ?? []).join(","),
258
+ (job?.external_previous ?? []).join(","),
259
+ job?.final ? "true" : "false",
260
+ fields.signed_at,
261
+ fields.nonce
262
+ ].join("\0"),
263
+ "utf8"
264
+ );
265
+ }
266
+ function signRequestV2(signingKey, req, options = {}) {
267
+ const key = loadSigningKey(signingKey);
268
+ const { request_signature: _unsigned, ...fields } = withExecuteFlags(req);
269
+ const signed_request = JSON.stringify(fields);
270
+ const signed_at = options.signedAt ?? signedAtNow();
271
+ const nonce = options.nonce ?? randomBytes(16).toString("hex");
272
+ const preimage = requestSignaturePreimageV2({
273
+ body_hash: sha256Hex(signed_request),
274
+ payload_hash: sha256Hex(fields.payload_text || fields.payload || ""),
275
+ program_id: fields.program_id,
276
+ route_id: fields.route_id,
277
+ registry_record_hash: fields.registry_record_hash,
278
+ ai_output_hash: fields.ai_output_text ? sha256Hex(fields.ai_output_text) : "",
279
+ job: fields.job,
280
+ signed_at,
281
+ nonce
282
+ });
283
+ return {
284
+ signed_request,
285
+ request_signature: {
286
+ version: REQUEST_SIGNATURE_VERSION_2,
287
+ signature: sign(null, preimage, key).toString("base64"),
288
+ signed_at,
289
+ nonce
290
+ }
291
+ };
292
+ }
100
293
  function signRequest(signingKey, req, options = {}) {
101
294
  const key = loadSigningKey(signingKey);
102
- const signed_at = options.signedAt ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
295
+ const signed_at = options.signedAt ?? signedAtNow();
103
296
  const nonce = options.nonce ?? randomBytes(16).toString("hex");
104
- const payload_hash = createHash("sha256").update(Buffer.from(req.payload_text || req.payload || "", "utf8")).digest("hex");
105
297
  const preimage = requestSignaturePreimage({
106
- payload_hash,
298
+ payload_hash: sha256Hex(req.payload_text || req.payload || ""),
107
299
  program_id: req.program_id,
108
300
  route_id: req.route_id,
109
301
  registry_record_hash: req.registry_record_hash,
@@ -126,38 +318,132 @@ function requestKeyRegistrationBody(signingKey) {
126
318
  ).toString("base64");
127
319
  return { public_key, proof };
128
320
  }
321
+ function requestKeyChangePreimage(fields) {
322
+ return Buffer.from(
323
+ [
324
+ REQUEST_KEY_CHANGE_FORM,
325
+ fields.new_public_key,
326
+ fields.signed_at,
327
+ fields.nonce
328
+ ].join("\0"),
329
+ "utf8"
330
+ );
331
+ }
332
+ function requestKeyChangeApproval(currentSigningKey, newPublicKey, options = {}) {
333
+ const key = loadSigningKey(currentSigningKey);
334
+ const signed_at = options.signedAt ?? signedAtNow();
335
+ const nonce = options.nonce ?? randomBytes(16).toString("hex");
336
+ const preimage = requestKeyChangePreimage({
337
+ new_public_key: newPublicKey,
338
+ signed_at,
339
+ nonce
340
+ });
341
+ return {
342
+ signature: sign(null, preimage, key).toString("base64"),
343
+ signed_at,
344
+ nonce
345
+ };
346
+ }
129
347
 
130
- // src/validation.ts
131
- function validateHex64(value, fieldName) {
132
- if (typeof value !== "string" || value.length !== 64) {
133
- throw new VerdifaxError(
134
- `verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
135
- );
348
+ // src/job.ts
349
+ var STEP_OPTIONS = {
350
+ payload: true,
351
+ payload_text: true,
352
+ program_id: true,
353
+ route_id: true,
354
+ registry_record_hash: true,
355
+ ai_output_text: true,
356
+ attested_context: true,
357
+ reproducibility_context: true,
358
+ request_substance: true,
359
+ authorization_chain: true,
360
+ regulatory_scaffolding: true,
361
+ causal_graph: true,
362
+ after: true,
363
+ external_previous: true,
364
+ final: true
365
+ };
366
+ var VerdifaxJob = class {
367
+ jobId;
368
+ client;
369
+ hashes = /* @__PURE__ */ new Map();
370
+ #signedRequests = /* @__PURE__ */ new Map();
371
+ lastStepId;
372
+ constructor(client, jobId) {
373
+ validateJobName(jobId, "job_id");
374
+ this.client = client;
375
+ this.jobId = jobId;
136
376
  }
137
- if (!/^[0-9a-f]{64}$/.test(value)) {
138
- throw new VerdifaxError(
139
- `verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
140
- );
377
+ /** Step id to record hash for every step that left a record, on a copy. */
378
+ get recordHashes() {
379
+ return Object.fromEntries(this.hashes);
141
380
  }
142
- }
143
- function validateRouteId(value) {
144
- if (!value || typeof value !== "string") {
145
- throw new VerdifaxError("verdifax: route_id is required");
381
+ /** Step id to the exact text that was signed, for every signed step that
382
+ * left a record, refused and halted steps included, on a copy. Each text
383
+ * can be passed to `verdifax-verify --request FILE`. */
384
+ get signedRequests() {
385
+ return Object.fromEntries(this.#signedRequests);
146
386
  }
147
- if (value.length > 256) {
148
- throw new VerdifaxError(
149
- `verdifax: route_id is too long (${value.length} > 256)`
150
- );
387
+ /** Runs one step and records its hash and signed text. A refused or halted
388
+ * step records both from the error, then throws the same error `attest`
389
+ * throws. */
390
+ async step(stepId, options) {
391
+ validateJobName(stepId, "step_id");
392
+ refuseUnknownFields(options, STEP_OPTIONS, "step options");
393
+ const { after, external_previous, final, ...attest } = options;
394
+ const job = {
395
+ job_id: this.jobId,
396
+ step_id: stepId,
397
+ previous_record_hashes: this.previousHashes(after),
398
+ external_previous,
399
+ final
400
+ };
401
+ let result;
402
+ try {
403
+ result = await this.client.attest({ ...attest, job });
404
+ } catch (err) {
405
+ if (err instanceof APIError && err.recordHash) {
406
+ this.record(stepId, err.recordHash, err.signedRequest);
407
+ }
408
+ throw err;
409
+ }
410
+ const hash = result?.record_hash || result?.manifest_hash || result?.manifest?.ManifestHash;
411
+ if (!hash) {
412
+ throw new VerdifaxError(
413
+ `verdifax: the server returned no record_hash or manifest_hash for step ${stepId}, so the job map cannot continue from it`
414
+ );
415
+ }
416
+ this.record(stepId, hash, result.signed_request);
417
+ return result;
151
418
  }
152
- if (!/^[\x20-\x7E]+$/.test(value)) {
153
- throw new VerdifaxError(
154
- "verdifax: route_id contains non-printable-ASCII character"
155
- );
419
+ previousHashes(after) {
420
+ if (after === void 0 || after === null) {
421
+ return this.lastStepId === void 0 ? [] : [this.hashes.get(this.lastStepId)];
422
+ }
423
+ if (!Array.isArray(after)) {
424
+ throw new VerdifaxError("verdifax: after must be a list of step ids");
425
+ }
426
+ return after.map((stepId) => {
427
+ const hash = this.hashes.get(stepId);
428
+ if (hash === void 0) {
429
+ throw new VerdifaxError(
430
+ `verdifax: after names ${String(stepId)}, which is not a step in job ${this.jobId}`
431
+ );
432
+ }
433
+ return hash;
434
+ });
156
435
  }
157
- }
436
+ record(stepId, hash, signedRequest) {
437
+ this.hashes.set(stepId, hash);
438
+ if (signedRequest !== void 0) {
439
+ this.#signedRequests.set(stepId, signedRequest);
440
+ }
441
+ this.lastStepId = stepId;
442
+ }
443
+ };
158
444
 
159
445
  // src/version.ts
160
- var SDK_VERSION = "0.3.0";
446
+ var SDK_VERSION = "1.1.0";
161
447
 
162
448
  // src/client.ts
163
449
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
@@ -167,7 +453,10 @@ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
167
453
  var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
168
454
  var HEADER_API_KEY = "X-Verdifax-Key";
169
455
  var REQUEST_KEY_PATH = "/keys/request-key";
456
+ var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
457
+ var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
170
458
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
459
+ var MAX_BATCH_ROWS = 1e3;
171
460
  var VerdifaxClient = class {
172
461
  baseURL;
173
462
  apiKey;
@@ -192,21 +481,73 @@ var VerdifaxClient = class {
192
481
  async version() {
193
482
  return this.request("GET", "/version");
194
483
  }
484
+ /** The server's own API description. */
485
+ async getOpenAPI() {
486
+ return this.request("GET", "/openapi.json");
487
+ }
488
+ /** Run counts for this API key. */
489
+ async getStats() {
490
+ this.requireAPIKey();
491
+ return this.request("GET", "/stats");
492
+ }
195
493
  // ── Execute / Attest (auth) ───────────────────────────────────
196
494
  async execute(req) {
197
- this.requireAPIKey();
198
- if (!req.payload && !req.payload_text) {
495
+ this.requireExecuteFields(req);
496
+ const sent = this.prepared(req);
497
+ const out = await this.request("POST", "/execute", sent);
498
+ return withSignedText(out, sent);
499
+ }
500
+ /** Runs one payload twice on the server and compares the manifest
501
+ * hashes. One signature covers the request, and the server claims
502
+ * its nonce once. Both runs are recorded. A run that happens twice
503
+ * cannot be one job step, so a `job` is refused here. */
504
+ async verifyDeterminism(req) {
505
+ this.requireExecuteFields(req);
506
+ if (req.job !== void 0 && req.job !== null) {
199
507
  throw new VerdifaxError(
200
- "verdifax: ExecuteRequest needs payload or payload_text"
508
+ "verdifax: a determinism check runs twice, so it cannot be a job step"
201
509
  );
202
510
  }
203
- if (!req.program_id || !req.route_id || !req.registry_record_hash) {
511
+ const sent = this.prepared(req);
512
+ const out = await this.request(
513
+ "POST",
514
+ "/execute/verify-determinism",
515
+ sent
516
+ );
517
+ return withSignedText(out, sent);
518
+ }
519
+ /** Sends up to {@link MAX_BATCH_ROWS} runs in one request. With a
520
+ * signing key each row carries its own signature and nonce. A row
521
+ * that fails comes back in `results` with `ok: false`, so the call
522
+ * does not throw for it. The rows are not checked here, except a job
523
+ * block, which is refused before anything is sent: the server reports
524
+ * any other bad row in that row's result.
525
+ *
526
+ * This is one POST and it is never retried. After a timeout the
527
+ * server may have run some or all of the rows. */
528
+ async executeBatch(runs) {
529
+ this.requireAPIKey();
530
+ if (!Array.isArray(runs) || runs.length === 0) {
531
+ throw new VerdifaxError("verdifax: executeBatch needs at least one run");
532
+ }
533
+ if (runs.length > MAX_BATCH_ROWS) {
204
534
  throw new VerdifaxError(
205
- "verdifax: program_id, route_id, and registry_record_hash are required"
535
+ `verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
206
536
  );
207
537
  }
208
- const body = this.signingKey ? { ...req, request_signature: signRequest(this.signingKey, req) } : req;
209
- return this.request("POST", "/execute", body);
538
+ const sent = runs.map((run) => this.prepared(run));
539
+ const out = await this.request(
540
+ "POST",
541
+ "/execute/batch",
542
+ { runs: sent }
543
+ );
544
+ if (!Array.isArray(out?.results)) return out;
545
+ return {
546
+ ...out,
547
+ results: out.results.map(
548
+ (row, i) => i < sent.length ? withSignedText(row, sent[i]) : row
549
+ )
550
+ };
210
551
  }
211
552
  /** High-level convenience wrapper around `execute()`. Accepts
212
553
  * either a Buffer (for binary payloads, auto-base64) or a
@@ -232,7 +573,13 @@ var VerdifaxClient = class {
232
573
  route_id: req.route_id,
233
574
  registry_record_hash: req.registry_record_hash,
234
575
  ai_output_text: req.ai_output_text,
235
- attested_context: req.attested_context
576
+ attested_context: req.attested_context,
577
+ reproducibility_context: req.reproducibility_context,
578
+ request_substance: req.request_substance,
579
+ authorization_chain: req.authorization_chain,
580
+ regulatory_scaffolding: req.regulatory_scaffolding,
581
+ causal_graph: req.causal_graph,
582
+ job: req.job
236
583
  };
237
584
  if (req.payload !== void 0) {
238
585
  body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
@@ -241,6 +588,12 @@ var VerdifaxClient = class {
241
588
  }
242
589
  return this.execute(body);
243
590
  }
591
+ /** A helper that runs the steps of one job through {@link attest} and
592
+ * keeps each step's record hash, so a later step can name it. It makes
593
+ * no request until a step runs. */
594
+ job(jobId) {
595
+ return new VerdifaxJob(this, jobId);
596
+ }
244
597
  // ── Request signing (auth) ───────────────────────────────────
245
598
  /** Registers this client's public key on its API key, with the proof
246
599
  * that the client holds the private key. From then on the server
@@ -258,11 +611,48 @@ var VerdifaxClient = class {
258
611
  requestKeyRegistrationBody(this.signingKey)
259
612
  );
260
613
  }
614
+ /** Replaces the registered key with `newSigningKey`. The new key proves
615
+ * itself and this client's current key approves the change. After
616
+ * success this client signs with the new key. If the call fails it
617
+ * keeps the current key, including after a timeout, when the server may
618
+ * have made the change. */
619
+ async rotateRequestKey(newSigningKey) {
620
+ this.requireAPIKey();
621
+ if (!this.signingKey) {
622
+ throw new VerdifaxError(
623
+ "verdifax: rotateRequestKey needs the current key in the signingKey client option"
624
+ );
625
+ }
626
+ const next = loadSigningKey(newSigningKey);
627
+ const registration = requestKeyRegistrationBody(next);
628
+ const out = await this.request(
629
+ "POST",
630
+ REQUEST_KEY_PATH,
631
+ {
632
+ ...registration,
633
+ change: requestKeyChangeApproval(
634
+ this.signingKey,
635
+ registration.public_key
636
+ )
637
+ }
638
+ );
639
+ this.signingKey = next;
640
+ return out;
641
+ }
261
642
  /** Clears the registered public key, so the server stops requiring
262
- * request signatures on this API key. */
643
+ * request signatures on this API key. With a signing key the client
644
+ * sends that key's approval, which the server requires while a key is
645
+ * registered. After success the client drops its signing key and sends
646
+ * unsigned requests, because the server refuses a signed run on an API
647
+ * key with no registered key. If the call fails it keeps the key. */
263
648
  async clearRequestKey() {
264
649
  this.requireAPIKey();
265
- await this.request("DELETE", REQUEST_KEY_PATH);
650
+ await this.request(
651
+ "DELETE",
652
+ REQUEST_KEY_PATH,
653
+ this.signingKey ? { change: requestKeyChangeApproval(this.signingKey, "") } : void 0
654
+ );
655
+ this.signingKey = void 0;
266
656
  }
267
657
  // ── Runs ──────────────────────────────────────────────────────
268
658
  async getRun(runId) {
@@ -289,6 +679,95 @@ var VerdifaxClient = class {
289
679
  const out = await this.request("GET", path);
290
680
  return { runs: out.runs ?? [], total: out.total ?? 0 };
291
681
  }
682
+ /** Searches this key's runs. `manifest_hash` matches a prefix. The
683
+ * server ignores a `from` or `to` it cannot parse here, unlike
684
+ * {@link exportRuns}, which refuses one. */
685
+ async searchRuns(params = {}) {
686
+ this.requireAPIKey();
687
+ const q = new URLSearchParams();
688
+ for (const name of [
689
+ "manifest_hash",
690
+ "program_id",
691
+ "route_id",
692
+ "status",
693
+ "from",
694
+ "to"
695
+ ]) {
696
+ const value = params[name];
697
+ if (value) q.set(name, value);
698
+ }
699
+ if (params.limit && params.limit > 0) {
700
+ q.set("limit", String(params.limit));
701
+ }
702
+ if (params.offset && params.offset > 0) {
703
+ q.set("offset", String(params.offset));
704
+ }
705
+ const qs = q.toString();
706
+ const path = qs ? `/runs/search?${qs}` : "/runs/search";
707
+ const out = await this.request("GET", path);
708
+ return { runs: out.runs ?? [], total: out.total ?? 0 };
709
+ }
710
+ /** Lists what the server holds for this key in a period, failures
711
+ * included, up to 10,000 rows. `truncated` is true when the period
712
+ * holds more. The list states what was stored, not a verdict, and a
713
+ * call that never reached Verdifax cannot appear in it. */
714
+ async exportRuns(params = {}) {
715
+ this.requireAPIKey();
716
+ return this.request("GET", this.exportPath(params, "json"));
717
+ }
718
+ /** The same rows as {@link exportRuns}: the raw CSV text with `total`
719
+ * and `truncated` from the response headers. Check `truncated`: when
720
+ * true, the CSV is missing rows for the period. Throws when the server
721
+ * does not send those headers, so that a cut-off export cannot pass
722
+ * for a complete one. */
723
+ async exportRunsCSV(params = {}) {
724
+ this.requireAPIKey();
725
+ const { text, headers } = await this.requestRaw(
726
+ this.exportPath(params, "csv")
727
+ );
728
+ const total = Number.parseInt(headers.get(HEADER_EXPORT_TOTAL) ?? "", 10);
729
+ const truncated = headers.get(HEADER_EXPORT_TRUNCATED);
730
+ if (!Number.isSafeInteger(total) || truncated !== "true" && truncated !== "false") {
731
+ throw new VerdifaxError(
732
+ `verdifax: the server did not report ${HEADER_EXPORT_TOTAL} and ${HEADER_EXPORT_TRUNCATED}, so this CSV may be cut off; use exportRuns instead`
733
+ );
734
+ }
735
+ return { csv: text, total, truncated: truncated === "true" };
736
+ }
737
+ exportPath(params, format) {
738
+ const q = new URLSearchParams();
739
+ if (params.from) q.set("from", params.from);
740
+ if (params.to) q.set("to", params.to);
741
+ if (params.job_id) q.set("job_id", params.job_id);
742
+ q.set("format", format);
743
+ return `/runs/export?${q.toString()}`;
744
+ }
745
+ // ── Attestation receipts (auth) ──────────────────────────────
746
+ async listAttestations(params = {}) {
747
+ this.requireAPIKey();
748
+ const q = new URLSearchParams();
749
+ if (params.limit && params.limit > 0) {
750
+ q.set("limit", String(params.limit));
751
+ }
752
+ if (params.offset && params.offset > 0) {
753
+ q.set("offset", String(params.offset));
754
+ }
755
+ const qs = q.toString();
756
+ return this.request(
757
+ "GET",
758
+ qs ? `/attestations?${qs}` : "/attestations"
759
+ );
760
+ }
761
+ async getAttestation(attestationId) {
762
+ this.requireAPIKey();
763
+ if (attestationId <= 0) {
764
+ throw new VerdifaxError("verdifax: attestationId must be > 0");
765
+ }
766
+ return this.request(
767
+ "GET",
768
+ `/attestations/${attestationId}`
769
+ );
770
+ }
292
771
  // ── Sealed-artifact retrieval (auth) ─────────────────────────
293
772
  async getAllowToken(runId) {
294
773
  return this.getRunArtifact(runId, "allow-token");
@@ -305,6 +784,39 @@ var VerdifaxClient = class {
305
784
  async getAivpT4HaltReceipt(runId) {
306
785
  return this.getRunArtifact(runId, "aivp-t4-halt-receipt");
307
786
  }
787
+ async getAttestationGapReceipt(runId) {
788
+ return this.getRunArtifact(runId, "attestation-gap-receipt");
789
+ }
790
+ async getCESAuditRecord(runId) {
791
+ return this.getRunArtifact(runId, "ces-audit-record");
792
+ }
793
+ /** The audit report for a run, as JSON. */
794
+ async getRunReport(runId) {
795
+ const out = await this.getRunArtifact(runId, "report");
796
+ return out.report;
797
+ }
798
+ /** The run's sealed audit bundle as the exact JSON text the server
799
+ * sent, to save and check with `verdifax-verify`. It is not parsed:
800
+ * JSON.parse would round a number above 2^53 and the recomputed hash
801
+ * would stop matching. */
802
+ async getRunArtifacts(runId) {
803
+ this.requireAPIKey();
804
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
805
+ return (await this.requestRaw(`/runs/${runId}/artifacts`)).text;
806
+ }
807
+ /** The public audit bundle of a record published by run id, as the
808
+ * exact JSON text the server sent. Needs no API key. */
809
+ async getPublicBundle(runId) {
810
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
811
+ return (await this.requestRaw(`/runs/${runId}/bundle.json`)).text;
812
+ }
813
+ /** The public audit bundle a share token points to, as the exact JSON
814
+ * text the server sent. Needs no API key. */
815
+ async getPublicBundleByToken(token) {
816
+ if (!token) throw new VerdifaxError("verdifax: token is required");
817
+ const path = `/public/runs/${encodeURIComponent(token)}/bundle.json`;
818
+ return (await this.requestRaw(path)).text;
819
+ }
308
820
  async getRunArtifact(runId, suffix) {
309
821
  this.requireAPIKey();
310
822
  if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
@@ -390,14 +902,26 @@ var VerdifaxClient = class {
390
902
  }
391
903
  await this.request("DELETE", `/admin/keys/${keyId}`);
392
904
  }
905
+ /** Clears the request key on an API key whose holder lost it, after a
906
+ * manual check. It is the one way to clear a key without that key. The
907
+ * id can be any API key, so this client keeps its own signing key. */
908
+ async adminClearRequestKey(keyId) {
909
+ this.requireAPIKey();
910
+ if (keyId <= 0) {
911
+ throw new VerdifaxError("verdifax: keyId must be > 0");
912
+ }
913
+ await this.request("DELETE", `/admin/keys/${keyId}/request-key`);
914
+ }
393
915
  // ── Provider helpers (Claude / OpenAI) ───────────────────────
394
- /** Convenience for attesting an Anthropic Claude API response.
395
- * Wires the response text into the AIVP governance pipeline
396
- * by setting `ai_output_text`. */
916
+ /** Convenience for attesting an Anthropic Claude API response. With
917
+ * `govern: true` the response text also goes to the AIVP review as
918
+ * `ai_output_text`; that is off by default, so the text leaves your
919
+ * machine for review only when you ask. */
397
920
  async attestClaudeResponse(args) {
398
921
  return this.attestProviderResponse({ ...args, provider: "anthropic" });
399
922
  }
400
- /** Convenience for attesting an OpenAI API response. */
923
+ /** Convenience for attesting an OpenAI API response. `govern` works as
924
+ * in {@link attestClaudeResponse}. */
401
925
  async attestOpenAIResponse(args) {
402
926
  return this.attestProviderResponse({ ...args, provider: "openai" });
403
927
  }
@@ -407,25 +931,25 @@ var VerdifaxClient = class {
407
931
  "verdifax: response is required for attestation"
408
932
  );
409
933
  }
410
- const payloadText = JSON.stringify({
411
- provider: args.provider,
412
- prompt: args.prompt,
413
- response: args.response
414
- });
415
934
  return this.attest({
416
- payload_text: payloadText,
935
+ payload_text: formatProviderPayload(
936
+ args.provider,
937
+ args.prompt,
938
+ args.response
939
+ ),
417
940
  program_id: args.program_id,
418
941
  route_id: args.route_id,
419
942
  registry_record_hash: args.registry_record_hash,
420
- ai_output_text: args.response,
943
+ ai_output_text: args.govern ? args.response : void 0,
421
944
  attested_context: {
422
945
  actor_role: "ai_assistant",
423
946
  model_provider: args.provider
424
- }
947
+ },
948
+ job: args.job
425
949
  });
426
950
  }
427
951
  // ── HTTP plumbing, retry-aware ──────────────────────────────
428
- async request(method, path, body) {
952
+ async request(method, path, body, rawText = false) {
429
953
  const url = this.baseURL + path;
430
954
  const headers = {
431
955
  "User-Agent": this.userAgent
@@ -468,9 +992,13 @@ var VerdifaxClient = class {
468
992
  continue;
469
993
  }
470
994
  let errMessage = text.trim();
995
+ let recordHash;
471
996
  try {
472
997
  const env = JSON.parse(text);
473
998
  if (env.error) errMessage = env.error;
999
+ if (typeof env.record_hash === "string" && env.record_hash) {
1000
+ recordHash = env.record_hash;
1001
+ }
474
1002
  } catch {
475
1003
  }
476
1004
  switch (resp.status) {
@@ -479,9 +1007,16 @@ var VerdifaxClient = class {
479
1007
  case 404:
480
1008
  throw new NotFoundError(path, errMessage);
481
1009
  default:
482
- throw new APIError(resp.status, path, errMessage);
1010
+ throw new APIError(
1011
+ resp.status,
1012
+ path,
1013
+ errMessage,
1014
+ recordHash,
1015
+ body?.signed_request
1016
+ );
483
1017
  }
484
1018
  }
1019
+ if (rawText) return { text, headers: resp.headers };
485
1020
  if (text.length === 0) return void 0;
486
1021
  try {
487
1022
  return JSON.parse(text);
@@ -495,17 +1030,46 @@ var VerdifaxClient = class {
495
1030
  `verdifax: retry budget exhausted${lastErr ? `: ${String(lastErr)}` : ""}`
496
1031
  );
497
1032
  }
1033
+ /** A GET whose body is kept as sent, retried like any other GET. */
1034
+ requestRaw(path) {
1035
+ return this.request("GET", path, void 0, true);
1036
+ }
498
1037
  requireAPIKey() {
499
1038
  if (!this.apiKey) throw new APIKeyRequiredError();
500
1039
  }
1040
+ requireExecuteFields(req) {
1041
+ this.requireAPIKey();
1042
+ if (!req.payload && !req.payload_text) {
1043
+ throw new VerdifaxError(
1044
+ "verdifax: ExecuteRequest needs payload or payload_text"
1045
+ );
1046
+ }
1047
+ if (!req.program_id || !req.route_id || !req.registry_record_hash) {
1048
+ throw new VerdifaxError(
1049
+ "verdifax: program_id, route_id, and registry_record_hash are required"
1050
+ );
1051
+ }
1052
+ validateHex64(req.program_id, "program_id");
1053
+ validateHex64(req.registry_record_hash, "registry_record_hash");
1054
+ validateRouteId(req.route_id);
1055
+ refuseCallerOriginHashes(req.request_substance);
1056
+ }
1057
+ /** The body to send, on a copy: the request with its derived flags or,
1058
+ * with a signing key, the signed text with a fresh signature and nonce. */
1059
+ prepared(req) {
1060
+ return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
1061
+ }
501
1062
  };
1063
+ function withSignedText(result, sent) {
1064
+ return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
1065
+ }
502
1066
 
503
1067
  // src/config.ts
504
1068
  function fromEnv() {
505
1069
  const cfg = {
506
- baseURL: process.env.VERDIFAX_BASE_URL ?? void 0,
1070
+ baseURL: process.env.VERDIFAX_BASE_URL || process.env.VERDIFAX_API_URL || void 0,
507
1071
  apiKey: process.env.VERDIFAX_API_KEY ?? void 0,
508
- userAgent: process.env.VERDIFAX_USER_AGENT ?? void 0
1072
+ userAgent: process.env.VERDIFAX_USER_AGENT || void 0
509
1073
  };
510
1074
  const timeoutRaw = process.env.VERDIFAX_TIMEOUT_MS;
511
1075
  if (timeoutRaw !== void 0 && timeoutRaw !== "") {
@@ -529,18 +1093,187 @@ function fromEnv() {
529
1093
  }
530
1094
  return new VerdifaxClient(cfg);
531
1095
  }
1096
+
1097
+ // src/environment.ts
1098
+ import { execFileSync } from "child_process";
1099
+ import { readdirSync, readFileSync } from "fs";
1100
+ import { join } from "path";
1101
+ var HASH_64 = /^[0-9a-f]{64}$/;
1102
+ var GIT_SHA = /^([0-9a-f]{40}|[0-9a-f]{64})$/;
1103
+ var PLATFORM_NAMES = { win32: "windows" };
1104
+ var ARCH_NAMES = { x64: "amd64", ia32: "386" };
1105
+ function packageVersion(dir) {
1106
+ try {
1107
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
1108
+ return pkg.name && pkg.version ? `${pkg.name}==${pkg.version}` : void 0;
1109
+ } catch {
1110
+ return void 0;
1111
+ }
1112
+ }
1113
+ function packageDirs(root) {
1114
+ const dirs = [];
1115
+ for (const entry of readdirSync(root)) {
1116
+ if (entry.startsWith(".")) continue;
1117
+ if (!entry.startsWith("@")) {
1118
+ dirs.push(join(root, entry));
1119
+ continue;
1120
+ }
1121
+ for (const scoped of readdirSync(join(root, entry))) {
1122
+ dirs.push(join(root, entry, scoped));
1123
+ }
1124
+ }
1125
+ return dirs;
1126
+ }
1127
+ function detectDependencies() {
1128
+ try {
1129
+ const pins = packageDirs(join(process.cwd(), "node_modules")).map(packageVersion).filter((pin) => pin !== void 0);
1130
+ pins.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1131
+ return pins.length > 0 ? pins : void 0;
1132
+ } catch {
1133
+ return void 0;
1134
+ }
1135
+ }
1136
+ function detectGitSHA() {
1137
+ try {
1138
+ const sha = execFileSync("git", ["rev-parse", "HEAD"], {
1139
+ encoding: "utf8",
1140
+ stdio: ["ignore", "pipe", "ignore"],
1141
+ timeout: 2e3
1142
+ }).trim().toLowerCase();
1143
+ return GIT_SHA.test(sha) ? sha : void 0;
1144
+ } catch {
1145
+ return void 0;
1146
+ }
1147
+ }
1148
+ function detectContainerHash() {
1149
+ try {
1150
+ const text = readFileSync("/proc/self/cgroup", "utf8");
1151
+ return text.split("\n").flatMap((line) => line.split("/")).map((segment) => segment.trim()).find((segment) => HASH_64.test(segment));
1152
+ } catch {
1153
+ return void 0;
1154
+ }
1155
+ }
1156
+ function captureEnvironment(options = {}) {
1157
+ const seeds = Object.entries(options.declaredSeeds ?? {}).map(([name, seed]) => `${name}=${seed}`).sort();
1158
+ const pinned = options.includeDependencies === false ? void 0 : detectDependencies();
1159
+ const git = options.includeGit === false ? void 0 : detectGitSHA();
1160
+ const container = options.includeContainer === false ? void 0 : detectContainerHash();
1161
+ const os = PLATFORM_NAMES[process.platform] ?? process.platform;
1162
+ const arch = ARCH_NAMES[process.arch] ?? process.arch;
1163
+ return {
1164
+ declared: true,
1165
+ runtime_name: "node",
1166
+ runtime_version: process.versions.node,
1167
+ platform: `${os}/${arch}`,
1168
+ ...pinned ? { pinned_dependencies: pinned } : {},
1169
+ ...git ? { git_commit_sha: git } : {},
1170
+ ...container ? { container_image_hash: container } : {},
1171
+ ...seeds.length > 0 ? { random_seeds: seeds } : {}
1172
+ };
1173
+ }
1174
+
1175
+ // src/types.ts
1176
+ var RUN_STATUSES = [
1177
+ "ok",
1178
+ "probe",
1179
+ "pepg_deny",
1180
+ "ccv_halt",
1181
+ "macc_halt",
1182
+ "aivp_t4_halt",
1183
+ "attestation_gap",
1184
+ "stage_error"
1185
+ ];
1186
+
1187
+ // src/section0.ts
1188
+ import { createHash as createHash2 } from "crypto";
1189
+ var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
1190
+ var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
1191
+ var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
1192
+ var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
1193
+ function sha256Hex2(preimage) {
1194
+ return createHash2("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
1195
+ }
1196
+ function requireHex64(value, name) {
1197
+ try {
1198
+ validateHex64(value, name);
1199
+ } catch (err) {
1200
+ throw new VerdifaxError(
1201
+ `verdifax: section0: ${err instanceof Error ? err.message : String(err)}`
1202
+ );
1203
+ }
1204
+ }
1205
+ function dseDispatchHash(fields) {
1206
+ requireHex64(fields.program_id, "program_id");
1207
+ requireHex64(fields.registry_record_hash, "registry_record_hash");
1208
+ return sha256Hex2(
1209
+ [
1210
+ DSE_FORMULA_VERSION,
1211
+ fields.envelope_id,
1212
+ fields.sequence_id,
1213
+ fields.program_id,
1214
+ fields.registry_record_hash
1215
+ ].join(".")
1216
+ );
1217
+ }
1218
+ function dcaeClosureHash(fields) {
1219
+ requireHex64(fields.manifest_hash, "manifest_hash");
1220
+ requireHex64(fields.aer_hash, "aer_hash");
1221
+ requireHex64(fields.zksp_binding_hash, "zksp_binding_hash");
1222
+ return sha256Hex2(
1223
+ [
1224
+ DCAE_PREIMAGE_VERSION,
1225
+ fields.manifest_hash,
1226
+ fields.aer_hash,
1227
+ fields.envelope_id,
1228
+ fields.zksp_binding_hash,
1229
+ fields.formal_verifier_status
1230
+ ].join("|")
1231
+ );
1232
+ }
1233
+ function cresReceiptHash(fields) {
1234
+ requireHex64(fields.ciphertext_hash_at_shred, "ciphertext_hash_at_shred");
1235
+ return sha256Hex2(
1236
+ [
1237
+ CRES_PREIMAGE_VERSION,
1238
+ fields.envelope_id,
1239
+ fields.field_path,
1240
+ fields.deletion_clock,
1241
+ fields.actor_id,
1242
+ fields.ciphertext_hash_at_shred,
1243
+ fields.dsar_reference,
1244
+ fields.engine_version ?? CRES_ENGINE_VERSION
1245
+ ].join("|")
1246
+ );
1247
+ }
532
1248
  export {
533
1249
  APIError,
534
1250
  APIKeyRequiredError,
535
1251
  AuthError,
1252
+ CRES_ENGINE_VERSION,
1253
+ CRES_PREIMAGE_VERSION,
1254
+ DCAE_PREIMAGE_VERSION,
1255
+ DSE_FORMULA_VERSION,
1256
+ MAX_BATCH_ROWS,
536
1257
  NotFoundError,
1258
+ RUN_STATUSES,
537
1259
  VerdifaxClient,
538
1260
  VerdifaxError,
1261
+ VerdifaxJob,
1262
+ captureEnvironment,
1263
+ cresReceiptHash,
1264
+ dcaeClosureHash,
1265
+ dseDispatchHash,
1266
+ formatProviderPayload,
539
1267
  fromEnv,
540
1268
  generateRequestKeyPair,
1269
+ requestKeyChangeApproval,
1270
+ requestKeyChangePreimage,
541
1271
  requestKeyRegistrationBody,
542
1272
  requestSignaturePreimage,
1273
+ requestSignaturePreimageV2,
543
1274
  signRequest,
1275
+ signRequestV2,
544
1276
  validateHex64,
1277
+ validateJobName,
545
1278
  validateRouteId
546
1279
  };