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.js CHANGED
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  RUN_STATUSES: () => RUN_STATUSES,
33
33
  VerdifaxClient: () => VerdifaxClient,
34
34
  VerdifaxError: () => VerdifaxError,
35
+ VerdifaxJob: () => VerdifaxJob,
35
36
  captureEnvironment: () => captureEnvironment,
36
37
  cresReceiptHash: () => cresReceiptHash,
37
38
  dcaeClosureHash: () => dcaeClosureHash,
@@ -39,10 +40,15 @@ __export(index_exports, {
39
40
  formatProviderPayload: () => formatProviderPayload,
40
41
  fromEnv: () => fromEnv,
41
42
  generateRequestKeyPair: () => generateRequestKeyPair,
43
+ requestKeyChangeApproval: () => requestKeyChangeApproval,
44
+ requestKeyChangePreimage: () => requestKeyChangePreimage,
42
45
  requestKeyRegistrationBody: () => requestKeyRegistrationBody,
43
46
  requestSignaturePreimage: () => requestSignaturePreimage,
47
+ requestSignaturePreimageV2: () => requestSignaturePreimageV2,
44
48
  signRequest: () => signRequest,
49
+ signRequestV2: () => signRequestV2,
45
50
  validateHex64: () => validateHex64,
51
+ validateJobName: () => validateJobName,
46
52
  validateRouteId: () => validateRouteId
47
53
  });
48
54
  module.exports = __toCommonJS(index_exports);
@@ -72,15 +78,23 @@ var NotFoundError = class extends VerdifaxError {
72
78
  name = "VerdifaxNotFoundError";
73
79
  };
74
80
  var APIError = class extends VerdifaxError {
75
- constructor(status, path, message) {
81
+ constructor(status, path, message, recordHash, signedRequest) {
76
82
  super(`verdifax: API error at ${path} (HTTP ${status}): ${message}`);
77
83
  this.status = status;
78
84
  this.path = path;
79
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
+ }
80
93
  }
81
94
  status;
82
95
  path;
83
96
  message;
97
+ recordHash;
84
98
  name = "VerdifaxAPIError";
85
99
  };
