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.js CHANGED
@@ -23,15 +23,32 @@ __export(index_exports, {
23
23
  APIError: () => APIError,
24
24
  APIKeyRequiredError: () => APIKeyRequiredError,
25
25
  AuthError: () => AuthError,
26
+ CRES_ENGINE_VERSION: () => CRES_ENGINE_VERSION,
27
+ CRES_PREIMAGE_VERSION: () => CRES_PREIMAGE_VERSION,
28
+ DCAE_PREIMAGE_VERSION: () => DCAE_PREIMAGE_VERSION,
29
+ DSE_FORMULA_VERSION: () => DSE_FORMULA_VERSION,
30
+ MAX_BATCH_ROWS: () => MAX_BATCH_ROWS,
26
31
  NotFoundError: () => NotFoundError,
32
+ RUN_STATUSES: () => RUN_STATUSES,
27
33
  VerdifaxClient: () => VerdifaxClient,
28
34
  VerdifaxError: () => VerdifaxError,
35
+ VerdifaxJob: () => VerdifaxJob,
36
+ captureEnvironment: () => captureEnvironment,
37
+ cresReceiptHash: () => cresReceiptHash,
38
+ dcaeClosureHash: () => dcaeClosureHash,
39
+ dseDispatchHash: () => dseDispatchHash,
40
+ formatProviderPayload: () => formatProviderPayload,
29
41
  fromEnv: () => fromEnv,
30
42
  generateRequestKeyPair: () => generateRequestKeyPair,
43
+ requestKeyChangeApproval: () => requestKeyChangeApproval,
44
+ requestKeyChangePreimage: () => requestKeyChangePreimage,
31
45
  requestKeyRegistrationBody: () => requestKeyRegistrationBody,
32
46
  requestSignaturePreimage: () => requestSignaturePreimage,
47
+ requestSignaturePreimageV2: () => requestSignaturePreimageV2,
33
48
  signRequest: () => signRequest,
49
+ signRequestV2: () => signRequestV2,
34
50
  validateHex64: () => validateHex64,
51
+ validateJobName: () => validateJobName,
35
52
  validateRouteId: () => validateRouteId
36
53
  });
37
54
  module.exports = __toCommonJS(index_exports);
@@ -61,15 +78,23 @@ var NotFoundError = class extends VerdifaxError {
61
78
  name = "VerdifaxNotFoundError";
62
79
  };
