verdifax 1.1.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/dist/index.js CHANGED
@@ -27,6 +27,7 @@ __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,
@@ -40,6 +41,8 @@ __export(index_exports, {
40
41
  formatProviderPayload: () => formatProviderPayload,
41
42
  fromEnv: () => fromEnv,
42
43
  generateRequestKeyPair: () => generateRequestKeyPair,
44
+ policyActivationApproval: () => policyActivationApproval,
45
+ policyActivationPreimage: () => policyActivationPreimage,
43
46
  requestKeyChangeApproval: () => requestKeyChangeApproval,
44
47
  requestKeyChangePreimage: () => requestKeyChangePreimage,
45
48
  requestKeyRegistrationBody: () => requestKeyRegistrationBody,
@@ -214,7 +217,144 @@ function normalizedJob(job) {
214
217
  if (job.final) out.final = true;
215
218
  return out;
216
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
+ }
217
356
  function withExecuteFlags(req) {
357
+ refuseUnknownRequestFields(req);
218
358
  const body = { ...req };
219
359
  if (req.job === void 0 || req.job === null) {
220
360
  delete body.job;
@@ -227,6 +367,17 @@ function withExecuteFlags(req) {
227
367
  if (req.reproducibility_context) {
228
368
  body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
229
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
+ }
230
381
  return body;
231
382
  }
232
383
 
@@ -237,6 +388,7 @@ var REQUEST_SIGNATURE_VERSION_2 = "2";
237
388
  var JOB_FORM = "verdifax.job.v1";
238
389
  var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
239
390
  var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
391
+ var POLICY_ACTIVATION_FORM = "verdifax.policy-activation.v1";
240
392
  var SIGNING_KEY_BYTES = 32;
241
393
  var PKCS8_ED25519_HEADER = Buffer.from(
242
394
  "302e020100300506032b657004220420",
@@ -391,6 +543,36 @@ function requestKeyChangeApproval(currentSigningKey, newPublicKey, options = {})
391
543
  nonce
392
544
  };
393
545
  }
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
+ );
558
+ }
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
+ };
575
+ }
394
576
 
395
577
  // src/job.ts
