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/README.md +60 -5
- package/dist/index.d.mts +219 -18
- package/dist/index.d.ts +219 -18
- package/dist/index.js +436 -92
- package/dist/index.mjs +430 -92
- package/package.json +1 -1
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 ?? (
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
270
|
-
*
|
|
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
|
-
|
|
285
|
-
|
|
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(
|
|
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(
|
|
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
|
|
728
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|