86
100
  var APIKeyRequiredError = class extends VerdifaxError {
@@ -92,8 +106,137 @@ var APIKeyRequiredError = class extends VerdifaxError {
92
106
 
93
107
  // src/signing.ts
94
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
95
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";
96
238
  var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
239
+ var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
97
240
  var SIGNING_KEY_BYTES = 32;
98
241
  var PKCS8_ED25519_HEADER = Buffer.from(
99
242
  "302e020100300506032b657004220420",
@@ -124,6 +267,12 @@ function loadSigningKey(key) {
124
267
  type: "pkcs8"
125
268
  });
126
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
+ }
127
276
  function requestSignaturePreimage(fields) {
128
277
  return Buffer.from(
129
278
  [
@@ -138,13 +287,62 @@ function requestSignaturePreimage(fields) {
138
287
  "utf8"
139
288
  );
140
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
+ }
141
340
  function signRequest(signingKey, req, options = {}) {
142
341
  const key = loadSigningKey(signingKey);
143
- const signed_at = options.signedAt ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
342
+ const signed_at = options.signedAt ?? signedAtNow();
144
343
  const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
145
- const payload_hash = (0, import_node_crypto.createHash)("sha256").update(Buffer.from(req.payload_text || req.payload || "", "utf8")).digest("hex");
146
344
  const preimage = requestSignaturePreimage({
147
- payload_hash,
345
+ payload_hash: sha256Hex(req.payload_text || req.payload || ""),
148
346
  program_id: req.program_id,
149
347
  route_id: req.route_id,
150
348
  registry_record_hash: req.registry_record_hash,
@@ -167,81 +365,132 @@ function requestKeyRegistrationBody(signingKey) {
167
365
  ).toString("base64");
168
366
  return { public_key, proof };
169
367
  }
170
-
171
- // src/helpers.ts
172
- function formatProviderPayload(provider, prompt, response) {
173
- if ([provider, prompt, response].some((text) => /[\uD800-\uDFFF]/u.test(text))) {
174
- throw new VerdifaxError(
175
- "verdifax: provider payload text cannot be encoded as UTF-8"
176
- );
177
- }
178
- return JSON.stringify({ prompt, provider, response });
179
- }
180
- function isProvided(value) {
181
- return value !== null && value !== void 0 && value !== "" && value !== false;
182
- }
183
- function withAttestedFlag(context) {
184
- const provided = Object.entries(context).some(
185
- ([name, value]) => name !== "attested" && isProvided(value)
186
- );
187
- return { ...context, attested: provided };
188
- }
189
- function withDeclaredFlag(context) {
190
- const declared = Object.entries(context).some(
191
- ([name, value]) => name !== "declared" && isProvided(value) && !(Array.isArray(value) && value.length === 0)
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"
192
377
  );
193
- return { ...context, declared };
194
378
  }
195
- function refuseCallerOriginHashes(substance) {
196
- const origin = substance?.origin;
197
- if (origin?.source_ip_hash || origin?.user_agent_hash) {
198
- throw new VerdifaxError(
199
- "verdifax: request_substance.origin.source_ip_hash and user_agent_hash are computed by the server and cannot be supplied"
200
- );
201
- }
202
- }
203
- function withExecuteFlags(req) {
204
- const body = { ...req };
205
- if (req.attested_context) {
206
- body.attested_context = withAttestedFlag(req.attested_context);
207
- }
208
- if (req.reproducibility_context) {
209
- body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
210
- }
211
- return body;
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
+ };
212
393
  }
213
394
 
214
- // src/validation.ts
215
- function validateHex64(value, fieldName) {
216
- if (typeof value !== "string" || value.length !== 64) {
217
- throw new VerdifaxError(
218
- `verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
219
- );
220
- }
221
- if (!/^[0-9a-f]{64}$/.test(value)) {
222
- throw new VerdifaxError(
223
- `verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
224
- );
225
- }
226
- }
227
- function validateRouteId(value) {
228
- if (!value || typeof value !== "string") {
229
- throw new VerdifaxError("verdifax: route_id is required");
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;
423
+ }
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);
427
+ }
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);
433
+ }
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;
230
465
  }
231
- if (value.length > 256) {
232
- throw new VerdifaxError(
233
- `verdifax: route_id is too long (${value.length} > 256)`
234
- );
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
+ });
235
482
  }
236
- if (!/^[\x20-\x7E]+$/.test(value)) {
237
- throw new VerdifaxError(
238
- "verdifax: route_id contains non-printable-ASCII character"
239
- );
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;
240
489
  }
241
- }
490
+ };
242
491
 
243
492
  // src/version.ts
244
- var SDK_VERSION = "1.0.0";
493
+ var SDK_VERSION = "1.1.0";
245
494
 
246
495
  // src/client.ts
