verdifax 1.0.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,81 +318,132 @@ function requestKeyRegistrationBody(signingKey) {
126
318
  ).toString("base64");
127
319
  return { public_key, proof };
128
320
  }
129
-
130
- // src/helpers.ts
131
- function formatProviderPayload(provider, prompt, response) {
132
- if ([provider, prompt, response].some((text) => /[\uD800-\uDFFF]/u.test(text))) {
133
- throw new VerdifaxError(
134
- "verdifax: provider payload text cannot be encoded as UTF-8"
135
- );
136
- }
137
- return JSON.stringify({ prompt, provider, response });
138
- }
139
- function isProvided(value) {
140
- return value !== null && value !== void 0 && value !== "" && value !== false;
141
- }
142
- function withAttestedFlag(context) {
143
- const provided = Object.entries(context).some(
144
- ([name, value]) => name !== "attested" && isProvided(value)
145
- );
146
- return { ...context, attested: provided };
147
- }
148
- function withDeclaredFlag(context) {
149
- const declared = Object.entries(context).some(
150
- ([name, value]) => name !== "declared" && isProvided(value) && !(Array.isArray(value) && value.length === 0)
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"
151
330
  );
152
- return { ...context, declared };
153
331
  }
154
- function refuseCallerOriginHashes(substance) {
155
- const origin = substance?.origin;
156
- if (origin?.source_ip_hash || origin?.user_agent_hash) {
157
- throw new VerdifaxError(
158
- "verdifax: request_substance.origin.source_ip_hash and user_agent_hash are computed by the server and cannot be supplied"
159
- );
160
- }
161
- }
162
- function withExecuteFlags(req) {
163
- const body = { ...req };
164
- if (req.attested_context) {
165
- body.attested_context = withAttestedFlag(req.attested_context);
166
- }
167
- if (req.reproducibility_context) {
168
- body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
169
- }
170
- return body;
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
+ };
171
346
  }
172
347
 
173
- // src/validation.ts
174
- function validateHex64(value, fieldName) {
175
- if (typeof value !== "string" || value.length !== 64) {
176
- throw new VerdifaxError(
177
- `verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
178
- );
179
- }
180
- if (!/^[0-9a-f]{64}$/.test(value)) {
181
- throw new VerdifaxError(
182
- `verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
183
- );
184
- }
185
- }
186
- function validateRouteId(value) {
187
- if (!value || typeof value !== "string") {
188
- throw new VerdifaxError("verdifax: route_id is required");
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;
376
+ }
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);
380
+ }
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);
386
+ }
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;
189
418
  }
190
- if (value.length > 256) {
191
- throw new VerdifaxError(
192
- `verdifax: route_id is too long (${value.length} > 256)`
193
- );
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
+ });
194
435
  }
195
- if (!/^[\x20-\x7E]+$/.test(value)) {
196
- throw new VerdifaxError(
197
- "verdifax: route_id contains non-printable-ASCII character"
198
- );
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;
199
442
  }
200
- }
443
+ };
201
444
 
202
445
  // src/version.ts
203
- var SDK_VERSION = "1.0.0";
446
+ var SDK_VERSION = "1.1.0";
204
447
 
205
448
  // src/client.ts