396
578
  var STEP_OPTIONS = {
@@ -406,6 +588,8 @@ var STEP_OPTIONS = {
406
588
  authorization_chain: true,
407
589
  regulatory_scaffolding: true,
408
590
  causal_graph: true,
591
+ policy_context: true,
592
+ expected_policy_hash: true,
409
593
  after: true,
410
594
  external_previous: true,
411
595
  final: true
@@ -490,7 +674,7 @@ var VerdifaxJob = class {
490
674
  };
491
675
 
492
676
  // src/version.ts
493
- var SDK_VERSION = "1.1.0";
677
+ var SDK_VERSION = "1.2.0";
494
678
 
495
679
  // src/client.ts
496
680
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
@@ -500,10 +684,42 @@ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
500
684
  var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
501
685
  var HEADER_API_KEY = "X-Verdifax-Key";
502
686
  var REQUEST_KEY_PATH = "/keys/request-key";
687
+ var POLICIES_PATH = "/policies";
503
688
  var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
504
689
  var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
505
690
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
506
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
+ );
507
723
  var VerdifaxClient = class {
508
724
  baseURL;
509
725
  apiKey;
@@ -539,6 +755,7 @@ var VerdifaxClient = class {
539
755
  }
540
756
  // ── Execute / Attest (auth) ───────────────────────────────────
541
757
  async execute(req) {
758
+ refuseUnknownRequestFields(req);
542
759
  this.requireExecuteFields(req);
543
760
  const sent = this.prepared(req);
544
761
  const out = await this.request("POST", "/execute", sent);
@@ -549,6 +766,7 @@ var VerdifaxClient = class {
549
766
  * its nonce once. Both runs are recorded. A run that happens twice
550
767
  * cannot be one job step, so a `job` is refused here. */
551
768
  async verifyDeterminism(req) {
769
+ refuseUnknownRequestFields(req);
552
770
  this.requireExecuteFields(req);
553
771
  if (req.job !== void 0 && req.job !== null) {
554
772
  throw new VerdifaxError(
@@ -567,8 +785,9 @@ var VerdifaxClient = class {
567
785
  * signing key each row carries its own signature and nonce. A row
568
786
  * that fails comes back in `results` with `ok: false`, so the call
569
787
  * does not throw for it. The rows are not checked here, except a job
570
- * block, which is refused before anything is sent: the server reports
571
- * any other bad row in that row's result.
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.
572
791
  *
573
792
  * This is one POST and it is never retried. After a timeout the
574
793
  * server may have run some or all of the rows. */
@@ -582,6 +801,7 @@ var VerdifaxClient = class {
582
801
  `verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
583
802
  );
584
803
  }
804
+ runs.forEach((run, i) => refuseUnknownRequestFields(run, `runs[${i}]`));
585
805
  const sent = runs.map((run) => this.prepared(run));
586
806
  const out = await this.request(
587
807
  "POST",
@@ -602,6 +822,8 @@ var VerdifaxClient = class {
602
822
  * routes the call through to the canonical /execute. */
603
823
  async attest(req) {
604
824
  this.requireAPIKey();
825
+ refuseUnknownRequestFields(req, "attest request");
826
+ refuseUnknownFields(req, ATTEST_FIELDS, "attest request");
605
827
  if (req.payload !== void 0 && req.payload_text !== void 0) {
606
828
  throw new VerdifaxError(
607
829
  "verdifax: attest accepts payload OR payload_text, not both"
@@ -626,7 +848,9 @@ var VerdifaxClient = class {
626
848
  authorization_chain: req.authorization_chain,
627
849
  regulatory_scaffolding: req.regulatory_scaffolding,
628
850
  causal_graph: req.causal_graph,
629
- job: req.job
851
+ job: req.job,
852
+ policy_context: req.policy_context,
853
+ expected_policy_hash: req.expected_policy_hash
630
854
  };
631
855
  if (req.payload !== void 0) {
632
856
  body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
@@ -701,6 +925,120 @@ var VerdifaxClient = class {
701
925
  );
702
926
  this.signingKey = void 0;
703
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;
1041
+ }
704
1042
  // ── Runs ──────────────────────────────────────────────────────
705
1043
  async getRun(runId) {
706
1044
  this.requireAPIKey();
@@ -965,11 +1303,13 @@ var VerdifaxClient = class {
965
1303
  * `ai_output_text`; that is off by default, so the text leaves your
966
1304
  * machine for review only when you ask. */
967
1305
  async attestClaudeResponse(args) {
1306
+ refuseUnknownFields(args, PROVIDER_ARGS, "attestClaudeResponse arguments");
968
1307
  return this.attestProviderResponse({ ...args, provider: "anthropic" });
969
1308
  }
970
1309
  /** Convenience for attesting an OpenAI API response. `govern` works as
971
1310
  * in {@link attestClaudeResponse}. */
972
1311
  async attestOpenAIResponse(args) {
1312
+ refuseUnknownFields(args, PROVIDER_ARGS, "attestOpenAIResponse arguments");
973
1313
  return this.attestProviderResponse({ ...args, provider: "openai" });
974
1314
  }
975
1315
  async attestProviderResponse(args) {
@@ -992,7 +1332,9 @@ var VerdifaxClient = class {
992
1332
  actor_role: "ai_assistant",
993
1333
  model_provider: args.provider
994
1334
  },
995
- job: args.job
1335
+ job: args.job,
1336
+ policy_context: args.policy_context,
1337
+ expected_policy_hash: args.expected_policy_hash
996
1338
  });
997
1339
  }
998
1340
  // ── HTTP plumbing, retry-aware ──────────────────────────────
@@ -1301,6 +1643,7 @@ function cresReceiptHash(fields) {
1301
1643
  CRES_PREIMAGE_VERSION,
1302
1644
  DCAE_PREIMAGE_VERSION,
1303
1645
  DSE_FORMULA_VERSION,
1646
+ EXECUTE_FIELDS,
1304
1647
  MAX_BATCH_ROWS,
1305
1648
  NotFoundError,
1306
1649
  RUN_STATUSES,
@@ -1314,6 +1657,8 @@ function cresReceiptHash(fields) {
1314
1657
  formatProviderPayload,
1315
1658
  fromEnv,
1316
1659
  generateRequestKeyPair,
1660
+ policyActivationApproval,
1661
+ policyActivationPreimage,
1317
1662
  requestKeyChangeApproval,
1318
1663
  requestKeyChangePreimage,
1319
1664
  requestKeyRegistrationBody,