verdifax 1.0.0 → 1.2.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 +127 -6
- package/dist/index.d.mts +402 -18
- package/dist/index.d.ts +402 -18
- package/dist/index.js +779 -90
- package/dist/index.mjs +770 -90
- 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,286 @@ 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
|
+
var POLICY_ACTOR_TYPES = ["agent", "user", "service", "system"];
|
|
171
|
+
var POLICY_OBJECT_TYPES = ["text", "transaction", "code", "data", "model", "file"];
|
|
172
|
+
var POLICY_CLASSIFICATIONS = [
|
|
173
|
+
"PUBLIC",
|
|
174
|
+
"INTERNAL",
|
|
175
|
+
"CONFIDENTIAL",
|
|
176
|
+
"RESTRICTED",
|
|
177
|
+
"TOP_SECRET",
|
|
178
|
+
"TS_SCI"
|
|
179
|
+
];
|
|
180
|
+
var MAX_POLICY_OBJECT_ID_BYTES = 256;
|
|
181
|
+
var POLICY_CONTEXT_FIELDS = {
|
|
182
|
+
actor_type: true,
|
|
183
|
+
mfa_satisfied: true,
|
|
184
|
+
session_age_seconds: true,
|
|
185
|
+
object_type: true,
|
|
186
|
+
object_id: true,
|
|
187
|
+
classification: true
|
|
188
|
+
};
|
|
189
|
+
function refuseOutsideList(value, allowed, fieldName) {
|
|
190
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
191
|
+
throw new VerdifaxError(
|
|
192
|
+
`verdifax: policy_context.${fieldName} must be one of ${allowed.join(", ")}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function checkedPolicyContext(context) {
|
|
197
|
+
if (typeof context !== "object" || context === null || Array.isArray(context)) {
|
|
198
|
+
throw new VerdifaxError("verdifax: policy_context must be an object");
|
|
199
|
+
}
|
|
200
|
+
refuseUnknownFields(context, POLICY_CONTEXT_FIELDS, "policy_context");
|
|
201
|
+
const out = {};
|
|
202
|
+
const {
|
|
203
|
+
actor_type,
|
|
204
|
+
mfa_satisfied,
|
|
205
|
+
session_age_seconds,
|
|
206
|
+
object_type,
|
|
207
|
+
object_id,
|
|
208
|
+
classification
|
|
209
|
+
} = context;
|
|
210
|
+
if (actor_type !== void 0) {
|
|
211
|
+
refuseOutsideList(actor_type, POLICY_ACTOR_TYPES, "actor_type");
|
|
212
|
+
out.actor_type = actor_type;
|
|
213
|
+
}
|
|
214
|
+
if (mfa_satisfied !== void 0) {
|
|
215
|
+
if (typeof mfa_satisfied !== "boolean") {
|
|
216
|
+
throw new VerdifaxError(
|
|
217
|
+
"verdifax: policy_context.mfa_satisfied must be true or false"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
out.mfa_satisfied = mfa_satisfied;
|
|
221
|
+
}
|
|
222
|
+
if (session_age_seconds !== void 0) {
|
|
223
|
+
if (!Number.isSafeInteger(session_age_seconds) || session_age_seconds < 0) {
|
|
224
|
+
throw new VerdifaxError(
|
|
225
|
+
"verdifax: policy_context.session_age_seconds must be a whole number of seconds, not negative"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
out.session_age_seconds = session_age_seconds;
|
|
229
|
+
}
|
|
230
|
+
if (object_type !== void 0) {
|
|
231
|
+
refuseOutsideList(object_type, POLICY_OBJECT_TYPES, "object_type");
|
|
232
|
+
out.object_type = object_type;
|
|
233
|
+
}
|
|
234
|
+
if (object_id !== void 0) {
|
|
235
|
+
if (typeof object_id !== "string") {
|
|
236
|
+
throw new VerdifaxError("verdifax: policy_context.object_id must be a string");
|
|
237
|
+
}
|
|
238
|
+
if (Buffer.byteLength(object_id, "utf8") > MAX_POLICY_OBJECT_ID_BYTES) {
|
|
239
|
+
throw new VerdifaxError(
|
|
240
|
+
`verdifax: policy_context.object_id is longer than ${MAX_POLICY_OBJECT_ID_BYTES} bytes`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
out.object_id = object_id;
|
|
244
|
+
}
|
|
245
|
+
if (classification !== void 0) {
|
|
246
|
+
refuseOutsideList(classification, POLICY_CLASSIFICATIONS, "classification");
|
|
247
|
+
out.classification = classification;
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
function refuseActorFactsWithoutActor(context, attested) {
|
|
252
|
+
const declaresActor = context.actor_type !== void 0 || context.mfa_satisfied === true || (context.session_age_seconds ?? 0) !== 0;
|
|
253
|
+
if (declaresActor && attested?.attested !== true) {
|
|
254
|
+
throw new VerdifaxError(
|
|
255
|
+
"verdifax: policy_context.actor_type, mfa_satisfied and session_age_seconds describe the attested actor; send attested_context with the actor, for example its actor_id"
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
var EXECUTE_FIELD_SET = {
|
|
260
|
+
payload: true,
|
|
261
|
+
payload_text: true,
|
|
262
|
+
program_id: true,
|
|
263
|
+
route_id: true,
|
|
264
|
+
registry_record_hash: true,
|
|
265
|
+
ai_output_text: true,
|
|
266
|
+
attested_context: true,
|
|
267
|
+
reproducibility_context: true,
|
|
268
|
+
request_substance: true,
|
|
269
|
+
authorization_chain: true,
|
|
270
|
+
regulatory_scaffolding: true,
|
|
271
|
+
causal_graph: true,
|
|
272
|
+
request_signature: true,
|
|
273
|
+
signed_request: true,
|
|
274
|
+
job: true,
|
|
275
|
+
policy_context: true,
|
|
276
|
+
expected_policy_hash: true
|
|
277
|
+
};
|
|
278
|
+
var EXECUTE_FIELDS = Object.freeze(
|
|
279
|
+
Object.keys(EXECUTE_FIELD_SET)
|
|
280
|
+
);
|
|
281
|
+
var ATTESTED_CONTEXT_FIELDS = {
|
|
282
|
+
attested: true,
|
|
283
|
+
actor_id: true,
|
|
284
|
+
actor_role: true,
|
|
285
|
+
authorization_policy: true,
|
|
286
|
+
actor_signature: true,
|
|
287
|
+
model_provider: true,
|
|
288
|
+
model_name: true,
|
|
289
|
+
model_version: true,
|
|
290
|
+
model_temperature: true,
|
|
291
|
+
prompt_hash: true,
|
|
292
|
+
decision_kind: true,
|
|
293
|
+
decision_result: true,
|
|
294
|
+
decision_note: true
|
|
295
|
+
};
|
|
296
|
+
function refuseUnknownRequestFields(req, label = "request") {
|
|
297
|
+
if (typeof req !== "object" || req === null || Array.isArray(req)) {
|
|
298
|
+
throw new VerdifaxError(`verdifax: ${label} must be an object`);
|
|
299
|
+
}
|
|
300
|
+
refuseUnknownFields(req, EXECUTE_FIELD_SET, label);
|
|
301
|
+
const context = req.attested_context;
|
|
302
|
+
if (typeof context === "object" && context !== null) {
|
|
303
|
+
refuseUnknownFields(context, ATTESTED_CONTEXT_FIELDS, "attested_context");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function withExecuteFlags(req) {
|
|
307
|
+
refuseUnknownRequestFields(req);
|
|
308
|
+
const body = { ...req };
|
|
309
|
+
if (req.job === void 0 || req.job === null) {
|
|
310
|
+
delete body.job;
|
|
311
|
+
} else {
|
|
312
|
+
body.job = normalizedJob(req.job);
|
|
313
|
+
}
|
|
314
|
+
if (req.attested_context) {
|
|
315
|
+
body.attested_context = withAttestedFlag(req.attested_context);
|
|
316
|
+
}
|
|
317
|
+
if (req.reproducibility_context) {
|
|
318
|
+
body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
|
|
319
|
+
}
|
|
320
|
+
if (req.policy_context === void 0 || req.policy_context === null) {
|
|
321
|
+
delete body.policy_context;
|
|
322
|
+
} else {
|
|
323
|
+
body.policy_context = checkedPolicyContext(req.policy_context);
|
|
324
|
+
refuseActorFactsWithoutActor(body.policy_context, body.attested_context);
|
|
325
|
+
}
|
|
326
|
+
if (req.expected_policy_hash === void 0 || req.expected_policy_hash === null) {
|
|
327
|
+
delete body.expected_policy_hash;
|
|
328
|
+
} else {
|
|
329
|
+
validateHex64(req.expected_policy_hash, "expected_policy_hash");
|
|
330
|
+
}
|
|
331
|
+
return body;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/signing.ts
|
|
54
335
|
var REQUEST_SIGNATURE_FORM = "verdifax.request.v1";
|
|
336
|
+
var REQUEST_SIGNATURE_FORM_V2 = "verdifax.request.v2";
|
|
337
|
+
var REQUEST_SIGNATURE_VERSION_2 = "2";
|
|
338
|
+
var JOB_FORM = "verdifax.job.v1";
|
|
55
339
|
var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
|
|
340
|
+
var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
|
|
341
|
+
var POLICY_ACTIVATION_FORM = "verdifax.policy-activation.v1";
|
|
56
342
|
var SIGNING_KEY_BYTES = 32;
|
|
57
343
|
var PKCS8_ED25519_HEADER = Buffer.from(
|
|
58
344
|
"302e020100300506032b657004220420",
|
|
@@ -83,6 +369,12 @@ function loadSigningKey(key) {
|
|
|
83
369
|
type: "pkcs8"
|
|
84
370
|
});
|
|
85
371
|
}
|
|
372
|
+
function sha256Hex(text) {
|
|
373
|
+
return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
374
|
+
}
|
|
375
|
+
function signedAtNow() {
|
|
376
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
377
|
+
}
|
|
86
378
|
function requestSignaturePreimage(fields) {
|
|
87
379
|
return Buffer.from(
|
|
88
380
|
[
|
|
@@ -97,13 +389,62 @@ function requestSignaturePreimage(fields) {
|
|
|
97
389
|
"utf8"
|
|
98
390
|
);
|
|
99
391
|
}
|
|
392
|
+
function requestSignaturePreimageV2(fields) {
|
|
393
|
+
const { job } = fields;
|
|
394
|
+
return Buffer.from(
|
|
395
|
+
[
|
|
396
|
+
REQUEST_SIGNATURE_FORM_V2,
|
|
397
|
+
fields.body_hash,
|
|
398
|
+
fields.payload_hash,
|
|
399
|
+
fields.program_id,
|
|
400
|
+
fields.route_id,
|
|
401
|
+
fields.registry_record_hash,
|
|
402
|
+
fields.ai_output_hash,
|
|
403
|
+
JOB_FORM,
|
|
404
|
+
job?.job_id ?? "",
|
|
405
|
+
job?.step_id ?? "",
|
|
406
|
+
(job?.previous_record_hashes ?? []).join(","),
|
|
407
|
+
(job?.external_previous ?? []).join(","),
|
|
408
|
+
job?.final ? "true" : "false",
|
|
409
|
+
fields.signed_at,
|
|
410
|
+
fields.nonce
|
|
411
|
+
].join("\0"),
|
|
412
|
+
"utf8"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
function signRequestV2(signingKey, req, options = {}) {
|
|
416
|
+
const key = loadSigningKey(signingKey);
|
|
417
|
+
const { request_signature: _unsigned, ...fields } = withExecuteFlags(req);
|
|
418
|
+
const signed_request = JSON.stringify(fields);
|
|
419
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
420
|
+
const nonce = options.nonce ?? randomBytes(16).toString("hex");
|
|
421
|
+
const preimage = requestSignaturePreimageV2({
|
|
422
|
+
body_hash: sha256Hex(signed_request),
|
|
423
|
+
payload_hash: sha256Hex(fields.payload_text || fields.payload || ""),
|
|
424
|
+
program_id: fields.program_id,
|
|
425
|
+
route_id: fields.route_id,
|
|
426
|
+
registry_record_hash: fields.registry_record_hash,
|
|
427
|
+
ai_output_hash: fields.ai_output_text ? sha256Hex(fields.ai_output_text) : "",
|
|
428
|
+
job: fields.job,
|
|
429
|
+
signed_at,
|
|
430
|
+
nonce
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
signed_request,
|
|
434
|
+
request_signature: {
|
|
435
|
+
version: REQUEST_SIGNATURE_VERSION_2,
|
|
436
|
+
signature: sign(null, preimage, key).toString("base64"),
|
|
437
|
+
signed_at,
|
|
438
|
+
nonce
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
100
442
|
function signRequest(signingKey, req, options = {}) {
|
|
101
443
|
const key = loadSigningKey(signingKey);
|
|
102
|
-
const signed_at = options.signedAt ?? (
|
|
444
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
103
445
|
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
446
|
const preimage = requestSignaturePreimage({
|
|
106
|
-
payload_hash,
|
|
447
|
+
payload_hash: sha256Hex(req.payload_text || req.payload || ""),
|
|
107
448
|
program_id: req.program_id,
|
|
108
449
|
route_id: req.route_id,
|
|
109
450
|
registry_record_hash: req.registry_record_hash,
|
|
@@ -126,81 +467,164 @@ function requestKeyRegistrationBody(signingKey) {
|
|
|
126
467
|
).toString("base64");
|
|
127
468
|
return { public_key, proof };
|
|
128
469
|
}
|
|
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)
|
|
470
|
+
function requestKeyChangePreimage(fields) {
|
|
471
|
+
return Buffer.from(
|
|
472
|
+
[
|
|
473
|
+
REQUEST_KEY_CHANGE_FORM,
|
|
474
|
+
fields.new_public_key,
|
|
475
|
+
fields.signed_at,
|
|
476
|
+
fields.nonce
|
|
477
|
+
].join("\0"),
|
|
478
|
+
"utf8"
|
|
145
479
|
);
|
|
146
|
-
return { ...context, attested: provided };
|
|
147
480
|
}
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
);
|
|
152
|
-
|
|
481
|
+
function requestKeyChangeApproval(currentSigningKey, newPublicKey, options = {}) {
|
|
482
|
+
const key = loadSigningKey(currentSigningKey);
|
|
483
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
484
|
+
const nonce = options.nonce ?? randomBytes(16).toString("hex");
|
|
485
|
+
const preimage = requestKeyChangePreimage({
|
|
486
|
+
new_public_key: newPublicKey,
|
|
487
|
+
signed_at,
|
|
488
|
+
nonce
|
|
489
|
+
});
|
|
490
|
+
return {
|
|
491
|
+
signature: sign(null, preimage, key).toString("base64"),
|
|
492
|
+
signed_at,
|
|
493
|
+
nonce
|
|
494
|
+
};
|
|
153
495
|
}
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
496
|
+
function policyActivationPreimage(fields) {
|
|
497
|
+
return Buffer.from(
|
|
498
|
+
[
|
|
499
|
+
POLICY_ACTIVATION_FORM,
|
|
500
|
+
fields.chain_id,
|
|
501
|
+
fields.document_hash,
|
|
502
|
+
fields.previous_activation_hash,
|
|
503
|
+
fields.signed_at,
|
|
504
|
+
fields.nonce
|
|
505
|
+
].join("\0"),
|
|
506
|
+
"utf8"
|
|
507
|
+
);
|
|
161
508
|
}
|
|
162
|
-
function
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
509
|
+
function policyActivationApproval(signingKey, fields, options = {}) {
|
|
510
|
+
const key = loadSigningKey(signingKey);
|
|
511
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
512
|
+
const nonce = options.nonce ?? randomBytes(16).toString("hex");
|
|
513
|
+
const preimage = policyActivationPreimage({
|
|
514
|
+
chain_id: fields.chain_id,
|
|
515
|
+
document_hash: fields.document_hash,
|
|
516
|
+
previous_activation_hash: fields.previous_activation_hash,
|
|
517
|
+
signed_at,
|
|
518
|
+
nonce
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
signature: sign(null, preimage, key).toString("base64"),
|
|
522
|
+
signed_at,
|
|
523
|
+
nonce
|
|
524
|
+
};
|
|
171
525
|
}
|
|
172
526
|
|
|
173
|
-
// src/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
527
|
+
// src/job.ts
|
|
528
|
+
var STEP_OPTIONS = {
|
|
529
|
+
payload: true,
|
|
530
|
+
payload_text: true,
|
|
531
|
+
program_id: true,
|
|
532
|
+
route_id: true,
|
|
533
|
+
registry_record_hash: true,
|
|
534
|
+
ai_output_text: true,
|
|
535
|
+
attested_context: true,
|
|
536
|
+
reproducibility_context: true,
|
|
537
|
+
request_substance: true,
|
|
538
|
+
authorization_chain: true,
|
|
539
|
+
regulatory_scaffolding: true,
|
|
540
|
+
causal_graph: true,
|
|
541
|
+
policy_context: true,
|
|
542
|
+
expected_policy_hash: true,
|
|
543
|
+
after: true,
|
|
544
|
+
external_previous: true,
|
|
545
|
+
final: true
|
|
546
|
+
};
|
|
547
|
+
var VerdifaxJob = class {
|
|
548
|
+
jobId;
|
|
549
|
+
client;
|
|
550
|
+
hashes = /* @__PURE__ */ new Map();
|
|
551
|
+
#signedRequests = /* @__PURE__ */ new Map();
|
|
552
|
+
lastStepId;
|
|
553
|
+
constructor(client, jobId) {
|
|
554
|
+
validateJobName(jobId, "job_id");
|
|
555
|
+
this.client = client;
|
|
556
|
+
this.jobId = jobId;
|
|
557
|
+
}
|
|
558
|
+
/** Step id to record hash for every step that left a record, on a copy. */
|
|
559
|
+
get recordHashes() {
|
|
560
|
+
return Object.fromEntries(this.hashes);
|
|
561
|
+
}
|
|
562
|
+
/** Step id to the exact text that was signed, for every signed step that
|
|
563
|
+
* left a record, refused and halted steps included, on a copy. Each text
|
|
564
|
+
* can be passed to `verdifax-verify --request FILE`. */
|
|
565
|
+
get signedRequests() {
|
|
566
|
+
return Object.fromEntries(this.#signedRequests);
|
|
567
|
+
}
|
|
568
|
+
/** Runs one step and records its hash and signed text. A refused or halted
|
|
569
|
+
* step records both from the error, then throws the same error `attest`
|
|
570
|
+
* throws. */
|
|
571
|
+
async step(stepId, options) {
|
|
572
|
+
validateJobName(stepId, "step_id");
|
|
573
|
+
refuseUnknownFields(options, STEP_OPTIONS, "step options");
|
|
574
|
+
const { after, external_previous, final, ...attest } = options;
|
|
575
|
+
const job = {
|
|
576
|
+
job_id: this.jobId,
|
|
577
|
+
step_id: stepId,
|
|
578
|
+
previous_record_hashes: this.previousHashes(after),
|
|
579
|
+
external_previous,
|
|
580
|
+
final
|
|
581
|
+
};
|
|
582
|
+
let result;
|
|
583
|
+
try {
|
|
584
|
+
result = await this.client.attest({ ...attest, job });
|
|
585
|
+
} catch (err) {
|
|
586
|
+
if (err instanceof APIError && err.recordHash) {
|
|
587
|
+
this.record(stepId, err.recordHash, err.signedRequest);
|
|
588
|
+
}
|
|
589
|
+
throw err;
|
|
590
|
+
}
|
|
591
|
+
const hash = result?.record_hash || result?.manifest_hash || result?.manifest?.ManifestHash;
|
|
592
|
+
if (!hash) {
|
|
593
|
+
throw new VerdifaxError(
|
|
594
|
+
`verdifax: the server returned no record_hash or manifest_hash for step ${stepId}, so the job map cannot continue from it`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
this.record(stepId, hash, result.signed_request);
|
|
598
|
+
return result;
|
|
189
599
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
600
|
+
previousHashes(after) {
|
|
601
|
+
if (after === void 0 || after === null) {
|
|
602
|
+
return this.lastStepId === void 0 ? [] : [this.hashes.get(this.lastStepId)];
|
|
603
|
+
}
|
|
604
|
+
if (!Array.isArray(after)) {
|
|
605
|
+
throw new VerdifaxError("verdifax: after must be a list of step ids");
|
|
606
|
+
}
|
|
607
|
+
return after.map((stepId) => {
|
|
608
|
+
const hash = this.hashes.get(stepId);
|
|
609
|
+
if (hash === void 0) {
|
|
610
|
+
throw new VerdifaxError(
|
|
611
|
+
`verdifax: after names ${String(stepId)}, which is not a step in job ${this.jobId}`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return hash;
|
|
615
|
+
});
|
|
194
616
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
617
|
+
record(stepId, hash, signedRequest) {
|
|
618
|
+
this.hashes.set(stepId, hash);
|
|
619
|
+
if (signedRequest !== void 0) {
|
|
620
|
+
this.#signedRequests.set(stepId, signedRequest);
|
|
621
|
+
}
|
|
622
|
+
this.lastStepId = stepId;
|
|
199
623
|
}
|
|
200
|
-
}
|
|
624
|
+
};
|
|
201
625
|
|
|
202
626
|
// src/version.ts
|
|
203
|
-
var SDK_VERSION = "1.
|
|
627
|
+
var SDK_VERSION = "1.2.0";
|
|
204
628
|
|
|
205
629
|
// src/client.ts
|
|
206
630
|
var DEFAULT_BASE_URL = "https://api.verdifax.com";
|
|
@@ -210,10 +634,42 @@ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
|
|
|
210
634
|
var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
|
|
211
635
|
var HEADER_API_KEY = "X-Verdifax-Key";
|
|
212
636
|
var REQUEST_KEY_PATH = "/keys/request-key";
|
|
637
|
+
var POLICIES_PATH = "/policies";
|
|
213
638
|
var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
|
|
214
639
|
var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
|
|
215
640
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
216
641
|
var MAX_BATCH_ROWS = 1e3;
|
|
642
|
+
var ATTEST_FIELDS = {
|
|
643
|
+
payload: true,
|
|
644
|
+
payload_text: true,
|
|
645
|
+
program_id: true,
|
|
646
|
+
route_id: true,
|
|
647
|
+
registry_record_hash: true,
|
|
648
|
+
ai_output_text: true,
|
|
649
|
+
attested_context: true,
|
|
650
|
+
reproducibility_context: true,
|
|
651
|
+
request_substance: true,
|
|
652
|
+
authorization_chain: true,
|
|
653
|
+
regulatory_scaffolding: true,
|
|
654
|
+
causal_graph: true,
|
|
655
|
+
job: true,
|
|
656
|
+
policy_context: true,
|
|
657
|
+
expected_policy_hash: true
|
|
658
|
+
};
|
|
659
|
+
var PROVIDER_ARGS = Object.fromEntries(
|
|
660
|
+
[
|
|
661
|
+
"program_id",
|
|
662
|
+
"route_id",
|
|
663
|
+
"registry_record_hash",
|
|
664
|
+
"prompt",
|
|
665
|
+
"response",
|
|
666
|
+
"govern",
|
|
667
|
+
"job",
|
|
668
|
+
"policy_context",
|
|
669
|
+
"expected_policy_hash",
|
|
670
|
+
...EXECUTE_FIELDS
|
|
671
|
+
].map((name) => [name, true])
|
|
672
|
+
);
|
|
217
673
|
var VerdifaxClient = class {
|
|
218
674
|
baseURL;
|
|
219
675
|
apiKey;
|
|
@@ -249,25 +705,39 @@ var VerdifaxClient = class {
|
|
|
249
705
|
}
|
|
250
706
|
// ── Execute / Attest (auth) ───────────────────────────────────
|
|
251
707
|
async execute(req) {
|
|
708
|
+
refuseUnknownRequestFields(req);
|
|
252
709
|
this.requireExecuteFields(req);
|
|
253
|
-
|
|
710
|
+
const sent = this.prepared(req);
|
|
711
|
+
const out = await this.request("POST", "/execute", sent);
|
|
712
|
+
return withSignedText(out, sent);
|
|
254
713
|
}
|
|
255
714
|
/** Runs one payload twice on the server and compares the manifest
|
|
256
715
|
* hashes. One signature covers the request, and the server claims
|
|
257
|
-
* its nonce once. Both runs are recorded.
|
|
716
|
+
* its nonce once. Both runs are recorded. A run that happens twice
|
|
717
|
+
* cannot be one job step, so a `job` is refused here. */
|
|
258
718
|
async verifyDeterminism(req) {
|
|
719
|
+
refuseUnknownRequestFields(req);
|
|
259
720
|
this.requireExecuteFields(req);
|
|
260
|
-
|
|
721
|
+
if (req.job !== void 0 && req.job !== null) {
|
|
722
|
+
throw new VerdifaxError(
|
|
723
|
+
"verdifax: a determinism check runs twice, so it cannot be a job step"
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
const sent = this.prepared(req);
|
|
727
|
+
const out = await this.request(
|
|
261
728
|
"POST",
|
|
262
729
|
"/execute/verify-determinism",
|
|
263
|
-
|
|
730
|
+
sent
|
|
264
731
|
);
|
|
732
|
+
return withSignedText(out, sent);
|
|
265
733
|
}
|
|
266
734
|
/** Sends up to {@link MAX_BATCH_ROWS} runs in one request. With a
|
|
267
735
|
* signing key each row carries its own signature and nonce. A row
|
|
268
736
|
* 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
|
-
*
|
|
737
|
+
* does not throw for it. The rows are not checked here, except a job
|
|
738
|
+
* block, a `policy_context` and an `expected_policy_hash`, which are
|
|
739
|
+
* refused before anything is sent: the server reports any other bad row
|
|
740
|
+
* in that row's result.
|
|
271
741
|
*
|
|
272
742
|
* This is one POST and it is never retried. After a timeout the
|
|
273
743
|
* server may have run some or all of the rows. */
|
|
@@ -281,9 +751,20 @@ var VerdifaxClient = class {
|
|
|
281
751
|
`verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
|
|
282
752
|
);
|
|
283
753
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
754
|
+
runs.forEach((run, i) => refuseUnknownRequestFields(run, `runs[${i}]`));
|
|
755
|
+
const sent = runs.map((run) => this.prepared(run));
|
|
756
|
+
const out = await this.request(
|
|
757
|
+
"POST",
|
|
758
|
+
"/execute/batch",
|
|
759
|
+
{ runs: sent }
|
|
760
|
+
);
|
|
761
|
+
if (!Array.isArray(out?.results)) return out;
|
|
762
|
+
return {
|
|
763
|
+
...out,
|
|
764
|
+
results: out.results.map(
|
|
765
|
+
(row, i) => i < sent.length ? withSignedText(row, sent[i]) : row
|
|
766
|
+
)
|
|
767
|
+
};
|
|
287
768
|
}
|
|
288
769
|
/** High-level convenience wrapper around `execute()`. Accepts
|
|
289
770
|
* either a Buffer (for binary payloads, auto-base64) or a
|
|
@@ -291,6 +772,8 @@ var VerdifaxClient = class {
|
|
|
291
772
|
* routes the call through to the canonical /execute. */
|
|
292
773
|
async attest(req) {
|
|
293
774
|
this.requireAPIKey();
|
|
775
|
+
refuseUnknownRequestFields(req, "attest request");
|
|
776
|
+
refuseUnknownFields(req, ATTEST_FIELDS, "attest request");
|
|
294
777
|
if (req.payload !== void 0 && req.payload_text !== void 0) {
|
|
295
778
|
throw new VerdifaxError(
|
|
296
779
|
"verdifax: attest accepts payload OR payload_text, not both"
|
|
@@ -314,7 +797,10 @@ var VerdifaxClient = class {
|
|
|
314
797
|
request_substance: req.request_substance,
|
|
315
798
|
authorization_chain: req.authorization_chain,
|
|
316
799
|
regulatory_scaffolding: req.regulatory_scaffolding,
|
|
317
|
-
causal_graph: req.causal_graph
|
|
800
|
+
causal_graph: req.causal_graph,
|
|
801
|
+
job: req.job,
|
|
802
|
+
policy_context: req.policy_context,
|
|
803
|
+
expected_policy_hash: req.expected_policy_hash
|
|
318
804
|
};
|
|
319
805
|
if (req.payload !== void 0) {
|
|
320
806
|
body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
|
|
@@ -323,6 +809,12 @@ var VerdifaxClient = class {
|
|
|
323
809
|
}
|
|
324
810
|
return this.execute(body);
|
|
325
811
|
}
|
|
812
|
+
/** A helper that runs the steps of one job through {@link attest} and
|
|
813
|
+
* keeps each step's record hash, so a later step can name it. It makes
|
|
814
|
+
* no request until a step runs. */
|
|
815
|
+
job(jobId) {
|
|
816
|
+
return new VerdifaxJob(this, jobId);
|
|
817
|
+
}
|
|
326
818
|
// ── Request signing (auth) ───────────────────────────────────
|
|
327
819
|
/** Registers this client's public key on its API key, with the proof
|
|
328
820
|
* that the client holds the private key. From then on the server
|
|
@@ -340,11 +832,162 @@ var VerdifaxClient = class {
|
|
|
340
832
|
requestKeyRegistrationBody(this.signingKey)
|
|
341
833
|
);
|
|
342
834
|
}
|
|
835
|
+
/** Replaces the registered key with `newSigningKey`. The new key proves
|
|
836
|
+
* itself and this client's current key approves the change. After
|
|
837
|
+
* success this client signs with the new key. If the call fails it
|
|
838
|
+
* keeps the current key, including after a timeout, when the server may
|
|
839
|
+
* have made the change. */
|
|
840
|
+
async rotateRequestKey(newSigningKey) {
|
|
841
|
+
this.requireAPIKey();
|
|
842
|
+
if (!this.signingKey) {
|
|
843
|
+
throw new VerdifaxError(
|
|
844
|
+
"verdifax: rotateRequestKey needs the current key in the signingKey client option"
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
const next = loadSigningKey(newSigningKey);
|
|
848
|
+
const registration = requestKeyRegistrationBody(next);
|
|
849
|
+
const out = await this.request(
|
|
850
|
+
"POST",
|
|
851
|
+
REQUEST_KEY_PATH,
|
|
852
|
+
{
|
|
853
|
+
...registration,
|
|
854
|
+
change: requestKeyChangeApproval(
|
|
855
|
+
this.signingKey,
|
|
856
|
+
registration.public_key
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
);
|
|
860
|
+
this.signingKey = next;
|
|
861
|
+
return out;
|
|
862
|
+
}
|
|
343
863
|
/** Clears the registered public key, so the server stops requiring
|
|
344
|
-
* request signatures on this API key.
|
|
864
|
+
* request signatures on this API key. With a signing key the client
|
|
865
|
+
* sends that key's approval, which the server requires while a key is
|
|
866
|
+
* registered. After success the client drops its signing key and sends
|
|
867
|
+
* unsigned requests, because the server refuses a signed run on an API
|
|
868
|
+
* key with no registered key. If the call fails it keeps the key. */
|
|
345
869
|
async clearRequestKey() {
|
|
346
870
|
this.requireAPIKey();
|
|
347
|
-
await this.request(
|
|
871
|
+
await this.request(
|
|
872
|
+
"DELETE",
|
|
873
|
+
REQUEST_KEY_PATH,
|
|
874
|
+
this.signingKey ? { change: requestKeyChangeApproval(this.signingKey, "") } : void 0
|
|
875
|
+
);
|
|
876
|
+
this.signingKey = void 0;
|
|
877
|
+
}
|
|
878
|
+
// ── Policies (auth) ──────────────────────────────────────────
|
|
879
|
+
/** Stores a policy document. Nothing changes until it is activated with
|
|
880
|
+
* {@link activatePolicy}. The server hashes the exact text, so this
|
|
881
|
+
* sends the string it is given unchanged and refuses anything that is
|
|
882
|
+
* not a string: serializing an object here would change the hash.
|
|
883
|
+
* Verdifax does not write, review or approve a customer's policy. */
|
|
884
|
+
async uploadPolicy(document) {
|
|
885
|
+
this.requireAPIKey();
|
|
886
|
+
if (typeof document !== "string") {
|
|
887
|
+
throw new VerdifaxError(
|
|
888
|
+
"verdifax: document must be the policy's exact text, as a string"
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return this.request("POST", POLICIES_PATH, { document });
|
|
892
|
+
}
|
|
893
|
+
/** This key's stored documents, with the chain its changes belong to,
|
|
894
|
+
* the newest change in it (64 zeros when there is none yet) and the
|
|
895
|
+
* document in force (empty under the built-in policy). */
|
|
896
|
+
async listPolicies() {
|
|
897
|
+
this.requireAPIKey();
|
|
898
|
+
return this.request("GET", POLICIES_PATH);
|
|
899
|
+
}
|
|
900
|
+
/** One stored document, with its exact text as `document`. */
|
|
901
|
+
async getPolicy(documentHash) {
|
|
902
|
+
this.requireAPIKey();
|
|
903
|
+
validateHex64(documentHash, "documentHash");
|
|
904
|
+
return this.request(
|
|
905
|
+
"GET",
|
|
906
|
+
`${POLICIES_PATH}/${documentHash}`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
/** Shows which rule would fire on sample inputs, for a document given as
|
|
910
|
+
* text or one already stored, named by its hash: exactly one of the two.
|
|
911
|
+
* It seals nothing. */
|
|
912
|
+
async testPolicy(req) {
|
|
913
|
+
this.requireAPIKey();
|
|
914
|
+
const { inputs, document, documentHash } = req ?? {};
|
|
915
|
+
if (document === void 0 === (documentHash === void 0)) {
|
|
916
|
+
throw new VerdifaxError(
|
|
917
|
+
"verdifax: testPolicy needs exactly one of document or documentHash"
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) {
|
|
921
|
+
throw new VerdifaxError("verdifax: testPolicy needs inputs as an object");
|
|
922
|
+
}
|
|
923
|
+
const body = { inputs };
|
|
924
|
+
if (document !== void 0) {
|
|
925
|
+
if (typeof document !== "string") {
|
|
926
|
+
throw new VerdifaxError(
|
|
927
|
+
"verdifax: document must be the policy's exact text, as a string"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
body.document = document;
|
|
931
|
+
} else {
|
|
932
|
+
validateHex64(documentHash, "documentHash");
|
|
933
|
+
body.document_hash = documentHash;
|
|
934
|
+
}
|
|
935
|
+
return this.request(
|
|
936
|
+
"POST",
|
|
937
|
+
`${POLICIES_PATH}/test`,
|
|
938
|
+
body
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
/** This key's policy changes, oldest first, as the list the server sends.
|
|
942
|
+
* Save it as JSON, for example with
|
|
943
|
+
* `writeFileSync("policy-history.json", JSON.stringify(history))`, and
|
|
944
|
+
* check it with `verdifax-verify --policy-history policy-history.json`. */
|
|
945
|
+
async getPolicyHistory() {
|
|
946
|
+
this.requireAPIKey();
|
|
947
|
+
return this.request("GET", `${POLICIES_PATH}/history`);
|
|
948
|
+
}
|
|
949
|
+
/** Makes a stored document the policy in force and returns the change
|
|
950
|
+
* record. It first reads GET /policies for the chain and its newest
|
|
951
|
+
* change, then names that change as the one this follows. With the
|
|
952
|
+
* `signingKey` option the request key signs the change, which the server
|
|
953
|
+
* requires while a request key is registered.
|
|
954
|
+
*
|
|
955
|
+
* Rolling back is activating an earlier document: it is one more change
|
|
956
|
+
* in the chain, so the change it undoes stays on the record.
|
|
957
|
+
*
|
|
958
|
+
* An HTTP 409 means the chain moved because another change came first,
|
|
959
|
+
* or the document is not yet effective. It throws the usual
|
|
960
|
+
* {@link APIError} and is not retried: read the policies again and
|
|
961
|
+
* decide. */
|
|
962
|
+
async activatePolicy(documentHash) {
|
|
963
|
+
this.requireAPIKey();
|
|
964
|
+
validateHex64(documentHash, "documentHash");
|
|
965
|
+
const chain = await this.listPolicies();
|
|
966
|
+
const previous = chain?.newest_activation_hash;
|
|
967
|
+
if (typeof previous !== "string") {
|
|
968
|
+
throw new VerdifaxError(
|
|
969
|
+
"verdifax: the server did not report newest_activation_hash, so the change cannot name the one it follows"
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
const body = { previous_activation_hash: previous };
|
|
973
|
+
if (this.signingKey) {
|
|
974
|
+
if (typeof chain.chain_id !== "string" || chain.chain_id === "") {
|
|
975
|
+
throw new VerdifaxError(
|
|
976
|
+
"verdifax: the server did not report chain_id, so the change cannot be signed"
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
body.signature = policyActivationApproval(this.signingKey, {
|
|
980
|
+
chain_id: chain.chain_id,
|
|
981
|
+
document_hash: documentHash,
|
|
982
|
+
previous_activation_hash: previous
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
const out = await this.request(
|
|
986
|
+
"POST",
|
|
987
|
+
`${POLICIES_PATH}/${documentHash}/activate`,
|
|
988
|
+
body
|
|
989
|
+
);
|
|
990
|
+
return out.activation;
|
|
348
991
|
}
|
|
349
992
|
// ── Runs ──────────────────────────────────────────────────────
|
|
350
993
|
async getRun(runId) {
|
|
@@ -430,6 +1073,7 @@ var VerdifaxClient = class {
|
|
|
430
1073
|
const q = new URLSearchParams();
|
|
431
1074
|
if (params.from) q.set("from", params.from);
|
|
432
1075
|
if (params.to) q.set("to", params.to);
|
|
1076
|
+
if (params.job_id) q.set("job_id", params.job_id);
|
|
433
1077
|
q.set("format", format);
|
|
434
1078
|
return `/runs/export?${q.toString()}`;
|
|
435
1079
|
}
|
|
@@ -593,17 +1237,29 @@ var VerdifaxClient = class {
|
|
|
593
1237
|
}
|
|
594
1238
|
await this.request("DELETE", `/admin/keys/${keyId}`);
|
|
595
1239
|
}
|
|
1240
|
+
/** Clears the request key on an API key whose holder lost it, after a
|
|
1241
|
+
* manual check. It is the one way to clear a key without that key. The
|
|
1242
|
+
* id can be any API key, so this client keeps its own signing key. */
|
|
1243
|
+
async adminClearRequestKey(keyId) {
|
|
1244
|
+
this.requireAPIKey();
|
|
1245
|
+
if (keyId <= 0) {
|
|
1246
|
+
throw new VerdifaxError("verdifax: keyId must be > 0");
|
|
1247
|
+
}
|
|
1248
|
+
await this.request("DELETE", `/admin/keys/${keyId}/request-key`);
|
|
1249
|
+
}
|
|
596
1250
|
// ── Provider helpers (Claude / OpenAI) ───────────────────────
|
|
597
1251
|
/** Convenience for attesting an Anthropic Claude API response. With
|
|
598
1252
|
* `govern: true` the response text also goes to the AIVP review as
|
|
599
1253
|
* `ai_output_text`; that is off by default, so the text leaves your
|
|
600
1254
|
* machine for review only when you ask. */
|
|
601
1255
|
async attestClaudeResponse(args) {
|
|
1256
|
+
refuseUnknownFields(args, PROVIDER_ARGS, "attestClaudeResponse arguments");
|
|
602
1257
|
return this.attestProviderResponse({ ...args, provider: "anthropic" });
|
|
603
1258
|
}
|
|
604
1259
|
/** Convenience for attesting an OpenAI API response. `govern` works as
|
|
605
1260
|
* in {@link attestClaudeResponse}. */
|
|
606
1261
|
async attestOpenAIResponse(args) {
|
|
1262
|
+
refuseUnknownFields(args, PROVIDER_ARGS, "attestOpenAIResponse arguments");
|
|
607
1263
|
return this.attestProviderResponse({ ...args, provider: "openai" });
|
|
608
1264
|
}
|
|
609
1265
|
async attestProviderResponse(args) {
|
|
@@ -625,7 +1281,10 @@ var VerdifaxClient = class {
|
|
|
625
1281
|
attested_context: {
|
|
626
1282
|
actor_role: "ai_assistant",
|
|
627
1283
|
model_provider: args.provider
|
|
628
|
-
}
|
|
1284
|
+
},
|
|
1285
|
+
job: args.job,
|
|
1286
|
+
policy_context: args.policy_context,
|
|
1287
|
+
expected_policy_hash: args.expected_policy_hash
|
|
629
1288
|
});
|
|
630
1289
|
}
|
|
631
1290
|
// ── HTTP plumbing, retry-aware ──────────────────────────────
|
|
@@ -672,9 +1331,13 @@ var VerdifaxClient = class {
|
|
|
672
1331
|
continue;
|
|
673
1332
|
}
|
|
674
1333
|
let errMessage = text.trim();
|
|
1334
|
+
let recordHash;
|
|
675
1335
|
try {
|
|
676
1336
|
const env = JSON.parse(text);
|
|
677
1337
|
if (env.error) errMessage = env.error;
|
|
1338
|
+
if (typeof env.record_hash === "string" && env.record_hash) {
|
|
1339
|
+
recordHash = env.record_hash;
|
|
1340
|
+
}
|
|
678
1341
|
} catch {
|
|
679
1342
|
}
|
|
680
1343
|
switch (resp.status) {
|
|
@@ -683,7 +1346,13 @@ var VerdifaxClient = class {
|
|
|
683
1346
|
case 404:
|
|
684
1347
|
throw new NotFoundError(path, errMessage);
|
|
685
1348
|
default:
|
|
686
|
-
throw new APIError(
|
|
1349
|
+
throw new APIError(
|
|
1350
|
+
resp.status,
|
|
1351
|
+
path,
|
|
1352
|
+
errMessage,
|
|
1353
|
+
recordHash,
|
|
1354
|
+
body?.signed_request
|
|
1355
|
+
);
|
|
687
1356
|
}
|
|
688
1357
|
}
|
|
689
1358
|
if (rawText) return { text, headers: resp.headers };
|
|
@@ -724,13 +1393,15 @@ var VerdifaxClient = class {
|
|
|
724
1393
|
validateRouteId(req.route_id);
|
|
725
1394
|
refuseCallerOriginHashes(req.request_substance);
|
|
726
1395
|
}
|
|
727
|
-
/** The request with its derived flags
|
|
728
|
-
*
|
|
1396
|
+
/** The body to send, on a copy: the request with its derived flags or,
|
|
1397
|
+
* with a signing key, the signed text with a fresh signature and nonce. */
|
|
729
1398
|
prepared(req) {
|
|
730
|
-
|
|
731
|
-
return this.signingKey ? { ...body, request_signature: signRequest(this.signingKey, body) } : body;
|
|
1399
|
+
return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
|
|
732
1400
|
}
|
|
733
1401
|
};
|
|
1402
|
+
function withSignedText(result, sent) {
|
|
1403
|
+
return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
|
|
1404
|
+
}
|
|
734
1405
|
|
|
735
1406
|
// src/config.ts
|
|
736
1407
|
function fromEnv() {
|
|
@@ -858,7 +1529,7 @@ var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
|
|
|
858
1529
|
var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
|
|
859
1530
|
var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
|
|
860
1531
|
var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
|
|
861
|
-
function
|
|
1532
|
+
function sha256Hex2(preimage) {
|
|
862
1533
|
return createHash2("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
|
|
863
1534
|
}
|
|
864
1535
|
function requireHex64(value, name) {
|
|
@@ -873,7 +1544,7 @@ function requireHex64(value, name) {
|
|
|
873
1544
|
function dseDispatchHash(fields) {
|
|
874
1545
|
requireHex64(fields.program_id, "program_id");
|
|
875
1546
|
requireHex64(fields.registry_record_hash, "registry_record_hash");
|
|
876
|
-
return
|
|
1547
|
+
return sha256Hex2(
|
|
877
1548
|
[
|
|
878
1549
|
DSE_FORMULA_VERSION,
|
|
879
1550
|
fields.envelope_id,
|
|
@@ -887,7 +1558,7 @@ function dcaeClosureHash(fields) {
|
|
|
887
1558
|
requireHex64(fields.manifest_hash, "manifest_hash");
|
|
888
1559
|
requireHex64(fields.aer_hash, "aer_hash");
|
|
889
1560
|
requireHex64(fields.zksp_binding_hash, "zksp_binding_hash");
|
|
890
|
-
return
|
|
1561
|
+
return sha256Hex2(
|
|
891
1562
|
[
|
|
892
1563
|
DCAE_PREIMAGE_VERSION,
|
|
893
1564
|
fields.manifest_hash,
|
|
@@ -900,7 +1571,7 @@ function dcaeClosureHash(fields) {
|
|
|
900
1571
|
}
|
|
901
1572
|
function cresReceiptHash(fields) {
|
|
902
1573
|
requireHex64(fields.ciphertext_hash_at_shred, "ciphertext_hash_at_shred");
|
|
903
|
-
return
|
|
1574
|
+
return sha256Hex2(
|
|
904
1575
|
[
|
|
905
1576
|
CRES_PREIMAGE_VERSION,
|
|
906
1577
|
fields.envelope_id,
|
|
@@ -921,11 +1592,13 @@ export {
|
|
|
921
1592
|
CRES_PREIMAGE_VERSION,
|
|
922
1593
|
DCAE_PREIMAGE_VERSION,
|
|
923
1594
|
DSE_FORMULA_VERSION,
|
|
1595
|
+
EXECUTE_FIELDS,
|
|
924
1596
|
MAX_BATCH_ROWS,
|
|
925
1597
|
NotFoundError,
|
|
926
1598
|
RUN_STATUSES,
|
|
927
1599
|
VerdifaxClient,
|
|
928
1600
|
VerdifaxError,
|
|
1601
|
+
VerdifaxJob,
|
|
929
1602
|
captureEnvironment,
|
|
930
1603
|
cresReceiptHash,
|
|
931
1604
|
dcaeClosureHash,
|
|
@@ -933,9 +1606,16 @@ export {
|
|
|
933
1606
|
formatProviderPayload,
|
|
934
1607
|
fromEnv,
|
|
935
1608
|
generateRequestKeyPair,
|
|
1609
|
+
policyActivationApproval,
|
|
1610
|
+
policyActivationPreimage,
|
|
1611
|
+
requestKeyChangeApproval,
|
|
1612
|
+
requestKeyChangePreimage,
|
|
936
1613
|
requestKeyRegistrationBody,
|
|
937
1614
|
requestSignaturePreimage,
|
|
1615
|
+
requestSignaturePreimageV2,
|
|
938
1616
|
signRequest,
|
|
1617
|
+
signRequestV2,
|
|
939
1618
|
validateHex64,
|
|
1619
|
+
validateJobName,
|
|
940
1620
|
validateRouteId
|
|
941
1621
|
};
|