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.mjs CHANGED
@@ -167,7 +167,144 @@ function normalizedJob(job) {
167
167
  if (job.final) out.final = true;
168
168
  return out;
169
169
  }
170
+ var POLICY_ACTOR_TYPES = ["agent", "user", "service", "system"];
171
+ var POLICY_OBJECT_TYPES = ["text", "transaction", "code", "data", "model", "file"];
172
+ var POLICY_CLASSIFICATIONS = [
173
+ "PUBLIC",
174
+ "INTERNAL",
175
+ "CONFIDENTIAL",
176
+ "RESTRICTED",
177
+ "TOP_SECRET",
178
+ "TS_SCI"
179
+ ];
180
+ var MAX_POLICY_OBJECT_ID_BYTES = 256;
181
+ var POLICY_CONTEXT_FIELDS = {
182
+ actor_type: true,
183
+ mfa_satisfied: true,
184
+ session_age_seconds: true,
185
+ object_type: true,
186
+ object_id: true,
187
+ classification: true
188
+ };
189
+ function refuseOutsideList(value, allowed, fieldName) {
190
+ if (typeof value !== "string" || !allowed.includes(value)) {
191
+ throw new VerdifaxError(
192
+ `verdifax: policy_context.${fieldName} must be one of ${allowed.join(", ")}`
193
+ );
194
+ }
195
+ }
196
+ function checkedPolicyContext(context) {
197
+ if (typeof context !== "object" || context === null || Array.isArray(context)) {
198
+ throw new VerdifaxError("verdifax: policy_context must be an object");
199
+ }
200
+ refuseUnknownFields(context, POLICY_CONTEXT_FIELDS, "policy_context");
201
+ const out = {};
202
+ const {
203
+ actor_type,
204
+ mfa_satisfied,
205
+ session_age_seconds,
206
+ object_type,
207
+ object_id,
208
+ classification
209
+ } = context;
210
+ if (actor_type !== void 0) {
211
+ refuseOutsideList(actor_type, POLICY_ACTOR_TYPES, "actor_type");
212
+ out.actor_type = actor_type;
213
+ }
214
+ if (mfa_satisfied !== void 0) {
215
+ if (typeof mfa_satisfied !== "boolean") {
216
+ throw new VerdifaxError(
217
+ "verdifax: policy_context.mfa_satisfied must be true or false"
218
+ );
219
+ }
220
+ out.mfa_satisfied = mfa_satisfied;
221
+ }
222
+ if (session_age_seconds !== void 0) {
223
+ if (!Number.isSafeInteger(session_age_seconds) || session_age_seconds < 0) {
224
+ throw new VerdifaxError(
225
+ "verdifax: policy_context.session_age_seconds must be a whole number of seconds, not negative"
226
+ );
227
+ }
228
+ out.session_age_seconds = session_age_seconds;
229
+ }
230
+ if (object_type !== void 0) {
231
+ refuseOutsideList(object_type, POLICY_OBJECT_TYPES, "object_type");
232
+ out.object_type = object_type;
233
+ }
234
+ if (object_id !== void 0) {
235
+ if (typeof object_id !== "string") {
236
+ throw new VerdifaxError("verdifax: policy_context.object_id must be a string");
237
+ }
238
+ if (Buffer.byteLength(object_id, "utf8") > MAX_POLICY_OBJECT_ID_BYTES) {
239
+ throw new VerdifaxError(
240
+ `verdifax: policy_context.object_id is longer than ${MAX_POLICY_OBJECT_ID_BYTES} bytes`
241
+ );
242
+ }
243
+ out.object_id = object_id;
244
+ }
245
+ if (classification !== void 0) {
246
+ refuseOutsideList(classification, POLICY_CLASSIFICATIONS, "classification");
247
+ out.classification = classification;
248
+ }
249
+ return out;
250
+ }
251
+ function refuseActorFactsWithoutActor(context, attested) {
252
+ const declaresActor = context.actor_type !== void 0 || context.mfa_satisfied === true || (context.session_age_seconds ?? 0) !== 0;
253
+ if (declaresActor && attested?.attested !== true) {
254
+ throw new VerdifaxError(
255
+ "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"
256
+ );
257
+ }
258
+ }
259
+ var EXECUTE_FIELD_SET = {
260
+ payload: true,
261
+ payload_text: true,
262
+ program_id: true,
263
+ route_id: true,
264
+ registry_record_hash: true,
265
+ ai_output_text: true,
266
+ attested_context: true,
267
+ reproducibility_context: true,
268
+ request_substance: true,
269
+ authorization_chain: true,
270
+ regulatory_scaffolding: true,
271
+ causal_graph: true,
272
+ request_signature: true,
273
+ signed_request: true,
274
+ job: true,
275
+ policy_context: true,
276
+ expected_policy_hash: true
277
+ };
278
+ var EXECUTE_FIELDS = Object.freeze(
279
+ Object.keys(EXECUTE_FIELD_SET)
280
+ );
281
+ var ATTESTED_CONTEXT_FIELDS = {
282
+ attested: true,
283
+ actor_id: true,
284
+ actor_role: true,
285
+ authorization_policy: true,
286
+ actor_signature: true,
287
+ model_provider: true,
288
+ model_name: true,
289
+ model_version: true,
290
+ model_temperature: true,
291
+ prompt_hash: true,
292
+ decision_kind: true,
293
+ decision_result: true,
294
+ decision_note: true
295
+ };
296
+ function refuseUnknownRequestFields(req, label = "request") {
297
+ if (typeof req !== "object" || req === null || Array.isArray(req)) {
298
+ throw new VerdifaxError(`verdifax: ${label} must be an object`);
299
+ }
300
+ refuseUnknownFields(req, EXECUTE_FIELD_SET, label);
301
+ const context = req.attested_context;
302
+ if (typeof context === "object" && context !== null) {
303
+ refuseUnknownFields(context, ATTESTED_CONTEXT_FIELDS, "attested_context");
304
+ }
305
+ }
170
306
  function withExecuteFlags(req) {
307
+ refuseUnknownRequestFields(req);
171
308
  const body = { ...req };
172
309
  if (req.job === void 0 || req.job === null) {
173
310
  delete body.job;
@@ -180,6 +317,17 @@ function withExecuteFlags(req) {
180
317
  if (req.reproducibility_context) {
181
318
  body.reproducibility_context = withDeclaredFlag(req.reproducibility_context);
182
319
  }
320
+ if (req.policy_context === void 0 || req.policy_context === null) {
321
+ delete body.policy_context;
322
+ } else {
323
+ body.policy_context = checkedPolicyContext(req.policy_context);
324
+ refuseActorFactsWithoutActor(body.policy_context, body.attested_context);
325
+ }
326
+ if (req.expected_policy_hash === void 0 || req.expected_policy_hash === null) {
327
+ delete body.expected_policy_hash;
328
+ } else {
329
+ validateHex64(req.expected_policy_hash, "expected_policy_hash");
330
+ }
183
331
  return body;
184
332
  }
185
333
 
@@ -190,6 +338,7 @@ var REQUEST_SIGNATURE_VERSION_2 = "2";
190
338
  var JOB_FORM = "verdifax.job.v1";
191
339
  var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
192
340
  var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
341
+ var POLICY_ACTIVATION_FORM = "verdifax.policy-activation.v1";
193
342
  var SIGNING_KEY_BYTES = 32;
194
343
  var PKCS8_ED25519_HEADER = Buffer.from(
195
344
  "302e020100300506032b657004220420",
@@ -344,6 +493,36 @@ function requestKeyChangeApproval(currentSigningKey, newPublicKey, options = {})
344
493
  nonce
345
494
  };
346
495
  }
496
+ function policyActivationPreimage(fields) {
497
+ return Buffer.from(
498
+ [
499
+ POLICY_ACTIVATION_FORM,
500
+ fields.chain_id,
501
+ fields.document_hash,
502
+ fields.previous_activation_hash,
503
+ fields.signed_at,
504
+ fields.nonce
505
+ ].join("\0"),
506
+ "utf8"
507
+ );
508
+ }
509
+ function policyActivationApproval(signingKey, fields, options = {}) {
510
+ const key = loadSigningKey(signingKey);
511
+ const signed_at = options.signedAt ?? signedAtNow();
512
+ const nonce = options.nonce ?? randomBytes(16).toString("hex");
513
+ const preimage = policyActivationPreimage({
514
+ chain_id: fields.chain_id,
515
+ document_hash: fields.document_hash,
516
+ previous_activation_hash: fields.previous_activation_hash,
517
+ signed_at,
518
+ nonce
519
+ });
520
+ return {
521
+ signature: sign(null, preimage, key).toString("base64"),
522
+ signed_at,
523
+ nonce
524
+ };
525
+ }
347
526
 
348
527
  // src/job.ts
349
528
  var STEP_OPTIONS = {
@@ -359,6 +538,8 @@ var STEP_OPTIONS = {
359
538
  authorization_chain: true,
360
539
  regulatory_scaffolding: true,
361
540
  causal_graph: true,
541
+ policy_context: true,
542
+ expected_policy_hash: true,
362
543
  after: true,
363
544
  external_previous: true,
364
545
  final: true
@@ -443,7 +624,7 @@ var VerdifaxJob = class {
443
624
  };
444
625
 
445
626
  // src/version.ts
446
- var SDK_VERSION = "1.1.0";
627
+ var SDK_VERSION = "1.2.0";
447
628
 
448
629
  // src/client.ts
449
630
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
@@ -453,10 +634,42 @@ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
453
634
  var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
454
635
  var HEADER_API_KEY = "X-Verdifax-Key";
455
636
  var REQUEST_KEY_PATH = "/keys/request-key";
637
+ var POLICIES_PATH = "/policies";
456
638
  var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
457
639
  var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
458
640
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
459
641
  var MAX_BATCH_ROWS = 1e3;
642
+ var ATTEST_FIELDS = {
643
+ payload: true,
644
+ payload_text: true,
645
+ program_id: true,
646
+ route_id: true,
647
+ registry_record_hash: true,
648
+ ai_output_text: true,
649
+ attested_context: true,
650
+ reproducibility_context: true,
651
+ request_substance: true,
652
+ authorization_chain: true,
653
+ regulatory_scaffolding: true,
654
+ causal_graph: true,
655
+ job: true,
656
+ policy_context: true,
657
+ expected_policy_hash: true
658
+ };
659
+ var PROVIDER_ARGS = Object.fromEntries(
660
+ [
661
+ "program_id",
662
+ "route_id",
663
+ "registry_record_hash",
664
+ "prompt",
665
+ "response",
666
+ "govern",
667
+ "job",
668
+ "policy_context",
669
+ "expected_policy_hash",
670
+ ...EXECUTE_FIELDS
671
+ ].map((name) => [name, true])
672
+ );
460
673
  var VerdifaxClient = class {
461
674
  baseURL;
462
675
  apiKey;
@@ -492,6 +705,7 @@ var VerdifaxClient = class {
492
705
  }
493
706
  // ── Execute / Attest (auth) ───────────────────────────────────
494
707
  async execute(req) {
708
+ refuseUnknownRequestFields(req);
495
709
  this.requireExecuteFields(req);
496
710
  const sent = this.prepared(req);
497
711
  const out = await this.request("POST", "/execute", sent);
@@ -502,6 +716,7 @@ var VerdifaxClient = class {
502
716
  * its nonce once. Both runs are recorded. A run that happens twice
503
717
  * cannot be one job step, so a `job` is refused here. */
504
718
  async verifyDeterminism(req) {
719
+ refuseUnknownRequestFields(req);
505
720
  this.requireExecuteFields(req);
506
721
  if (req.job !== void 0 && req.job !== null) {
507
722
  throw new VerdifaxError(
@@ -520,8 +735,9 @@ var VerdifaxClient = class {
520
735
  * signing key each row carries its own signature and nonce. A row
521
736
  * that fails comes back in `results` with `ok: false`, so the call
522
737
  * 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.
738
+ * block, a `policy_context` and an `expected_policy_hash`, which are
739
+ * refused before anything is sent: the server reports any other bad row
740
+ * in that row's result.
525
741
  *
526
742
  * This is one POST and it is never retried. After a timeout the
527
743
  * server may have run some or all of the rows. */
@@ -535,6 +751,7 @@ var VerdifaxClient = class {
535
751
  `verdifax: executeBatch accepts at most ${MAX_BATCH_ROWS} runs (got ${runs.length})`
536
752
  );
537
753
  }
754
+ runs.forEach((run, i) => refuseUnknownRequestFields(run, `runs[${i}]`));
538
755
  const sent = runs.map((run) => this.prepared(run));
539
756
  const out = await this.request(
540
757
  "POST",
@@ -555,6 +772,8 @@ var VerdifaxClient = class {
555
772
  * routes the call through to the canonical /execute. */
556
773
  async attest(req) {
557
774
  this.requireAPIKey();
775
+ refuseUnknownRequestFields(req, "attest request");
776
+ refuseUnknownFields(req, ATTEST_FIELDS, "attest request");
558
777
  if (req.payload !== void 0 && req.payload_text !== void 0) {
559
778
  throw new VerdifaxError(
560
779
  "verdifax: attest accepts payload OR payload_text, not both"
@@ -579,7 +798,9 @@ var VerdifaxClient = class {
579
798
  authorization_chain: req.authorization_chain,
580
799
  regulatory_scaffolding: req.regulatory_scaffolding,
581
800
  causal_graph: req.causal_graph,
582
- job: req.job
801
+ job: req.job,
802
+ policy_context: req.policy_context,
803
+ expected_policy_hash: req.expected_policy_hash
583
804
  };
584
805
  if (req.payload !== void 0) {
585
806
  body.payload = Buffer.isBuffer(req.payload) ? req.payload.toString("base64") : Buffer.from(req.payload).toString("base64");
@@ -654,6 +875,120 @@ var VerdifaxClient = class {
654
875
  );
655
876
  this.signingKey = void 0;
656
877
  }
878
+ // ── Policies (auth) ──────────────────────────────────────────
879
+ /** Stores a policy document. Nothing changes until it is activated with
880
+ * {@link activatePolicy}. The server hashes the exact text, so this
881
+ * sends the string it is given unchanged and refuses anything that is
882
+ * not a string: serializing an object here would change the hash.
883
+ * Verdifax does not write, review or approve a customer's policy. */
884
+ async uploadPolicy(document) {
885
+ this.requireAPIKey();
886
+ if (typeof document !== "string") {
887
+ throw new VerdifaxError(
888
+ "verdifax: document must be the policy's exact text, as a string"
889
+ );
890
+ }
891
+ return this.request("POST", POLICIES_PATH, { document });
892
+ }
893
+ /** This key's stored documents, with the chain its changes belong to,
894
+ * the newest change in it (64 zeros when there is none yet) and the
895
+ * document in force (empty under the built-in policy). */
896
+ async listPolicies() {
897
+ this.requireAPIKey();
898
+ return this.request("GET", POLICIES_PATH);
899
+ }
900
+ /** One stored document, with its exact text as `document`. */
901
+ async getPolicy(documentHash) {
902
+ this.requireAPIKey();
903
+ validateHex64(documentHash, "documentHash");
904
+ return this.request(
905
+ "GET",
906
+ `${POLICIES_PATH}/${documentHash}`
907
+ );
908
+ }
909
+ /** Shows which rule would fire on sample inputs, for a document given as
910
+ * text or one already stored, named by its hash: exactly one of the two.
911
+ * It seals nothing. */
912
+ async testPolicy(req) {
913
+ this.requireAPIKey();
914
+ const { inputs, document, documentHash } = req ?? {};
915
+ if (document === void 0 === (documentHash === void 0)) {
916
+ throw new VerdifaxError(
917
+ "verdifax: testPolicy needs exactly one of document or documentHash"
918
+ );
919
+ }
920
+ if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) {
921
+ throw new VerdifaxError("verdifax: testPolicy needs inputs as an object");
922
+ }
923
+ const body = { inputs };
924
+ if (document !== void 0) {
925
+ if (typeof document !== "string") {
926
+ throw new VerdifaxError(
927
+ "verdifax: document must be the policy's exact text, as a string"
928
+ );
929
+ }
930
+ body.document = document;
931
+ } else {
932
+ validateHex64(documentHash, "documentHash");
933
+ body.document_hash = documentHash;
934
+ }
935
+ return this.request(
936
+ "POST",
937
+ `${POLICIES_PATH}/test`,
938
+ body
939
+ );
940
+ }
941
+ /** This key's policy changes, oldest first, as the list the server sends.
942
+ * Save it as JSON, for example with
943
+ * `writeFileSync("policy-history.json", JSON.stringify(history))`, and
944
+ * check it with `verdifax-verify --policy-history policy-history.json`. */
945
+ async getPolicyHistory() {
946
+ this.requireAPIKey();
947
+ return this.request("GET", `${POLICIES_PATH}/history`);
948
+ }
949
+ /** Makes a stored document the policy in force and returns the change
950
+ * record. It first reads GET /policies for the chain and its newest
951
+ * change, then names that change as the one this follows. With the
952
+ * `signingKey` option the request key signs the change, which the server
953
+ * requires while a request key is registered.
954
+ *
955
+ * Rolling back is activating an earlier document: it is one more change
956
+ * in the chain, so the change it undoes stays on the record.
957
+ *
958
+ * An HTTP 409 means the chain moved because another change came first,
959
+ * or the document is not yet effective. It throws the usual
960
+ * {@link APIError} and is not retried: read the policies again and
961
+ * decide. */
962
+ async activatePolicy(documentHash) {
963
+ this.requireAPIKey();
964
+ validateHex64(documentHash, "documentHash");
965
+ const chain = await this.listPolicies();
966
+ const previous = chain?.newest_activation_hash;
967
+ if (typeof previous !== "string") {
968
+ throw new VerdifaxError(
969
+ "verdifax: the server did not report newest_activation_hash, so the change cannot name the one it follows"
970
+ );
971
+ }
972
+ const body = { previous_activation_hash: previous };
973
+ if (this.signingKey) {
974
+ if (typeof chain.chain_id !== "string" || chain.chain_id === "") {
975
+ throw new VerdifaxError(
976
+ "verdifax: the server did not report chain_id, so the change cannot be signed"
977
+ );
978
+ }
979
+ body.signature = policyActivationApproval(this.signingKey, {
980
+ chain_id: chain.chain_id,
981
+ document_hash: documentHash,
982
+ previous_activation_hash: previous
983
+ });
984
+ }
985
+ const out = await this.request(
986
+ "POST",
987
+ `${POLICIES_PATH}/${documentHash}/activate`,
988
+ body
989
+ );
990
+ return out.activation;
991
+ }
657
992
  // ── Runs ──────────────────────────────────────────────────────
658
993
  async getRun(runId) {
659
994
  this.requireAPIKey();
@@ -918,11 +1253,13 @@ var VerdifaxClient = class {
918
1253
  * `ai_output_text`; that is off by default, so the text leaves your
919
1254
  * machine for review only when you ask. */
920
1255
  async attestClaudeResponse(args) {
1256
+ refuseUnknownFields(args, PROVIDER_ARGS, "attestClaudeResponse arguments");
921
1257
  return this.attestProviderResponse({ ...args, provider: "anthropic" });
922
1258
  }
923
1259
  /** Convenience for attesting an OpenAI API response. `govern` works as
924
1260
  * in {@link attestClaudeResponse}. */
925
1261
  async attestOpenAIResponse(args) {
1262
+ refuseUnknownFields(args, PROVIDER_ARGS, "attestOpenAIResponse arguments");
926
1263
  return this.attestProviderResponse({ ...args, provider: "openai" });
927
1264
  }
928
1265
  async attestProviderResponse(args) {
@@ -945,7 +1282,9 @@ var VerdifaxClient = class {
945
1282
  actor_role: "ai_assistant",
946
1283
  model_provider: args.provider
947
1284
  },
948
- job: args.job
1285
+ job: args.job,
1286
+ policy_context: args.policy_context,
1287
+ expected_policy_hash: args.expected_policy_hash
949
1288
  });
950
1289
  }
951
1290
  // ── HTTP plumbing, retry-aware ──────────────────────────────
@@ -1253,6 +1592,7 @@ export {
1253
1592
  CRES_PREIMAGE_VERSION,
1254
1593
  DCAE_PREIMAGE_VERSION,
1255
1594
  DSE_FORMULA_VERSION,
1595
+ EXECUTE_FIELDS,
1256
1596
  MAX_BATCH_ROWS,
1257
1597
  NotFoundError,
1258
1598
  RUN_STATUSES,
@@ -1266,6 +1606,8 @@ export {
1266
1606
  formatProviderPayload,
1267
1607
  fromEnv,
1268
1608
  generateRequestKeyPair,
1609
+ policyActivationApproval,
1610
+ policyActivationPreimage,
1269
1611
  requestKeyChangeApproval,
1270
1612
  requestKeyChangePreimage,
1271
1613
  requestKeyRegistrationBody,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdifax",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Official Node.js / TypeScript SDK for the Verdifax orchestrator REST API.",
5
5
  "license": "MIT",
6
6
  "author": "Verdifax <team@verdifax.com>",