247
496
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
@@ -291,24 +540,35 @@ var VerdifaxClient = class {
291
540
  // ── Execute / Attest (auth) ───────────────────────────────────
292
541
  async execute(req) {
293
542
  this.requireExecuteFields(req);
294
- return this.request("POST", "/execute", this.prepared(req));
543
+ const sent = this.prepared(req);
544
+ const out = await this.request("POST", "/execute", sent);
545
+ return withSignedText(out, sent);
295
546
  }
296
547
  /** Runs one payload twice on the server and compares the manifest
297
548
  * hashes. One signature covers the request, and the server claims
298
- * its nonce once. Both runs are recorded. */
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. */
299
551
  async verifyDeterminism(req) {
300
552
  this.requireExecuteFields(req);
301
- return this.request(
553
+ if (req.job !== void 0 && req.job !== null) {
554
+ throw new VerdifaxError(
555
+ "verdifax: a determinism check runs twice, so it cannot be a job step"
556
+ );
557
+ }
558
+ const sent = this.prepared(req);
559
+ const out = await this.request(
302
560
  "POST",
303
561
  "/execute/verify-determinism",
304
- this.prepared(req)
562
+ sent
305
563
  );
564
+ return withSignedText(out, sent);
306
565
  }
307
566
  /** Sends up to {@link MAX_BATCH_ROWS} runs in one request. With a
308
567
  * signing key each row carries its own signature and nonce. A row
309
568
  * that fails comes back in `results` with `ok: false`, so the call
310
- * does not throw for it. The rows are not checked here: the server
311
- * reports a bad row in that row's result.
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.
312
572
  *
313
573
  * This is one POST and it is never retried. After a timeout the
314
574
  * server may have run some or all of the rows. */
@@ -322,9 +582,19 @@ var VerdifaxClient = class {
322
582
  `verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
323
583
  );
324
584
  }
325
- return this.request("POST", "/execute/batch", {
326
- runs: runs.map((run) => this.prepared(run))
327
- });
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
+ };
328
598
  }
329
599
  /** High-level convenience wrapper around `execute()`. Accepts
330
600
  * either a Buffer (for binary payloads, auto-base64) or a
@@ -355,7 +625,8 @@ var VerdifaxClient = class {
355
625
  request_substance: req.request_substance,
356
626
  authorization_chain: req.authorization_chain,
357
627
  regulatory_scaffolding: req.regulatory_scaffolding,
358
- causal_graph: req.causal_graph
628
+ causal_graph: req.causal_graph,
629
+ job: req.job
359
630
  };
360
631
  if (req.payload !== void 0) {
361
632
  body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
@@ -364,6 +635,12 @@ var VerdifaxClient = class {
364
635
  }
365
636
  return this.execute(body);
366
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
+ }
367
644
  // ── Request signing (auth) ───────────────────────────────────
368
645
  /** Registers this client's public key on its API key, with the proof
369
646
  * that the client holds the private key. From then on the server
@@ -381,11 +658,48 @@ var VerdifaxClient = class {
381
658
  requestKeyRegistrationBody(this.signingKey)
382
659
  );
383
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
+ }
384
689
  /** Clears the registered public key, so the server stops requiring
385
- * 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. */
386
695
  async clearRequestKey() {
387
696
  this.requireAPIKey();
388
- 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;
389
703
  }
390
704
  // ── Runs ──────────────────────────────────────────────────────
391
705
  async getRun(runId) {
@@ -471,6 +785,7 @@ var VerdifaxClient = class {
471
785
  const q = new URLSearchParams();
472
786
  if (params.from) q.set("from", params.from);
473
787
  if (params.to) q.set("to", params.to);
788
+ if (params.job_id) q.set("job_id", params.job_id);
474
789
  q.set("format", format);
475
790
  return `/runs/export?${q.toString()}`;
476
791
  }
@@ -634,6 +949,16 @@ var VerdifaxClient = class {
634
949
  }
635
950
  await this.request("DELETE", `/admin/keys/${keyId}`);
636
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
+ }
637
962
  // ── Provider helpers (Claude / OpenAI) ───────────────────────
638
963
  /** Convenience for attesting an Anthropic Claude API response. With
639
964
  * `govern: true` the response text also goes to the AIVP review as
@@ -666,7 +991,8 @@ var VerdifaxClient = class {
666
991
  attested_context: {
667
992
  actor_role: "ai_assistant",
668
993
  model_provider: args.provider
669
- }
994
+ },
995
+ job: args.job
670
996
  });
671
997
  }
672
998
  // ── HTTP plumbing, retry-aware ──────────────────────────────
@@ -713,9 +1039,13 @@ var VerdifaxClient = class {
713
1039
  continue;
714
1040
  }
715
1041
  let errMessage = text.trim();
1042
+ let recordHash;
716
1043
  try {
717
1044
  const env = JSON.parse(text);
718
1045
  if (env.error) errMessage = env.error;
1046
+ if (typeof env.record_hash === "string" && env.record_hash) {
1047
+ recordHash = env.record_hash;
1048
+ }
719
1049
  } catch {
720
1050
  }
721
1051
  switch (resp.status) {
@@ -724,7 +1054,13 @@ var VerdifaxClient = class {
724
1054
  case 404:
725
1055
  throw new NotFoundError(path, errMessage);
726
1056
  default:
727
- 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
+ );
728
1064
  }
729
1065
  }
730
1066
  if (rawText) return { text, headers: resp.headers };
@@ -765,13 +1101,15 @@ var VerdifaxClient = class {
765
1101
  validateRouteId(req.route_id);
766
1102
  refuseCallerOriginHashes(req.request_substance);
767
1103
  }
768
- /** The request with its derived flags and, with a signing key, a fresh
769
- * signature and nonce, on a copy. */
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. */
770
1106
  prepared(req) {
771
- const body = withExecuteFlags(req);
772
- return this.signingKey ? { ...body, request_signature: signRequest(this.signingKey, body) } : body;
1107
+ return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
773
1108
  }
774
1109
  };
1110
+ function withSignedText(result, sent) {
1111
+ return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
1112
+ }
775
1113
 
776
1114
  // src/config.ts
777
1115
  function fromEnv() {
@@ -899,7 +1237,7 @@ var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
899
1237
  var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
900
1238
  var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
901
1239
  var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
902
- function sha256Hex(preimage) {
1240
+ function sha256Hex2(preimage) {
903
1241
  return (0, import_node_crypto2.createHash)("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
904
1242
  }
905
1243
  function requireHex64(value, name) {
@@ -914,7 +1252,7 @@ function requireHex64(value, name) {
914
1252
  function dseDispatchHash(fields) {
915
1253
  requireHex64(fields.program_id, "program_id");
916
1254
  requireHex64(fields.registry_record_hash, "registry_record_hash");
917
- return sha256Hex(
1255
+ return sha256Hex2(
918
1256
  [
919
1257
  DSE_FORMULA_VERSION,
920
1258
  fields.envelope_id,
@@ -928,7 +1266,7 @@ function dcaeClosureHash(fields) {
928
1266
  requireHex64(fields.manifest_hash, "manifest_hash");
929
1267
  requireHex64(fields.aer_hash, "aer_hash");
930
1268
  requireHex64(fields.zksp_binding_hash, "zksp_binding_hash");
931
- return sha256Hex(
1269
+ return sha256Hex2(
932
1270
  [
933
1271
  DCAE_PREIMAGE_VERSION,
934
1272
  fields.manifest_hash,
@@ -941,7 +1279,7 @@ function dcaeClosureHash(fields) {
941
1279
  }
942
1280
  function cresReceiptHash(fields) {
943
1281
  requireHex64(fields.ciphertext_hash_at_shred, "ciphertext_hash_at_shred");
944
- return sha256Hex(
1282
+ return sha256Hex2(
945
1283
  [
946
1284
  CRES_PREIMAGE_VERSION,
947
1285
  fields.envelope_id,
@@ -968,6 +1306,7 @@ function cresReceiptHash(fields) {
968
1306
  RUN_STATUSES,
969
1307
  VerdifaxClient,
970
1308
  VerdifaxError,
1309
+ VerdifaxJob,
971
1310
  captureEnvironment,
972
1311
  cresReceiptHash,
973
1312
  dcaeClosureHash,
@@ -975,9 +1314,14 @@ function cresReceiptHash(fields) {
975
1314
  formatProviderPayload,
976
1315
  fromEnv,
977
1316
  generateRequestKeyPair,
1317
+ requestKeyChangeApproval,
1318
+ requestKeyChangePreimage,
978
1319
  requestKeyRegistrationBody,
979
1320
  requestSignaturePreimage,
1321
+ requestSignaturePreimageV2,
980
1322
  signRequest,
1323
+ signRequestV2,
981
1324
  validateHex64,
1325
+ validateJobName,
982
1326
  validateRouteId
983
1327
  });