63
80
  var APIError = class extends VerdifaxError {
64
- constructor(status, path, message) {
81
+ constructor(status, path, message, recordHash, signedRequest) {
65
82
  super(`verdifax: API error at ${path} (HTTP ${status}): ${message}`);
66
83
  this.status = status;
67
84
  this.path = path;
68
85
  this.message = message;
86
+ this.recordHash = recordHash;
87
+ if (signedRequest !== void 0) {
88
+ Object.defineProperty(this, "signedRequest", {
89
+ value: signedRequest,
90
+ enumerable: false
91
+ });
92
+ }
69
93
  }
70
94
  status;
71
95
  path;
72
96
  message;
97
+ recordHash;
73
98
  name = "VerdifaxAPIError";
74
99
  };
75
100
  var APIKeyRequiredError = class extends VerdifaxError {
@@ -81,8 +106,137 @@ var APIKeyRequiredError = class extends VerdifaxError {
81
106
 
82
107
  // src/signing.ts
83
108
  var import_node_crypto = require("crypto");
109
+
110
+ // src/validation.ts
111
+ function validateHex64(value, fieldName) {
112
+ if (typeof value !== "string" || value.length !== 64) {
113
+ throw new VerdifaxError(
114
+ `verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
115
+ );
116
+ }
117
+ if (!/^[0-9a-f]{64}$/.test(value)) {
118
+ throw new VerdifaxError(
119
+ `verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
120
+ );
121
+ }
122
+ }
123
+ function validateRouteId(value) {
124
+ if (!value || typeof value !== "string") {
125
+ throw new VerdifaxError("verdifax: route_id is required");
126
+ }
127
+ if (value.length > 256) {
128
+ throw new VerdifaxError(
129
+ `verdifax: route_id is too long (${value.length} > 256)`
130
+ );
131
+ }
132
+ if (!/^[\x20-\x7E]+$/.test(value)) {
133
+ throw new VerdifaxError(
134
+ "verdifax: route_id contains non-printable-ASCII character"
135
+ );
136
+ }
137
+ }
138
+ function validateJobName(value, fieldName) {
139
+ if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
140
+ throw new VerdifaxError(
141
+ `verdifax: ${fieldName} must be 1 to 128 letters, digits, dots, underscores, colons or dashes`
142
+ );
143
+ }
144
+ }
145
+
146
+ // src/helpers.ts
147
+ function formatProviderPayload(provider, prompt, response) {
148
+ if ([provider, prompt, response].some((text) => /[\uD800-\uDFFF]/u.test(text))) {
149
+ throw new VerdifaxError(
150
+ "verdifax: provider payload text cannot be encoded as UTF-8"
151
+ );
152
+ }
153
+ return JSON.stringify({ prompt, provider, response });
154
+ }
155
+ function isProvided(value) {
156
+ return value !== null && value !== void 0 && value !== "" && value !== false;
157
+ }
158
+ function withAttestedFlag(context) {
159
+ const provided = Object.entries(context).some(
160
+ ([name, value]) => name !== "attested" && isProvided(value)
161
+ );
162
+ return { ...context, attested: provided };
163
+ }
164
+ function withDeclaredFlag(context) {
165
+ const declared = Object.entries(context).some(
166
+ ([name, value]) => name !== "declared" && isProvided(value) && !(Array.isArray(value) && value.length === 0)
167
+ );
168
+ return { ...context, declared };
169
+ }
170
+ function refuseCallerOriginHashes(substance) {
171
+ const origin = substance?.origin;
172
+ if (origin?.source_ip_hash || origin?.user_agent_hash) {
173
+ throw new VerdifaxError(
174
+ "verdifax: request_substance.origin.source_ip_hash and user_agent_hash are computed by the server and cannot be supplied"
175
+ );
176
+ }
177
+ }
178
+ var JOB_FIELDS = {
179
+ job_id: true,
180
+ step_id: true,
181
+ previous_record_hashes: true,
182
+ external_previous: true,
183
+ final: true
184
+ };
185
+ function refuseUnknownFields(value, known, label) {
186
+ for (const name of Object.keys(value)) {
187
+ if (!Object.hasOwn(known, name)) {
188
+ throw new VerdifaxError(`verdifax: ${label} has an unknown field: ${name}`);
189
+ }
190
+ }
191
+ }
192
+ function sortedUnique(hashes, fieldName) {
193
+ if (hashes === void 0) return [];
194
+ if (!Array.isArray(hashes) || hashes.some((hash) => typeof hash !== "string")) {
195
+ throw new VerdifaxError(`verdifax: ${fieldName} must be a list of strings`);
196
+ }
197
+ return [...new Set(hashes)].sort();
198
+ }
199
+ function normalizedJob(job) {
200
+ if (typeof job !== "object" || job === null || Array.isArray(job)) {
201
+ throw new VerdifaxError("verdifax: job must be an object");
202
+ }
203
+ refuseUnknownFields(job, JOB_FIELDS, "job");
204
+ validateJobName(job.job_id, "job_id");
205
+ validateJobName(job.step_id, "step_id");
206
+ if (job.final !== void 0 && typeof job.final !== "boolean") {
207
+ throw new VerdifaxError("verdifax: job.final must be true or false");
208
+ }
209
+ const out = { job_id: job.job_id, step_id: job.step_id };
210
+ const previous = sortedUnique(job.previous_record_hashes, "previous_record_hashes");
211
+ if (previous.length > 0) out.previous_record_hashes = previous;
212
+ const external = sortedUnique(job.external_previous, "external_previous");
213
+ if (external.length > 0) out.external_previous = external;
214
+ if (job.final) out.final = true;
215
+ return out;
216
+ }
217
+ function withExecuteFlags(req) {
218
+ const body = { ...req };
219
+ if (req.job === void 0 || req.job === null) {
220
+ delete body.job;
221
+ } else {
222
+ body.job = normalizedJob(req.job);
223
+ }
224
+ if (req.attested_context) {
225
+ body.attested_context = withAttestedFlag(req.attested_context);
226
+ }
227
+ if (req.reproducibility_context) {
228
+ body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
229
+ }
230
+ return body;
231
+ }
232
+
233
+ // src/signing.ts
84
234
  var REQUEST_SIGNATURE_FORM = "verdifax.request.v1";
235
+ var REQUEST_SIGNATURE_FORM_V2 = "verdifax.request.v2";
236
+ var REQUEST_SIGNATURE_VERSION_2 = "2";
237
+ var JOB_FORM = "verdifax.job.v1";
85
238
  var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
239
+ var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
86
240
  var SIGNING_KEY_BYTES = 32;
87
241
  var PKCS8_ED25519_HEADER = Buffer.from(
88
242
  "302e020100300506032b657004220420",
@@ -113,6 +267,12 @@ function loadSigningKey(key) {
113
267
  type: "pkcs8"
114
268
  });
115
269
  }
270
+ function sha256Hex(text) {
271
+ return (0, import_node_crypto.createHash)("sha256").update(Buffer.from(text, "utf8")).digest("hex");
272
+ }
273
+ function signedAtNow() {
274
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
275
+ }
116
276
  function requestSignaturePreimage(fields) {
117
277
  return Buffer.from(
118
278
  [
@@ -127,13 +287,62 @@ function requestSignaturePreimage(fields) {
127
287
  "utf8"
128
288
  );
129
289
  }
290
+ function requestSignaturePreimageV2(fields) {
291
+ const { job } = fields;
292
+ return Buffer.from(
293
+ [
294
+ REQUEST_SIGNATURE_FORM_V2,
295
+ fields.body_hash,
296
+ fields.payload_hash,
297
+ fields.program_id,
298
+ fields.route_id,
299
+ fields.registry_record_hash,
300
+ fields.ai_output_hash,
301
+ JOB_FORM,
302
+ job?.job_id ?? "",
303
+ job?.step_id ?? "",
304
+ (job?.previous_record_hashes ?? []).join(","),
305
+ (job?.external_previous ?? []).join(","),
306
+ job?.final ? "true" : "false",
307
+ fields.signed_at,
308
+ fields.nonce
309
+ ].join("\0"),
310
+ "utf8"
311
+ );
312
+ }
313
+ function signRequestV2(signingKey, req, options = {}) {
314
+ const key = loadSigningKey(signingKey);
315
+ const { request_signature: _unsigned, ...fields } = withExecuteFlags(req);
316
+ const signed_request = JSON.stringify(fields);
317
+ const signed_at = options.signedAt ?? signedAtNow();
318
+ const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
319
+ const preimage = requestSignaturePreimageV2({
320
+ body_hash: sha256Hex(signed_request),
321
+ payload_hash: sha256Hex(fields.payload_text || fields.payload || ""),
322
+ program_id: fields.program_id,
323
+ route_id: fields.route_id,
324
+ registry_record_hash: fields.registry_record_hash,
325
+ ai_output_hash: fields.ai_output_text ? sha256Hex(fields.ai_output_text) : "",
326
+ job: fields.job,
327
+ signed_at,
328
+ nonce
329
+ });
330
+ return {
331
+ signed_request,
332
+ request_signature: {
333
+ version: REQUEST_SIGNATURE_VERSION_2,
334
+ signature: (0, import_node_crypto.sign)(null, preimage, key).toString("base64"),
335
+ signed_at,
336
+ nonce
337
+ }
338
+ };
339
+ }
130
340
  function signRequest(signingKey, req, options = {}) {
131
341
  const key = loadSigningKey(signingKey);
132
- const signed_at = options.signedAt ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
342
+ const signed_at = options.signedAt ?? signedAtNow();
133
343
  const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
134
- const payload_hash = (0, import_node_crypto.createHash)("sha256").update(Buffer.from(req.payload_text || req.payload || "", "utf8")).digest("hex");
135
344
  const preimage = requestSignaturePreimage({
136
- payload_hash,
345
+ payload_hash: sha256Hex(req.payload_text || req.payload || ""),
137
346
  program_id: req.program_id,
138
347
  route_id: req.route_id,
139
348
  registry_record_hash: req.registry_record_hash,
@@ -156,38 +365,132 @@ function requestKeyRegistrationBody(signingKey) {
156
365
  ).toString("base64");
157
366
  return { public_key, proof };
158
367
  }
368
+ function requestKeyChangePreimage(fields) {
369
+ return Buffer.from(
370
+ [
371
+ REQUEST_KEY_CHANGE_FORM,
372
+ fields.new_public_key,
373
+ fields.signed_at,
374
+ fields.nonce
375
+ ].join("\0"),
376
+ "utf8"
377
+ );
378
+ }
379
+ function requestKeyChangeApproval(currentSigningKey, newPublicKey, options = {}) {
380
+ const key = loadSigningKey(currentSigningKey);
381
+ const signed_at = options.signedAt ?? signedAtNow();
382
+ const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
383
+ const preimage = requestKeyChangePreimage({
384
+ new_public_key: newPublicKey,
385
+ signed_at,
386
+ nonce
387
+ });
388
+ return {
389
+ signature: (0, import_node_crypto.sign)(null, preimage, key).toString("base64"),
390
+ signed_at,
391
+ nonce
392
+ };
393
+ }
159
394
 
160
- // src/validation.ts
161
- function validateHex64(value, fieldName) {
162
- if (typeof value !== "string" || value.length !== 64) {
163
- throw new VerdifaxError(
164
- `verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
165
- );
395
+ // src/job.ts
396
+ var STEP_OPTIONS = {
397
+ payload: true,
398
+ payload_text: true,
399
+ program_id: true,
400
+ route_id: true,
401
+ registry_record_hash: true,
402
+ ai_output_text: true,
403
+ attested_context: true,
404
+ reproducibility_context: true,
405
+ request_substance: true,
406
+ authorization_chain: true,
407
+ regulatory_scaffolding: true,
408
+ causal_graph: true,
409
+ after: true,
410
+ external_previous: true,
411
+ final: true
412
+ };
413
+ var VerdifaxJob = class {
414
+ jobId;
415
+ client;
416
+ hashes = /* @__PURE__ */ new Map();
417
+ #signedRequests = /* @__PURE__ */ new Map();
418
+ lastStepId;
419
+ constructor(client, jobId) {
420
+ validateJobName(jobId, "job_id");
421
+ this.client = client;
422
+ this.jobId = jobId;
166
423
  }
167
- if (!/^[0-9a-f]{64}$/.test(value)) {
168
- throw new VerdifaxError(
169
- `verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
170
- );
424
+ /** Step id to record hash for every step that left a record, on a copy. */
425
+ get recordHashes() {
426
+ return Object.fromEntries(this.hashes);
171
427
  }
172
- }
173
- function validateRouteId(value) {
174
- if (!value || typeof value !== "string") {
175
- throw new VerdifaxError("verdifax: route_id is required");
428
+ /** Step id to the exact text that was signed, for every signed step that
429
+ * left a record, refused and halted steps included, on a copy. Each text
430
+ * can be passed to `verdifax-verify --request FILE`. */
431
+ get signedRequests() {
432
+ return Object.fromEntries(this.#signedRequests);
176
433
  }
177
- if (value.length > 256) {
178
- throw new VerdifaxError(
179
- `verdifax: route_id is too long (${value.length} > 256)`
180
- );
434
+ /** Runs one step and records its hash and signed text. A refused or halted
435
+ * step records both from the error, then throws the same error `attest`
436
+ * throws. */
437
+ async step(stepId, options) {
438
+ validateJobName(stepId, "step_id");
439
+ refuseUnknownFields(options, STEP_OPTIONS, "step options");
440
+ const { after, external_previous, final, ...attest } = options;
441
+ const job = {
442
+ job_id: this.jobId,
443
+ step_id: stepId,
444
+ previous_record_hashes: this.previousHashes(after),
445
+ external_previous,
446
+ final
447
+ };
448
+ let result;
449
+ try {
450
+ result = await this.client.attest({ ...attest, job });
451
+ } catch (err) {
452
+ if (err instanceof APIError && err.recordHash) {
453
+ this.record(stepId, err.recordHash, err.signedRequest);
454
+ }
455
+ throw err;
456
+ }
457
+ const hash = result?.record_hash || result?.manifest_hash || result?.manifest?.ManifestHash;
458
+ if (!hash) {
459
+ throw new VerdifaxError(
460
+ `verdifax: the server returned no record_hash or manifest_hash for step ${stepId}, so the job map cannot continue from it`
461
+ );
462
+ }
463
+ this.record(stepId, hash, result.signed_request);
464
+ return result;
181
465
  }
182
- if (!/^[\x20-\x7E]+$/.test(value)) {
183
- throw new VerdifaxError(
184
- "verdifax: route_id contains non-printable-ASCII character"
185
- );
466
+ previousHashes(after) {
467
+ if (after === void 0 || after === null) {
468
+ return this.lastStepId === void 0 ? [] : [this.hashes.get(this.lastStepId)];
469
+ }
470
+ if (!Array.isArray(after)) {
471
+ throw new VerdifaxError("verdifax: after must be a list of step ids");
472
+ }
473
+ return after.map((stepId) => {
474
+ const hash = this.hashes.get(stepId);
475
+ if (hash === void 0) {
476
+ throw new VerdifaxError(
477
+ `verdifax: after names ${String(stepId)}, which is not a step in job ${this.jobId}`
478
+ );
479
+ }
480
+ return hash;
481
+ });
186
482
  }
187
- }
483
+ record(stepId, hash, signedRequest) {
484
+ this.hashes.set(stepId, hash);
485
+ if (signedRequest !== void 0) {
486
+ this.#signedRequests.set(stepId, signedRequest);
487
+ }
488
+ this.lastStepId = stepId;
489
+ }
490
+ };
188
491
 
189
492
  // src/version.ts
190
- var SDK_VERSION = "0.3.0";
493
+ var SDK_VERSION = "1.1.0";
191
494
 
192
495
  // src/client.ts
193
496
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
@@ -197,7 +500,10 @@ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
197
500
  var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
198
501
  var HEADER_API_KEY = "X-Verdifax-Key";
199
502
  var REQUEST_KEY_PATH = "/keys/request-key";
503
+ var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
504
+ var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
200
505
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
506
+ var MAX_BATCH_ROWS = 1e3;
201
507
  var VerdifaxClient = class {
202
508
  baseURL;
203
509
  apiKey;
@@ -222,21 +528,73 @@ var VerdifaxClient = class {
222
528
  async version() {
223
529
  return this.request("GET", "/version");
224
530
  }
531
+ /** The server's own API description. */
532
+ async getOpenAPI() {
533
+ return this.request("GET", "/openapi.json");
534
+ }
535
+ /** Run counts for this API key. */
536
+ async getStats() {
537
+ this.requireAPIKey();
538
+ return this.request("GET", "/stats");
539
+ }
225
540
  // ── Execute / Attest (auth) ───────────────────────────────────
226
541
  async execute(req) {
227
- this.requireAPIKey();
228
- if (!req.payload && !req.payload_text) {
542
+ this.requireExecuteFields(req);
543
+ const sent = this.prepared(req);
544
+ const out = await this.request("POST", "/execute", sent);
545
+ return withSignedText(out, sent);
546
+ }
547
+ /** Runs one payload twice on the server and compares the manifest
548
+ * hashes. One signature covers the request, and the server claims
549
+ * its nonce once. Both runs are recorded. A run that happens twice
550
+ * cannot be one job step, so a `job` is refused here. */
551
+ async verifyDeterminism(req) {
552
+ this.requireExecuteFields(req);
553
+ if (req.job !== void 0 && req.job !== null) {
229
554
  throw new VerdifaxError(
230
- "verdifax: ExecuteRequest needs payload or payload_text"
555
+ "verdifax: a determinism check runs twice, so it cannot be a job step"
231
556
  );
232
557
  }
233
- if (!req.program_id || !req.route_id || !req.registry_record_hash) {
558
+ const sent = this.prepared(req);
559
+ const out = await this.request(
560
+ "POST",
561
+ "/execute/verify-determinism",
562
+ sent
563
+ );
564
+ return withSignedText(out, sent);
565
+ }
566
+ /** Sends up to {@link MAX_BATCH_ROWS} runs in one request. With a
567
+ * signing key each row carries its own signature and nonce. A row
568
+ * that fails comes back in `results` with `ok: false`, so the call
569
+ * does not throw for it. The rows are not checked here, except a job
570
+ * block, which is refused before anything is sent: the server reports
571
+ * any other bad row in that row's result.
572
+ *
573
+ * This is one POST and it is never retried. After a timeout the
574
+ * server may have run some or all of the rows. */
575
+ async executeBatch(runs) {
576
+ this.requireAPIKey();
577
+ if (!Array.isArray(runs) || runs.length === 0) {
578
+ throw new VerdifaxError("verdifax: executeBatch needs at least one run");
579
+ }
580
+ if (runs.length > MAX_BATCH_ROWS) {
234
581
  throw new VerdifaxError(
235
- "verdifax: program_id, route_id, and registry_record_hash are required"
582
+ `verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
236
583
  );
237
584
  }
238
- const body = this.signingKey ? { ...req, request_signature: signRequest(this.signingKey, req) } : req;
239
- return this.request("POST", "/execute", body);
585
+ const sent = runs.map((run) => this.prepared(run));
586
+ const out = await this.request(
587
+ "POST",
588
+ "/execute/batch",
589
+ { runs: sent }
590
+ );
591
+ if (!Array.isArray(out?.results)) return out;
592
+ return {
593
+ ...out,
594
+ results: out.results.map(
595
+ (row, i) => i < sent.length ? withSignedText(row, sent[i]) : row
596
+ )
597
+ };
240
598
  }
241
599
  /** High-level convenience wrapper around `execute()`. Accepts
242
600
  * either a Buffer (for binary payloads, auto-base64) or a
@@ -262,7 +620,13 @@ var VerdifaxClient = class {
262
620
  route_id: req.route_id,
263
621
  registry_record_hash: req.registry_record_hash,
264
622
  ai_output_text: req.ai_output_text,
265
- attested_context: req.attested_context
623
+ attested_context: req.attested_context,
624
+ reproducibility_context: req.reproducibility_context,
625
+ request_substance: req.request_substance,
626
+ authorization_chain: req.authorization_chain,
627
+ regulatory_scaffolding: req.regulatory_scaffolding,
628
+ causal_graph: req.causal_graph,
629
+ job: req.job
266
630
  };
267
631
  if (req.payload !== void 0) {
268
632
  body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
@@ -271,6 +635,12 @@ var VerdifaxClient = class {
271
635
  }
272
636
  return this.execute(body);
273
637
  }
638
+ /** A helper that runs the steps of one job through {@link attest} and
639
+ * keeps each step's record hash, so a later step can name it. It makes
640
+ * no request until a step runs. */
641
+ job(jobId) {
642
+ return new VerdifaxJob(this, jobId);
643
+ }
274
644
  // ── Request signing (auth) ───────────────────────────────────
275
645
  /** Registers this client's public key on its API key, with the proof
276
646
  * that the client holds the private key. From then on the server
@@ -288,11 +658,48 @@ var VerdifaxClient = class {
288
658
  requestKeyRegistrationBody(this.signingKey)
289
659
  );
290
660
  }
661
+ /** Replaces the registered key with `newSigningKey`. The new key proves
662
+ * itself and this client's current key approves the change. After
663
+ * success this client signs with the new key. If the call fails it
664
+ * keeps the current key, including after a timeout, when the server may
665
+ * have made the change. */
666
+ async rotateRequestKey(newSigningKey) {
667
+ this.requireAPIKey();
668
+ if (!this.signingKey) {
669
+ throw new VerdifaxError(
670
+ "verdifax: rotateRequestKey needs the current key in the signingKey client option"
671
+ );
672
+ }
673
+ const next = loadSigningKey(newSigningKey);
674
+ const registration = requestKeyRegistrationBody(next);
675
+ const out = await this.request(
676
+ "POST",
677
+ REQUEST_KEY_PATH,
678
+ {
679
+ ...registration,
680
+ change: requestKeyChangeApproval(
681
+ this.signingKey,
682
+ registration.public_key
683
+ )
684
+ }
685
+ );
686
+ this.signingKey = next;
687
+ return out;
688
+ }
291
689
  /** Clears the registered public key, so the server stops requiring
292
- * request signatures on this API key. */
690
+ * request signatures on this API key. With a signing key the client
691
+ * sends that key's approval, which the server requires while a key is
692
+ * registered. After success the client drops its signing key and sends
693
+ * unsigned requests, because the server refuses a signed run on an API
694
+ * key with no registered key. If the call fails it keeps the key. */
293
695
  async clearRequestKey() {
294
696
  this.requireAPIKey();
295
- await this.request("DELETE", REQUEST_KEY_PATH);
697
+ await this.request(
698
+ "DELETE",
699
+ REQUEST_KEY_PATH,
700
+ this.signingKey ? { change: requestKeyChangeApproval(this.signingKey, "") } : void 0
701
+ );
702
+ this.signingKey = void 0;
296
703
  }
297
704
  // ── Runs ──────────────────────────────────────────────────────
298
705
  async getRun(runId) {
@@ -319,6 +726,95 @@ var VerdifaxClient = class {
319
726
  const out = await this.request("GET", path);
320
727
  return { runs: out.runs ?? [], total: out.total ?? 0 };
321
728
  }
729
+ /** Searches this key's runs. `manifest_hash` matches a prefix. The
730
+ * server ignores a `from` or `to` it cannot parse here, unlike
731
+ * {@link exportRuns}, which refuses one. */
732
+ async searchRuns(params = {}) {
733
+ this.requireAPIKey();
734
+ const q = new URLSearchParams();
735
+ for (const name of [
736
+ "manifest_hash",
737
+ "program_id",
738
+ "route_id",
739
+ "status",
740
+ "from",
741
+ "to"
742
+ ]) {
743
+ const value = params[name];
744
+ if (value) q.set(name, value);
745
+ }
746
+ if (params.limit && params.limit > 0) {
747
+ q.set("limit", String(params.limit));
748
+ }
749
+ if (params.offset && params.offset > 0) {
750
+ q.set("offset", String(params.offset));
751
+ }
752
+ const qs = q.toString();
753
+ const path = qs ? `/runs/search?${qs}` : "/runs/search";
754
+ const out = await this.request("GET", path);
755
+ return { runs: out.runs ?? [], total: out.total ?? 0 };
756
+ }
757
+ /** Lists what the server holds for this key in a period, failures
758
+ * included, up to 10,000 rows. `truncated` is true when the period
759
+ * holds more. The list states what was stored, not a verdict, and a
760
+ * call that never reached Verdifax cannot appear in it. */
761
+ async exportRuns(params = {}) {
762
+ this.requireAPIKey();
763
+ return this.request("GET", this.exportPath(params, "json"));
764
+ }
765
+ /** The same rows as {@link exportRuns}: the raw CSV text with `total`
766
+ * and `truncated` from the response headers. Check `truncated`: when
767
+ * true, the CSV is missing rows for the period. Throws when the server
768
+ * does not send those headers, so that a cut-off export cannot pass
769
+ * for a complete one. */
770
+ async exportRunsCSV(params = {}) {
771
+ this.requireAPIKey();
772
+ const { text, headers } = await this.requestRaw(
773
+ this.exportPath(params, "csv")
774
+ );
775
+ const total = Number.parseInt(headers.get(HEADER_EXPORT_TOTAL) ?? "", 10);
776
+ const truncated = headers.get(HEADER_EXPORT_TRUNCATED);
777
+ if (!Number.isSafeInteger(total) || truncated !== "true" && truncated !== "false") {
778
+ throw new VerdifaxError(
779
+ `verdifax: the server did not report ${HEADER_EXPORT_TOTAL} and ${HEADER_EXPORT_TRUNCATED}, so this CSV may be cut off; use exportRuns instead`
780
+ );
781
+ }
782
+ return { csv: text, total, truncated: truncated === "true" };
783
+ }
784
+ exportPath(params, format) {
785
+ const q = new URLSearchParams();
786
+ if (params.from) q.set("from", params.from);
787
+ if (params.to) q.set("to", params.to);
788
+ if (params.job_id) q.set("job_id", params.job_id);
789
+ q.set("format", format);
790
+ return `/runs/export?${q.toString()}`;
791
+ }
792
+ // ── Attestation receipts (auth) ──────────────────────────────
793
+ async listAttestations(params = {}) {
794
+ this.requireAPIKey();
795
+ const q = new URLSearchParams();
796
+ if (params.limit && params.limit > 0) {
797
+ q.set("limit", String(params.limit));
798
+ }
799
+ if (params.offset && params.offset > 0) {
800
+ q.set("offset", String(params.offset));
801
+ }
802
+ const qs = q.toString();
803
+ return this.request(
804
+ "GET",
805
+ qs ? `/attestations?${qs}` : "/attestations"
806
+ );
807
+ }
808
+ async getAttestation(attestationId) {
809
+ this.requireAPIKey();
810
+ if (attestationId <= 0) {
811
+ throw new VerdifaxError("verdifax: attestationId must be > 0");
812
+ }
813
+ return this.request(
814
+ "GET",
815
+ `/attestations/${attestationId}`
816
+ );
817
+ }
322
818
  // ── Sealed-artifact retrieval (auth) ─────────────────────────
323
819
  async getAllowToken(runId) {
324
820
  return this.getRunArtifact(runId, "allow-token");
@@ -335,6 +831,39 @@ var VerdifaxClient = class {
335
831
  async getAivpT4HaltReceipt(runId) {
336
832
  return this.getRunArtifact(runId, "aivp-t4-halt-receipt");
337
833
  }
834
+ async getAttestationGapReceipt(runId) {
835
+ return this.getRunArtifact(runId, "attestation-gap-receipt");
836
+ }
837
+ async getCESAuditRecord(runId) {
838
+ return this.getRunArtifact(runId, "ces-audit-record");
839
+ }
840
+ /** The audit report for a run, as JSON. */
841
+ async getRunReport(runId) {
842
+ const out = await this.getRunArtifact(runId, "report");
843
+ return out.report;
844
+ }
845
+ /** The run's sealed audit bundle as the exact JSON text the server
846
+ * sent, to save and check with `verdifax-verify`. It is not parsed:
847
+ * JSON.parse would round a number above 2^53 and the recomputed hash
848
+ * would stop matching. */
849
+ async getRunArtifacts(runId) {
850
+ this.requireAPIKey();
851
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
852
+ return (await this.requestRaw(`/runs/${runId}/artifacts`)).text;
853
+ }
854
+ /** The public audit bundle of a record published by run id, as the
855
+ * exact JSON text the server sent. Needs no API key. */
856
+ async getPublicBundle(runId) {
857
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
858
+ return (await this.requestRaw(`/runs/${runId}/bundle.json`)).text;
859
+ }
860
+ /** The public audit bundle a share token points to, as the exact JSON
861
+ * text the server sent. Needs no API key. */
862
+ async getPublicBundleByToken(token) {
863
+ if (!token) throw new VerdifaxError("verdifax: token is required");
864
+ const path = `/public/runs/${encodeURIComponent(token)}/bundle.json`;
865
+ return (await this.requestRaw(path)).text;
866
+ }
338
867
  async getRunArtifact(runId, suffix) {
339
868
  this.requireAPIKey();
340
869
  if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
@@ -420,14 +949,26 @@ var VerdifaxClient = class {
420
949
  }
421
950
  await this.request("DELETE", `/admin/keys/${keyId}`);
422
951
  }
952
+ /** Clears the request key on an API key whose holder lost it, after a
953
+ * manual check. It is the one way to clear a key without that key. The
954
+ * id can be any API key, so this client keeps its own signing key. */
955
+ async adminClearRequestKey(keyId) {
956
+ this.requireAPIKey();
957
+ if (keyId <= 0) {
958
+ throw new VerdifaxError("verdifax: keyId must be > 0");
959
+ }
960
+ await this.request("DELETE", `/admin/keys/${keyId}/request-key`);
961
+ }
423
962
  // ── Provider helpers (Claude / OpenAI) ───────────────────────
424
- /** Convenience for attesting an Anthropic Claude API response.
425
- * Wires the response text into the AIVP governance pipeline
426
- * by setting `ai_output_text`. */
963
+ /** Convenience for attesting an Anthropic Claude API response. With
964
+ * `govern: true` the response text also goes to the AIVP review as
965
+ * `ai_output_text`; that is off by default, so the text leaves your
966
+ * machine for review only when you ask. */
427
967
  async attestClaudeResponse(args) {
428
968
  return this.attestProviderResponse({ ...args, provider: "anthropic" });
429
969
  }
430
- /** Convenience for attesting an OpenAI API response. */
970
+ /** Convenience for attesting an OpenAI API response. `govern` works as
971
+ * in {@link attestClaudeResponse}. */
431
972
  async attestOpenAIResponse(args) {
432
973
  return this.attestProviderResponse({ ...args, provider: "openai" });
433
974
  }
@@ -437,25 +978,25 @@ var VerdifaxClient = class {
437
978
  "verdifax: response is required for attestation"
438
979
  );
439
980
  }
440
- const payloadText = JSON.stringify({
441
- provider: args.provider,
442
- prompt: args.prompt,
443
- response: args.response
444
- });
445
981
  return this.attest({
446
- payload_text: payloadText,
982
+ payload_text: formatProviderPayload(
983
+ args.provider,
984
+ args.prompt,
985
+ args.response
986
+ ),
447
987
  program_id: args.program_id,
448
988
  route_id: args.route_id,
449
989
  registry_record_hash: args.registry_record_hash,
450
- ai_output_text: args.response,
990
+ ai_output_text: args.govern ? args.response : void 0,
451
991
  attested_context: {
452
992
  actor_role: "ai_assistant",
453
993
  model_provider: args.provider
454
- }
994
+ },
995
+ job: args.job
455
996
  });
456
997
  }
457
998
  // ── HTTP plumbing, retry-aware ──────────────────────────────
458
- async request(method, path, body) {
999
+ async request(method, path, body, rawText = false) {
459
1000
  const url = this.baseURL + path;
460
1001
  const headers = {
461
1002
  "User-Agent": this.userAgent
@@ -498,9 +1039,13 @@ var VerdifaxClient = class {
498
1039
  continue;
499
1040
  }
500
1041
  let errMessage = text.trim();
1042
+ let recordHash;
501
1043
  try {
502
1044
  const env = JSON.parse(text);
503
1045
  if (env.error) errMessage = env.error;
1046
+ if (typeof env.record_hash === "string" && env.record_hash) {
1047
+ recordHash = env.record_hash;
1048
+ }
504
1049
  } catch {
505
1050
  }
506
1051
  switch (resp.status) {
@@ -509,9 +1054,16 @@ var VerdifaxClient = class {
509
1054
  case 404:
510
1055
  throw new NotFoundError(path, errMessage);
511
1056
  default:
512
- throw new APIError(resp.status, path, errMessage);
1057
+ throw new APIError(
1058
+ resp.status,
1059
+ path,
1060
+ errMessage,
1061
+ recordHash,
1062
+ body?.signed_request
1063
+ );
513
1064
  }
514
1065
  }
1066
+ if (rawText) return { text, headers: resp.headers };
515
1067
  if (text.length === 0) return void 0;
516
1068
  try {
517
1069
  return JSON.parse(text);
@@ -525,17 +1077,46 @@ var VerdifaxClient = class {
525
1077
  `verdifax: retry budget exhausted${lastErr ? `: ${String(lastErr)}` : ""}`
526
1078
  );
527
1079
  }
1080
+ /** A GET whose body is kept as sent, retried like any other GET. */
1081
+ requestRaw(path) {
1082
+ return this.request("GET", path, void 0, true);
1083
+ }
528
1084
  requireAPIKey() {
529
1085
  if (!this.apiKey) throw new APIKeyRequiredError();
530
1086
  }
1087
+ requireExecuteFields(req) {
1088
+ this.requireAPIKey();
1089
+ if (!req.payload && !req.payload_text) {
1090
+ throw new VerdifaxError(
1091
+ "verdifax: ExecuteRequest needs payload or payload_text"
1092
+ );
1093
+ }
1094
+ if (!req.program_id || !req.route_id || !req.registry_record_hash) {
1095
+ throw new VerdifaxError(
1096
+ "verdifax: program_id, route_id, and registry_record_hash are required"
1097
+ );
1098
+ }
1099
+ validateHex64(req.program_id, "program_id");
1100
+ validateHex64(req.registry_record_hash, "registry_record_hash");
1101
+ validateRouteId(req.route_id);
1102
+ refuseCallerOriginHashes(req.request_substance);
1103
+ }
1104
+ /** The body to send, on a copy: the request with its derived flags or,
1105
+ * with a signing key, the signed text with a fresh signature and nonce. */
1106
+ prepared(req) {
1107
+ return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
1108
+ }
531
1109
  };
1110
+ function withSignedText(result, sent) {
1111
+ return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
1112
+ }
532
1113
 
533
1114
  // src/config.ts
534
1115
  function fromEnv() {
535
1116
  const cfg = {
536
- baseURL: process.env.VERDIFAX_BASE_URL ?? void 0,
1117
+ baseURL: process.env.VERDIFAX_BASE_URL || process.env.VERDIFAX_API_URL || void 0,
537
1118
  apiKey: process.env.VERDIFAX_API_KEY ?? void 0,
538
- userAgent: process.env.VERDIFAX_USER_AGENT ?? void 0
1119
+ userAgent: process.env.VERDIFAX_USER_AGENT || void 0
539
1120
  };
540
1121
  const timeoutRaw = process.env.VERDIFAX_TIMEOUT_MS;
541
1122
  if (timeoutRaw !== void 0 && timeoutRaw !== "") {
@@ -559,19 +1140,188 @@ function fromEnv() {
559
1140
  }
560
1141
  return new VerdifaxClient(cfg);
561
1142
  }
1143
+
1144
+ // src/environment.ts
1145
+ var import_node_child_process = require("child_process");
1146
+ var import_node_fs = require("fs");
1147
+ var import_node_path = require("path");
1148
+ var HASH_64 = /^[0-9a-f]{64}$/;
1149
+ var GIT_SHA = /^([0-9a-f]{40}|[0-9a-f]{64})$/;
1150
+ var PLATFORM_NAMES = { win32: "windows" };
1151
+ var ARCH_NAMES = { x64: "amd64", ia32: "386" };
1152
+ function packageVersion(dir) {
1153
+ try {
1154
+ const pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(dir, "package.json"), "utf8"));
1155
+ return pkg.name && pkg.version ? `${pkg.name}==${pkg.version}` : void 0;
1156
+ } catch {
1157
+ return void 0;
1158
+ }
1159
+ }
1160
+ function packageDirs(root) {
1161
+ const dirs = [];
1162
+ for (const entry of (0, import_node_fs.readdirSync)(root)) {
1163
+ if (entry.startsWith(".")) continue;
1164
+ if (!entry.startsWith("@")) {
1165
+ dirs.push((0, import_node_path.join)(root, entry));
1166
+ continue;
1167
+ }
1168
+ for (const scoped of (0, import_node_fs.readdirSync)((0, import_node_path.join)(root, entry))) {
1169
+ dirs.push((0, import_node_path.join)(root, entry, scoped));
1170
+ }
1171
+ }
1172
+ return dirs;
1173
+ }
1174
+ function detectDependencies() {
1175
+ try {
1176
+ const pins = packageDirs((0, import_node_path.join)(process.cwd(), "node_modules")).map(packageVersion).filter((pin) => pin !== void 0);
1177
+ pins.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1178
+ return pins.length > 0 ? pins : void 0;
1179
+ } catch {
1180
+ return void 0;
1181
+ }
1182
+ }
1183
+ function detectGitSHA() {
1184
+ try {
1185
+ const sha = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "HEAD"], {
1186
+ encoding: "utf8",
1187
+ stdio: ["ignore", "pipe", "ignore"],
1188
+ timeout: 2e3
1189
+ }).trim().toLowerCase();
1190
+ return GIT_SHA.test(sha) ? sha : void 0;
1191
+ } catch {
1192
+ return void 0;
1193
+ }
1194
+ }
1195
+ function detectContainerHash() {
1196
+ try {
1197
+ const text = (0, import_node_fs.readFileSync)("/proc/self/cgroup", "utf8");
1198
+ return text.split("\n").flatMap((line) => line.split("/")).map((segment) => segment.trim()).find((segment) => HASH_64.test(segment));
1199
+ } catch {
1200
+ return void 0;
1201
+ }
1202
+ }
1203
+ function captureEnvironment(options = {}) {
1204
+ const seeds = Object.entries(options.declaredSeeds ?? {}).map(([name, seed]) => `${name}=${seed}`).sort();
1205
+ const pinned = options.includeDependencies === false ? void 0 : detectDependencies();
1206
+ const git = options.includeGit === false ? void 0 : detectGitSHA();
1207
+ const container = options.includeContainer === false ? void 0 : detectContainerHash();
1208
+ const os = PLATFORM_NAMES[process.platform] ?? process.platform;
1209
+ const arch = ARCH_NAMES[process.arch] ?? process.arch;
1210
+ return {
1211
+ declared: true,
1212
+ runtime_name: "node",
1213
+ runtime_version: process.versions.node,
1214
+ platform: `${os}/${arch}`,
1215
+ ...pinned ? { pinned_dependencies: pinned } : {},
1216
+ ...git ? { git_commit_sha: git } : {},
1217
+ ...container ? { container_image_hash: container } : {},
1218
+ ...seeds.length > 0 ? { random_seeds: seeds } : {}
1219
+ };
1220
+ }
1221
+
1222
+ // src/types.ts
1223
+ var RUN_STATUSES = [
1224
+ "ok",
1225
+ "probe",
1226
+ "pepg_deny",
1227
+ "ccv_halt",
1228
+ "macc_halt",
1229
+ "aivp_t4_halt",
1230
+ "attestation_gap",
1231
+ "stage_error"
1232
+ ];
1233
+
1234
+ // src/section0.ts
1235
+ var import_node_crypto2 = require("crypto");
1236
+ var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
1237
+ var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
1238
+ var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
1239
+ var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
1240
+ function sha256Hex2(preimage) {
1241
+ return (0, import_node_crypto2.createHash)("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
1242
+ }
1243
+ function requireHex64(value, name) {
1244
+ try {
1245
+ validateHex64(value, name);
1246
+ } catch (err) {
1247
+ throw new VerdifaxError(
1248
+ `verdifax: section0: ${err instanceof Error ? err.message : String(err)}`
1249
+ );
1250
+ }
1251
+ }
1252
+ function dseDispatchHash(fields) {
1253
+ requireHex64(fields.program_id, "program_id");
1254
+ requireHex64(fields.registry_record_hash, "registry_record_hash");
1255
+ return sha256Hex2(
1256
+ [
1257
+ DSE_FORMULA_VERSION,
1258
+ fields.envelope_id,
1259
+ fields.sequence_id,
1260
+ fields.program_id,
1261
+ fields.registry_record_hash
1262
+ ].join(".")
1263
+ );
1264
+ }
1265
+ function dcaeClosureHash(fields) {
1266
+ requireHex64(fields.manifest_hash, "manifest_hash");
1267
+ requireHex64(fields.aer_hash, "aer_hash");
1268
+ requireHex64(fields.zksp_binding_hash, "zksp_binding_hash");
1269
+ return sha256Hex2(
1270
+ [
1271
+ DCAE_PREIMAGE_VERSION,
1272
+ fields.manifest_hash,
1273
+ fields.aer_hash,
1274
+ fields.envelope_id,
1275
+ fields.zksp_binding_hash,
1276
+ fields.formal_verifier_status
1277
+ ].join("|")
1278
+ );
1279
+ }
1280
+ function cresReceiptHash(fields) {
1281
+ requireHex64(fields.ciphertext_hash_at_shred, "ciphertext_hash_at_shred");
1282
+ return sha256Hex2(
1283
+ [
1284
+ CRES_PREIMAGE_VERSION,
1285
+ fields.envelope_id,
1286
+ fields.field_path,
1287
+ fields.deletion_clock,
1288
+ fields.actor_id,
1289
+ fields.ciphertext_hash_at_shred,
1290
+ fields.dsar_reference,
1291
+ fields.engine_version ?? CRES_ENGINE_VERSION
1292
+ ].join("|")
1293
+ );
1294
+ }
562
1295
  // Annotate the CommonJS export names for ESM import in node:
563
1296
  0 && (module.exports = {
564
1297
  APIError,
565
1298
  APIKeyRequiredError,
566
1299
  AuthError,
1300
+ CRES_ENGINE_VERSION,
1301
+ CRES_PREIMAGE_VERSION,
1302
+ DCAE_PREIMAGE_VERSION,
1303
+ DSE_FORMULA_VERSION,
1304
+ MAX_BATCH_ROWS,
567
1305
  NotFoundError,
1306
+ RUN_STATUSES,
568
1307
  VerdifaxClient,
569
1308
  VerdifaxError,
1309
+ VerdifaxJob,
1310
+ captureEnvironment,
1311
+ cresReceiptHash,
1312
+ dcaeClosureHash,
1313
+ dseDispatchHash,
1314
+ formatProviderPayload,
570
1315
  fromEnv,
571
1316
  generateRequestKeyPair,
1317
+ requestKeyChangeApproval,
1318
+ requestKeyChangePreimage,
572
1319
  requestKeyRegistrationBody,
573
1320
  requestSignaturePreimage,
1321
+ requestSignaturePreimageV2,
574
1322
  signRequest,
1323
+ signRequestV2,
575
1324
  validateHex64,
1325
+ validateJobName,
576
1326
  validateRouteId
577
1327
  });