verdifax 0.3.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -10
- package/dist/index.d.mts +569 -21
- package/dist/index.d.ts +569 -21
- package/dist/index.js +804 -54
- package/dist/index.mjs +787 -54
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -23,15 +23,23 @@ var NotFoundError = class extends VerdifaxError {
|
|
|
23
23
|
name = "VerdifaxNotFoundError";
|
|
24
24
|
};
|
|
25
25
|
var APIError = class extends VerdifaxError {
|
|
26
|
-
constructor(status, path, message) {
|
|
26
|
+
constructor(status, path, message, recordHash, signedRequest) {
|
|
27
27
|
super(`verdifax: API error at ${path} (HTTP ${status}): ${message}`);
|
|
28
28
|
this.status = status;
|
|
29
29
|
this.path = path;
|
|
30
30
|
this.message = message;
|
|
31
|
+
this.recordHash = recordHash;
|
|
32
|
+
if (signedRequest !== void 0) {
|
|
33
|
+
Object.defineProperty(this, "signedRequest", {
|
|
34
|
+
value: signedRequest,
|
|
35
|
+
enumerable: false
|
|
36
|
+
});
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
status;
|
|
33
40
|
path;
|
|
34
41
|
message;
|
|
42
|
+
recordHash;
|
|
35
43
|
name = "VerdifaxAPIError";
|
|
36
44
|
};
|
|
37
45
|
var APIKeyRequiredError = class extends VerdifaxError {
|
|
@@ -51,8 +59,137 @@ import {
|
|
|
51
59
|
randomBytes,
|
|
52
60
|
sign
|
|
53
61
|
} from "crypto";
|
|
62
|
+
|
|
63
|
+
// src/validation.ts
|
|
64
|
+
function validateHex64(value, fieldName) {
|
|
65
|
+
if (typeof value !== "string" || value.length !== 64) {
|
|
66
|
+
throw new VerdifaxError(
|
|
67
|
+
`verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
71
|
+
throw new VerdifaxError(
|
|
72
|
+
`verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function validateRouteId(value) {
|
|
77
|
+
if (!value || typeof value !== "string") {
|
|
78
|
+
throw new VerdifaxError("verdifax: route_id is required");
|
|
79
|
+
}
|
|
80
|
+
if (value.length > 256) {
|
|
81
|
+
throw new VerdifaxError(
|
|
82
|
+
`verdifax: route_id is too long (${value.length} > 256)`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (!/^[\x20-\x7E]+$/.test(value)) {
|
|
86
|
+
throw new VerdifaxError(
|
|
87
|
+
"verdifax: route_id contains non-printable-ASCII character"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function validateJobName(value, fieldName) {
|
|
92
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
|
|
93
|
+
throw new VerdifaxError(
|
|
94
|
+
`verdifax: ${fieldName} must be 1 to 128 letters, digits, dots, underscores, colons or dashes`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/helpers.ts
|
|
100
|
+
function formatProviderPayload(provider, prompt, response) {
|
|
101
|
+
if ([provider, prompt, response].some((text) => /[\uD800-\uDFFF]/u.test(text))) {
|
|
102
|
+
throw new VerdifaxError(
|
|
103
|
+
"verdifax: provider payload text cannot be encoded as UTF-8"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return JSON.stringify({ prompt, provider, response });
|
|
107
|
+
}
|
|
108
|
+
function isProvided(value) {
|
|
109
|
+
return value !== null && value !== void 0 && value !== "" && value !== false;
|
|
110
|
+
}
|
|
111
|
+
function withAttestedFlag(context) {
|
|
112
|
+
const provided = Object.entries(context).some(
|
|
113
|
+
([name, value]) => name !== "attested" && isProvided(value)
|
|
114
|
+
);
|
|
115
|
+
return { ...context, attested: provided };
|
|
116
|
+
}
|
|
117
|
+
function withDeclaredFlag(context) {
|
|
118
|
+
const declared = Object.entries(context).some(
|
|
119
|
+
([name, value]) => name !== "declared" && isProvided(value) && !(Array.isArray(value) && value.length === 0)
|
|
120
|
+
);
|
|
121
|
+
return { ...context, declared };
|
|
122
|
+
}
|
|
123
|
+
function refuseCallerOriginHashes(substance) {
|
|
124
|
+
const origin = substance?.origin;
|
|
125
|
+
if (origin?.source_ip_hash || origin?.user_agent_hash) {
|
|
126
|
+
throw new VerdifaxError(
|
|
127
|
+
"verdifax: request_substance.origin.source_ip_hash and user_agent_hash are computed by the server and cannot be supplied"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
var JOB_FIELDS = {
|
|
132
|
+
job_id: true,
|
|
133
|
+
step_id: true,
|
|
134
|
+
previous_record_hashes: true,
|
|
135
|
+
external_previous: true,
|
|
136
|
+
final: true
|
|
137
|
+
};
|
|
138
|
+
function refuseUnknownFields(value, known, label) {
|
|
139
|
+
for (const name of Object.keys(value)) {
|
|
140
|
+
if (!Object.hasOwn(known, name)) {
|
|
141
|
+
throw new VerdifaxError(`verdifax: ${label} has an unknown field: ${name}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function sortedUnique(hashes, fieldName) {
|
|
146
|
+
if (hashes === void 0) return [];
|
|
147
|
+
if (!Array.isArray(hashes) || hashes.some((hash) => typeof hash !== "string")) {
|
|
148
|
+
throw new VerdifaxError(`verdifax: ${fieldName} must be a list of strings`);
|
|
149
|
+
}
|
|
150
|
+
return [...new Set(hashes)].sort();
|
|
151
|
+
}
|
|
152
|
+
function normalizedJob(job) {
|
|
153
|
+
if (typeof job !== "object" || job === null || Array.isArray(job)) {
|
|
154
|
+
throw new VerdifaxError("verdifax: job must be an object");
|
|
155
|
+
}
|
|
156
|
+
refuseUnknownFields(job, JOB_FIELDS, "job");
|
|
157
|
+
validateJobName(job.job_id, "job_id");
|
|
158
|
+
validateJobName(job.step_id, "step_id");
|
|
159
|
+
if (job.final !== void 0 && typeof job.final !== "boolean") {
|
|
160
|
+
throw new VerdifaxError("verdifax: job.final must be true or false");
|
|
161
|
+
}
|
|
162
|
+
const out = { job_id: job.job_id, step_id: job.step_id };
|
|
163
|
+
const previous = sortedUnique(job.previous_record_hashes, "previous_record_hashes");
|
|
164
|
+
if (previous.length > 0) out.previous_record_hashes = previous;
|
|
165
|
+
const external = sortedUnique(job.external_previous, "external_previous");
|
|
166
|
+
if (external.length > 0) out.external_previous = external;
|
|
167
|
+
if (job.final) out.final = true;
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
function withExecuteFlags(req) {
|
|
171
|
+
const body = { ...req };
|
|
172
|
+
if (req.job === void 0 || req.job === null) {
|
|
173
|
+
delete body.job;
|
|
174
|
+
} else {
|
|
175
|
+
body.job = normalizedJob(req.job);
|
|
176
|
+
}
|
|
177
|
+
if (req.attested_context) {
|
|
178
|
+
body.attested_context = withAttestedFlag(req.attested_context);
|
|
179
|
+
}
|
|
180
|
+
if (req.reproducibility_context) {
|
|
181
|
+
body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
|
|
182
|
+
}
|
|
183
|
+
return body;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/signing.ts
|
|
54
187
|
var REQUEST_SIGNATURE_FORM = "verdifax.request.v1";
|
|
188
|
+
var REQUEST_SIGNATURE_FORM_V2 = "verdifax.request.v2";
|
|
189
|
+
var REQUEST_SIGNATURE_VERSION_2 = "2";
|
|
190
|
+
var JOB_FORM = "verdifax.job.v1";
|
|
55
191
|
var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
|
|
192
|
+
var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
|
|
56
193
|
var SIGNING_KEY_BYTES = 32;
|
|
57
194
|
var PKCS8_ED25519_HEADER = Buffer.from(
|
|
58
195
|
"302e020100300506032b657004220420",
|
|
@@ -83,6 +220,12 @@ function loadSigningKey(key) {
|
|
|
83
220
|
type: "pkcs8"
|
|
84
221
|
});
|
|
85
222
|
}
|
|
223
|
+
function sha256Hex(text) {
|
|
224
|
+
return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
|
|
225
|
+
}
|
|
226
|
+
function signedAtNow() {
|
|
227
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
228
|
+
}
|
|
86
229
|
function requestSignaturePreimage(fields) {
|
|
87
230
|
return Buffer.from(
|
|
88
231
|
[
|
|
@@ -97,13 +240,62 @@ function requestSignaturePreimage(fields) {
|
|
|
97
240
|
"utf8"
|
|
98
241
|
);
|
|
99
242
|
}
|
|
243
|
+
function requestSignaturePreimageV2(fields) {
|
|
244
|
+
const { job } = fields;
|
|
245
|
+
return Buffer.from(
|
|
246
|
+
[
|
|
247
|
+
REQUEST_SIGNATURE_FORM_V2,
|
|
248
|
+
fields.body_hash,
|
|
249
|
+
fields.payload_hash,
|
|
250
|
+
fields.program_id,
|
|
251
|
+
fields.route_id,
|
|
252
|
+
fields.registry_record_hash,
|
|
253
|
+
fields.ai_output_hash,
|
|
254
|
+
JOB_FORM,
|
|
255
|
+
job?.job_id ?? "",
|
|
256
|
+
job?.step_id ?? "",
|
|
257
|
+
(job?.previous_record_hashes ?? []).join(","),
|
|
258
|
+
(job?.external_previous ?? []).join(","),
|
|
259
|
+
job?.final ? "true" : "false",
|
|
260
|
+
fields.signed_at,
|
|
261
|
+
fields.nonce
|
|
262
|
+
].join("\0"),
|
|
263
|
+
"utf8"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
function signRequestV2(signingKey, req, options = {}) {
|
|
267
|
+
const key = loadSigningKey(signingKey);
|
|
268
|
+
const { request_signature: _unsigned, ...fields } = withExecuteFlags(req);
|
|
269
|
+
const signed_request = JSON.stringify(fields);
|
|
270
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
271
|
+
const nonce = options.nonce ?? randomBytes(16).toString("hex");
|
|
272
|
+
const preimage = requestSignaturePreimageV2({
|
|
273
|
+
body_hash: sha256Hex(signed_request),
|
|
274
|
+
payload_hash: sha256Hex(fields.payload_text || fields.payload || ""),
|
|
275
|
+
program_id: fields.program_id,
|
|
276
|
+
route_id: fields.route_id,
|
|
277
|
+
registry_record_hash: fields.registry_record_hash,
|
|
278
|
+
ai_output_hash: fields.ai_output_text ? sha256Hex(fields.ai_output_text) : "",
|
|
279
|
+
job: fields.job,
|
|
280
|
+
signed_at,
|
|
281
|
+
nonce
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
signed_request,
|
|
285
|
+
request_signature: {
|
|
286
|
+
version: REQUEST_SIGNATURE_VERSION_2,
|
|
287
|
+
signature: sign(null, preimage, key).toString("base64"),
|
|
288
|
+
signed_at,
|
|
289
|
+
nonce
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
100
293
|
function signRequest(signingKey, req, options = {}) {
|
|
101
294
|
const key = loadSigningKey(signingKey);
|
|
102
|
-
const signed_at = options.signedAt ?? (
|
|
295
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
103
296
|
const nonce = options.nonce ?? randomBytes(16).toString("hex");
|
|
104
|
-
const payload_hash = createHash("sha256").update(Buffer.from(req.payload_text || req.payload || "", "utf8")).digest("hex");
|
|
105
297
|
const preimage = requestSignaturePreimage({
|
|
106
|
-
payload_hash,
|
|
298
|
+
payload_hash: sha256Hex(req.payload_text || req.payload || ""),
|
|
107
299
|
program_id: req.program_id,
|
|
108
300
|
route_id: req.route_id,
|
|
109
301
|
registry_record_hash: req.registry_record_hash,
|
|
@@ -126,38 +318,132 @@ function requestKeyRegistrationBody(signingKey) {
|
|
|
126
318
|
).toString("base64");
|
|
127
319
|
return { public_key, proof };
|
|
128
320
|
}
|
|
321
|
+
function requestKeyChangePreimage(fields) {
|
|
322
|
+
return Buffer.from(
|
|
323
|
+
[
|
|
324
|
+
REQUEST_KEY_CHANGE_FORM,
|
|
325
|
+
fields.new_public_key,
|
|
326
|
+
fields.signed_at,
|
|
327
|
+
fields.nonce
|
|
328
|
+
].join("\0"),
|
|
329
|
+
"utf8"
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
function requestKeyChangeApproval(currentSigningKey, newPublicKey, options = {}) {
|
|
333
|
+
const key = loadSigningKey(currentSigningKey);
|
|
334
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
335
|
+
const nonce = options.nonce ?? randomBytes(16).toString("hex");
|
|
336
|
+
const preimage = requestKeyChangePreimage({
|
|
337
|
+
new_public_key: newPublicKey,
|
|
338
|
+
signed_at,
|
|
339
|
+
nonce
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
signature: sign(null, preimage, key).toString("base64"),
|
|
343
|
+
signed_at,
|
|
344
|
+
nonce
|
|
345
|
+
};
|
|
346
|
+
}
|
|
129
347
|
|
|
130
|
-
// src/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
348
|
+
// src/job.ts
|
|
349
|
+
var STEP_OPTIONS = {
|
|
350
|
+
payload: true,
|
|
351
|
+
payload_text: true,
|
|
352
|
+
program_id: true,
|
|
353
|
+
route_id: true,
|
|
354
|
+
registry_record_hash: true,
|
|
355
|
+
ai_output_text: true,
|
|
356
|
+
attested_context: true,
|
|
357
|
+
reproducibility_context: true,
|
|
358
|
+
request_substance: true,
|
|
359
|
+
authorization_chain: true,
|
|
360
|
+
regulatory_scaffolding: true,
|
|
361
|
+
causal_graph: true,
|
|
362
|
+
after: true,
|
|
363
|
+
external_previous: true,
|
|
364
|
+
final: true
|
|
365
|
+
};
|
|
366
|
+
var VerdifaxJob = class {
|
|
367
|
+
jobId;
|
|
368
|
+
client;
|
|
369
|
+
hashes = /* @__PURE__ */ new Map();
|
|
370
|
+
#signedRequests = /* @__PURE__ */ new Map();
|
|
371
|
+
lastStepId;
|
|
372
|
+
constructor(client, jobId) {
|
|
373
|
+
validateJobName(jobId, "job_id");
|
|
374
|
+
this.client = client;
|
|
375
|
+
this.jobId = jobId;
|
|
136
376
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
377
|
+
/** Step id to record hash for every step that left a record, on a copy. */
|
|
378
|
+
get recordHashes() {
|
|
379
|
+
return Object.fromEntries(this.hashes);
|
|
141
380
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
381
|
+
/** Step id to the exact text that was signed, for every signed step that
|
|
382
|
+
* left a record, refused and halted steps included, on a copy. Each text
|
|
383
|
+
* can be passed to `verdifax-verify --request FILE`. */
|
|
384
|
+
get signedRequests() {
|
|
385
|
+
return Object.fromEntries(this.#signedRequests);
|
|
146
386
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
387
|
+
/** Runs one step and records its hash and signed text. A refused or halted
|
|
388
|
+
* step records both from the error, then throws the same error `attest`
|
|
389
|
+
* throws. */
|
|
390
|
+
async step(stepId, options) {
|
|
391
|
+
validateJobName(stepId, "step_id");
|
|
392
|
+
refuseUnknownFields(options, STEP_OPTIONS, "step options");
|
|
393
|
+
const { after, external_previous, final, ...attest } = options;
|
|
394
|
+
const job = {
|
|
395
|
+
job_id: this.jobId,
|
|
396
|
+
step_id: stepId,
|
|
397
|
+
previous_record_hashes: this.previousHashes(after),
|
|
398
|
+
external_previous,
|
|
399
|
+
final
|
|
400
|
+
};
|
|
401
|
+
let result;
|
|
402
|
+
try {
|
|
403
|
+
result = await this.client.attest({ ...attest, job });
|
|
404
|
+
} catch (err) {
|
|
405
|
+
if (err instanceof APIError && err.recordHash) {
|
|
406
|
+
this.record(stepId, err.recordHash, err.signedRequest);
|
|
407
|
+
}
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
const hash = result?.record_hash || result?.manifest_hash || result?.manifest?.ManifestHash;
|
|
411
|
+
if (!hash) {
|
|
412
|
+
throw new VerdifaxError(
|
|
413
|
+
`verdifax: the server returned no record_hash or manifest_hash for step ${stepId}, so the job map cannot continue from it`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
this.record(stepId, hash, result.signed_request);
|
|
417
|
+
return result;
|
|
151
418
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
419
|
+
previousHashes(after) {
|
|
420
|
+
if (after === void 0 || after === null) {
|
|
421
|
+
return this.lastStepId === void 0 ? [] : [this.hashes.get(this.lastStepId)];
|
|
422
|
+
}
|
|
423
|
+
if (!Array.isArray(after)) {
|
|
424
|
+
throw new VerdifaxError("verdifax: after must be a list of step ids");
|
|
425
|
+
}
|
|
426
|
+
return after.map((stepId) => {
|
|
427
|
+
const hash = this.hashes.get(stepId);
|
|
428
|
+
if (hash === void 0) {
|
|
429
|
+
throw new VerdifaxError(
|
|
430
|
+
`verdifax: after names ${String(stepId)}, which is not a step in job ${this.jobId}`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return hash;
|
|
434
|
+
});
|
|
156
435
|
}
|
|
157
|
-
|
|
436
|
+
record(stepId, hash, signedRequest) {
|
|
437
|
+
this.hashes.set(stepId, hash);
|
|
438
|
+
if (signedRequest !== void 0) {
|
|
439
|
+
this.#signedRequests.set(stepId, signedRequest);
|
|
440
|
+
}
|
|
441
|
+
this.lastStepId = stepId;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
158
444
|
|
|
159
445
|
// src/version.ts
|
|
160
|
-
var SDK_VERSION = "
|
|
446
|
+
var SDK_VERSION = "1.1.0";
|
|
161
447
|
|
|
162
448
|
// src/client.ts
|
|
163
449
|
var DEFAULT_BASE_URL = "https://api.verdifax.com";
|
|
@@ -167,7 +453,10 @@ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
|
|
|
167
453
|
var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
|
|
168
454
|
var HEADER_API_KEY = "X-Verdifax-Key";
|
|
169
455
|
var REQUEST_KEY_PATH = "/keys/request-key";
|
|
456
|
+
var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
|
|
457
|
+
var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
|
|
170
458
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
459
|
+
var MAX_BATCH_ROWS = 1e3;
|
|
171
460
|
var VerdifaxClient = class {
|
|
172
461
|
baseURL;
|
|
173
462
|
apiKey;
|
|
@@ -192,21 +481,73 @@ var VerdifaxClient = class {
|
|
|
192
481
|
async version() {
|
|
193
482
|
return this.request("GET", "/version");
|
|
194
483
|
}
|
|
484
|
+
/** The server's own API description. */
|
|
485
|
+
async getOpenAPI() {
|
|
486
|
+
return this.request("GET", "/openapi.json");
|
|
487
|
+
}
|
|
488
|
+
/** Run counts for this API key. */
|
|
489
|
+
async getStats() {
|
|
490
|
+
this.requireAPIKey();
|
|
491
|
+
return this.request("GET", "/stats");
|
|
492
|
+
}
|
|
195
493
|
// ── Execute / Attest (auth) ───────────────────────────────────
|
|
196
494
|
async execute(req) {
|
|
197
|
-
this.
|
|
198
|
-
|
|
495
|
+
this.requireExecuteFields(req);
|
|
496
|
+
const sent = this.prepared(req);
|
|
497
|
+
const out = await this.request("POST", "/execute", sent);
|
|
498
|
+
return withSignedText(out, sent);
|
|
499
|
+
}
|
|
500
|
+
/** Runs one payload twice on the server and compares the manifest
|
|
501
|
+
* hashes. One signature covers the request, and the server claims
|
|
502
|
+
* its nonce once. Both runs are recorded. A run that happens twice
|
|
503
|
+
* cannot be one job step, so a `job` is refused here. */
|
|
504
|
+
async verifyDeterminism(req) {
|
|
505
|
+
this.requireExecuteFields(req);
|
|
506
|
+
if (req.job !== void 0 && req.job !== null) {
|
|
199
507
|
throw new VerdifaxError(
|
|
200
|
-
"verdifax:
|
|
508
|
+
"verdifax: a determinism check runs twice, so it cannot be a job step"
|
|
201
509
|
);
|
|
202
510
|
}
|
|
203
|
-
|
|
511
|
+
const sent = this.prepared(req);
|
|
512
|
+
const out = await this.request(
|
|
513
|
+
"POST",
|
|
514
|
+
"/execute/verify-determinism",
|
|
515
|
+
sent
|
|
516
|
+
);
|
|
517
|
+
return withSignedText(out, sent);
|
|
518
|
+
}
|
|
519
|
+
/** Sends up to {@link MAX_BATCH_ROWS} runs in one request. With a
|
|
520
|
+
* signing key each row carries its own signature and nonce. A row
|
|
521
|
+
* that fails comes back in `results` with `ok: false`, so the call
|
|
522
|
+
* does not throw for it. The rows are not checked here, except a job
|
|
523
|
+
* block, which is refused before anything is sent: the server reports
|
|
524
|
+
* any other bad row in that row's result.
|
|
525
|
+
*
|
|
526
|
+
* This is one POST and it is never retried. After a timeout the
|
|
527
|
+
* server may have run some or all of the rows. */
|
|
528
|
+
async executeBatch(runs) {
|
|
529
|
+
this.requireAPIKey();
|
|
530
|
+
if (!Array.isArray(runs) || runs.length === 0) {
|
|
531
|
+
throw new VerdifaxError("verdifax: executeBatch needs at least one run");
|
|
532
|
+
}
|
|
533
|
+
if (runs.length > MAX_BATCH_ROWS) {
|
|
204
534
|
throw new VerdifaxError(
|
|
205
|
-
|
|
535
|
+
`verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
|
|
206
536
|
);
|
|
207
537
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
538
|
+
const sent = runs.map((run) => this.prepared(run));
|
|
539
|
+
const out = await this.request(
|
|
540
|
+
"POST",
|
|
541
|
+
"/execute/batch",
|
|
542
|
+
{ runs: sent }
|
|
543
|
+
);
|
|
544
|
+
if (!Array.isArray(out?.results)) return out;
|
|
545
|
+
return {
|
|
546
|
+
...out,
|
|
547
|
+
results: out.results.map(
|
|
548
|
+
(row, i) => i < sent.length ? withSignedText(row, sent[i]) : row
|
|
549
|
+
)
|
|
550
|
+
};
|
|
210
551
|
}
|
|
211
552
|
/** High-level convenience wrapper around `execute()`. Accepts
|
|
212
553
|
* either a Buffer (for binary payloads, auto-base64) or a
|
|
@@ -232,7 +573,13 @@ var VerdifaxClient = class {
|
|
|
232
573
|
route_id: req.route_id,
|
|
233
574
|
registry_record_hash: req.registry_record_hash,
|
|
234
575
|
ai_output_text: req.ai_output_text,
|
|
235
|
-
attested_context: req.attested_context
|
|
576
|
+
attested_context: req.attested_context,
|
|
577
|
+
reproducibility_context: req.reproducibility_context,
|
|
578
|
+
request_substance: req.request_substance,
|
|
579
|
+
authorization_chain: req.authorization_chain,
|
|
580
|
+
regulatory_scaffolding: req.regulatory_scaffolding,
|
|
581
|
+
causal_graph: req.causal_graph,
|
|
582
|
+
job: req.job
|
|
236
583
|
};
|
|
237
584
|
if (req.payload !== void 0) {
|
|
238
585
|
body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
|
|
@@ -241,6 +588,12 @@ var VerdifaxClient = class {
|
|
|
241
588
|
}
|
|
242
589
|
return this.execute(body);
|
|
243
590
|
}
|
|
591
|
+
/** A helper that runs the steps of one job through {@link attest} and
|
|
592
|
+
* keeps each step's record hash, so a later step can name it. It makes
|
|
593
|
+
* no request until a step runs. */
|
|
594
|
+
job(jobId) {
|
|
595
|
+
return new VerdifaxJob(this, jobId);
|
|
596
|
+
}
|
|
244
597
|
// ── Request signing (auth) ───────────────────────────────────
|
|
245
598
|
/** Registers this client's public key on its API key, with the proof
|
|
246
599
|
* that the client holds the private key. From then on the server
|
|
@@ -258,11 +611,48 @@ var VerdifaxClient = class {
|
|
|
258
611
|
requestKeyRegistrationBody(this.signingKey)
|
|
259
612
|
);
|
|
260
613
|
}
|
|
614
|
+
/** Replaces the registered key with `newSigningKey`. The new key proves
|
|
615
|
+
* itself and this client's current key approves the change. After
|
|
616
|
+
* success this client signs with the new key. If the call fails it
|
|
617
|
+
* keeps the current key, including after a timeout, when the server may
|
|
618
|
+
* have made the change. */
|
|
619
|
+
async rotateRequestKey(newSigningKey) {
|
|
620
|
+
this.requireAPIKey();
|
|
621
|
+
if (!this.signingKey) {
|
|
622
|
+
throw new VerdifaxError(
|
|
623
|
+
"verdifax: rotateRequestKey needs the current key in the signingKey client option"
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const next = loadSigningKey(newSigningKey);
|
|
627
|
+
const registration = requestKeyRegistrationBody(next);
|
|
628
|
+
const out = await this.request(
|
|
629
|
+
"POST",
|
|
630
|
+
REQUEST_KEY_PATH,
|
|
631
|
+
{
|
|
632
|
+
...registration,
|
|
633
|
+
change: requestKeyChangeApproval(
|
|
634
|
+
this.signingKey,
|
|
635
|
+
registration.public_key
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
this.signingKey = next;
|
|
640
|
+
return out;
|
|
641
|
+
}
|
|
261
642
|
/** Clears the registered public key, so the server stops requiring
|
|
262
|
-
* request signatures on this API key.
|
|
643
|
+
* request signatures on this API key. With a signing key the client
|
|
644
|
+
* sends that key's approval, which the server requires while a key is
|
|
645
|
+
* registered. After success the client drops its signing key and sends
|
|
646
|
+
* unsigned requests, because the server refuses a signed run on an API
|
|
647
|
+
* key with no registered key. If the call fails it keeps the key. */
|
|
263
648
|
async clearRequestKey() {
|
|
264
649
|
this.requireAPIKey();
|
|
265
|
-
await this.request(
|
|
650
|
+
await this.request(
|
|
651
|
+
"DELETE",
|
|
652
|
+
REQUEST_KEY_PATH,
|
|
653
|
+
this.signingKey ? { change: requestKeyChangeApproval(this.signingKey, "") } : void 0
|
|
654
|
+
);
|
|
655
|
+
this.signingKey = void 0;
|
|
266
656
|
}
|
|
267
657
|
// ── Runs ──────────────────────────────────────────────────────
|
|
268
658
|
async getRun(runId) {
|
|
@@ -289,6 +679,95 @@ var VerdifaxClient = class {
|
|
|
289
679
|
const out = await this.request("GET", path);
|
|
290
680
|
return { runs: out.runs ?? [], total: out.total ?? 0 };
|
|
291
681
|
}
|
|
682
|
+
/** Searches this key's runs. `manifest_hash` matches a prefix. The
|
|
683
|
+
* server ignores a `from` or `to` it cannot parse here, unlike
|
|
684
|
+
* {@link exportRuns}, which refuses one. */
|
|
685
|
+
async searchRuns(params = {}) {
|
|
686
|
+
this.requireAPIKey();
|
|
687
|
+
const q = new URLSearchParams();
|
|
688
|
+
for (const name of [
|
|
689
|
+
"manifest_hash",
|
|
690
|
+
"program_id",
|
|
691
|
+
"route_id",
|
|
692
|
+
"status",
|
|
693
|
+
"from",
|
|
694
|
+
"to"
|
|
695
|
+
]) {
|
|
696
|
+
const value = params[name];
|
|
697
|
+
if (value) q.set(name, value);
|
|
698
|
+
}
|
|
699
|
+
if (params.limit && params.limit > 0) {
|
|
700
|
+
q.set("limit", String(params.limit));
|
|
701
|
+
}
|
|
702
|
+
if (params.offset && params.offset > 0) {
|
|
703
|
+
q.set("offset", String(params.offset));
|
|
704
|
+
}
|
|
705
|
+
const qs = q.toString();
|
|
706
|
+
const path = qs ? `/runs/search?${qs}` : "/runs/search";
|
|
707
|
+
const out = await this.request("GET", path);
|
|
708
|
+
return { runs: out.runs ?? [], total: out.total ?? 0 };
|
|
709
|
+
}
|
|
710
|
+
/** Lists what the server holds for this key in a period, failures
|
|
711
|
+
* included, up to 10,000 rows. `truncated` is true when the period
|
|
712
|
+
* holds more. The list states what was stored, not a verdict, and a
|
|
713
|
+
* call that never reached Verdifax cannot appear in it. */
|
|
714
|
+
async exportRuns(params = {}) {
|
|
715
|
+
this.requireAPIKey();
|
|
716
|
+
return this.request("GET", this.exportPath(params, "json"));
|
|
717
|
+
}
|
|
718
|
+
/** The same rows as {@link exportRuns}: the raw CSV text with `total`
|
|
719
|
+
* and `truncated` from the response headers. Check `truncated`: when
|
|
720
|
+
* true, the CSV is missing rows for the period. Throws when the server
|
|
721
|
+
* does not send those headers, so that a cut-off export cannot pass
|
|
722
|
+
* for a complete one. */
|
|
723
|
+
async exportRunsCSV(params = {}) {
|
|
724
|
+
this.requireAPIKey();
|
|
725
|
+
const { text, headers } = await this.requestRaw(
|
|
726
|
+
this.exportPath(params, "csv")
|
|
727
|
+
);
|
|
728
|
+
const total = Number.parseInt(headers.get(HEADER_EXPORT_TOTAL) ?? "", 10);
|
|
729
|
+
const truncated = headers.get(HEADER_EXPORT_TRUNCATED);
|
|
730
|
+
if (!Number.isSafeInteger(total) || truncated !== "true" && truncated !== "false") {
|
|
731
|
+
throw new VerdifaxError(
|
|
732
|
+
`verdifax: the server did not report ${HEADER_EXPORT_TOTAL} and ${HEADER_EXPORT_TRUNCATED}, so this CSV may be cut off; use exportRuns instead`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
return { csv: text, total, truncated: truncated === "true" };
|
|
736
|
+
}
|
|
737
|
+
exportPath(params, format) {
|
|
738
|
+
const q = new URLSearchParams();
|
|
739
|
+
if (params.from) q.set("from", params.from);
|
|
740
|
+
if (params.to) q.set("to", params.to);
|
|
741
|
+
if (params.job_id) q.set("job_id", params.job_id);
|
|
742
|
+
q.set("format", format);
|
|
743
|
+
return `/runs/export?${q.toString()}`;
|
|
744
|
+
}
|
|
745
|
+
// ── Attestation receipts (auth) ──────────────────────────────
|
|
746
|
+
async listAttestations(params = {}) {
|
|
747
|
+
this.requireAPIKey();
|
|
748
|
+
const q = new URLSearchParams();
|
|
749
|
+
if (params.limit && params.limit > 0) {
|
|
750
|
+
q.set("limit", String(params.limit));
|
|
751
|
+
}
|
|
752
|
+
if (params.offset && params.offset > 0) {
|
|
753
|
+
q.set("offset", String(params.offset));
|
|
754
|
+
}
|
|
755
|
+
const qs = q.toString();
|
|
756
|
+
return this.request(
|
|
757
|
+
"GET",
|
|
758
|
+
qs ? `/attestations?${qs}` : "/attestations"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
async getAttestation(attestationId) {
|
|
762
|
+
this.requireAPIKey();
|
|
763
|
+
if (attestationId <= 0) {
|
|
764
|
+
throw new VerdifaxError("verdifax: attestationId must be > 0");
|
|
765
|
+
}
|
|
766
|
+
return this.request(
|
|
767
|
+
"GET",
|
|
768
|
+
`/attestations/${attestationId}`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
292
771
|
// ── Sealed-artifact retrieval (auth) ─────────────────────────
|
|
293
772
|
async getAllowToken(runId) {
|
|
294
773
|
return this.getRunArtifact(runId, "allow-token");
|
|
@@ -305,6 +784,39 @@ var VerdifaxClient = class {
|
|
|
305
784
|
async getAivpT4HaltReceipt(runId) {
|
|
306
785
|
return this.getRunArtifact(runId, "aivp-t4-halt-receipt");
|
|
307
786
|
}
|
|
787
|
+
async getAttestationGapReceipt(runId) {
|
|
788
|
+
return this.getRunArtifact(runId, "attestation-gap-receipt");
|
|
789
|
+
}
|
|
790
|
+
async getCESAuditRecord(runId) {
|
|
791
|
+
return this.getRunArtifact(runId, "ces-audit-record");
|
|
792
|
+
}
|
|
793
|
+
/** The audit report for a run, as JSON. */
|
|
794
|
+
async getRunReport(runId) {
|
|
795
|
+
const out = await this.getRunArtifact(runId, "report");
|
|
796
|
+
return out.report;
|
|
797
|
+
}
|
|
798
|
+
/** The run's sealed audit bundle as the exact JSON text the server
|
|
799
|
+
* sent, to save and check with `verdifax-verify`. It is not parsed:
|
|
800
|
+
* JSON.parse would round a number above 2^53 and the recomputed hash
|
|
801
|
+
* would stop matching. */
|
|
802
|
+
async getRunArtifacts(runId) {
|
|
803
|
+
this.requireAPIKey();
|
|
804
|
+
if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
|
|
805
|
+
return (await this.requestRaw(`/runs/${runId}/artifacts`)).text;
|
|
806
|
+
}
|
|
807
|
+
/** The public audit bundle of a record published by run id, as the
|
|
808
|
+
* exact JSON text the server sent. Needs no API key. */
|
|
809
|
+
async getPublicBundle(runId) {
|
|
810
|
+
if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
|
|
811
|
+
return (await this.requestRaw(`/runs/${runId}/bundle.json`)).text;
|
|
812
|
+
}
|
|
813
|
+
/** The public audit bundle a share token points to, as the exact JSON
|
|
814
|
+
* text the server sent. Needs no API key. */
|
|
815
|
+
async getPublicBundleByToken(token) {
|
|
816
|
+
if (!token) throw new VerdifaxError("verdifax: token is required");
|
|
817
|
+
const path = `/public/runs/${encodeURIComponent(token)}/bundle.json`;
|
|
818
|
+
return (await this.requestRaw(path)).text;
|
|
819
|
+
}
|
|
308
820
|
async getRunArtifact(runId, suffix) {
|
|
309
821
|
this.requireAPIKey();
|
|
310
822
|
if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
|
|
@@ -390,14 +902,26 @@ var VerdifaxClient = class {
|
|
|
390
902
|
}
|
|
391
903
|
await this.request("DELETE", `/admin/keys/${keyId}`);
|
|
392
904
|
}
|
|
905
|
+
/** Clears the request key on an API key whose holder lost it, after a
|
|
906
|
+
* manual check. It is the one way to clear a key without that key. The
|
|
907
|
+
* id can be any API key, so this client keeps its own signing key. */
|
|
908
|
+
async adminClearRequestKey(keyId) {
|
|
909
|
+
this.requireAPIKey();
|
|
910
|
+
if (keyId <= 0) {
|
|
911
|
+
throw new VerdifaxError("verdifax: keyId must be > 0");
|
|
912
|
+
}
|
|
913
|
+
await this.request("DELETE", `/admin/keys/${keyId}/request-key`);
|
|
914
|
+
}
|
|
393
915
|
// ── Provider helpers (Claude / OpenAI) ───────────────────────
|
|
394
|
-
/** Convenience for attesting an Anthropic Claude API response.
|
|
395
|
-
*
|
|
396
|
-
* by
|
|
916
|
+
/** Convenience for attesting an Anthropic Claude API response. With
|
|
917
|
+
* `govern: true` the response text also goes to the AIVP review as
|
|
918
|
+
* `ai_output_text`; that is off by default, so the text leaves your
|
|
919
|
+
* machine for review only when you ask. */
|
|
397
920
|
async attestClaudeResponse(args) {
|
|
398
921
|
return this.attestProviderResponse({ ...args, provider: "anthropic" });
|
|
399
922
|
}
|
|
400
|
-
/** Convenience for attesting an OpenAI API response.
|
|
923
|
+
/** Convenience for attesting an OpenAI API response. `govern` works as
|
|
924
|
+
* in {@link attestClaudeResponse}. */
|
|
401
925
|
async attestOpenAIResponse(args) {
|
|
402
926
|
return this.attestProviderResponse({ ...args, provider: "openai" });
|
|
403
927
|
}
|
|
@@ -407,25 +931,25 @@ var VerdifaxClient = class {
|
|
|
407
931
|
"verdifax: response is required for attestation"
|
|
408
932
|
);
|
|
409
933
|
}
|
|
410
|
-
const payloadText = JSON.stringify({
|
|
411
|
-
provider: args.provider,
|
|
412
|
-
prompt: args.prompt,
|
|
413
|
-
response: args.response
|
|
414
|
-
});
|
|
415
934
|
return this.attest({
|
|
416
|
-
payload_text:
|
|
935
|
+
payload_text: formatProviderPayload(
|
|
936
|
+
args.provider,
|
|
937
|
+
args.prompt,
|
|
938
|
+
args.response
|
|
939
|
+
),
|
|
417
940
|
program_id: args.program_id,
|
|
418
941
|
route_id: args.route_id,
|
|
419
942
|
registry_record_hash: args.registry_record_hash,
|
|
420
|
-
ai_output_text: args.response,
|
|
943
|
+
ai_output_text: args.govern ? args.response : void 0,
|
|
421
944
|
attested_context: {
|
|
422
945
|
actor_role: "ai_assistant",
|
|
423
946
|
model_provider: args.provider
|
|
424
|
-
}
|
|
947
|
+
},
|
|
948
|
+
job: args.job
|
|
425
949
|
});
|
|
426
950
|
}
|
|
427
951
|
// ── HTTP plumbing, retry-aware ──────────────────────────────
|
|
428
|
-
async request(method, path, body) {
|
|
952
|
+
async request(method, path, body, rawText = false) {
|
|
429
953
|
const url = this.baseURL + path;
|
|
430
954
|
const headers = {
|
|
431
955
|
"User-Agent": this.userAgent
|
|
@@ -468,9 +992,13 @@ var VerdifaxClient = class {
|
|
|
468
992
|
continue;
|
|
469
993
|
}
|
|
470
994
|
let errMessage = text.trim();
|
|
995
|
+
let recordHash;
|
|
471
996
|
try {
|
|
472
997
|
const env = JSON.parse(text);
|
|
473
998
|
if (env.error) errMessage = env.error;
|
|
999
|
+
if (typeof env.record_hash === "string" && env.record_hash) {
|
|
1000
|
+
recordHash = env.record_hash;
|
|
1001
|
+
}
|
|
474
1002
|
} catch {
|
|
475
1003
|
}
|
|
476
1004
|
switch (resp.status) {
|
|
@@ -479,9 +1007,16 @@ var VerdifaxClient = class {
|
|
|
479
1007
|
case 404:
|
|
480
1008
|
throw new NotFoundError(path, errMessage);
|
|
481
1009
|
default:
|
|
482
|
-
throw new APIError(
|
|
1010
|
+
throw new APIError(
|
|
1011
|
+
resp.status,
|
|
1012
|
+
path,
|
|
1013
|
+
errMessage,
|
|
1014
|
+
recordHash,
|
|
1015
|
+
body?.signed_request
|
|
1016
|
+
);
|
|
483
1017
|
}
|
|
484
1018
|
}
|
|
1019
|
+
if (rawText) return { text, headers: resp.headers };
|
|
485
1020
|
if (text.length === 0) return void 0;
|
|
486
1021
|
try {
|
|
487
1022
|
return JSON.parse(text);
|
|
@@ -495,17 +1030,46 @@ var VerdifaxClient = class {
|
|
|
495
1030
|
`verdifax: retry budget exhausted${lastErr ? `: ${String(lastErr)}` : ""}`
|
|
496
1031
|
);
|
|
497
1032
|
}
|
|
1033
|
+
/** A GET whose body is kept as sent, retried like any other GET. */
|
|
1034
|
+
requestRaw(path) {
|
|
1035
|
+
return this.request("GET", path, void 0, true);
|
|
1036
|
+
}
|
|
498
1037
|
requireAPIKey() {
|
|
499
1038
|
if (!this.apiKey) throw new APIKeyRequiredError();
|
|
500
1039
|
}
|
|
1040
|
+
requireExecuteFields(req) {
|
|
1041
|
+
this.requireAPIKey();
|
|
1042
|
+
if (!req.payload && !req.payload_text) {
|
|
1043
|
+
throw new VerdifaxError(
|
|
1044
|
+
"verdifax: ExecuteRequest needs payload or payload_text"
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
if (!req.program_id || !req.route_id || !req.registry_record_hash) {
|
|
1048
|
+
throw new VerdifaxError(
|
|
1049
|
+
"verdifax: program_id, route_id, and registry_record_hash are required"
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
validateHex64(req.program_id, "program_id");
|
|
1053
|
+
validateHex64(req.registry_record_hash, "registry_record_hash");
|
|
1054
|
+
validateRouteId(req.route_id);
|
|
1055
|
+
refuseCallerOriginHashes(req.request_substance);
|
|
1056
|
+
}
|
|
1057
|
+
/** The body to send, on a copy: the request with its derived flags or,
|
|
1058
|
+
* with a signing key, the signed text with a fresh signature and nonce. */
|
|
1059
|
+
prepared(req) {
|
|
1060
|
+
return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
|
|
1061
|
+
}
|
|
501
1062
|
};
|
|
1063
|
+
function withSignedText(result, sent) {
|
|
1064
|
+
return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
|
|
1065
|
+
}
|
|
502
1066
|
|
|
503
1067
|
// src/config.ts
|
|
504
1068
|
function fromEnv() {
|
|
505
1069
|
const cfg = {
|
|
506
|
-
baseURL: process.env.VERDIFAX_BASE_URL
|
|
1070
|
+
baseURL: process.env.VERDIFAX_BASE_URL || process.env.VERDIFAX_API_URL || void 0,
|
|
507
1071
|
apiKey: process.env.VERDIFAX_API_KEY ?? void 0,
|
|
508
|
-
userAgent: process.env.VERDIFAX_USER_AGENT
|
|
1072
|
+
userAgent: process.env.VERDIFAX_USER_AGENT || void 0
|
|
509
1073
|
};
|
|
510
1074
|
const timeoutRaw = process.env.VERDIFAX_TIMEOUT_MS;
|
|
511
1075
|
if (timeoutRaw !== void 0 && timeoutRaw !== "") {
|
|
@@ -529,18 +1093,187 @@ function fromEnv() {
|
|
|
529
1093
|
}
|
|
530
1094
|
return new VerdifaxClient(cfg);
|
|
531
1095
|
}
|
|
1096
|
+
|
|
1097
|
+
// src/environment.ts
|
|
1098
|
+
import { execFileSync } from "child_process";
|
|
1099
|
+
import { readdirSync, readFileSync } from "fs";
|
|
1100
|
+
import { join } from "path";
|
|
1101
|
+
var HASH_64 = /^[0-9a-f]{64}$/;
|
|
1102
|
+
var GIT_SHA = /^([0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
1103
|
+
var PLATFORM_NAMES = { win32: "windows" };
|
|
1104
|
+
var ARCH_NAMES = { x64: "amd64", ia32: "386" };
|
|
1105
|
+
function packageVersion(dir) {
|
|
1106
|
+
try {
|
|
1107
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
1108
|
+
return pkg.name && pkg.version ? `${pkg.name}==${pkg.version}` : void 0;
|
|
1109
|
+
} catch {
|
|
1110
|
+
return void 0;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function packageDirs(root) {
|
|
1114
|
+
const dirs = [];
|
|
1115
|
+
for (const entry of readdirSync(root)) {
|
|
1116
|
+
if (entry.startsWith(".")) continue;
|
|
1117
|
+
if (!entry.startsWith("@")) {
|
|
1118
|
+
dirs.push(join(root, entry));
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
for (const scoped of readdirSync(join(root, entry))) {
|
|
1122
|
+
dirs.push(join(root, entry, scoped));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return dirs;
|
|
1126
|
+
}
|
|
1127
|
+
function detectDependencies() {
|
|
1128
|
+
try {
|
|
1129
|
+
const pins = packageDirs(join(process.cwd(), "node_modules")).map(packageVersion).filter((pin) => pin !== void 0);
|
|
1130
|
+
pins.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
1131
|
+
return pins.length > 0 ? pins : void 0;
|
|
1132
|
+
} catch {
|
|
1133
|
+
return void 0;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function detectGitSHA() {
|
|
1137
|
+
try {
|
|
1138
|
+
const sha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
1139
|
+
encoding: "utf8",
|
|
1140
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1141
|
+
timeout: 2e3
|
|
1142
|
+
}).trim().toLowerCase();
|
|
1143
|
+
return GIT_SHA.test(sha) ? sha : void 0;
|
|
1144
|
+
} catch {
|
|
1145
|
+
return void 0;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function detectContainerHash() {
|
|
1149
|
+
try {
|
|
1150
|
+
const text = readFileSync("/proc/self/cgroup", "utf8");
|
|
1151
|
+
return text.split("\n").flatMap((line) => line.split("/")).map((segment) => segment.trim()).find((segment) => HASH_64.test(segment));
|
|
1152
|
+
} catch {
|
|
1153
|
+
return void 0;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
function captureEnvironment(options = {}) {
|
|
1157
|
+
const seeds = Object.entries(options.declaredSeeds ?? {}).map(([name, seed]) => `${name}=${seed}`).sort();
|
|
1158
|
+
const pinned = options.includeDependencies === false ? void 0 : detectDependencies();
|
|
1159
|
+
const git = options.includeGit === false ? void 0 : detectGitSHA();
|
|
1160
|
+
const container = options.includeContainer === false ? void 0 : detectContainerHash();
|
|
1161
|
+
const os = PLATFORM_NAMES[process.platform] ?? process.platform;
|
|
1162
|
+
const arch = ARCH_NAMES[process.arch] ?? process.arch;
|
|
1163
|
+
return {
|
|
1164
|
+
declared: true,
|
|
1165
|
+
runtime_name: "node",
|
|
1166
|
+
runtime_version: process.versions.node,
|
|
1167
|
+
platform: `${os}/${arch}`,
|
|
1168
|
+
...pinned ? { pinned_dependencies: pinned } : {},
|
|
1169
|
+
...git ? { git_commit_sha: git } : {},
|
|
1170
|
+
...container ? { container_image_hash: container } : {},
|
|
1171
|
+
...seeds.length > 0 ? { random_seeds: seeds } : {}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/types.ts
|
|
1176
|
+
var RUN_STATUSES = [
|
|
1177
|
+
"ok",
|
|
1178
|
+
"probe",
|
|
1179
|
+
"pepg_deny",
|
|
1180
|
+
"ccv_halt",
|
|
1181
|
+
"macc_halt",
|
|
1182
|
+
"aivp_t4_halt",
|
|
1183
|
+
"attestation_gap",
|
|
1184
|
+
"stage_error"
|
|
1185
|
+
];
|
|
1186
|
+
|
|
1187
|
+
// src/section0.ts
|
|
1188
|
+
import { createHash as createHash2 } from "crypto";
|
|
1189
|
+
var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
|
|
1190
|
+
var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
|
|
1191
|
+
var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
|
|
1192
|
+
var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
|
|
1193
|
+
function sha256Hex2(preimage) {
|
|
1194
|
+
return createHash2("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
|
|
1195
|
+
}
|
|
1196
|
+
function requireHex64(value, name) {
|
|
1197
|
+
try {
|
|
1198
|
+
validateHex64(value, name);
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
throw new VerdifaxError(
|
|
1201
|
+
`verdifax: section0: ${err instanceof Error ? err.message : String(err)}`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function dseDispatchHash(fields) {
|
|
1206
|
+
requireHex64(fields.program_id, "program_id");
|
|
1207
|
+
requireHex64(fields.registry_record_hash, "registry_record_hash");
|
|
1208
|
+
return sha256Hex2(
|
|
1209
|
+
[
|
|
1210
|
+
DSE_FORMULA_VERSION,
|
|
1211
|
+
fields.envelope_id,
|
|
1212
|
+
fields.sequence_id,
|
|
1213
|
+
fields.program_id,
|
|
1214
|
+
fields.registry_record_hash
|
|
1215
|
+
].join(".")
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
function dcaeClosureHash(fields) {
|
|
1219
|
+
requireHex64(fields.manifest_hash, "manifest_hash");
|
|
1220
|
+
requireHex64(fields.aer_hash, "aer_hash");
|
|
1221
|
+
requireHex64(fields.zksp_binding_hash, "zksp_binding_hash");
|
|
1222
|
+
return sha256Hex2(
|
|
1223
|
+
[
|
|
1224
|
+
DCAE_PREIMAGE_VERSION,
|
|
1225
|
+
fields.manifest_hash,
|
|
1226
|
+
fields.aer_hash,
|
|
1227
|
+
fields.envelope_id,
|
|
1228
|
+
fields.zksp_binding_hash,
|
|
1229
|
+
fields.formal_verifier_status
|
|
1230
|
+
].join("|")
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
function cresReceiptHash(fields) {
|
|
1234
|
+
requireHex64(fields.ciphertext_hash_at_shred, "ciphertext_hash_at_shred");
|
|
1235
|
+
return sha256Hex2(
|
|
1236
|
+
[
|
|
1237
|
+
CRES_PREIMAGE_VERSION,
|
|
1238
|
+
fields.envelope_id,
|
|
1239
|
+
fields.field_path,
|
|
1240
|
+
fields.deletion_clock,
|
|
1241
|
+
fields.actor_id,
|
|
1242
|
+
fields.ciphertext_hash_at_shred,
|
|
1243
|
+
fields.dsar_reference,
|
|
1244
|
+
fields.engine_version ?? CRES_ENGINE_VERSION
|
|
1245
|
+
].join("|")
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
532
1248
|
export {
|
|
533
1249
|
APIError,
|
|
534
1250
|
APIKeyRequiredError,
|
|
535
1251
|
AuthError,
|
|
1252
|
+
CRES_ENGINE_VERSION,
|
|
1253
|
+
CRES_PREIMAGE_VERSION,
|
|
1254
|
+
DCAE_PREIMAGE_VERSION,
|
|
1255
|
+
DSE_FORMULA_VERSION,
|
|
1256
|
+
MAX_BATCH_ROWS,
|
|
536
1257
|
NotFoundError,
|
|
1258
|
+
RUN_STATUSES,
|
|
537
1259
|
VerdifaxClient,
|
|
538
1260
|
VerdifaxError,
|
|
1261
|
+
VerdifaxJob,
|
|
1262
|
+
captureEnvironment,
|
|
1263
|
+
cresReceiptHash,
|
|
1264
|
+
dcaeClosureHash,
|
|
1265
|
+
dseDispatchHash,
|
|
1266
|
+
formatProviderPayload,
|
|
539
1267
|
fromEnv,
|
|
540
1268
|
generateRequestKeyPair,
|
|
1269
|
+
requestKeyChangeApproval,
|
|
1270
|
+
requestKeyChangePreimage,
|
|
541
1271
|
requestKeyRegistrationBody,
|
|
542
1272
|
requestSignaturePreimage,
|
|
1273
|
+
requestSignaturePreimageV2,
|
|
543
1274
|
signRequest,
|
|
1275
|
+
signRequestV2,
|
|
544
1276
|
validateHex64,
|
|
1277
|
+
validateJobName,
|
|
545
1278
|
validateRouteId
|
|
546
1279
|
};
|