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.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 ?? (
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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/
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
311
|
-
*
|
|
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
|
-
|
|
326
|
-
|
|
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(
|
|
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(
|
|
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
|
|
769
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|