verdifax 0.2.11 → 1.0.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 +158 -5
- package/dist/index.d.mts +443 -9
- package/dist/index.d.ts +443 -9
- package/dist/index.js +542 -23
- package/dist/index.mjs +535 -23
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -41,6 +41,135 @@ var APIKeyRequiredError = class extends VerdifaxError {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
// src/signing.ts
|
|
45
|
+
import {
|
|
46
|
+
createHash,
|
|
47
|
+
createPrivateKey,
|
|
48
|
+
createPublicKey,
|
|
49
|
+
generateKeyPairSync,
|
|
50
|
+
KeyObject,
|
|
51
|
+
randomBytes,
|
|
52
|
+
sign
|
|
53
|
+
} from "crypto";
|
|
54
|
+
var REQUEST_SIGNATURE_FORM = "verdifax.request.v1";
|
|
55
|
+
var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
|
|
56
|
+
var SIGNING_KEY_BYTES = 32;
|
|
57
|
+
var PKCS8_ED25519_HEADER = Buffer.from(
|
|
58
|
+
"302e020100300506032b657004220420",
|
|
59
|
+
"hex"
|
|
60
|
+
);
|
|
61
|
+
function rawPublicKey(key) {
|
|
62
|
+
return Buffer.from(key.export({ format: "der", type: "spki" }).subarray(-32));
|
|
63
|
+
}
|
|
64
|
+
function generateRequestKeyPair() {
|
|
65
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
66
|
+
return {
|
|
67
|
+
publicKey: rawPublicKey(publicKey),
|
|
68
|
+
privateKey: Buffer.from(
|
|
69
|
+
privateKey.export({ format: "der", type: "pkcs8" }).subarray(-32)
|
|
70
|
+
)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function loadSigningKey(key) {
|
|
74
|
+
if (key instanceof KeyObject) return key;
|
|
75
|
+
if (!(key instanceof Uint8Array) || key.length !== SIGNING_KEY_BYTES) {
|
|
76
|
+
throw new VerdifaxError(
|
|
77
|
+
"verdifax: signingKey must be the 32 raw bytes of an Ed25519 private key"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return createPrivateKey({
|
|
81
|
+
key: Buffer.concat([PKCS8_ED25519_HEADER, key]),
|
|
82
|
+
format: "der",
|
|
83
|
+
type: "pkcs8"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function requestSignaturePreimage(fields) {
|
|
87
|
+
return Buffer.from(
|
|
88
|
+
[
|
|
89
|
+
REQUEST_SIGNATURE_FORM,
|
|
90
|
+
fields.payload_hash,
|
|
91
|
+
fields.program_id,
|
|
92
|
+
fields.route_id,
|
|
93
|
+
fields.registry_record_hash,
|
|
94
|
+
fields.signed_at,
|
|
95
|
+
fields.nonce
|
|
96
|
+
].join("\0"),
|
|
97
|
+
"utf8"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
function signRequest(signingKey, req, options = {}) {
|
|
101
|
+
const key = loadSigningKey(signingKey);
|
|
102
|
+
const signed_at = options.signedAt ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
103
|
+
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
|
+
const preimage = requestSignaturePreimage({
|
|
106
|
+
payload_hash,
|
|
107
|
+
program_id: req.program_id,
|
|
108
|
+
route_id: req.route_id,
|
|
109
|
+
registry_record_hash: req.registry_record_hash,
|
|
110
|
+
signed_at,
|
|
111
|
+
nonce
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
signature: sign(null, preimage, key).toString("base64"),
|
|
115
|
+
signed_at,
|
|
116
|
+
nonce
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function requestKeyRegistrationBody(signingKey) {
|
|
120
|
+
const key = loadSigningKey(signingKey);
|
|
121
|
+
const public_key = rawPublicKey(createPublicKey(key)).toString("base64");
|
|
122
|
+
const proof = sign(
|
|
123
|
+
null,
|
|
124
|
+
Buffer.from(`${REQUEST_KEY_PROOF_FORM}\0${public_key}`, "utf8"),
|
|
125
|
+
key
|
|
126
|
+
).toString("base64");
|
|
127
|
+
return { public_key, proof };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/helpers.ts
|
|
131
|
+
function formatProviderPayload(provider, prompt, response) {
|
|
132
|
+
if ([provider, prompt, response].some((text) => /[\uD800-\uDFFF]/u.test(text))) {
|
|
133
|
+
throw new VerdifaxError(
|
|
134
|
+
"verdifax: provider payload text cannot be encoded as UTF-8"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return JSON.stringify({ prompt, provider, response });
|
|
138
|
+
}
|
|
139
|
+
function isProvided(value) {
|
|
140
|
+
return value !== null && value !== void 0 && value !== "" && value !== false;
|
|
141
|
+
}
|
|
142
|
+
function withAttestedFlag(context) {
|
|
143
|
+
const provided = Object.entries(context).some(
|
|
144
|
+
([name, value]) => name !== "attested" && isProvided(value)
|
|
145
|
+
);
|
|
146
|
+
return { ...context, attested: provided };
|
|
147
|
+
}
|
|
148
|
+
function withDeclaredFlag(context) {
|
|
149
|
+
const declared = Object.entries(context).some(
|
|
150
|
+
([name, value]) => name !== "declared" && isProvided(value) && !(Array.isArray(value) && value.length === 0)
|
|
151
|
+
);
|
|
152
|
+
return { ...context, declared };
|
|
153
|
+
}
|
|
154
|
+
function refuseCallerOriginHashes(substance) {
|
|
155
|
+
const origin = substance?.origin;
|
|
156
|
+
if (origin?.source_ip_hash || origin?.user_agent_hash) {
|
|
157
|
+
throw new VerdifaxError(
|
|
158
|
+
"verdifax: request_substance.origin.source_ip_hash and user_agent_hash are computed by the server and cannot be supplied"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function withExecuteFlags(req) {
|
|
163
|
+
const body = { ...req };
|
|
164
|
+
if (req.attested_context) {
|
|
165
|
+
body.attested_context = withAttestedFlag(req.attested_context);
|
|
166
|
+
}
|
|
167
|
+
if (req.reproducibility_context) {
|
|
168
|
+
body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
|
|
169
|
+
}
|
|
170
|
+
return body;
|
|
171
|
+
}
|
|
172
|
+
|
|
44
173
|
// src/validation.ts
|
|
45
174
|
function validateHex64(value, fieldName) {
|
|
46
175
|
if (typeof value !== "string" || value.length !== 64) {
|
|
@@ -71,7 +200,7 @@ function validateRouteId(value) {
|
|
|
71
200
|
}
|
|
72
201
|
|
|
73
202
|
// src/version.ts
|
|
74
|
-
var SDK_VERSION = "0.
|
|
203
|
+
var SDK_VERSION = "1.0.0";
|
|
75
204
|
|
|
76
205
|
// src/client.ts
|
|
77
206
|
var DEFAULT_BASE_URL = "https://api.verdifax.com";
|
|
@@ -80,7 +209,11 @@ var DEFAULT_MAX_RETRIES = 3;
|
|
|
80
209
|
var DEFAULT_RETRY_BASE_DELAY_MS = 200;
|
|
81
210
|
var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
|
|
82
211
|
var HEADER_API_KEY = "X-Verdifax-Key";
|
|
212
|
+
var REQUEST_KEY_PATH = "/keys/request-key";
|
|
213
|
+
var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
|
|
214
|
+
var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
|
|
83
215
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
216
|
+
var MAX_BATCH_ROWS = 1e3;
|
|
84
217
|
var VerdifaxClient = class {
|
|
85
218
|
baseURL;
|
|
86
219
|
apiKey;
|
|
@@ -88,6 +221,7 @@ var VerdifaxClient = class {
|
|
|
88
221
|
userAgent;
|
|
89
222
|
maxRetries;
|
|
90
223
|
retryBaseDelayMs;
|
|
224
|
+
signingKey;
|
|
91
225
|
constructor(config = {}) {
|
|
92
226
|
this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
93
227
|
this.apiKey = config.apiKey ?? "";
|
|
@@ -95,6 +229,7 @@ var VerdifaxClient = class {
|
|
|
95
229
|
this.userAgent = config.userAgent ?? SDK_USER_AGENT;
|
|
96
230
|
this.maxRetries = config.maxRetries === void 0 ? DEFAULT_MAX_RETRIES : Math.max(0, config.maxRetries);
|
|
97
231
|
this.retryBaseDelayMs = config.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
|
|
232
|
+
this.signingKey = config.signingKey === void 0 ? void 0 : loadSigningKey(config.signingKey);
|
|
98
233
|
}
|
|
99
234
|
// ── Health + Version (anonymous) ──────────────────────────────
|
|
100
235
|
async health() {
|
|
@@ -103,20 +238,52 @@ var VerdifaxClient = class {
|
|
|
103
238
|
async version() {
|
|
104
239
|
return this.request("GET", "/version");
|
|
105
240
|
}
|
|
241
|
+
/** The server's own API description. */
|
|
242
|
+
async getOpenAPI() {
|
|
243
|
+
return this.request("GET", "/openapi.json");
|
|
244
|
+
}
|
|
245
|
+
/** Run counts for this API key. */
|
|
246
|
+
async getStats() {
|
|
247
|
+
this.requireAPIKey();
|
|
248
|
+
return this.request("GET", "/stats");
|
|
249
|
+
}
|
|
106
250
|
// ── Execute / Attest (auth) ───────────────────────────────────
|
|
107
251
|
async execute(req) {
|
|
252
|
+
this.requireExecuteFields(req);
|
|
253
|
+
return this.request("POST", "/execute", this.prepared(req));
|
|
254
|
+
}
|
|
255
|
+
/** Runs one payload twice on the server and compares the manifest
|
|
256
|
+
* hashes. One signature covers the request, and the server claims
|
|
257
|
+
* its nonce once. Both runs are recorded. */
|
|
258
|
+
async verifyDeterminism(req) {
|
|
259
|
+
this.requireExecuteFields(req);
|
|
260
|
+
return this.request(
|
|
261
|
+
"POST",
|
|
262
|
+
"/execute/verify-determinism",
|
|
263
|
+
this.prepared(req)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
/** Sends up to {@link MAX_BATCH_ROWS} runs in one request. With a
|
|
267
|
+
* signing key each row carries its own signature and nonce. A row
|
|
268
|
+
* that fails comes back in `results` with `ok: false`, so the call
|
|
269
|
+
* does not throw for it. The rows are not checked here: the server
|
|
270
|
+
* reports a bad row in that row's result.
|
|
271
|
+
*
|
|
272
|
+
* This is one POST and it is never retried. After a timeout the
|
|
273
|
+
* server may have run some or all of the rows. */
|
|
274
|
+
async executeBatch(runs) {
|
|
108
275
|
this.requireAPIKey();
|
|
109
|
-
if (!
|
|
110
|
-
throw new VerdifaxError(
|
|
111
|
-
"verdifax: ExecuteRequest needs payload or payload_text"
|
|
112
|
-
);
|
|
276
|
+
if (!Array.isArray(runs) || runs.length === 0) {
|
|
277
|
+
throw new VerdifaxError("verdifax: executeBatch needs at least one run");
|
|
113
278
|
}
|
|
114
|
-
if (
|
|
279
|
+
if (runs.length > MAX_BATCH_ROWS) {
|
|
115
280
|
throw new VerdifaxError(
|
|
116
|
-
|
|
281
|
+
`verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
|
|
117
282
|
);
|
|
118
283
|
}
|
|
119
|
-
return this.request("POST", "/execute",
|
|
284
|
+
return this.request("POST", "/execute/batch", {
|
|
285
|
+
runs: runs.map((run) => this.prepared(run))
|
|
286
|
+
});
|
|
120
287
|
}
|
|
121
288
|
/** High-level convenience wrapper around `execute()`. Accepts
|
|
122
289
|
* either a Buffer (for binary payloads, auto-base64) or a
|
|
@@ -142,7 +309,12 @@ var VerdifaxClient = class {
|
|
|
142
309
|
route_id: req.route_id,
|
|
143
310
|
registry_record_hash: req.registry_record_hash,
|
|
144
311
|
ai_output_text: req.ai_output_text,
|
|
145
|
-
attested_context: req.attested_context
|
|
312
|
+
attested_context: req.attested_context,
|
|
313
|
+
reproducibility_context: req.reproducibility_context,
|
|
314
|
+
request_substance: req.request_substance,
|
|
315
|
+
authorization_chain: req.authorization_chain,
|
|
316
|
+
regulatory_scaffolding: req.regulatory_scaffolding,
|
|
317
|
+
causal_graph: req.causal_graph
|
|
146
318
|
};
|
|
147
319
|
if (req.payload !== void 0) {
|
|
148
320
|
body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
|
|
@@ -151,6 +323,29 @@ var VerdifaxClient = class {
|
|
|
151
323
|
}
|
|
152
324
|
return this.execute(body);
|
|
153
325
|
}
|
|
326
|
+
// ── Request signing (auth) ───────────────────────────────────
|
|
327
|
+
/** Registers this client's public key on its API key, with the proof
|
|
328
|
+
* that the client holds the private key. From then on the server
|
|
329
|
+
* refuses a run on this API key without a request signature. */
|
|
330
|
+
async registerRequestKey() {
|
|
331
|
+
this.requireAPIKey();
|
|
332
|
+
if (!this.signingKey) {
|
|
333
|
+
throw new VerdifaxError(
|
|
334
|
+
"verdifax: registerRequestKey needs the signingKey client option"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return this.request(
|
|
338
|
+
"POST",
|
|
339
|
+
REQUEST_KEY_PATH,
|
|
340
|
+
requestKeyRegistrationBody(this.signingKey)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
/** Clears the registered public key, so the server stops requiring
|
|
344
|
+
* request signatures on this API key. */
|
|
345
|
+
async clearRequestKey() {
|
|
346
|
+
this.requireAPIKey();
|
|
347
|
+
await this.request("DELETE", REQUEST_KEY_PATH);
|
|
348
|
+
}
|
|
154
349
|
// ── Runs ──────────────────────────────────────────────────────
|
|
155
350
|
async getRun(runId) {
|
|
156
351
|
this.requireAPIKey();
|
|
@@ -176,6 +371,94 @@ var VerdifaxClient = class {
|
|
|
176
371
|
const out = await this.request("GET", path);
|
|
177
372
|
return { runs: out.runs ?? [], total: out.total ?? 0 };
|
|
178
373
|
}
|
|
374
|
+
/** Searches this key's runs. `manifest_hash` matches a prefix. The
|
|
375
|
+
* server ignores a `from` or `to` it cannot parse here, unlike
|
|
376
|
+
* {@link exportRuns}, which refuses one. */
|
|
377
|
+
async searchRuns(params = {}) {
|
|
378
|
+
this.requireAPIKey();
|
|
379
|
+
const q = new URLSearchParams();
|
|
380
|
+
for (const name of [
|
|
381
|
+
"manifest_hash",
|
|
382
|
+
"program_id",
|
|
383
|
+
"route_id",
|
|
384
|
+
"status",
|
|
385
|
+
"from",
|
|
386
|
+
"to"
|
|
387
|
+
]) {
|
|
388
|
+
const value = params[name];
|
|
389
|
+
if (value) q.set(name, value);
|
|
390
|
+
}
|
|
391
|
+
if (params.limit && params.limit > 0) {
|
|
392
|
+
q.set("limit", String(params.limit));
|
|
393
|
+
}
|
|
394
|
+
if (params.offset && params.offset > 0) {
|
|
395
|
+
q.set("offset", String(params.offset));
|
|
396
|
+
}
|
|
397
|
+
const qs = q.toString();
|
|
398
|
+
const path = qs ? `/runs/search?${qs}` : "/runs/search";
|
|
399
|
+
const out = await this.request("GET", path);
|
|
400
|
+
return { runs: out.runs ?? [], total: out.total ?? 0 };
|
|
401
|
+
}
|
|
402
|
+
/** Lists what the server holds for this key in a period, failures
|
|
403
|
+
* included, up to 10,000 rows. `truncated` is true when the period
|
|
404
|
+
* holds more. The list states what was stored, not a verdict, and a
|
|
405
|
+
* call that never reached Verdifax cannot appear in it. */
|
|
406
|
+
async exportRuns(params = {}) {
|
|
407
|
+
this.requireAPIKey();
|
|
408
|
+
return this.request("GET", this.exportPath(params, "json"));
|
|
409
|
+
}
|
|
410
|
+
/** The same rows as {@link exportRuns}: the raw CSV text with `total`
|
|
411
|
+
* and `truncated` from the response headers. Check `truncated`: when
|
|
412
|
+
* true, the CSV is missing rows for the period. Throws when the server
|
|
413
|
+
* does not send those headers, so that a cut-off export cannot pass
|
|
414
|
+
* for a complete one. */
|
|
415
|
+
async exportRunsCSV(params = {}) {
|
|
416
|
+
this.requireAPIKey();
|
|
417
|
+
const { text, headers } = await this.requestRaw(
|
|
418
|
+
this.exportPath(params, "csv")
|
|
419
|
+
);
|
|
420
|
+
const total = Number.parseInt(headers.get(HEADER_EXPORT_TOTAL) ?? "", 10);
|
|
421
|
+
const truncated = headers.get(HEADER_EXPORT_TRUNCATED);
|
|
422
|
+
if (!Number.isSafeInteger(total) || truncated !== "true" && truncated !== "false") {
|
|
423
|
+
throw new VerdifaxError(
|
|
424
|
+
`verdifax: the server did not report ${HEADER_EXPORT_TOTAL} and ${HEADER_EXPORT_TRUNCATED}, so this CSV may be cut off; use exportRuns instead`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return { csv: text, total, truncated: truncated === "true" };
|
|
428
|
+
}
|
|
429
|
+
exportPath(params, format) {
|
|
430
|
+
const q = new URLSearchParams();
|
|
431
|
+
if (params.from) q.set("from", params.from);
|
|
432
|
+
if (params.to) q.set("to", params.to);
|
|
433
|
+
q.set("format", format);
|
|
434
|
+
return `/runs/export?${q.toString()}`;
|
|
435
|
+
}
|
|
436
|
+
// ── Attestation receipts (auth) ──────────────────────────────
|
|
437
|
+
async listAttestations(params = {}) {
|
|
438
|
+
this.requireAPIKey();
|
|
439
|
+
const q = new URLSearchParams();
|
|
440
|
+
if (params.limit && params.limit > 0) {
|
|
441
|
+
q.set("limit", String(params.limit));
|
|
442
|
+
}
|
|
443
|
+
if (params.offset && params.offset > 0) {
|
|
444
|
+
q.set("offset", String(params.offset));
|
|
445
|
+
}
|
|
446
|
+
const qs = q.toString();
|
|
447
|
+
return this.request(
|
|
448
|
+
"GET",
|
|
449
|
+
qs ? `/attestations?${qs}` : "/attestations"
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
async getAttestation(attestationId) {
|
|
453
|
+
this.requireAPIKey();
|
|
454
|
+
if (attestationId <= 0) {
|
|
455
|
+
throw new VerdifaxError("verdifax: attestationId must be > 0");
|
|
456
|
+
}
|
|
457
|
+
return this.request(
|
|
458
|
+
"GET",
|
|
459
|
+
`/attestations/${attestationId}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
179
462
|
// ── Sealed-artifact retrieval (auth) ─────────────────────────
|
|
180
463
|
async getAllowToken(runId) {
|
|
181
464
|
return this.getRunArtifact(runId, "allow-token");
|
|
@@ -192,6 +475,39 @@ var VerdifaxClient = class {
|
|
|
192
475
|
async getAivpT4HaltReceipt(runId) {
|
|
193
476
|
return this.getRunArtifact(runId, "aivp-t4-halt-receipt");
|
|
194
477
|
}
|
|
478
|
+
async getAttestationGapReceipt(runId) {
|
|
479
|
+
return this.getRunArtifact(runId, "attestation-gap-receipt");
|
|
480
|
+
}
|
|
481
|
+
async getCESAuditRecord(runId) {
|
|
482
|
+
return this.getRunArtifact(runId, "ces-audit-record");
|
|
483
|
+
}
|
|
484
|
+
/** The audit report for a run, as JSON. */
|
|
485
|
+
async getRunReport(runId) {
|
|
486
|
+
const out = await this.getRunArtifact(runId, "report");
|
|
487
|
+
return out.report;
|
|
488
|
+
}
|
|
489
|
+
/** The run's sealed audit bundle as the exact JSON text the server
|
|
490
|
+
* sent, to save and check with `verdifax-verify`. It is not parsed:
|
|
491
|
+
* JSON.parse would round a number above 2^53 and the recomputed hash
|
|
492
|
+
* would stop matching. */
|
|
493
|
+
async getRunArtifacts(runId) {
|
|
494
|
+
this.requireAPIKey();
|
|
495
|
+
if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
|
|
496
|
+
return (await this.requestRaw(`/runs/${runId}/artifacts`)).text;
|
|
497
|
+
}
|
|
498
|
+
/** The public audit bundle of a record published by run id, as the
|
|
499
|
+
* exact JSON text the server sent. Needs no API key. */
|
|
500
|
+
async getPublicBundle(runId) {
|
|
501
|
+
if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
|
|
502
|
+
return (await this.requestRaw(`/runs/${runId}/bundle.json`)).text;
|
|
503
|
+
}
|
|
504
|
+
/** The public audit bundle a share token points to, as the exact JSON
|
|
505
|
+
* text the server sent. Needs no API key. */
|
|
506
|
+
async getPublicBundleByToken(token) {
|
|
507
|
+
if (!token) throw new VerdifaxError("verdifax: token is required");
|
|
508
|
+
const path = `/public/runs/${encodeURIComponent(token)}/bundle.json`;
|
|
509
|
+
return (await this.requestRaw(path)).text;
|
|
510
|
+
}
|
|
195
511
|
async getRunArtifact(runId, suffix) {
|
|
196
512
|
this.requireAPIKey();
|
|
197
513
|
if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
|
|
@@ -278,13 +594,15 @@ var VerdifaxClient = class {
|
|
|
278
594
|
await this.request("DELETE", `/admin/keys/${keyId}`);
|
|
279
595
|
}
|
|
280
596
|
// ── Provider helpers (Claude / OpenAI) ───────────────────────
|
|
281
|
-
/** Convenience for attesting an Anthropic Claude API response.
|
|
282
|
-
*
|
|
283
|
-
* by
|
|
597
|
+
/** Convenience for attesting an Anthropic Claude API response. With
|
|
598
|
+
* `govern: true` the response text also goes to the AIVP review as
|
|
599
|
+
* `ai_output_text`; that is off by default, so the text leaves your
|
|
600
|
+
* machine for review only when you ask. */
|
|
284
601
|
async attestClaudeResponse(args) {
|
|
285
602
|
return this.attestProviderResponse({ ...args, provider: "anthropic" });
|
|
286
603
|
}
|
|
287
|
-
/** Convenience for attesting an OpenAI API response.
|
|
604
|
+
/** Convenience for attesting an OpenAI API response. `govern` works as
|
|
605
|
+
* in {@link attestClaudeResponse}. */
|
|
288
606
|
async attestOpenAIResponse(args) {
|
|
289
607
|
return this.attestProviderResponse({ ...args, provider: "openai" });
|
|
290
608
|
}
|
|
@@ -294,17 +612,16 @@ var VerdifaxClient = class {
|
|
|
294
612
|
"verdifax: response is required for attestation"
|
|
295
613
|
);
|
|
296
614
|
}
|
|
297
|
-
const payloadText = JSON.stringify({
|
|
298
|
-
provider: args.provider,
|
|
299
|
-
prompt: args.prompt,
|
|
300
|
-
response: args.response
|
|
301
|
-
});
|
|
302
615
|
return this.attest({
|
|
303
|
-
payload_text:
|
|
616
|
+
payload_text: formatProviderPayload(
|
|
617
|
+
args.provider,
|
|
618
|
+
args.prompt,
|
|
619
|
+
args.response
|
|
620
|
+
),
|
|
304
621
|
program_id: args.program_id,
|
|
305
622
|
route_id: args.route_id,
|
|
306
623
|
registry_record_hash: args.registry_record_hash,
|
|
307
|
-
ai_output_text: args.response,
|
|
624
|
+
ai_output_text: args.govern ? args.response : void 0,
|
|
308
625
|
attested_context: {
|
|
309
626
|
actor_role: "ai_assistant",
|
|
310
627
|
model_provider: args.provider
|
|
@@ -312,7 +629,7 @@ var VerdifaxClient = class {
|
|
|
312
629
|
});
|
|
313
630
|
}
|
|
314
631
|
// ── HTTP plumbing, retry-aware ──────────────────────────────
|
|
315
|
-
async request(method, path, body) {
|
|
632
|
+
async request(method, path, body, rawText = false) {
|
|
316
633
|
const url = this.baseURL + path;
|
|
317
634
|
const headers = {
|
|
318
635
|
"User-Agent": this.userAgent
|
|
@@ -369,6 +686,7 @@ var VerdifaxClient = class {
|
|
|
369
686
|
throw new APIError(resp.status, path, errMessage);
|
|
370
687
|
}
|
|
371
688
|
}
|
|
689
|
+
if (rawText) return { text, headers: resp.headers };
|
|
372
690
|
if (text.length === 0) return void 0;
|
|
373
691
|
try {
|
|
374
692
|
return JSON.parse(text);
|
|
@@ -382,17 +700,44 @@ var VerdifaxClient = class {
|
|
|
382
700
|
`verdifax: retry budget exhausted${lastErr ? `: ${String(lastErr)}` : ""}`
|
|
383
701
|
);
|
|
384
702
|
}
|
|
703
|
+
/** A GET whose body is kept as sent, retried like any other GET. */
|
|
704
|
+
requestRaw(path) {
|
|
705
|
+
return this.request("GET", path, void 0, true);
|
|
706
|
+
}
|
|
385
707
|
requireAPIKey() {
|
|
386
708
|
if (!this.apiKey) throw new APIKeyRequiredError();
|
|
387
709
|
}
|
|
710
|
+
requireExecuteFields(req) {
|
|
711
|
+
this.requireAPIKey();
|
|
712
|
+
if (!req.payload && !req.payload_text) {
|
|
713
|
+
throw new VerdifaxError(
|
|
714
|
+
"verdifax: ExecuteRequest needs payload or payload_text"
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (!req.program_id || !req.route_id || !req.registry_record_hash) {
|
|
718
|
+
throw new VerdifaxError(
|
|
719
|
+
"verdifax: program_id, route_id, and registry_record_hash are required"
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
validateHex64(req.program_id, "program_id");
|
|
723
|
+
validateHex64(req.registry_record_hash, "registry_record_hash");
|
|
724
|
+
validateRouteId(req.route_id);
|
|
725
|
+
refuseCallerOriginHashes(req.request_substance);
|
|
726
|
+
}
|
|
727
|
+
/** The request with its derived flags and, with a signing key, a fresh
|
|
728
|
+
* signature and nonce, on a copy. */
|
|
729
|
+
prepared(req) {
|
|
730
|
+
const body = withExecuteFlags(req);
|
|
731
|
+
return this.signingKey ? { ...body, request_signature: signRequest(this.signingKey, body) } : body;
|
|
732
|
+
}
|
|
388
733
|
};
|
|
389
734
|
|
|
390
735
|
// src/config.ts
|
|
391
736
|
function fromEnv() {
|
|
392
737
|
const cfg = {
|
|
393
|
-
baseURL: process.env.VERDIFAX_BASE_URL
|
|
738
|
+
baseURL: process.env.VERDIFAX_BASE_URL || process.env.VERDIFAX_API_URL || void 0,
|
|
394
739
|
apiKey: process.env.VERDIFAX_API_KEY ?? void 0,
|
|
395
|
-
userAgent: process.env.VERDIFAX_USER_AGENT
|
|
740
|
+
userAgent: process.env.VERDIFAX_USER_AGENT || void 0
|
|
396
741
|
};
|
|
397
742
|
const timeoutRaw = process.env.VERDIFAX_TIMEOUT_MS;
|
|
398
743
|
if (timeoutRaw !== void 0 && timeoutRaw !== "") {
|
|
@@ -416,14 +761,181 @@ function fromEnv() {
|
|
|
416
761
|
}
|
|
417
762
|
return new VerdifaxClient(cfg);
|
|
418
763
|
}
|
|
764
|
+
|
|
765
|
+
// src/environment.ts
|
|
766
|
+
import { execFileSync } from "child_process";
|
|
767
|
+
import { readdirSync, readFileSync } from "fs";
|
|
768
|
+
import { join } from "path";
|
|
769
|
+
var HASH_64 = /^[0-9a-f]{64}$/;
|
|
770
|
+
var GIT_SHA = /^([0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
771
|
+
var PLATFORM_NAMES = { win32: "windows" };
|
|
772
|
+
var ARCH_NAMES = { x64: "amd64", ia32: "386" };
|
|
773
|
+
function packageVersion(dir) {
|
|
774
|
+
try {
|
|
775
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
776
|
+
return pkg.name && pkg.version ? `${pkg.name}==${pkg.version}` : void 0;
|
|
777
|
+
} catch {
|
|
778
|
+
return void 0;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function packageDirs(root) {
|
|
782
|
+
const dirs = [];
|
|
783
|
+
for (const entry of readdirSync(root)) {
|
|
784
|
+
if (entry.startsWith(".")) continue;
|
|
785
|
+
if (!entry.startsWith("@")) {
|
|
786
|
+
dirs.push(join(root, entry));
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
for (const scoped of readdirSync(join(root, entry))) {
|
|
790
|
+
dirs.push(join(root, entry, scoped));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return dirs;
|
|
794
|
+
}
|
|
795
|
+
function detectDependencies() {
|
|
796
|
+
try {
|
|
797
|
+
const pins = packageDirs(join(process.cwd(), "node_modules")).map(packageVersion).filter((pin) => pin !== void 0);
|
|
798
|
+
pins.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
799
|
+
return pins.length > 0 ? pins : void 0;
|
|
800
|
+
} catch {
|
|
801
|
+
return void 0;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function detectGitSHA() {
|
|
805
|
+
try {
|
|
806
|
+
const sha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
807
|
+
encoding: "utf8",
|
|
808
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
809
|
+
timeout: 2e3
|
|
810
|
+
}).trim().toLowerCase();
|
|
811
|
+
return GIT_SHA.test(sha) ? sha : void 0;
|
|
812
|
+
} catch {
|
|
813
|
+
return void 0;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function detectContainerHash() {
|
|
817
|
+
try {
|
|
818
|
+
const text = readFileSync("/proc/self/cgroup", "utf8");
|
|
819
|
+
return text.split("\n").flatMap((line) => line.split("/")).map((segment) => segment.trim()).find((segment) => HASH_64.test(segment));
|
|
820
|
+
} catch {
|
|
821
|
+
return void 0;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function captureEnvironment(options = {}) {
|
|
825
|
+
const seeds = Object.entries(options.declaredSeeds ?? {}).map(([name, seed]) => `${name}=${seed}`).sort();
|
|
826
|
+
const pinned = options.includeDependencies === false ? void 0 : detectDependencies();
|
|
827
|
+
const git = options.includeGit === false ? void 0 : detectGitSHA();
|
|
828
|
+
const container = options.includeContainer === false ? void 0 : detectContainerHash();
|
|
829
|
+
const os = PLATFORM_NAMES[process.platform] ?? process.platform;
|
|
830
|
+
const arch = ARCH_NAMES[process.arch] ?? process.arch;
|
|
831
|
+
return {
|
|
832
|
+
declared: true,
|
|
833
|
+
runtime_name: "node",
|
|
834
|
+
runtime_version: process.versions.node,
|
|
835
|
+
platform: `${os}/${arch}`,
|
|
836
|
+
...pinned ? { pinned_dependencies: pinned } : {},
|
|
837
|
+
...git ? { git_commit_sha: git } : {},
|
|
838
|
+
...container ? { container_image_hash: container } : {},
|
|
839
|
+
...seeds.length > 0 ? { random_seeds: seeds } : {}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/types.ts
|
|
844
|
+
var RUN_STATUSES = [
|
|
845
|
+
"ok",
|
|
846
|
+
"probe",
|
|
847
|
+
"pepg_deny",
|
|
848
|
+
"ccv_halt",
|
|
849
|
+
"macc_halt",
|
|
850
|
+
"aivp_t4_halt",
|
|
851
|
+
"attestation_gap",
|
|
852
|
+
"stage_error"
|
|
853
|
+
];
|
|
854
|
+
|
|
855
|
+
// src/section0.ts
|
|
856
|
+
import { createHash as createHash2 } from "crypto";
|
|
857
|
+
var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
|
|
858
|
+
var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
|
|
859
|
+
var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
|
|
860
|
+
var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
|
|
861
|
+
function sha256Hex(preimage) {
|
|
862
|
+
return createHash2("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
|
|
863
|
+
}
|
|
864
|
+
function requireHex64(value, name) {
|
|
865
|
+
try {
|
|
866
|
+
validateHex64(value, name);
|
|
867
|
+
} catch (err) {
|
|
868
|
+
throw new VerdifaxError(
|
|
869
|
+
`verdifax: section0: ${err instanceof Error ? err.message : String(err)}`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function dseDispatchHash(fields) {
|
|
874
|
+
requireHex64(fields.program_id, "program_id");
|
|
875
|
+
requireHex64(fields.registry_record_hash, "registry_record_hash");
|
|
876
|
+
return sha256Hex(
|
|
877
|
+
[
|
|
878
|
+
DSE_FORMULA_VERSION,
|
|
879
|
+
fields.envelope_id,
|
|
880
|
+
fields.sequence_id,
|
|
881
|
+
fields.program_id,
|
|
882
|
+
fields.registry_record_hash
|
|
883
|
+
].join(".")
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
function dcaeClosureHash(fields) {
|
|
887
|
+
requireHex64(fields.manifest_hash, "manifest_hash");
|
|
888
|
+
requireHex64(fields.aer_hash, "aer_hash");
|
|
889
|
+
requireHex64(fields.zksp_binding_hash, "zksp_binding_hash");
|
|
890
|
+
return sha256Hex(
|
|
891
|
+
[
|
|
892
|
+
DCAE_PREIMAGE_VERSION,
|
|
893
|
+
fields.manifest_hash,
|
|
894
|
+
fields.aer_hash,
|
|
895
|
+
fields.envelope_id,
|
|
896
|
+
fields.zksp_binding_hash,
|
|
897
|
+
fields.formal_verifier_status
|
|
898
|
+
].join("|")
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
function cresReceiptHash(fields) {
|
|
902
|
+
requireHex64(fields.ciphertext_hash_at_shred, "ciphertext_hash_at_shred");
|
|
903
|
+
return sha256Hex(
|
|
904
|
+
[
|
|
905
|
+
CRES_PREIMAGE_VERSION,
|
|
906
|
+
fields.envelope_id,
|
|
907
|
+
fields.field_path,
|
|
908
|
+
fields.deletion_clock,
|
|
909
|
+
fields.actor_id,
|
|
910
|
+
fields.ciphertext_hash_at_shred,
|
|
911
|
+
fields.dsar_reference,
|
|
912
|
+
fields.engine_version ?? CRES_ENGINE_VERSION
|
|
913
|
+
].join("|")
|
|
914
|
+
);
|
|
915
|
+
}
|
|
419
916
|
export {
|
|
420
917
|
APIError,
|
|
421
918
|
APIKeyRequiredError,
|
|
422
919
|
AuthError,
|
|
920
|
+
CRES_ENGINE_VERSION,
|
|
921
|
+
CRES_PREIMAGE_VERSION,
|
|
922
|
+
DCAE_PREIMAGE_VERSION,
|
|
923
|
+
DSE_FORMULA_VERSION,
|
|
924
|
+
MAX_BATCH_ROWS,
|
|
423
925
|
NotFoundError,
|
|
926
|
+
RUN_STATUSES,
|
|
424
927
|
VerdifaxClient,
|
|
425
928
|
VerdifaxError,
|
|
929
|
+
captureEnvironment,
|
|
930
|
+
cresReceiptHash,
|
|
931
|
+
dcaeClosureHash,
|
|
932
|
+
dseDispatchHash,
|
|
933
|
+
formatProviderPayload,
|
|
426
934
|
fromEnv,
|
|
935
|
+
generateRequestKeyPair,
|
|
936
|
+
requestKeyRegistrationBody,
|
|
937
|
+
requestSignaturePreimage,
|
|
938
|
+
signRequest,
|
|
427
939
|
validateHex64,
|
|
428
940
|
validateRouteId
|
|
429
941
|
};
|