206
449
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
@@ -250,24 +493,35 @@ var VerdifaxClient = class {
250
493
  // ── Execute / Attest (auth) ───────────────────────────────────
251
494
  async execute(req) {
252
495
  this.requireExecuteFields(req);
253
- return this.request("POST", "/execute", this.prepared(req));
496
+ const sent = this.prepared(req);
497
+ const out = await this.request("POST", "/execute", sent);
498
+ return withSignedText(out, sent);
254
499
  }
255
500
  /** Runs one payload twice on the server and compares the manifest
256
501
  * hashes. One signature covers the request, and the server claims
257
- * its nonce once. Both runs are recorded. */
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. */
258
504
  async verifyDeterminism(req) {
259
505
  this.requireExecuteFields(req);
260
- return this.request(
506
+ if (req.job !== void 0 && req.job !== null) {
507
+ throw new VerdifaxError(
508
+ "verdifax: a determinism check runs twice, so it cannot be a job step"
509
+ );
510
+ }
511
+ const sent = this.prepared(req);
512
+ const out = await this.request(
261
513
  "POST",
262
514
  "/execute/verify-determinism",
263
- this.prepared(req)
515
+ sent
264
516
  );
517
+ return withSignedText(out, sent);
265
518
  }
266
519
  /** Sends up to {@link MAX_BATCH_ROWS} runs in one request. With a
267
520
  * signing key each row carries its own signature and nonce. A row
268
521
  * that fails comes back in `results` with `ok: false`, so the call
269
- * does not throw for it. The rows are not checked here: the server
270
- * reports a bad row in that row's result.
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.
271
525
  *
272
526
  * This is one POST and it is never retried. After a timeout the
273
527
  * server may have run some or all of the rows. */
@@ -281,9 +535,19 @@ var VerdifaxClient = class {
281
535
  `verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
282
536
  );
283
537
  }
284
- return this.request("POST", "/execute/batch", {
285
- runs: runs.map((run) => this.prepared(run))
286
- });
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
+ };
287
551
  }
288
552
  /** High-level convenience wrapper around `execute()`. Accepts
289
553
  * either a Buffer (for binary payloads, auto-base64) or a
@@ -314,7 +578,8 @@ var VerdifaxClient = class {
314
578
  request_substance: req.request_substance,
315
579
  authorization_chain: req.authorization_chain,
316
580
  regulatory_scaffolding: req.regulatory_scaffolding,
317
- causal_graph: req.causal_graph
581
+ causal_graph: req.causal_graph,
582
+ job: req.job
318
583
  };
319
584
  if (req.payload !== void 0) {
320
585
  body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
@@ -323,6 +588,12 @@ var VerdifaxClient = class {
323
588
  }
324
589
  return this.execute(body);
325
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
+ }
326
597
  // ── Request signing (auth) ───────────────────────────────────
327
598
  /** Registers this client's public key on its API key, with the proof
328
599
  * that the client holds the private key. From then on the server
@@ -340,11 +611,48 @@ var VerdifaxClient = class {
340
611
  requestKeyRegistrationBody(this.signingKey)
341
612
  );
342
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
+ }
343
642
  /** Clears the registered public key, so the server stops requiring
344
- * 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. */
345
648
  async clearRequestKey() {
346
649
  this.requireAPIKey();
347
- 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;
348
656
  }
349
657
  // ── Runs ──────────────────────────────────────────────────────
350
658
  async getRun(runId) {
@@ -430,6 +738,7 @@ var VerdifaxClient = class {
430
738
  const q = new URLSearchParams();
431
739
  if (params.from) q.set("from", params.from);
432
740
  if (params.to) q.set("to", params.to);
741
+ if (params.job_id) q.set("job_id", params.job_id);
433
742
  q.set("format", format);
434
743
  return `/runs/export?${q.toString()}`;
435
744
  }
@@ -593,6 +902,16 @@ var VerdifaxClient = class {
593
902
  }
594
903
  await this.request("DELETE", `/admin/keys/${keyId}`);
595
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
+ }
596
915
  // ── Provider helpers (Claude / OpenAI) ───────────────────────
597
916
  /** Convenience for attesting an Anthropic Claude API response. With
598
917
  * `govern: true` the response text also goes to the AIVP review as
@@ -625,7 +944,8 @@ var VerdifaxClient = class {
625
944
  attested_context: {
626
945
  actor_role: "ai_assistant",
627
946
  model_provider: args.provider
628
- }
947
+ },
948
+ job: args.job
629
949
  });
630
950
  }
631
951
  // ── HTTP plumbing, retry-aware ──────────────────────────────
@@ -672,9 +992,13 @@ var VerdifaxClient = class {
672
992
  continue;
673
993
  }
674
994
  let errMessage = text.trim();
995
+ let recordHash;
675
996
  try {
676
997
  const env = JSON.parse(text);
677
998
  if (env.error) errMessage = env.error;
999
+ if (typeof env.record_hash === "string" && env.record_hash) {
1000
+ recordHash = env.record_hash;
1001
+ }
678
1002
  } catch {
679
1003
  }
680
1004
  switch (resp.status) {
@@ -683,7 +1007,13 @@ var VerdifaxClient = class {
683
1007
  case 404:
684
1008
  throw new NotFoundError(path, errMessage);
685
1009
  default:
686
- 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
+ );
687
1017
  }
688
1018
  }
689
1019
  if (rawText) return { text, headers: resp.headers };
@@ -724,13 +1054,15 @@ var VerdifaxClient = class {
724
1054
  validateRouteId(req.route_id);
725
1055
  refuseCallerOriginHashes(req.request_substance);
726
1056
  }
727
- /** The request with its derived flags and, with a signing key, a fresh
728
- * signature and nonce, on a copy. */
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. */
729
1059
  prepared(req) {
730
- const body = withExecuteFlags(req);
731
- return this.signingKey ? { ...body, request_signature: signRequest(this.signingKey, body) } : body;
1060
+ return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
732
1061
  }
733
1062
  };
1063
+ function withSignedText(result, sent) {
1064
+ return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
1065
+ }
734
1066
 
735
1067
  // src/config.ts
736
1068
  function fromEnv() {
@@ -858,7 +1190,7 @@ var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
858
1190
  var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
859
1191
  var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
860
1192
  var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
861
- function sha256Hex(preimage) {
1193
+ function sha256Hex2(preimage) {
862
1194
  return createHash2("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
863
1195
  }
864
1196
  function requireHex64(value, name) {
@@ -873,7 +1205,7 @@ function requireHex64(value, name) {
873
1205
  function dseDispatchHash(fields) {
874
1206
  requireHex64(fields.program_id, "program_id");
875
1207
  requireHex64(fields.registry_record_hash, "registry_record_hash");
876
- return sha256Hex(
1208
+ return sha256Hex2(
877
1209
  [
878
1210
  DSE_FORMULA_VERSION,
879
1211
  fields.envelope_id,
@@ -887,7 +1219,7 @@ function dcaeClosureHash(fields) {
887
1219
  requireHex64(fields.manifest_hash, "manifest_hash");
888
1220
  requireHex64(fields.aer_hash, "aer_hash");
889
1221
  requireHex64(fields.zksp_binding_hash, "zksp_binding_hash");
890
- return sha256Hex(
1222
+ return sha256Hex2(
891
1223
  [
892
1224
  DCAE_PREIMAGE_VERSION,
893
1225
  fields.manifest_hash,
@@ -900,7 +1232,7 @@ function dcaeClosureHash(fields) {
900
1232
  }
901
1233
  function cresReceiptHash(fields) {
902
1234
  requireHex64(fields.ciphertext_hash_at_shred, "ciphertext_hash_at_shred");
903
- return sha256Hex(
1235
+ return sha256Hex2(
904
1236
  [
905
1237
  CRES_PREIMAGE_VERSION,
906
1238
  fields.envelope_id,
@@ -926,6 +1258,7 @@ export {
926
1258
  RUN_STATUSES,
927
1259
  VerdifaxClient,
928
1260
  VerdifaxError,
1261
+ VerdifaxJob,
929
1262
  captureEnvironment,
930
1263
  cresReceiptHash,
931
1264
  dcaeClosureHash,
@@ -933,9 +1266,14 @@ export {
933
1266
  formatProviderPayload,
934
1267
  fromEnv,
935
1268
  generateRequestKeyPair,
1269
+ requestKeyChangeApproval,
1270
+ requestKeyChangePreimage,
936
1271
  requestKeyRegistrationBody,
937
1272
  requestSignaturePreimage,
1273
+ requestSignaturePreimageV2,
938
1274
  signRequest,
1275
+ signRequestV2,
939
1276
  validateHex64,
1277
+ validateJobName,
940
1278
  validateRouteId
941
1279
  };