verdifax 0.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/dist/index.mjs ADDED
@@ -0,0 +1,426 @@
1
+ // src/errors.ts
2
+ var VerdifaxError = class extends Error {
3
+ name = "VerdifaxError";
4
+ };
5
+ var AuthError = class extends VerdifaxError {
6
+ constructor(status, message) {
7
+ super(`verdifax: auth error (HTTP ${status}): ${message}`);
8
+ this.status = status;
9
+ this.message = message;
10
+ }
11
+ status;
12
+ message;
13
+ name = "VerdifaxAuthError";
14
+ };
15
+ var NotFoundError = class extends VerdifaxError {
16
+ constructor(path, message) {
17
+ super(`verdifax: not found at ${path}: ${message}`);
18
+ this.path = path;
19
+ this.message = message;
20
+ }
21
+ path;
22
+ message;
23
+ name = "VerdifaxNotFoundError";
24
+ };
25
+ var APIError = class extends VerdifaxError {
26
+ constructor(status, path, message) {
27
+ super(`verdifax: API error at ${path} (HTTP ${status}): ${message}`);
28
+ this.status = status;
29
+ this.path = path;
30
+ this.message = message;
31
+ }
32
+ status;
33
+ path;
34
+ message;
35
+ name = "VerdifaxAPIError";
36
+ };
37
+ var APIKeyRequiredError = class extends VerdifaxError {
38
+ name = "VerdifaxAPIKeyRequiredError";
39
+ constructor() {
40
+ super("verdifax: apiKey is required for this endpoint");
41
+ }
42
+ };
43
+
44
+ // src/validation.ts
45
+ function validateHex64(value, fieldName) {
46
+ if (typeof value !== "string" || value.length !== 64) {
47
+ throw new VerdifaxError(
48
+ `verdifax: ${fieldName} must be 64 hex characters (got ${typeof value === "string" ? value.length : typeof value})`
49
+ );
50
+ }
51
+ if (!/^[0-9a-f]{64}$/.test(value)) {
52
+ throw new VerdifaxError(
53
+ `verdifax: ${fieldName} must contain only lowercase hex (0-9, a-f)`
54
+ );
55
+ }
56
+ }
57
+ function validateRouteId(value) {
58
+ if (!value || typeof value !== "string") {
59
+ throw new VerdifaxError("verdifax: route_id is required");
60
+ }
61
+ if (value.length > 256) {
62
+ throw new VerdifaxError(
63
+ `verdifax: route_id is too long (${value.length} > 256)`
64
+ );
65
+ }
66
+ if (!/^[\x20-\x7E]+$/.test(value)) {
67
+ throw new VerdifaxError(
68
+ "verdifax: route_id contains non-printable-ASCII character"
69
+ );
70
+ }
71
+ }
72
+
73
+ // src/client.ts
74
+ var DEFAULT_BASE_URL = "https://api.verdifax.com";
75
+ var DEFAULT_TIMEOUT_MS = 3e4;
76
+ var DEFAULT_MAX_RETRIES = 3;
77
+ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
78
+ var SDK_USER_AGENT = "verdifax-sdk-node/0.2.0";
79
+ var HEADER_API_KEY = "X-Verdifax-Key";
80
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
81
+ var VerdifaxClient = class {
82
+ baseURL;
83
+ apiKey;
84
+ timeoutMs;
85
+ userAgent;
86
+ maxRetries;
87
+ retryBaseDelayMs;
88
+ constructor(config = {}) {
89
+ this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
90
+ this.apiKey = config.apiKey ?? "";
91
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
92
+ this.userAgent = config.userAgent ?? SDK_USER_AGENT;
93
+ this.maxRetries = config.maxRetries === void 0 ? DEFAULT_MAX_RETRIES : Math.max(0, config.maxRetries);
94
+ this.retryBaseDelayMs = config.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
95
+ }
96
+ // ── Health + Version (anonymous) ──────────────────────────────
97
+ async health() {
98
+ return this.request("GET", "/health");
99
+ }
100
+ async version() {
101
+ return this.request("GET", "/version");
102
+ }
103
+ // ── Execute / Attest (auth) ───────────────────────────────────
104
+ async execute(req) {
105
+ this.requireAPIKey();
106
+ if (!req.payload && !req.payload_text) {
107
+ throw new VerdifaxError(
108
+ "verdifax: ExecuteRequest needs payload or payload_text"
109
+ );
110
+ }
111
+ if (!req.program_id || !req.route_id || !req.registry_record_hash) {
112
+ throw new VerdifaxError(
113
+ "verdifax: program_id, route_id, and registry_record_hash are required"
114
+ );
115
+ }
116
+ return this.request("POST", "/execute", req);
117
+ }
118
+ /** High-level convenience wrapper around `execute()`. Accepts
119
+ * either a Buffer (for binary payloads — auto-base64) or a
120
+ * string (for plain text). Validates the hex fields and
121
+ * routes the call through to the canonical /execute. */
122
+ async attest(req) {
123
+ this.requireAPIKey();
124
+ if (req.payload !== void 0 && req.payload_text !== void 0) {
125
+ throw new VerdifaxError(
126
+ "verdifax: attest accepts payload OR payload_text, not both"
127
+ );
128
+ }
129
+ if (req.payload === void 0 && req.payload_text === void 0) {
130
+ throw new VerdifaxError(
131
+ "verdifax: attest needs payload or payload_text"
132
+ );
133
+ }
134
+ validateHex64(req.program_id, "program_id");
135
+ validateHex64(req.registry_record_hash, "registry_record_hash");
136
+ validateRouteId(req.route_id);
137
+ const body = {
138
+ program_id: req.program_id,
139
+ route_id: req.route_id,
140
+ registry_record_hash: req.registry_record_hash,
141
+ ai_output_text: req.ai_output_text,
142
+ attested_context: req.attested_context
143
+ };
144
+ if (req.payload !== void 0) {
145
+ body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
146
+ } else {
147
+ body.payload_text = req.payload_text;
148
+ }
149
+ return this.execute(body);
150
+ }
151
+ // ── Runs ──────────────────────────────────────────────────────
152
+ async getRun(runId) {
153
+ this.requireAPIKey();
154
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
155
+ return this.request("GET", `/runs/${runId}`);
156
+ }
157
+ async verifyRun(runId) {
158
+ this.requireAPIKey();
159
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
160
+ return this.request("GET", `/runs/${runId}/verify`);
161
+ }
162
+ async listRuns(params = {}) {
163
+ this.requireAPIKey();
164
+ const q = new URLSearchParams();
165
+ if (params.limit && params.limit > 0) {
166
+ q.set("limit", String(params.limit));
167
+ }
168
+ if (params.offset && params.offset > 0) {
169
+ q.set("offset", String(params.offset));
170
+ }
171
+ const qs = q.toString();
172
+ const path = qs ? `/runs?${qs}` : "/runs";
173
+ const out = await this.request("GET", path);
174
+ return { runs: out.runs ?? [], total: out.total ?? 0 };
175
+ }
176
+ // ── Sealed-artifact retrieval (auth) ─────────────────────────
177
+ async getAllowToken(runId) {
178
+ return this.getRunArtifact(runId, "allow-token");
179
+ }
180
+ async getDenyReceipt(runId) {
181
+ return this.getRunArtifact(runId, "deny-receipt");
182
+ }
183
+ async getCCVHaltReceipt(runId) {
184
+ return this.getRunArtifact(runId, "ccv-halt-receipt");
185
+ }
186
+ async getMACCHaltReceipt(runId) {
187
+ return this.getRunArtifact(runId, "macc-halt-receipt");
188
+ }
189
+ async getAivpT4HaltReceipt(runId) {
190
+ return this.getRunArtifact(runId, "aivp-t4-halt-receipt");
191
+ }
192
+ async getRunArtifact(runId, suffix) {
193
+ this.requireAPIKey();
194
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
195
+ return this.request(
196
+ "GET",
197
+ `/runs/${runId}/${suffix}`
198
+ );
199
+ }
200
+ // ── Audit PDF (auth) ─────────────────────────────────────────
201
+ /** Streams the audit PDF bytes as a Buffer. Format defaults to
202
+ * "comprehensive" when not specified. */
203
+ async downloadAuditPDF(runId, format = "comprehensive") {
204
+ this.requireAPIKey();
205
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
206
+ const path = `/runs/${runId}/report.pdf?format=${encodeURIComponent(format)}`;
207
+ const headers = {
208
+ "User-Agent": this.userAgent,
209
+ [HEADER_API_KEY]: this.apiKey
210
+ };
211
+ const controller = new AbortController();
212
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
213
+ let resp;
214
+ try {
215
+ resp = await fetch(this.baseURL + path, {
216
+ method: "GET",
217
+ headers,
218
+ signal: controller.signal
219
+ });
220
+ } finally {
221
+ clearTimeout(timer);
222
+ }
223
+ if (!resp.ok) {
224
+ throw new APIError(resp.status, path, "audit PDF download failed");
225
+ }
226
+ const buf = await resp.arrayBuffer();
227
+ return Buffer.from(buf);
228
+ }
229
+ // ── CRES — deletion receipts (auth) ──────────────────────────
230
+ async listDeletionReceipts(runId) {
231
+ this.requireAPIKey();
232
+ if (runId <= 0) throw new VerdifaxError("verdifax: runId must be > 0");
233
+ const out = await this.request("GET", `/runs/${runId}/deletion-receipts`);
234
+ return out.receipts ?? [];
235
+ }
236
+ async adminErase(req) {
237
+ this.requireAPIKey();
238
+ if (!req.run_id || req.run_id <= 0) {
239
+ throw new VerdifaxError("verdifax: run_id must be > 0");
240
+ }
241
+ if (!req.field_path) {
242
+ throw new VerdifaxError("verdifax: field_path is required");
243
+ }
244
+ validateHex64(req.ciphertext_hash, "ciphertext_hash");
245
+ const out = await this.request("POST", "/admin/erase", req);
246
+ return out.deletion_receipt;
247
+ }
248
+ // ── DCAE stateless verification (anonymous) ──────────────────
249
+ async dcaeVerify(bundle) {
250
+ return this.request("POST", "/dcae/verify", bundle);
251
+ }
252
+ // ── Admin: API keys (auth) ───────────────────────────────────
253
+ async adminCreateKey(name) {
254
+ this.requireAPIKey();
255
+ if (!name) {
256
+ throw new VerdifaxError("verdifax: name is required for adminCreateKey");
257
+ }
258
+ return this.request("POST", "/admin/keys", {
259
+ name
260
+ });
261
+ }
262
+ async adminListKeys() {
263
+ this.requireAPIKey();
264
+ const out = await this.request(
265
+ "GET",
266
+ "/admin/keys"
267
+ );
268
+ return out.keys ?? [];
269
+ }
270
+ async adminRevokeKey(keyId) {
271
+ this.requireAPIKey();
272
+ if (keyId <= 0) {
273
+ throw new VerdifaxError("verdifax: keyId must be > 0");
274
+ }
275
+ await this.request("DELETE", `/admin/keys/${keyId}`);
276
+ }
277
+ // ── Provider helpers (Claude / OpenAI) ───────────────────────
278
+ /** Convenience for attesting an Anthropic Claude API response.
279
+ * Wires the response text into the AIVP-T4 governance pipeline
280
+ * by setting `ai_output_text`. */
281
+ async attestClaudeResponse(args) {
282
+ return this.attestProviderResponse({ ...args, provider: "anthropic" });
283
+ }
284
+ /** Convenience for attesting an OpenAI API response. */
285
+ async attestOpenAIResponse(args) {
286
+ return this.attestProviderResponse({ ...args, provider: "openai" });
287
+ }
288
+ async attestProviderResponse(args) {
289
+ if (!args.response) {
290
+ throw new VerdifaxError(
291
+ "verdifax: response is required for attestation"
292
+ );
293
+ }
294
+ const payloadText = JSON.stringify({
295
+ provider: args.provider,
296
+ prompt: args.prompt,
297
+ response: args.response
298
+ });
299
+ return this.attest({
300
+ payload_text: payloadText,
301
+ program_id: args.program_id,
302
+ route_id: args.route_id,
303
+ registry_record_hash: args.registry_record_hash,
304
+ ai_output_text: args.response,
305
+ attested_context: {
306
+ actor_role: "ai_assistant",
307
+ model_provider: args.provider
308
+ }
309
+ });
310
+ }
311
+ // ── HTTP plumbing — retry-aware ──────────────────────────────
312
+ async request(method, path, body) {
313
+ const url = this.baseURL + path;
314
+ const headers = {
315
+ "User-Agent": this.userAgent
316
+ };
317
+ if (this.apiKey) headers[HEADER_API_KEY] = this.apiKey;
318
+ if (body !== void 0) headers["Content-Type"] = "application/json";
319
+ const isRetryable = method === "GET" || method === "HEAD" || method === "OPTIONS";
320
+ const maxAttempts = isRetryable ? this.maxRetries + 1 : 1;
321
+ let lastErr;
322
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
323
+ if (attempt > 0) {
324
+ const max = this.retryBaseDelayMs * Math.pow(2, attempt - 1);
325
+ const sleep = Math.floor(Math.random() * (max + 1));
326
+ await new Promise((r) => setTimeout(r, sleep));
327
+ }
328
+ const controller = new AbortController();
329
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
330
+ let resp;
331
+ try {
332
+ resp = await fetch(url, {
333
+ method,
334
+ headers,
335
+ body: body === void 0 ? void 0 : JSON.stringify(body),
336
+ signal: controller.signal
337
+ });
338
+ } catch (err) {
339
+ lastErr = err;
340
+ if (!isRetryable || attempt === maxAttempts - 1) {
341
+ throw new VerdifaxError(
342
+ `verdifax: HTTP transport: ${err instanceof Error ? err.message : String(err)}`
343
+ );
344
+ }
345
+ continue;
346
+ } finally {
347
+ clearTimeout(timer);
348
+ }
349
+ const text = await resp.text();
350
+ if (!resp.ok) {
351
+ if (isRetryable && RETRYABLE_STATUSES.has(resp.status) && attempt < maxAttempts - 1) {
352
+ continue;
353
+ }
354
+ let errMessage = text.trim();
355
+ try {
356
+ const env = JSON.parse(text);
357
+ if (env.error) errMessage = env.error;
358
+ } catch {
359
+ }
360
+ switch (resp.status) {
361
+ case 401:
362
+ throw new AuthError(resp.status, errMessage);
363
+ case 404:
364
+ throw new NotFoundError(path, errMessage);
365
+ default:
366
+ throw new APIError(resp.status, path, errMessage);
367
+ }
368
+ }
369
+ if (text.length === 0) return void 0;
370
+ try {
371
+ return JSON.parse(text);
372
+ } catch (err) {
373
+ throw new VerdifaxError(
374
+ `verdifax: failed to decode response body at ${path}: ${err instanceof Error ? err.message : String(err)}`
375
+ );
376
+ }
377
+ }
378
+ throw new VerdifaxError(
379
+ `verdifax: retry budget exhausted${lastErr ? `: ${String(lastErr)}` : ""}`
380
+ );
381
+ }
382
+ requireAPIKey() {
383
+ if (!this.apiKey) throw new APIKeyRequiredError();
384
+ }
385
+ };
386
+
387
+ // src/config.ts
388
+ function fromEnv() {
389
+ const cfg = {
390
+ baseURL: process.env.VERDIFAX_BASE_URL ?? void 0,
391
+ apiKey: process.env.VERDIFAX_API_KEY ?? void 0,
392
+ userAgent: process.env.VERDIFAX_USER_AGENT ?? void 0
393
+ };
394
+ const timeoutRaw = process.env.VERDIFAX_TIMEOUT_MS;
395
+ if (timeoutRaw !== void 0 && timeoutRaw !== "") {
396
+ const ms = Number.parseInt(timeoutRaw, 10);
397
+ if (!Number.isFinite(ms)) {
398
+ throw new VerdifaxError(
399
+ `verdifax: VERDIFAX_TIMEOUT_MS=${timeoutRaw} is not an integer`
400
+ );
401
+ }
402
+ cfg.timeoutMs = ms;
403
+ }
404
+ const retriesRaw = process.env.VERDIFAX_MAX_RETRIES;
405
+ if (retriesRaw !== void 0 && retriesRaw !== "") {
406
+ const n = Number.parseInt(retriesRaw, 10);
407
+ if (!Number.isFinite(n)) {
408
+ throw new VerdifaxError(
409
+ `verdifax: VERDIFAX_MAX_RETRIES=${retriesRaw} is not an integer`
410
+ );
411
+ }
412
+ cfg.maxRetries = n;
413
+ }
414
+ return new VerdifaxClient(cfg);
415
+ }
416
+ export {
417
+ APIError,
418
+ APIKeyRequiredError,
419
+ AuthError,
420
+ NotFoundError,
421
+ VerdifaxClient,
422
+ VerdifaxError,
423
+ fromEnv,
424
+ validateHex64,
425
+ validateRouteId
426
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "verdifax",
3
+ "version": "0.2.0",
4
+ "description": "Official Node.js / TypeScript SDK for the Verdifax orchestrator REST API.",
5
+ "license": "MIT",
6
+ "author": "Verdifax <team@verdifax.com>",
7
+ "homepage": "https://verdifax.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Verdifax/verdifax-sdk-node.git"
11
+ },
12
+ "keywords": [
13
+ "verdifax",
14
+ "attestation",
15
+ "ai",
16
+ "audit",
17
+ "compliance",
18
+ "manifest",
19
+ "hash",
20
+ "cryptographic",
21
+ "provenance",
22
+ "gdpr",
23
+ "hipaa"
24
+ ],
25
+ "main": "./dist/index.cjs",
26
+ "module": "./dist/index.mjs",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.mjs",
32
+ "require": "./dist/index.cjs"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
45
+ "test": "node --test --test-reporter=spec test/*.test.mjs",
46
+ "test:build": "npm run build && npm test",
47
+ "lint": "tsc --noEmit"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.0.0",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.4.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "provenance": true
57
+ }
58
+ }