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.js
CHANGED
|
@@ -27,11 +27,13 @@ __export(index_exports, {
|
|
|
27
27
|
CRES_PREIMAGE_VERSION: () => CRES_PREIMAGE_VERSION,
|
|
28
28
|
DCAE_PREIMAGE_VERSION: () => DCAE_PREIMAGE_VERSION,
|
|
29
29
|
DSE_FORMULA_VERSION: () => DSE_FORMULA_VERSION,
|
|
30
|
+
EXECUTE_FIELDS: () => EXECUTE_FIELDS,
|
|
30
31
|
MAX_BATCH_ROWS: () => MAX_BATCH_ROWS,
|
|
31
32
|
NotFoundError: () => NotFoundError,
|
|
32
33
|
RUN_STATUSES: () => RUN_STATUSES,
|
|
33
34
|
VerdifaxClient: () => VerdifaxClient,
|
|
34
35
|
VerdifaxError: () => VerdifaxError,
|
|
36
|
+
VerdifaxJob: () => VerdifaxJob,
|
|
35
37
|
captureEnvironment: () => captureEnvironment,
|
|
36
38
|
cresReceiptHash: () => cresReceiptHash,
|
|
37
39
|
dcaeClosureHash: () => dcaeClosureHash,
|
|
@@ -39,10 +41,17 @@ __export(index_exports, {
|
|
|
39
41
|
formatProviderPayload: () => formatProviderPayload,
|
|
40
42
|
fromEnv: () => fromEnv,
|
|
41
43
|
generateRequestKeyPair: () => generateRequestKeyPair,
|
|
44
|
+
policyActivationApproval: () => policyActivationApproval,
|
|
45
|
+
policyActivationPreimage: () => policyActivationPreimage,
|
|
46
|
+
requestKeyChangeApproval: () => requestKeyChangeApproval,
|
|
47
|
+
requestKeyChangePreimage: () => requestKeyChangePreimage,
|
|
42
48
|
requestKeyRegistrationBody: () => requestKeyRegistrationBody,
|
|
43
49
|
requestSignaturePreimage: () => requestSignaturePreimage,
|
|
50
|
+
requestSignaturePreimageV2: () => requestSignaturePreimageV2,
|
|
44
51
|
signRequest: () => signRequest,
|
|
52
|
+
signRequestV2: () => signRequestV2,
|
|
45
53
|
validateHex64: () => validateHex64,
|
|
54
|
+
validateJobName: () => validateJobName,
|
|
46
55
|
validateRouteId: () => validateRouteId
|
|
47
56
|
});
|
|
48
57
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -72,15 +81,23 @@ var NotFoundError = class extends VerdifaxError {
|
|
|
72
81
|
name = "VerdifaxNotFoundError";
|
|
73
82
|
};
|
|
74
83
|
var APIError = class extends VerdifaxError {
|
|
75
|
-
constructor(status, path, message) {
|
|
84
|
+
constructor(status, path, message, recordHash, signedRequest) {
|
|
76
85
|
super(`verdifax: API error at ${path} (HTTP ${status}): ${message}`);
|
|
77
86
|
this.status = status;
|
|
78
87
|
this.path = path;
|
|
79
88
|
this.message = message;
|
|
89
|
+
this.recordHash = recordHash;
|
|
90
|
+
if (signedRequest !== void 0) {
|
|
91
|
+
Object.defineProperty(this, "signedRequest", {
|
|
92
|
+
value: signedRequest,
|
|
93
|
+
enumerable: false
|
|
94
|
+
});
|
|
95
|
+
}
|
|
80
96
|
}
|
|
81
97
|
status;
|
|
82
98
|
path;
|
|
83
99
|
message;
|
|
100
|
+
recordHash;
|
|
84
101
|
name = "VerdifaxAPIError";
|
|
85
102
|
};
|
|
86
103
|
var APIKeyRequiredError = class extends VerdifaxError {
|
|
@@ -92,8 +109,286 @@ var APIKeyRequiredError = class extends VerdifaxError {
|
|
|
92
109
|
|
|
93
110
|
// src/signing.ts
|
|
94
111
|
var import_node_crypto = require("crypto");
|
|
112
|
+
|
|
113
|
+
// src/validation.ts
|
|
114
|
+
function validateHex64(value, fieldName) {
|
|
115
|
+
if (typeof value !== "string" || value.length !== 64) {
|
|
116
|
+
throw new VerdifaxError(
|
|
117
|
+
`verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
121
|
+
throw new VerdifaxError(
|
|
122
|
+
`verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function validateRouteId(value) {
|
|
127
|
+
if (!value || typeof value !== "string") {
|
|
128
|
+
throw new VerdifaxError("verdifax: route_id is required");
|
|
129
|
+
}
|
|
130
|
+
if (value.length > 256) {
|
|
131
|
+
throw new VerdifaxError(
|
|
132
|
+
`verdifax: route_id is too long (${value.length} > 256)`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (!/^[\x20-\x7E]+$/.test(value)) {
|
|
136
|
+
throw new VerdifaxError(
|
|
137
|
+
"verdifax: route_id contains non-printable-ASCII character"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function validateJobName(value, fieldName) {
|
|
142
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
|
|
143
|
+
throw new VerdifaxError(
|
|
144
|
+
`verdifax: ${fieldName} must be 1 to 128 letters, digits, dots, underscores, colons or dashes`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/helpers.ts
|
|
150
|
+
function formatProviderPayload(provider, prompt, response) {
|
|
151
|
+
if ([provider, prompt, response].some((text) => /[\uD800-\uDFFF]/u.test(text))) {
|
|
152
|
+
throw new VerdifaxError(
|
|
153
|
+
"verdifax: provider payload text cannot be encoded as UTF-8"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return JSON.stringify({ prompt, provider, response });
|
|
157
|
+
}
|
|
158
|
+
function isProvided(value) {
|
|
159
|
+
return value !== null && value !== void 0 && value !== "" && value !== false;
|
|
160
|
+
}
|
|
161
|
+
function withAttestedFlag(context) {
|
|
162
|
+
const provided = Object.entries(context).some(
|
|
163
|
+
([name, value]) => name !== "attested" && isProvided(value)
|
|
164
|
+
);
|
|
165
|
+
return { ...context, attested: provided };
|
|
166
|
+
}
|
|
167
|
+
function withDeclaredFlag(context) {
|
|
168
|
+
const declared = Object.entries(context).some(
|
|
169
|
+
([name, value]) => name !== "declared" && isProvided(value) && !(Array.isArray(value) && value.length === 0)
|
|
170
|
+
);
|
|
171
|
+
return { ...context, declared };
|
|
172
|
+
}
|
|
173
|
+
function refuseCallerOriginHashes(substance) {
|
|
174
|
+
const origin = substance?.origin;
|
|
175
|
+
if (origin?.source_ip_hash || origin?.user_agent_hash) {
|
|
176
|
+
throw new VerdifaxError(
|
|
177
|
+
"verdifax: request_substance.origin.source_ip_hash and user_agent_hash are computed by the server and cannot be supplied"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
var JOB_FIELDS = {
|
|
182
|
+
job_id: true,
|
|
183
|
+
step_id: true,
|
|
184
|
+
previous_record_hashes: true,
|
|
185
|
+
external_previous: true,
|
|
186
|
+
final: true
|
|
187
|
+
};
|
|
188
|
+
function refuseUnknownFields(value, known, label) {
|
|
189
|
+
for (const name of Object.keys(value)) {
|
|
190
|
+
if (!Object.hasOwn(known, name)) {
|
|
191
|
+
throw new VerdifaxError(`verdifax: ${label} has an unknown field: ${name}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function sortedUnique(hashes, fieldName) {
|
|
196
|
+
if (hashes === void 0) return [];
|
|
197
|
+
if (!Array.isArray(hashes) || hashes.some((hash) => typeof hash !== "string")) {
|
|
198
|
+
throw new VerdifaxError(`verdifax: ${fieldName} must be a list of strings`);
|
|
199
|
+
}
|
|
200
|
+
return [...new Set(hashes)].sort();
|
|
201
|
+
}
|
|
202
|
+
function normalizedJob(job) {
|
|
203
|
+
if (typeof job !== "object" || job === null || Array.isArray(job)) {
|
|
204
|
+
throw new VerdifaxError("verdifax: job must be an object");
|
|
205
|
+
}
|
|
206
|
+
refuseUnknownFields(job, JOB_FIELDS, "job");
|
|
207
|
+
validateJobName(job.job_id, "job_id");
|
|
208
|
+
validateJobName(job.step_id, "step_id");
|
|
209
|
+
if (job.final !== void 0 && typeof job.final !== "boolean") {
|
|
210
|
+
throw new VerdifaxError("verdifax: job.final must be true or false");
|
|
211
|
+
}
|
|
212
|
+
const out = { job_id: job.job_id, step_id: job.step_id };
|
|
213
|
+
const previous = sortedUnique(job.previous_record_hashes, "previous_record_hashes");
|
|
214
|
+
if (previous.length > 0) out.previous_record_hashes = previous;
|
|
215
|
+
const external = sortedUnique(job.external_previous, "external_previous");
|
|
216
|
+
if (external.length > 0) out.external_previous = external;
|
|
217
|
+
if (job.final) out.final = true;
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
var POLICY_ACTOR_TYPES = ["agent", "user", "service", "system"];
|
|
221
|
+
var POLICY_OBJECT_TYPES = ["text", "transaction", "code", "data", "model", "file"];
|
|
222
|
+
var POLICY_CLASSIFICATIONS = [
|
|
223
|
+
"PUBLIC",
|
|
224
|
+
"INTERNAL",
|
|
225
|
+
"CONFIDENTIAL",
|
|
226
|
+
"RESTRICTED",
|
|
227
|
+
"TOP_SECRET",
|
|
228
|
+
"TS_SCI"
|
|
229
|
+
];
|
|
230
|
+
var MAX_POLICY_OBJECT_ID_BYTES = 256;
|
|
231
|
+
var POLICY_CONTEXT_FIELDS = {
|
|
232
|
+
actor_type: true,
|
|
233
|
+
mfa_satisfied: true,
|
|
234
|
+
session_age_seconds: true,
|
|
235
|
+
object_type: true,
|
|
236
|
+
object_id: true,
|
|
237
|
+
classification: true
|
|
238
|
+
};
|
|
239
|
+
function refuseOutsideList(value, allowed, fieldName) {
|
|
240
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
241
|
+
throw new VerdifaxError(
|
|
242
|
+
`verdifax: policy_context.${fieldName} must be one of ${allowed.join(", ")}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function checkedPolicyContext(context) {
|
|
247
|
+
if (typeof context !== "object" || context === null || Array.isArray(context)) {
|
|
248
|
+
throw new VerdifaxError("verdifax: policy_context must be an object");
|
|
249
|
+
}
|
|
250
|
+
refuseUnknownFields(context, POLICY_CONTEXT_FIELDS, "policy_context");
|
|
251
|
+
const out = {};
|
|
252
|
+
const {
|
|
253
|
+
actor_type,
|
|
254
|
+
mfa_satisfied,
|
|
255
|
+
session_age_seconds,
|
|
256
|
+
object_type,
|
|
257
|
+
object_id,
|
|
258
|
+
classification
|
|
259
|
+
} = context;
|
|
260
|
+
if (actor_type !== void 0) {
|
|
261
|
+
refuseOutsideList(actor_type, POLICY_ACTOR_TYPES, "actor_type");
|
|
262
|
+
out.actor_type = actor_type;
|
|
263
|
+
}
|
|
264
|
+
if (mfa_satisfied !== void 0) {
|
|
265
|
+
if (typeof mfa_satisfied !== "boolean") {
|
|
266
|
+
throw new VerdifaxError(
|
|
267
|
+
"verdifax: policy_context.mfa_satisfied must be true or false"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
out.mfa_satisfied = mfa_satisfied;
|
|
271
|
+
}
|
|
272
|
+
if (session_age_seconds !== void 0) {
|
|
273
|
+
if (!Number.isSafeInteger(session_age_seconds) || session_age_seconds < 0) {
|
|
274
|
+
throw new VerdifaxError(
|
|
275
|
+
"verdifax: policy_context.session_age_seconds must be a whole number of seconds, not negative"
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
out.session_age_seconds = session_age_seconds;
|
|
279
|
+
}
|
|
280
|
+
if (object_type !== void 0) {
|
|
281
|
+
refuseOutsideList(object_type, POLICY_OBJECT_TYPES, "object_type");
|
|
282
|
+
out.object_type = object_type;
|
|
283
|
+
}
|
|
284
|
+
if (object_id !== void 0) {
|
|
285
|
+
if (typeof object_id !== "string") {
|
|
286
|
+
throw new VerdifaxError("verdifax: policy_context.object_id must be a string");
|
|
287
|
+
}
|
|
288
|
+
if (Buffer.byteLength(object_id, "utf8") > MAX_POLICY_OBJECT_ID_BYTES) {
|
|
289
|
+
throw new VerdifaxError(
|
|
290
|
+
`verdifax: policy_context.object_id is longer than ${MAX_POLICY_OBJECT_ID_BYTES} bytes`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
out.object_id = object_id;
|
|
294
|
+
}
|
|
295
|
+
if (classification !== void 0) {
|
|
296
|
+
refuseOutsideList(classification, POLICY_CLASSIFICATIONS, "classification");
|
|
297
|
+
out.classification = classification;
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
function refuseActorFactsWithoutActor(context, attested) {
|
|
302
|
+
const declaresActor = context.actor_type !== void 0 || context.mfa_satisfied === true || (context.session_age_seconds ?? 0) !== 0;
|
|
303
|
+
if (declaresActor && attested?.attested !== true) {
|
|
304
|
+
throw new VerdifaxError(
|
|
305
|
+
"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"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
var EXECUTE_FIELD_SET = {
|
|
310
|
+
payload: true,
|
|
311
|
+
payload_text: true,
|
|
312
|
+
program_id: true,
|
|
313
|
+
route_id: true,
|
|
314
|
+
registry_record_hash: true,
|
|
315
|
+
ai_output_text: true,
|
|
316
|
+
attested_context: true,
|
|
317
|
+
reproducibility_context: true,
|
|
318
|
+
request_substance: true,
|
|
319
|
+
authorization_chain: true,
|
|
320
|
+
regulatory_scaffolding: true,
|
|
321
|
+
causal_graph: true,
|
|
322
|
+
request_signature: true,
|
|
323
|
+
signed_request: true,
|
|
324
|
+
job: true,
|
|
325
|
+
policy_context: true,
|
|
326
|
+
expected_policy_hash: true
|
|
327
|
+
};
|
|
328
|
+
var EXECUTE_FIELDS = Object.freeze(
|
|
329
|
+
Object.keys(EXECUTE_FIELD_SET)
|
|
330
|
+
);
|
|
331
|
+
var ATTESTED_CONTEXT_FIELDS = {
|
|
332
|
+
attested: true,
|
|
333
|
+
actor_id: true,
|
|
334
|
+
actor_role: true,
|
|
335
|
+
authorization_policy: true,
|
|
336
|
+
actor_signature: true,
|
|
337
|
+
model_provider: true,
|
|
338
|
+
model_name: true,
|
|
339
|
+
model_version: true,
|
|
340
|
+
model_temperature: true,
|
|
341
|
+
prompt_hash: true,
|
|
342
|
+
decision_kind: true,
|
|
343
|
+
decision_result: true,
|
|
344
|
+
decision_note: true
|
|
345
|
+
};
|
|
346
|
+
function refuseUnknownRequestFields(req, label = "request") {
|
|
347
|
+
if (typeof req !== "object" || req === null || Array.isArray(req)) {
|
|
348
|
+
throw new VerdifaxError(`verdifax: ${label} must be an object`);
|
|
349
|
+
}
|
|
350
|
+
refuseUnknownFields(req, EXECUTE_FIELD_SET, label);
|
|
351
|
+
const context = req.attested_context;
|
|
352
|
+
if (typeof context === "object" && context !== null) {
|
|
353
|
+
refuseUnknownFields(context, ATTESTED_CONTEXT_FIELDS, "attested_context");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function withExecuteFlags(req) {
|
|
357
|
+
refuseUnknownRequestFields(req);
|
|
358
|
+
const body = { ...req };
|
|
359
|
+
if (req.job === void 0 || req.job === null) {
|
|
360
|
+
delete body.job;
|
|
361
|
+
} else {
|
|
362
|
+
body.job = normalizedJob(req.job);
|
|
363
|
+
}
|
|
364
|
+
if (req.attested_context) {
|
|
365
|
+
body.attested_context = withAttestedFlag(req.attested_context);
|
|
366
|
+
}
|
|
367
|
+
if (req.reproducibility_context) {
|
|
368
|
+
body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
|
|
369
|
+
}
|
|
370
|
+
if (req.policy_context === void 0 || req.policy_context === null) {
|
|
371
|
+
delete body.policy_context;
|
|
372
|
+
} else {
|
|
373
|
+
body.policy_context = checkedPolicyContext(req.policy_context);
|
|
374
|
+
refuseActorFactsWithoutActor(body.policy_context, body.attested_context);
|
|
375
|
+
}
|
|
376
|
+
if (req.expected_policy_hash === void 0 || req.expected_policy_hash === null) {
|
|
377
|
+
delete body.expected_policy_hash;
|
|
378
|
+
} else {
|
|
379
|
+
validateHex64(req.expected_policy_hash, "expected_policy_hash");
|
|
380
|
+
}
|
|
381
|
+
return body;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/signing.ts
|
|
95
385
|
var REQUEST_SIGNATURE_FORM = "verdifax.request.v1";
|
|
386
|
+
var REQUEST_SIGNATURE_FORM_V2 = "verdifax.request.v2";
|
|
387
|
+
var REQUEST_SIGNATURE_VERSION_2 = "2";
|
|
388
|
+
var JOB_FORM = "verdifax.job.v1";
|
|
96
389
|
var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
|
|
390
|
+
var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
|
|
391
|
+
var POLICY_ACTIVATION_FORM = "verdifax.policy-activation.v1";
|
|
97
392
|
var SIGNING_KEY_BYTES = 32;
|
|
98
393
|
var PKCS8_ED25519_HEADER = Buffer.from(
|
|
99
394
|
"302e020100300506032b657004220420",
|
|
@@ -124,6 +419,12 @@ function loadSigningKey(key) {
|
|
|
124
419
|
type: "pkcs8"
|
|
125
420
|
});
|
|
126
421
|
}
|
|
422
|
+
function sha256Hex(text) {
|
|
423
|
+
return (0, import_node_crypto.createHash)("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
424
|
+
}
|
|
425
|
+
function signedAtNow() {
|
|
426
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
427
|
+
}
|
|
127
428
|
function requestSignaturePreimage(fields) {
|
|
128
429
|
return Buffer.from(
|
|
129
430
|
[
|
|
@@ -138,13 +439,62 @@ function requestSignaturePreimage(fields) {
|
|
|
138
439
|
"utf8"
|
|
139
440
|
);
|
|
140
441
|
}
|
|
442
|
+
function requestSignaturePreimageV2(fields) {
|
|
443
|
+
const { job } = fields;
|
|
444
|
+
return Buffer.from(
|
|
445
|
+
[
|
|
446
|
+
REQUEST_SIGNATURE_FORM_V2,
|
|
447
|
+
fields.body_hash,
|
|
448
|
+
fields.payload_hash,
|
|
449
|
+
fields.program_id,
|
|
450
|
+
fields.route_id,
|
|
451
|
+
fields.registry_record_hash,
|
|
452
|
+
fields.ai_output_hash,
|
|
453
|
+
JOB_FORM,
|
|
454
|
+
job?.job_id ?? "",
|
|
455
|
+
job?.step_id ?? "",
|
|
456
|
+
(job?.previous_record_hashes ?? []).join(","),
|
|
457
|
+
(job?.external_previous ?? []).join(","),
|
|
458
|
+
job?.final ? "true" : "false",
|
|
459
|
+
fields.signed_at,
|
|
460
|
+
fields.nonce
|
|
461
|
+
].join("\0"),
|
|
462
|
+
"utf8"
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
function signRequestV2(signingKey, req, options = {}) {
|
|
466
|
+
const key = loadSigningKey(signingKey);
|
|
467
|
+
const { request_signature: _unsigned, ...fields } = withExecuteFlags(req);
|
|
468
|
+
const signed_request = JSON.stringify(fields);
|
|
469
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
470
|
+
const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
471
|
+
const preimage = requestSignaturePreimageV2({
|
|
472
|
+
body_hash: sha256Hex(signed_request),
|
|
473
|
+
payload_hash: sha256Hex(fields.payload_text || fields.payload || ""),
|
|
474
|
+
program_id: fields.program_id,
|
|
475
|
+
route_id: fields.route_id,
|
|
476
|
+
registry_record_hash: fields.registry_record_hash,
|
|
477
|
+
ai_output_hash: fields.ai_output_text ? sha256Hex(fields.ai_output_text) : "",
|
|
478
|
+
job: fields.job,
|
|
479
|
+
signed_at,
|
|
480
|
+
nonce
|
|
481
|
+
});
|
|
482
|
+
return {
|
|
483
|
+
signed_request,
|
|
484
|
+
request_signature: {
|
|
485
|
+
version: REQUEST_SIGNATURE_VERSION_2,
|
|
486
|
+
signature: (0, import_node_crypto.sign)(null, preimage, key).toString("base64"),
|
|
487
|
+
signed_at,
|
|
488
|
+
nonce
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
141
492
|
function signRequest(signingKey, req, options = {}) {
|
|
142
493
|
const key = loadSigningKey(signingKey);
|
|
143
|
-
const signed_at = options.signedAt ?? (
|
|
494
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
144
495
|
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
496
|
const preimage = requestSignaturePreimage({
|
|
147
|
-
payload_hash,
|
|
497
|
+
payload_hash: sha256Hex(req.payload_text || req.payload || ""),
|
|
148
498
|
program_id: req.program_id,
|
|
149
499
|
route_id: req.route_id,
|
|
150
500
|
registry_record_hash: req.registry_record_hash,
|
|
@@ -167,81 +517,164 @@ function requestKeyRegistrationBody(signingKey) {
|
|
|
167
517
|
).toString("base64");
|
|
168
518
|
return { public_key, proof };
|
|
169
519
|
}
|
|
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)
|
|
520
|
+
function requestKeyChangePreimage(fields) {
|
|
521
|
+
return Buffer.from(
|
|
522
|
+
[
|
|
523
|
+
REQUEST_KEY_CHANGE_FORM,
|
|
524
|
+
fields.new_public_key,
|
|
525
|
+
fields.signed_at,
|
|
526
|
+
fields.nonce
|
|
527
|
+
].join("\0"),
|
|
528
|
+
"utf8"
|
|
186
529
|
);
|
|
187
|
-
return { ...context, attested: provided };
|
|
188
530
|
}
|
|
189
|
-
function
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
);
|
|
193
|
-
|
|
531
|
+
function requestKeyChangeApproval(currentSigningKey, newPublicKey, options = {}) {
|
|
532
|
+
const key = loadSigningKey(currentSigningKey);
|
|
533
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
534
|
+
const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
535
|
+
const preimage = requestKeyChangePreimage({
|
|
536
|
+
new_public_key: newPublicKey,
|
|
537
|
+
signed_at,
|
|
538
|
+
nonce
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
541
|
+
signature: (0, import_node_crypto.sign)(null, preimage, key).toString("base64"),
|
|
542
|
+
signed_at,
|
|
543
|
+
nonce
|
|
544
|
+
};
|
|
194
545
|
}
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
546
|
+
function policyActivationPreimage(fields) {
|
|
547
|
+
return Buffer.from(
|
|
548
|
+
[
|
|
549
|
+
POLICY_ACTIVATION_FORM,
|
|
550
|
+
fields.chain_id,
|
|
551
|
+
fields.document_hash,
|
|
552
|
+
fields.previous_activation_hash,
|
|
553
|
+
fields.signed_at,
|
|
554
|
+
fields.nonce
|
|
555
|
+
].join("\0"),
|
|
556
|
+
"utf8"
|
|
557
|
+
);
|
|
202
558
|
}
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
559
|
+
function policyActivationApproval(signingKey, fields, options = {}) {
|
|
560
|
+
const key = loadSigningKey(signingKey);
|
|
561
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
562
|
+
const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
563
|
+
const preimage = policyActivationPreimage({
|
|
564
|
+
chain_id: fields.chain_id,
|
|
565
|
+
document_hash: fields.document_hash,
|
|
566
|
+
previous_activation_hash: fields.previous_activation_hash,
|
|
567
|
+
signed_at,
|
|
568
|
+
nonce
|
|
569
|
+
});
|
|
570
|
+
return {
|
|
571
|
+
signature: (0, import_node_crypto.sign)(null, preimage, key).toString("base64"),
|
|
572
|
+
signed_at,
|
|
573
|
+
nonce
|
|
574
|
+
};
|
|
212
575
|
}
|
|
213
576
|
|
|
214
|
-
// src/
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
577
|
+
// src/job.ts
|
|
578
|
+
var STEP_OPTIONS = {
|
|
579
|
+
payload: true,
|
|
580
|
+
payload_text: true,
|
|
581
|
+
program_id: true,
|
|
582
|
+
route_id: true,
|
|
583
|
+
registry_record_hash: true,
|
|
584
|
+
ai_output_text: true,
|
|
585
|
+
attested_context: true,
|
|
586
|
+
reproducibility_context: true,
|
|
587
|
+
request_substance: true,
|
|
588
|
+
authorization_chain: true,
|
|
589
|
+
regulatory_scaffolding: true,
|
|
590
|
+
causal_graph: true,
|
|
591
|
+
policy_context: true,
|
|
592
|
+
expected_policy_hash: true,
|
|
593
|
+
after: true,
|
|
594
|
+
external_previous: true,
|
|
595
|
+
final: true
|
|
596
|
+
};
|
|
597
|
+
var VerdifaxJob = class {
|
|
598
|
+
jobId;
|
|
599
|
+
client;
|
|
600
|
+
hashes = /* @__PURE__ */ new Map();
|
|
601
|
+
#signedRequests = /* @__PURE__ */ new Map();
|
|
602
|
+
lastStepId;
|
|
603
|
+
constructor(client, jobId) {
|
|
604
|
+
validateJobName(jobId, "job_id");
|
|
605
|
+
this.client = client;
|
|
606
|
+
this.jobId = jobId;
|
|
607
|
+
}
|
|
608
|
+
/** Step id to record hash for every step that left a record, on a copy. */
|
|
609
|
+
get recordHashes() {
|
|
610
|
+
return Object.fromEntries(this.hashes);
|
|
611
|
+
}
|
|
612
|
+
/** Step id to the exact text that was signed, for every signed step that
|
|
613
|
+
* left a record, refused and halted steps included, on a copy. Each text
|
|
614
|
+
* can be passed to `verdifax-verify --request FILE`. */
|
|
615
|
+
get signedRequests() {
|
|
616
|
+
return Object.fromEntries(this.#signedRequests);
|
|
617
|
+
}
|
|
618
|
+
/** Runs one step and records its hash and signed text. A refused or halted
|
|
619
|
+
* step records both from the error, then throws the same error `attest`
|
|
620
|
+
* throws. */
|
|
621
|
+
async step(stepId, options) {
|
|
622
|
+
validateJobName(stepId, "step_id");
|
|
623
|
+
refuseUnknownFields(options, STEP_OPTIONS, "step options");
|
|
624
|
+
const { after, external_previous, final, ...attest } = options;
|
|
625
|
+
const job = {
|
|
626
|
+
job_id: this.jobId,
|
|
627
|
+
step_id: stepId,
|
|
628
|
+
previous_record_hashes: this.previousHashes(after),
|
|
629
|
+
external_previous,
|
|
630
|
+
final
|
|
631
|
+
};
|
|
632
|
+
let result;
|
|
633
|
+
try {
|
|
634
|
+
result = await this.client.attest({ ...attest, job });
|
|
635
|
+
} catch (err) {
|
|
636
|
+
if (err instanceof APIError && err.recordHash) {
|
|
637
|
+
this.record(stepId, err.recordHash, err.signedRequest);
|
|
638
|
+
}
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
const hash = result?.record_hash || result?.manifest_hash || result?.manifest?.ManifestHash;
|
|
642
|
+
if (!hash) {
|
|
643
|
+
throw new VerdifaxError(
|
|
644
|
+
`verdifax: the server returned no record_hash or manifest_hash for step ${stepId}, so the job map cannot continue from it`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
this.record(stepId, hash, result.signed_request);
|
|
648
|
+
return result;
|
|
230
649
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
650
|
+
previousHashes(after) {
|
|
651
|
+
if (after === void 0 || after === null) {
|
|
652
|
+
return this.lastStepId === void 0 ? [] : [this.hashes.get(this.lastStepId)];
|
|
653
|
+
}
|
|
654
|
+
if (!Array.isArray(after)) {
|
|
655
|
+
throw new VerdifaxError("verdifax: after must be a list of step ids");
|
|
656
|
+
}
|
|
657
|
+
return after.map((stepId) => {
|
|
658
|
+
const hash = this.hashes.get(stepId);
|
|
659
|
+
if (hash === void 0) {
|
|
660
|
+
throw new VerdifaxError(
|
|
661
|
+
`verdifax: after names ${String(stepId)}, which is not a step in job ${this.jobId}`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return hash;
|
|
665
|
+
});
|
|
235
666
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
667
|
+
record(stepId, hash, signedRequest) {
|
|
668
|
+
this.hashes.set(stepId, hash);
|
|
669
|
+
if (signedRequest !== void 0) {
|
|
670
|
+
this.#signedRequests.set(stepId, signedRequest);
|
|
671
|
+
}
|
|
672
|
+
this.lastStepId = stepId;
|
|
240
673
|
}
|
|
241
|
-
}
|
|
674
|
+
};
|
|
242
675
|
|
|
243
676
|
// src/version.ts
|
|
244
|
-
var SDK_VERSION = "1.
|
|
677
|
+
var SDK_VERSION = "1.2.0";
|
|
245
678
|
|
|
246
679
|
// src/client.ts
|
|
247
680
|
var DEFAULT_BASE_URL = "https://api.verdifax.com";
|
|
@@ -251,10 +684,42 @@ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
|
|
|
251
684
|
var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
|
|
252
685
|
var HEADER_API_KEY = "X-Verdifax-Key";
|
|
253
686
|
var REQUEST_KEY_PATH = "/keys/request-key";
|
|
687
|
+
var POLICIES_PATH = "/policies";
|
|
254
688
|
var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
|
|
255
689
|
var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
|
|
256
690
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
257
691
|
var MAX_BATCH_ROWS = 1e3;
|
|
692
|
+
var ATTEST_FIELDS = {
|
|
693
|
+
payload: true,
|
|
694
|
+
payload_text: true,
|
|
695
|
+
program_id: true,
|
|
696
|
+
route_id: true,
|
|
697
|
+
registry_record_hash: true,
|
|
698
|
+
ai_output_text: true,
|
|
699
|
+
attested_context: true,
|
|
700
|
+
reproducibility_context: true,
|
|
701
|
+
request_substance: true,
|
|
702
|
+
authorization_chain: true,
|
|
703
|
+
regulatory_scaffolding: true,
|
|
704
|
+
causal_graph: true,
|
|
705
|
+
job: true,
|
|
706
|
+
policy_context: true,
|
|
707
|
+
expected_policy_hash: true
|
|
708
|
+
};
|
|
709
|
+
var PROVIDER_ARGS = Object.fromEntries(
|
|
710
|
+
[
|
|
711
|
+
"program_id",
|
|
712
|
+
"route_id",
|
|
713
|
+
"registry_record_hash",
|
|
714
|
+
"prompt",
|
|
715
|
+
"response",
|
|
716
|
+
"govern",
|
|
717
|
+
"job",
|
|
718
|
+
"policy_context",
|
|
719
|
+
"expected_policy_hash",
|
|
720
|
+
...EXECUTE_FIELDS
|
|
721
|
+
].map((name) => [name, true])
|
|
722
|
+
);
|
|
258
723
|
var VerdifaxClient = class {
|
|
259
724
|
baseURL;
|
|
260
725
|
apiKey;
|
|
@@ -290,25 +755,39 @@ var VerdifaxClient = class {
|
|
|
290
755
|
}
|
|
291
756
|
// ── Execute / Attest (auth) ───────────────────────────────────
|
|
292
757
|
async execute(req) {
|
|
758
|
+
refuseUnknownRequestFields(req);
|
|
293
759
|
this.requireExecuteFields(req);
|
|
294
|
-
|
|
760
|
+
const sent = this.prepared(req);
|
|
761
|
+
const out = await this.request("POST", "/execute", sent);
|
|
762
|
+
return withSignedText(out, sent);
|
|
295
763
|
}
|
|
296
764
|
/** Runs one payload twice on the server and compares the manifest
|
|
297
765
|
* hashes. One signature covers the request, and the server claims
|
|
298
|
-
* its nonce once. Both runs are recorded.
|
|
766
|
+
* its nonce once. Both runs are recorded. A run that happens twice
|
|
767
|
+
* cannot be one job step, so a `job` is refused here. */
|
|
299
768
|
async verifyDeterminism(req) {
|
|
769
|
+
refuseUnknownRequestFields(req);
|
|
300
770
|
this.requireExecuteFields(req);
|
|
301
|
-
|
|
771
|
+
if (req.job !== void 0 && req.job !== null) {
|
|
772
|
+
throw new VerdifaxError(
|
|
773
|
+
"verdifax: a determinism check runs twice, so it cannot be a job step"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
const sent = this.prepared(req);
|
|
777
|
+
const out = await this.request(
|
|
302
778
|
"POST",
|
|
303
779
|
"/execute/verify-determinism",
|
|
304
|
-
|
|
780
|
+
sent
|
|
305
781
|
);
|
|
782
|
+
return withSignedText(out, sent);
|
|
306
783
|
}
|
|
307
784
|
/** Sends up to {@link MAX_BATCH_ROWS} runs in one request. With a
|
|
308
785
|
* signing key each row carries its own signature and nonce. A row
|
|
309
786
|
* 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
|
-
*
|
|
787
|
+
* does not throw for it. The rows are not checked here, except a job
|
|
788
|
+
* block, a `policy_context` and an `expected_policy_hash`, which are
|
|
789
|
+
* refused before anything is sent: the server reports any other bad row
|
|
790
|
+
* in that row's result.
|
|
312
791
|
*
|
|
313
792
|
* This is one POST and it is never retried. After a timeout the
|
|
314
793
|
* server may have run some or all of the rows. */
|
|
@@ -322,9 +801,20 @@ var VerdifaxClient = class {
|
|
|
322
801
|
`verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
|
|
323
802
|
);
|
|
324
803
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
804
|
+
runs.forEach((run, i) => refuseUnknownRequestFields(run, `runs[${i}]`));
|
|
805
|
+
const sent = runs.map((run) => this.prepared(run));
|
|
806
|
+
const out = await this.request(
|
|
807
|
+
"POST",
|
|
808
|
+
"/execute/batch",
|
|
809
|
+
{ runs: sent }
|
|
810
|
+
);
|
|
811
|
+
if (!Array.isArray(out?.results)) return out;
|
|
812
|
+
return {
|
|
813
|
+
...out,
|
|
814
|
+
results: out.results.map(
|
|
815
|
+
(row, i) => i < sent.length ? withSignedText(row, sent[i]) : row
|
|
816
|
+
)
|
|
817
|
+
};
|
|
328
818
|
}
|
|
329
819
|
/** High-level convenience wrapper around `execute()`. Accepts
|
|
330
820
|
* either a Buffer (for binary payloads, auto-base64) or a
|
|
@@ -332,6 +822,8 @@ var VerdifaxClient = class {
|
|
|
332
822
|
* routes the call through to the canonical /execute. */
|
|
333
823
|
async attest(req) {
|
|
334
824
|
this.requireAPIKey();
|
|
825
|
+
refuseUnknownRequestFields(req, "attest request");
|
|
826
|
+
refuseUnknownFields(req, ATTEST_FIELDS, "attest request");
|
|
335
827
|
if (req.payload !== void 0 && req.payload_text !== void 0) {
|
|
336
828
|
throw new VerdifaxError(
|
|
337
829
|
"verdifax: attest accepts payload OR payload_text, not both"
|
|
@@ -355,7 +847,10 @@ var VerdifaxClient = class {
|
|
|
355
847
|
request_substance: req.request_substance,
|
|
356
848
|
authorization_chain: req.authorization_chain,
|
|
357
849
|
regulatory_scaffolding: req.regulatory_scaffolding,
|
|
358
|
-
causal_graph: req.causal_graph
|
|
850
|
+
causal_graph: req.causal_graph,
|
|
851
|
+
job: req.job,
|
|
852
|
+
policy_context: req.policy_context,
|
|
853
|
+
expected_policy_hash: req.expected_policy_hash
|
|
359
854
|
};
|
|
360
855
|
if (req.payload !== void 0) {
|
|
361
856
|
body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
|
|
@@ -364,6 +859,12 @@ var VerdifaxClient = class {
|
|
|
364
859
|
}
|
|
365
860
|
return this.execute(body);
|
|
366
861
|
}
|
|
862
|
+
/** A helper that runs the steps of one job through {@link attest} and
|
|
863
|
+
* keeps each step's record hash, so a later step can name it. It makes
|
|
864
|
+
* no request until a step runs. */
|
|
865
|
+
job(jobId) {
|
|
866
|
+
return new VerdifaxJob(this, jobId);
|
|
867
|
+
}
|
|
367
868
|
// ── Request signing (auth) ───────────────────────────────────
|
|
368
869
|
/** Registers this client's public key on its API key, with the proof
|
|
369
870
|
* that the client holds the private key. From then on the server
|
|
@@ -381,11 +882,162 @@ var VerdifaxClient = class {
|
|
|
381
882
|
requestKeyRegistrationBody(this.signingKey)
|
|
382
883
|
);
|
|
383
884
|
}
|
|
885
|
+
/** Replaces the registered key with `newSigningKey`. The new key proves
|
|
886
|
+
* itself and this client's current key approves the change. After
|
|
887
|
+
* success this client signs with the new key. If the call fails it
|
|
888
|
+
* keeps the current key, including after a timeout, when the server may
|
|
889
|
+
* have made the change. */
|
|
890
|
+
async rotateRequestKey(newSigningKey) {
|
|
891
|
+
this.requireAPIKey();
|
|
892
|
+
if (!this.signingKey) {
|
|
893
|
+
throw new VerdifaxError(
|
|
894
|
+
"verdifax: rotateRequestKey needs the current key in the signingKey client option"
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
const next = loadSigningKey(newSigningKey);
|
|
898
|
+
const registration = requestKeyRegistrationBody(next);
|
|
899
|
+
const out = await this.request(
|
|
900
|
+
"POST",
|
|
901
|
+
REQUEST_KEY_PATH,
|
|
902
|
+
{
|
|
903
|
+
...registration,
|
|
904
|
+
change: requestKeyChangeApproval(
|
|
905
|
+
this.signingKey,
|
|
906
|
+
registration.public_key
|
|
907
|
+
)
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
this.signingKey = next;
|
|
911
|
+
return out;
|
|
912
|
+
}
|
|
384
913
|
/** Clears the registered public key, so the server stops requiring
|
|
385
|
-
* request signatures on this API key.
|
|
914
|
+
* request signatures on this API key. With a signing key the client
|
|
915
|
+
* sends that key's approval, which the server requires while a key is
|
|
916
|
+
* registered. After success the client drops its signing key and sends
|
|
917
|
+
* unsigned requests, because the server refuses a signed run on an API
|
|
918
|
+
* key with no registered key. If the call fails it keeps the key. */
|
|
386
919
|
async clearRequestKey() {
|
|
387
920
|
this.requireAPIKey();
|
|
388
|
-
await this.request(
|
|
921
|
+
await this.request(
|
|
922
|
+
"DELETE",
|
|
923
|
+
REQUEST_KEY_PATH,
|
|
924
|
+
this.signingKey ? { change: requestKeyChangeApproval(this.signingKey, "") } : void 0
|
|
925
|
+
);
|
|
926
|
+
this.signingKey = void 0;
|
|
927
|
+
}
|
|
928
|
+
// ── Policies (auth) ──────────────────────────────────────────
|
|
929
|
+
/** Stores a policy document. Nothing changes until it is activated with
|
|
930
|
+
* {@link activatePolicy}. The server hashes the exact text, so this
|
|
931
|
+
* sends the string it is given unchanged and refuses anything that is
|
|
932
|
+
* not a string: serializing an object here would change the hash.
|
|
933
|
+
* Verdifax does not write, review or approve a customer's policy. */
|
|
934
|
+
async uploadPolicy(document) {
|
|
935
|
+
this.requireAPIKey();
|
|
936
|
+
if (typeof document !== "string") {
|
|
937
|
+
throw new VerdifaxError(
|
|
938
|
+
"verdifax: document must be the policy's exact text, as a string"
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
return this.request("POST", POLICIES_PATH, { document });
|
|
942
|
+
}
|
|
943
|
+
/** This key's stored documents, with the chain its changes belong to,
|
|
944
|
+
* the newest change in it (64 zeros when there is none yet) and the
|
|
945
|
+
* document in force (empty under the built-in policy). */
|
|
946
|
+
async listPolicies() {
|
|
947
|
+
this.requireAPIKey();
|
|
948
|
+
return this.request("GET", POLICIES_PATH);
|
|
949
|
+
}
|
|
950
|
+
/** One stored document, with its exact text as `document`. */
|
|
951
|
+
async getPolicy(documentHash) {
|
|
952
|
+
this.requireAPIKey();
|
|
953
|
+
validateHex64(documentHash, "documentHash");
|
|
954
|
+
return this.request(
|
|
955
|
+
"GET",
|
|
956
|
+
`${POLICIES_PATH}/${documentHash}`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
/** Shows which rule would fire on sample inputs, for a document given as
|
|
960
|
+
* text or one already stored, named by its hash: exactly one of the two.
|
|
961
|
+
* It seals nothing. */
|
|
962
|
+
async testPolicy(req) {
|
|
963
|
+
this.requireAPIKey();
|
|
964
|
+
const { inputs, document, documentHash } = req ?? {};
|
|
965
|
+
if (document === void 0 === (documentHash === void 0)) {
|
|
966
|
+
throw new VerdifaxError(
|
|
967
|
+
"verdifax: testPolicy needs exactly one of document or documentHash"
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) {
|
|
971
|
+
throw new VerdifaxError("verdifax: testPolicy needs inputs as an object");
|
|
972
|
+
}
|
|
973
|
+
const body = { inputs };
|
|
974
|
+
if (document !== void 0) {
|
|
975
|
+
if (typeof document !== "string") {
|
|
976
|
+
throw new VerdifaxError(
|
|
977
|
+
"verdifax: document must be the policy's exact text, as a string"
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
body.document = document;
|
|
981
|
+
} else {
|
|
982
|
+
validateHex64(documentHash, "documentHash");
|
|
983
|
+
body.document_hash = documentHash;
|
|
984
|
+
}
|
|
985
|
+
return this.request(
|
|
986
|
+
"POST",
|
|
987
|
+
`${POLICIES_PATH}/test`,
|
|
988
|
+
body
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
/** This key's policy changes, oldest first, as the list the server sends.
|
|
992
|
+
* Save it as JSON, for example with
|
|
993
|
+
* `writeFileSync("policy-history.json", JSON.stringify(history))`, and
|
|
994
|
+
* check it with `verdifax-verify --policy-history policy-history.json`. */
|
|
995
|
+
async getPolicyHistory() {
|
|
996
|
+
this.requireAPIKey();
|
|
997
|
+
return this.request("GET", `${POLICIES_PATH}/history`);
|
|
998
|
+
}
|
|
999
|
+
/** Makes a stored document the policy in force and returns the change
|
|
1000
|
+
* record. It first reads GET /policies for the chain and its newest
|
|
1001
|
+
* change, then names that change as the one this follows. With the
|
|
1002
|
+
* `signingKey` option the request key signs the change, which the server
|
|
1003
|
+
* requires while a request key is registered.
|
|
1004
|
+
*
|
|
1005
|
+
* Rolling back is activating an earlier document: it is one more change
|
|
1006
|
+
* in the chain, so the change it undoes stays on the record.
|
|
1007
|
+
*
|
|
1008
|
+
* An HTTP 409 means the chain moved because another change came first,
|
|
1009
|
+
* or the document is not yet effective. It throws the usual
|
|
1010
|
+
* {@link APIError} and is not retried: read the policies again and
|
|
1011
|
+
* decide. */
|
|
1012
|
+
async activatePolicy(documentHash) {
|
|
1013
|
+
this.requireAPIKey();
|
|
1014
|
+
validateHex64(documentHash, "documentHash");
|
|
1015
|
+
const chain = await this.listPolicies();
|
|
1016
|
+
const previous = chain?.newest_activation_hash;
|
|
1017
|
+
if (typeof previous !== "string") {
|
|
1018
|
+
throw new VerdifaxError(
|
|
1019
|
+
"verdifax: the server did not report newest_activation_hash, so the change cannot name the one it follows"
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
const body = { previous_activation_hash: previous };
|
|
1023
|
+
if (this.signingKey) {
|
|
1024
|
+
if (typeof chain.chain_id !== "string" || chain.chain_id === "") {
|
|
1025
|
+
throw new VerdifaxError(
|
|
1026
|
+
"verdifax: the server did not report chain_id, so the change cannot be signed"
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
body.signature = policyActivationApproval(this.signingKey, {
|
|
1030
|
+
chain_id: chain.chain_id,
|
|
1031
|
+
document_hash: documentHash,
|
|
1032
|
+
previous_activation_hash: previous
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
const out = await this.request(
|
|
1036
|
+
"POST",
|
|
1037
|
+
`${POLICIES_PATH}/${documentHash}/activate`,
|
|
1038
|
+
body
|
|
1039
|
+
);
|
|
1040
|
+
return out.activation;
|
|
389
1041
|
}
|
|
390
1042
|
// ── Runs ──────────────────────────────────────────────────────
|
|
391
1043
|
async getRun(runId) {
|
|
@@ -471,6 +1123,7 @@ var VerdifaxClient = class {
|
|
|
471
1123
|
const q = new URLSearchParams();
|
|
472
1124
|
if (params.from) q.set("from", params.from);
|
|
473
1125
|
if (params.to) q.set("to", params.to);
|
|
1126
|
+
if (params.job_id) q.set("job_id", params.job_id);
|
|
474
1127
|
q.set("format", format);
|
|
475
1128
|
return `/runs/export?${q.toString()}`;
|
|
476
1129
|
}
|
|
@@ -634,17 +1287,29 @@ var VerdifaxClient = class {
|
|
|
634
1287
|
}
|
|
635
1288
|
await this.request("DELETE", `/admin/keys/${keyId}`);
|
|
636
1289
|
}
|
|
1290
|
+
/** Clears the request key on an API key whose holder lost it, after a
|
|
1291
|
+
* manual check. It is the one way to clear a key without that key. The
|
|
1292
|
+
* id can be any API key, so this client keeps its own signing key. */
|
|
1293
|
+
async adminClearRequestKey(keyId) {
|
|
1294
|
+
this.requireAPIKey();
|
|
1295
|
+
if (keyId <= 0) {
|
|
1296
|
+
throw new VerdifaxError("verdifax: keyId must be > 0");
|
|
1297
|
+
}
|
|
1298
|
+
await this.request("DELETE", `/admin/keys/${keyId}/request-key`);
|
|
1299
|
+
}
|
|
637
1300
|
// ── Provider helpers (Claude / OpenAI) ───────────────────────
|
|
638
1301
|
/** Convenience for attesting an Anthropic Claude API response. With
|
|
639
1302
|
* `govern: true` the response text also goes to the AIVP review as
|
|
640
1303
|
* `ai_output_text`; that is off by default, so the text leaves your
|
|
641
1304
|
* machine for review only when you ask. */
|
|
642
1305
|
async attestClaudeResponse(args) {
|
|
1306
|
+
refuseUnknownFields(args, PROVIDER_ARGS, "attestClaudeResponse arguments");
|
|
643
1307
|
return this.attestProviderResponse({ ...args, provider: "anthropic" });
|
|
644
1308
|
}
|
|
645
1309
|
/** Convenience for attesting an OpenAI API response. `govern` works as
|
|
646
1310
|
* in {@link attestClaudeResponse}. */
|
|
647
1311
|
async attestOpenAIResponse(args) {
|
|
1312
|
+
refuseUnknownFields(args, PROVIDER_ARGS, "attestOpenAIResponse arguments");
|
|
648
1313
|
return this.attestProviderResponse({ ...args, provider: "openai" });
|
|
649
1314
|
}
|
|
650
1315
|
async attestProviderResponse(args) {
|
|
@@ -666,7 +1331,10 @@ var VerdifaxClient = class {
|
|
|
666
1331
|
attested_context: {
|
|
667
1332
|
actor_role: "ai_assistant",
|
|
668
1333
|
model_provider: args.provider
|
|
669
|
-
}
|
|
1334
|
+
},
|
|
1335
|
+
job: args.job,
|
|
1336
|
+
policy_context: args.policy_context,
|
|
1337
|
+
expected_policy_hash: args.expected_policy_hash
|
|
670
1338
|
});
|
|
671
1339
|
}
|
|
672
1340
|
// ── HTTP plumbing, retry-aware ──────────────────────────────
|
|
@@ -713,9 +1381,13 @@ var VerdifaxClient = class {
|
|
|
713
1381
|
continue;
|
|
714
1382
|
}
|
|
715
1383
|
let errMessage = text.trim();
|
|
1384
|
+
let recordHash;
|
|
716
1385
|
try {
|
|
717
1386
|
const env = JSON.parse(text);
|
|
718
1387
|
if (env.error) errMessage = env.error;
|
|
1388
|
+
if (typeof env.record_hash === "string" && env.record_hash) {
|
|
1389
|
+
recordHash = env.record_hash;
|
|
1390
|
+
}
|
|
719
1391
|
} catch {
|
|
720
1392
|
}
|
|
721
1393
|
switch (resp.status) {
|
|
@@ -724,7 +1396,13 @@ var VerdifaxClient = class {
|
|
|
724
1396
|
case 404:
|
|
725
1397
|
throw new NotFoundError(path, errMessage);
|
|
726
1398
|
default:
|
|
727
|
-
throw new APIError(
|
|
1399
|
+
throw new APIError(
|
|
1400
|
+
resp.status,
|
|
1401
|
+
path,
|
|
1402
|
+
errMessage,
|
|
1403
|
+
recordHash,
|
|
1404
|
+
body?.signed_request
|
|
1405
|
+
);
|
|
728
1406
|
}
|
|
729
1407
|
}
|
|
730
1408
|
if (rawText) return { text, headers: resp.headers };
|
|
@@ -765,13 +1443,15 @@ var VerdifaxClient = class {
|
|
|
765
1443
|
validateRouteId(req.route_id);
|
|
766
1444
|
refuseCallerOriginHashes(req.request_substance);
|
|
767
1445
|
}
|
|
768
|
-
/** The request with its derived flags
|
|
769
|
-
*
|
|
1446
|
+
/** The body to send, on a copy: the request with its derived flags or,
|
|
1447
|
+
* with a signing key, the signed text with a fresh signature and nonce. */
|
|
770
1448
|
prepared(req) {
|
|
771
|
-
|
|
772
|
-
return this.signingKey ? { ...body, request_signature: signRequest(this.signingKey, body) } : body;
|
|
1449
|
+
return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
|
|
773
1450
|
}
|
|
774
1451
|
};
|
|
1452
|
+
function withSignedText(result, sent) {
|
|
1453
|
+
return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
|
|
1454
|
+
}
|
|
775
1455
|
|
|
776
1456
|
// src/config.ts
|
|
777
1457
|
function fromEnv() {
|
|
@@ -899,7 +1579,7 @@ var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
|
|
|
899
1579
|
var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
|
|
900
1580
|
var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
|
|
901
1581
|
var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
|
|
902
|
-
function
|
|
1582
|
+
function sha256Hex2(preimage) {
|
|
903
1583
|
return (0, import_node_crypto2.createHash)("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
|
|
904
1584
|
}
|
|
905
1585
|
function requireHex64(value, name) {
|
|
@@ -914,7 +1594,7 @@ function requireHex64(value, name) {
|
|
|
914
1594
|
function dseDispatchHash(fields) {
|
|
915
1595
|
requireHex64(fields.program_id, "program_id");
|
|
916
1596
|
requireHex64(fields.registry_record_hash, "registry_record_hash");
|
|
917
|
-
return
|
|
1597
|
+
return sha256Hex2(
|
|
918
1598
|
[
|
|
919
1599
|
DSE_FORMULA_VERSION,
|
|
920
1600
|
fields.envelope_id,
|
|
@@ -928,7 +1608,7 @@ function dcaeClosureHash(fields) {
|
|
|
928
1608
|
requireHex64(fields.manifest_hash, "manifest_hash");
|
|
929
1609
|
requireHex64(fields.aer_hash, "aer_hash");
|
|
930
1610
|
requireHex64(fields.zksp_binding_hash, "zksp_binding_hash");
|
|
931
|
-
return
|
|
1611
|
+
return sha256Hex2(
|
|
932
1612
|
[
|
|
933
1613
|
DCAE_PREIMAGE_VERSION,
|
|
934
1614
|
fields.manifest_hash,
|
|
@@ -941,7 +1621,7 @@ function dcaeClosureHash(fields) {
|
|
|
941
1621
|
}
|
|
942
1622
|
function cresReceiptHash(fields) {
|
|
943
1623
|
requireHex64(fields.ciphertext_hash_at_shred, "ciphertext_hash_at_shred");
|
|
944
|
-
return
|
|
1624
|
+
return sha256Hex2(
|
|
945
1625
|
[
|
|
946
1626
|
CRES_PREIMAGE_VERSION,
|
|
947
1627
|
fields.envelope_id,
|
|
@@ -963,11 +1643,13 @@ function cresReceiptHash(fields) {
|
|
|
963
1643
|
CRES_PREIMAGE_VERSION,
|
|
964
1644
|
DCAE_PREIMAGE_VERSION,
|
|
965
1645
|
DSE_FORMULA_VERSION,
|
|
1646
|
+
EXECUTE_FIELDS,
|
|
966
1647
|
MAX_BATCH_ROWS,
|
|
967
1648
|
NotFoundError,
|
|
968
1649
|
RUN_STATUSES,
|
|
969
1650
|
VerdifaxClient,
|
|
970
1651
|
VerdifaxError,
|
|
1652
|
+
VerdifaxJob,
|
|
971
1653
|
captureEnvironment,
|
|
972
1654
|
cresReceiptHash,
|
|
973
1655
|
dcaeClosureHash,
|
|
@@ -975,9 +1657,16 @@ function cresReceiptHash(fields) {
|
|
|
975
1657
|
formatProviderPayload,
|
|
976
1658
|
fromEnv,
|
|
977
1659
|
generateRequestKeyPair,
|
|
1660
|
+
policyActivationApproval,
|
|
1661
|
+
policyActivationPreimage,
|
|
1662
|
+
requestKeyChangeApproval,
|
|
1663
|
+
requestKeyChangePreimage,
|
|
978
1664
|
requestKeyRegistrationBody,
|
|
979
1665
|
requestSignaturePreimage,
|
|
1666
|
+
requestSignaturePreimageV2,
|
|
980
1667
|
signRequest,
|
|
1668
|
+
signRequestV2,
|
|
981
1669
|
validateHex64,
|
|
1670
|
+
validateJobName,
|
|
982
1671
|
validateRouteId
|
|
983
1672
|
});
|