verdifax 1.2.0 → 1.4.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
@@ -57,7 +57,8 @@ import {
57
57
  generateKeyPairSync,
58
58
  KeyObject,
59
59
  randomBytes,
60
- sign
60
+ sign,
61
+ verify
61
62
  } from "crypto";
62
63
 
63
64
  // src/validation.ts
@@ -339,7 +340,10 @@ var JOB_FORM = "verdifax.job.v1";
339
340
  var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
340
341
  var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
341
342
  var POLICY_ACTIVATION_FORM = "verdifax.policy-activation.v1";
343
+ var SIGNOFF_FORM = "verdifax.signoff.v1";
344
+ var APPROVER_KEY_PROOF_FORM = "verdifax.approver-key.v1";
342
345
  var SIGNING_KEY_BYTES = 32;
346
+ var SPKI_ED25519_HEADER = Buffer.from("302a300506032b6570032100", "hex");
343
347
  var PKCS8_ED25519_HEADER = Buffer.from(
344
348
  "302e020100300506032b657004220420",
345
349
  "hex"
@@ -523,6 +527,71 @@ function policyActivationApproval(signingKey, fields, options = {}) {
523
527
  nonce
524
528
  };
525
529
  }
530
+ function signOffPreimage(fields) {
531
+ const { target } = fields;
532
+ return Buffer.from(
533
+ [
534
+ SIGNOFF_FORM,
535
+ fields.chain_id,
536
+ fields.previous_signoff_hash,
537
+ target.kind,
538
+ String(target.run_id),
539
+ target.digest,
540
+ target.job_id ?? "",
541
+ fields.decision,
542
+ fields.note_hash,
543
+ fields.signed_at,
544
+ fields.nonce
545
+ ].join("\0"),
546
+ "utf8"
547
+ );
548
+ }
549
+ function signOffSignature(approverKey, fields, options = {}) {
550
+ const key = loadSigningKey(approverKey);
551
+ const signed_at = options.signedAt ?? signedAtNow();
552
+ const nonce = options.nonce ?? randomBytes(16).toString("hex");
553
+ const preimage = signOffPreimage({ ...fields, signed_at, nonce });
554
+ return {
555
+ approver_key: rawPublicKey(createPublicKey(key)).toString("base64"),
556
+ signature: sign(null, preimage, key).toString("base64"),
557
+ signed_at,
558
+ nonce
559
+ };
560
+ }
561
+ function approverKeyProof(approverKey) {
562
+ const key = loadSigningKey(approverKey);
563
+ const public_key = rawPublicKey(createPublicKey(key)).toString("base64");
564
+ const proof = sign(
565
+ null,
566
+ Buffer.from(`${APPROVER_KEY_PROOF_FORM}\0${public_key}`, "utf8"),
567
+ key
568
+ ).toString("base64");
569
+ return { public_key, proof };
570
+ }
571
+ function approverKeyProofVerifies(proof) {
572
+ if (typeof proof?.public_key !== "string" || typeof proof?.proof !== "string") {
573
+ return false;
574
+ }
575
+ const raw = Buffer.from(proof.public_key, "base64");
576
+ const signature = Buffer.from(proof.proof, "base64");
577
+ if (raw.length !== 32 || raw.toString("base64") !== proof.public_key || signature.length !== 64 || signature.toString("base64") !== proof.proof) {
578
+ return false;
579
+ }
580
+ const publicKey = createPublicKey({
581
+ key: Buffer.concat([SPKI_ED25519_HEADER, raw]),
582
+ format: "der",
583
+ type: "spki"
584
+ });
585
+ return verify(
586
+ null,
587
+ Buffer.from(`${APPROVER_KEY_PROOF_FORM}\0${proof.public_key}`, "utf8"),
588
+ publicKey,
589
+ signature
590
+ );
591
+ }
592
+ function publicKeyOf(key) {
593
+ return rawPublicKey(createPublicKey(key)).toString("base64");
594
+ }
526
595
 
527
596
  // src/job.ts
528
597
  var STEP_OPTIONS = {
@@ -624,9 +693,10 @@ var VerdifaxJob = class {
624
693
  };
625
694
 
626
695
  // src/version.ts
627
- var SDK_VERSION = "1.2.0";
696
+ var SDK_VERSION = "1.4.0";
628
697
 
629
698
  // src/client.ts
699
+ import { KeyObject as KeyObject2 } from "crypto";
630
700
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
631
701
  var DEFAULT_TIMEOUT_MS = 9e4;
632
702
  var DEFAULT_MAX_RETRIES = 3;
@@ -635,6 +705,16 @@ var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
635
705
  var HEADER_API_KEY = "X-Verdifax-Key";
636
706
  var REQUEST_KEY_PATH = "/keys/request-key";
637
707
  var POLICIES_PATH = "/policies";
708
+ var ACCOUNT_PATH = "/account";
709
+ var ACCOUNT_REQUEST_KEY_PATH = "/account/request-key";
710
+ var ACCOUNT_POLICIES_PATH = "/account/policies";
711
+ var ACCOUNT_CHAIN_ID = /^account-[1-9][0-9]*$/;
712
+ var SIGNOFFS_PATH = "/signoffs";
713
+ var ACCOUNT_SIGNOFFS_PATH = "/account/signoffs";
714
+ var APPROVERS_PATH = "/account/approvers";
715
+ var SIGNOFF_TARGET_KINDS = /* @__PURE__ */ new Set(["record", "receipt", "job"]);
716
+ var SIGNOFF_DECISIONS = /* @__PURE__ */ new Set(["approve", "reject"]);
717
+ var MAX_APPROVER_NAME_BYTES = 200;
638
718
  var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
639
719
  var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
640
720
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
@@ -820,15 +900,18 @@ var VerdifaxClient = class {
820
900
  * that the client holds the private key. From then on the server
821
901
  * refuses a run on this API key without a request signature. */
822
902
  async registerRequestKey() {
903
+ return this.registerRequestKeyAt(REQUEST_KEY_PATH, "registerRequestKey");
904
+ }
905
+ async registerRequestKeyAt(path, method) {
823
906
  this.requireAPIKey();
824
907
  if (!this.signingKey) {
825
908
  throw new VerdifaxError(
826
- "verdifax: registerRequestKey needs the signingKey client option"
909
+ `verdifax: ${method} needs the signingKey client option`
827
910
  );
828
911
  }
829
912
  return this.request(
830
913
  "POST",
831
- REQUEST_KEY_PATH,
914
+ path,
832
915
  requestKeyRegistrationBody(this.signingKey)
833
916
  );
834
917
  }
@@ -867,10 +950,13 @@ var VerdifaxClient = class {
867
950
  * unsigned requests, because the server refuses a signed run on an API
868
951
  * key with no registered key. If the call fails it keeps the key. */
869
952
  async clearRequestKey() {
953
+ await this.clearRequestKeyAt(REQUEST_KEY_PATH);
954
+ }
955
+ async clearRequestKeyAt(path) {
870
956
  this.requireAPIKey();
871
957
  await this.request(
872
958
  "DELETE",
873
- REQUEST_KEY_PATH,
959
+ path,
874
960
  this.signingKey ? { change: requestKeyChangeApproval(this.signingKey, "") } : void 0
875
961
  );
876
962
  this.signingKey = void 0;
@@ -882,13 +968,7 @@ var VerdifaxClient = class {
882
968
  * not a string: serializing an object here would change the hash.
883
969
  * Verdifax does not write, review or approve a customer's policy. */
884
970
  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 });
971
+ return this.uploadPolicyAt(POLICIES_PATH, document);
892
972
  }
893
973
  /** This key's stored documents, with the chain its changes belong to,
894
974
  * the newest change in it (64 zeros when there is none yet) and the
@@ -899,44 +979,13 @@ var VerdifaxClient = class {
899
979
  }
900
980
  /** One stored document, with its exact text as `document`. */
901
981
  async getPolicy(documentHash) {
902
- this.requireAPIKey();
903
- validateHex64(documentHash, "documentHash");
904
- return this.request(
905
- "GET",
906
- `${POLICIES_PATH}/${documentHash}`
907
- );
982
+ return this.getPolicyAt(POLICIES_PATH, documentHash);
908
983
  }
909
984
  /** Shows which rule would fire on sample inputs, for a document given as
910
985
  * text or one already stored, named by its hash: exactly one of the two.
911
986
  * It seals nothing. */
912
987
  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
- );
988
+ return this.testPolicyAt(POLICIES_PATH, req, "testPolicy");
940
989
  }
941
990
  /** This key's policy changes, oldest first, as the list the server sends.
942
991
  * Save it as JSON, for example with
@@ -962,7 +1011,54 @@ var VerdifaxClient = class {
962
1011
  async activatePolicy(documentHash) {
963
1012
  this.requireAPIKey();
964
1013
  validateHex64(documentHash, "documentHash");
965
- const chain = await this.listPolicies();
1014
+ return this.activatePolicyAt(
1015
+ POLICIES_PATH,
1016
+ documentHash,
1017
+ await this.listPolicies()
1018
+ );
1019
+ }
1020
+ async uploadPolicyAt(base, document) {
1021
+ this.requireAPIKey();
1022
+ if (typeof document !== "string") {
1023
+ throw new VerdifaxError(
1024
+ "verdifax: document must be the policy's exact text, as a string"
1025
+ );
1026
+ }
1027
+ return this.request("POST", base, { document });
1028
+ }
1029
+ async getPolicyAt(base, documentHash) {
1030
+ this.requireAPIKey();
1031
+ validateHex64(documentHash, "documentHash");
1032
+ return this.request("GET", `${base}/${documentHash}`);
1033
+ }
1034
+ async testPolicyAt(base, req, method) {
1035
+ this.requireAPIKey();
1036
+ const { inputs, document, documentHash } = req ?? {};
1037
+ if (document === void 0 === (documentHash === void 0)) {
1038
+ throw new VerdifaxError(
1039
+ `verdifax: ${method} needs exactly one of document or documentHash`
1040
+ );
1041
+ }
1042
+ if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) {
1043
+ throw new VerdifaxError(`verdifax: ${method} needs inputs as an object`);
1044
+ }
1045
+ const body = { inputs };
1046
+ if (document !== void 0) {
1047
+ if (typeof document !== "string") {
1048
+ throw new VerdifaxError(
1049
+ "verdifax: document must be the policy's exact text, as a string"
1050
+ );
1051
+ }
1052
+ body.document = document;
1053
+ } else {
1054
+ validateHex64(documentHash, "documentHash");
1055
+ body.document_hash = documentHash;
1056
+ }
1057
+ return this.request("POST", `${base}/test`, body);
1058
+ }
1059
+ /** Posts the change that follows the newest one in `chain`, signed by
1060
+ * the request key when the client has one. */
1061
+ async activatePolicyAt(base, documentHash, chain) {
966
1062
  const previous = chain?.newest_activation_hash;
967
1063
  if (typeof previous !== "string") {
968
1064
  throw new VerdifaxError(
@@ -984,11 +1080,315 @@ var VerdifaxClient = class {
984
1080
  }
985
1081
  const out = await this.request(
986
1082
  "POST",
987
- `${POLICIES_PATH}/${documentHash}/activate`,
1083
+ `${base}/${documentHash}/activate`,
988
1084
  body
989
1085
  );
990
1086
  return out.activation;
991
1087
  }
1088
+ // ── Account, with its owner key (auth) ───────────────────────
1089
+ //
1090
+ // An account is a company on Verdifax. Every key belongs to one account
1091
+ // for life. These methods need the account's owner key, which manages the
1092
+ // account and cannot run anything: the server refuses it on every other
1093
+ // route. The account number is a label Verdifax assigned, not a verified
1094
+ // company identity.
1095
+ /** The owner's account: its number, name, tier, key counts and the
1096
+ * chain of its policy changes. */
1097
+ async getAccount() {
1098
+ this.requireAPIKey();
1099
+ return this.request("GET", ACCOUNT_PATH);
1100
+ }
1101
+ /** Creates a member key in the account. It takes the account's tier and
1102
+ * stays in the account for life. The secret is in the result this once.
1103
+ * On a trial account the key needs a request key before it can run. */
1104
+ async createAccountKey(name, options = {}) {
1105
+ this.requireAPIKey();
1106
+ if (typeof name !== "string" || name.trim() === "") {
1107
+ throw new VerdifaxError(
1108
+ "verdifax: name is required for createAccountKey"
1109
+ );
1110
+ }
1111
+ const body = { name };
1112
+ if (options.expiresAt !== void 0) body.expires_at = options.expiresAt;
1113
+ return this.request(
1114
+ "POST",
1115
+ `${ACCOUNT_PATH}/keys`,
1116
+ body
1117
+ );
1118
+ }
1119
+ /** Every key in the account, owner keys included, without secrets. */
1120
+ async listAccountKeys() {
1121
+ this.requireAPIKey();
1122
+ const out = await this.request(
1123
+ "GET",
1124
+ `${ACCOUNT_PATH}/keys`
1125
+ );
1126
+ return { ...out, keys: out?.keys ?? [] };
1127
+ }
1128
+ /** Revokes one of the account's member keys. A key in another account,
1129
+ * or an owner key, throws {@link NotFoundError}. */
1130
+ async revokeAccountKey(keyId) {
1131
+ this.requireAPIKey();
1132
+ requirePositiveId(keyId, "keyId");
1133
+ return this.request(
1134
+ "DELETE",
1135
+ `${ACCOUNT_PATH}/keys/${keyId}`
1136
+ );
1137
+ }
1138
+ /** Registers this client's public key as the owner key's request key,
1139
+ * which must sign every change to the account's policy. It works as
1140
+ * {@link registerRequestKey} does. */
1141
+ async registerAccountRequestKey() {
1142
+ return this.registerRequestKeyAt(
1143
+ ACCOUNT_REQUEST_KEY_PATH,
1144
+ "registerAccountRequestKey"
1145
+ );
1146
+ }
1147
+ /** Clears the owner key's request key, as {@link clearRequestKey} does
1148
+ * for a key's own. With none registered the account's policy cannot be
1149
+ * changed. A trial owner key's request key cannot be cleared. */
1150
+ async clearAccountRequestKey() {
1151
+ await this.clearRequestKeyAt(ACCOUNT_REQUEST_KEY_PATH);
1152
+ }
1153
+ /** The runs of every key in the account, paged as {@link listRuns}. */
1154
+ async listAccountRuns(params = {}) {
1155
+ return this.listRunsAt(`${ACCOUNT_PATH}/runs`, params);
1156
+ }
1157
+ /** The runs of every key in the account in a period, as
1158
+ * {@link exportRuns} lists a key's, with the account's number, its
1159
+ * policy documents and its chain of changes. The account's view covers
1160
+ * the runs its keys made through Verdifax, not runs made elsewhere. */
1161
+ async exportAccountRuns(params = {}) {
1162
+ this.requireAPIKey();
1163
+ return this.request(
1164
+ "GET",
1165
+ this.exportPath(`${ACCOUNT_PATH}/runs/export`, params, "json")
1166
+ );
1167
+ }
1168
+ /** The same rows as {@link exportAccountRuns} as raw CSV text, checked
1169
+ * as {@link exportRunsCSV} checks a key's. */
1170
+ async exportAccountRunsCSV(params = {}) {
1171
+ return this.exportCSVAt(`${ACCOUNT_PATH}/runs/export`, params);
1172
+ }
1173
+ /** Stores an account policy document, as {@link uploadPolicy} stores a
1174
+ * key's. Every key in the account must pass the account's policy on top
1175
+ * of its own. Verdifax does not write, review or approve it. */
1176
+ async uploadAccountPolicy(document) {
1177
+ return this.uploadPolicyAt(ACCOUNT_POLICIES_PATH, document);
1178
+ }
1179
+ /** The account's stored documents, its chain `account-N`, the newest
1180
+ * change in it and the document in force. */
1181
+ async listAccountPolicies() {
1182
+ this.requireAPIKey();
1183
+ return this.request("GET", ACCOUNT_POLICIES_PATH);
1184
+ }
1185
+ /** One of the account's documents, with its exact text. */
1186
+ async getAccountPolicy(documentHash) {
1187
+ return this.getPolicyAt(ACCOUNT_POLICIES_PATH, documentHash);
1188
+ }
1189
+ /** {@link testPolicy} for the account's documents. It seals nothing. */
1190
+ async testAccountPolicy(req) {
1191
+ return this.testPolicyAt(ACCOUNT_POLICIES_PATH, req, "testAccountPolicy");
1192
+ }
1193
+ /** The account's policy changes, oldest first, in the form
1194
+ * `verdifax-verify --policy-history FILE` reads. */
1195
+ async getAccountPolicyHistory() {
1196
+ this.requireAPIKey();
1197
+ return this.request(
1198
+ "GET",
1199
+ `${ACCOUNT_POLICIES_PATH}/history`
1200
+ );
1201
+ }
1202
+ /** Makes a stored document the account's policy in force, as
1203
+ * {@link activatePolicy} does for a key's. It reads
1204
+ * {@link listAccountPolicies} for the chain and its newest change, and
1205
+ * the owner key's request key signs the change over that chain,
1206
+ * `account-N`. The server refuses an unsigned account change, so this
1207
+ * needs the `signingKey` option and refuses to send without it. */
1208
+ async activateAccountPolicy(documentHash) {
1209
+ this.requireAPIKey();
1210
+ validateHex64(documentHash, "documentHash");
1211
+ if (!this.signingKey) {
1212
+ throw new VerdifaxError(
1213
+ "verdifax: activateAccountPolicy needs the owner key's request key in the signingKey client option, because the server refuses an unsigned account policy change"
1214
+ );
1215
+ }
1216
+ const chain = await this.listAccountPolicies();
1217
+ if (typeof chain?.chain_id !== "string" || !ACCOUNT_CHAIN_ID.test(chain.chain_id)) {
1218
+ throw new VerdifaxError(
1219
+ "verdifax: the server did not report an account chain_id (account-N), so the change cannot be signed"
1220
+ );
1221
+ }
1222
+ return this.activatePolicyAt(ACCOUNT_POLICIES_PATH, documentHash, chain);
1223
+ }
1224
+ // ── Sign-off ─────────────────────────────────────────────────
1225
+ //
1226
+ // An account's owner registers approver keys. An approver signs a
1227
+ // decision, approve or reject, on one exact result on their own machine,
1228
+ // and any key in the account, or the owner key with asOwner, submits it.
1229
+ // A sign-off shows that the holder of a registered approver key signed
1230
+ // that decision on that exact result and that Verdifax logged it on the
1231
+ // public log. It does not show the review was careful, the result right,
1232
+ // or who the person is, and it gates nothing on its own.
1233
+ /** Registers an approver key to the owner's account. `approver` is the
1234
+ * approver's signing key, from which the proof is made on this machine,
1235
+ * or the `{ public_key, proof }` that {@link approverKeyProof} made on
1236
+ * the approver's own machine, so the owner need not hold the private
1237
+ * key. A proof that does not verify is refused before sending. The
1238
+ * server refuses a key that is a request key in the same account.
1239
+ * `name` is a label, not an identity. */
1240
+ async registerApprover(name, approver) {
1241
+ this.requireAPIKey();
1242
+ if (typeof name !== "string" || name.trim() === "" || Buffer.byteLength(name.trim(), "utf8") > MAX_APPROVER_NAME_BYTES) {
1243
+ throw new VerdifaxError(
1244
+ "verdifax: registerApprover needs a name of at most 200 bytes"
1245
+ );
1246
+ }
1247
+ const body = approver instanceof Uint8Array || approver instanceof KeyObject2 ? approverKeyProof(approver) : { public_key: approver?.public_key, proof: approver?.proof };
1248
+ if (!approverKeyProofVerifies(body)) {
1249
+ throw new VerdifaxError(
1250
+ "verdifax: registerApprover needs the approver's signing key, or a public_key and proof from approverKeyProof that verify"
1251
+ );
1252
+ }
1253
+ if (this.signingKey && publicKeyOf(this.signingKey) === body.public_key) {
1254
+ throw new VerdifaxError(
1255
+ "verdifax: an approver key must not be a request key in the same account, and this is the client's signingKey"
1256
+ );
1257
+ }
1258
+ return this.request("POST", APPROVERS_PATH, {
1259
+ public_key: body.public_key,
1260
+ name,
1261
+ proof: body.proof
1262
+ });
1263
+ }
1264
+ /** The account's approver keys, revoked ones included. */
1265
+ async listApprovers() {
1266
+ this.requireAPIKey();
1267
+ const out = await this.request("GET", APPROVERS_PATH);
1268
+ return { ...out, approvers: out?.approvers ?? [] };
1269
+ }
1270
+ /** Revokes one of the account's approver keys. Its earlier sign-offs
1271
+ * stay on the record. A key that is not an active approver in the
1272
+ * account throws {@link NotFoundError}. */
1273
+ async revokeApprover(approverId) {
1274
+ this.requireAPIKey();
1275
+ requirePositiveId(approverId, "approverId");
1276
+ return this.request(
1277
+ "DELETE",
1278
+ `${APPROVERS_PATH}/${approverId}`
1279
+ );
1280
+ }
1281
+ /** What an approver signs against: the account's chain `account-N` and
1282
+ * its newest sign-off (64 zeros when there is none yet). With
1283
+ * `asOwner` it reads the owner key's route. */
1284
+ async getNewestSignOff(options = {}) {
1285
+ this.requireAPIKey();
1286
+ return this.request(
1287
+ "GET",
1288
+ `${signOffsBase(options, "getNewestSignOff")}/newest`
1289
+ );
1290
+ }
1291
+ /** Signs an approver's decision, approve or reject, on one exact result
1292
+ * and submits it. It first reads {@link getNewestSignOff} for the
1293
+ * account's chain and its newest sign-off, then signs over them with
1294
+ * `approverKey` on this machine. The approver's private key is never
1295
+ * sent.
1296
+ *
1297
+ * `target.digest` is what the result's public-log entry covers: for a
1298
+ * record, the `bundle_anchor.anchored_digest` of the bundle
1299
+ * {@link getRunArtifacts} returns; for a receipt, its hash; for a job,
1300
+ * the final step's record digest, read as a record's, with `job_id`.
1301
+ *
1302
+ * `note` is hashed here and only its SHA-256 is sent and signed: the
1303
+ * text never reaches Verdifax. With `asOwner` it uses the owner key's
1304
+ * routes. Bad input is refused before anything is sent.
1305
+ *
1306
+ * The record the server returns must carry the signature,
1307
+ * approver_key, decision, previous_signoff_hash and target that were
1308
+ * sent, or this throws {@link VerdifaxError}.
1309
+ *
1310
+ * A later sign-off on the same result supersedes this one, and neither
1311
+ * is deleted. An HTTP 409 means the chain moved or the target is not
1312
+ * this account's result: it throws the usual {@link APIError} and is
1313
+ * not retried. */
1314
+ async signOff(target, decision, approverKey, options = {}) {
1315
+ this.requireAPIKey();
1316
+ const base = signOffsBase(options, "signOff");
1317
+ const exact = signOffTargetOf(target);
1318
+ if (!SIGNOFF_DECISIONS.has(decision)) {
1319
+ throw new VerdifaxError(
1320
+ "verdifax: signOff decision must be approve or reject"
1321
+ );
1322
+ }
1323
+ const note = options?.note;
1324
+ if (note !== void 0 && (typeof note !== "string" || note === "")) {
1325
+ throw new VerdifaxError(
1326
+ "verdifax: signOff note must be non-empty text when given"
1327
+ );
1328
+ }
1329
+ const key = loadSigningKey(approverKey);
1330
+ const note_hash = note === void 0 ? "" : sha256Hex(note);
1331
+ const newest = await this.getNewestSignOff(options);
1332
+ if (typeof newest?.chain_id !== "string" || !ACCOUNT_CHAIN_ID.test(newest.chain_id)) {
1333
+ throw new VerdifaxError(
1334
+ "verdifax: the server did not report an account chain_id (account-N), so the sign-off cannot be signed"
1335
+ );
1336
+ }
1337
+ const previous = newest.newest_signoff_hash;
1338
+ if (typeof previous !== "string" || !/^[0-9a-f]{64}$/.test(previous)) {
1339
+ throw new VerdifaxError(
1340
+ "verdifax: the server did not report newest_signoff_hash, so the sign-off cannot name the one it follows"
1341
+ );
1342
+ }
1343
+ const signed = signOffSignature(key, {
1344
+ chain_id: newest.chain_id,
1345
+ previous_signoff_hash: previous,
1346
+ target: exact,
1347
+ decision,
1348
+ note_hash
1349
+ });
1350
+ const body = {
1351
+ previous_signoff_hash: previous,
1352
+ target: exact,
1353
+ decision,
1354
+ ...note_hash ? { note_hash } : {},
1355
+ ...signed
1356
+ };
1357
+ const out = await this.request(
1358
+ "POST",
1359
+ base,
1360
+ body
1361
+ );
1362
+ const record = out?.signoff;
1363
+ if (!recordMatchesSent(record, exact, decision, previous, signed)) {
1364
+ throw new VerdifaxError(
1365
+ "verdifax: the sign-off the server returned does not carry the signature, approver_key, decision, previous_signoff_hash and target that were sent"
1366
+ );
1367
+ }
1368
+ return record;
1369
+ }
1370
+ /** The sign-offs on one run, oldest first, in the form
1371
+ * `verdifax-verify --signoff FILE` reads: for a member key, a run it
1372
+ * made; with `asOwner`, any run of the account. The latest is the
1373
+ * decision in force. */
1374
+ async getSignOffs(runId, options = {}) {
1375
+ this.requireAPIKey();
1376
+ requirePositiveId(runId, "runId");
1377
+ const base = asOwnerOf(options, "getSignOffs") ? `${ACCOUNT_PATH}/runs` : "/runs";
1378
+ return await this.request(
1379
+ "GET",
1380
+ `${base}/${runId}/signoffs`
1381
+ ) ?? [];
1382
+ }
1383
+ /** The account's whole chain of sign-offs, oldest first, in the form
1384
+ * `verdifax-verify --signoff-history FILE` reads. Needs the owner key. */
1385
+ async getSignOffHistory() {
1386
+ this.requireAPIKey();
1387
+ return await this.request(
1388
+ "GET",
1389
+ `${ACCOUNT_SIGNOFFS_PATH}/history`
1390
+ ) ?? [];
1391
+ }
992
1392
  // ── Runs ──────────────────────────────────────────────────────
993
1393
  async getRun(runId) {
994
1394
  this.requireAPIKey();
@@ -1001,6 +1401,9 @@ var VerdifaxClient = class {
1001
1401
  return this.request("GET", `/runs/${runId}/verify`);
1002
1402
  }
1003
1403
  async listRuns(params = {}) {
1404
+ return this.listRunsAt("/runs", params);
1405
+ }
1406
+ async listRunsAt(base, params) {
1004
1407
  this.requireAPIKey();
1005
1408
  const q = new URLSearchParams();
1006
1409
  if (params.limit && params.limit > 0) {
@@ -1010,7 +1413,7 @@ var VerdifaxClient = class {
1010
1413
  q.set("offset", String(params.offset));
1011
1414
  }
1012
1415
  const qs = q.toString();
1013
- const path = qs ? `/runs?${qs}` : "/runs";
1416
+ const path = qs ? `${base}?${qs}` : base;
1014
1417
  const out = await this.request("GET", path);
1015
1418
  return { runs: out.runs ?? [], total: out.total ?? 0 };
1016
1419
  }
@@ -1048,7 +1451,10 @@ var VerdifaxClient = class {
1048
1451
  * call that never reached Verdifax cannot appear in it. */
1049
1452
  async exportRuns(params = {}) {
1050
1453
  this.requireAPIKey();
1051
- return this.request("GET", this.exportPath(params, "json"));
1454
+ return this.request(
1455
+ "GET",
1456
+ this.exportPath("/runs/export", params, "json")
1457
+ );
1052
1458
  }
1053
1459
  /** The same rows as {@link exportRuns}: the raw CSV text with `total`
1054
1460
  * and `truncated` from the response headers. Check `truncated`: when
@@ -1056,9 +1462,12 @@ var VerdifaxClient = class {
1056
1462
  * does not send those headers, so that a cut-off export cannot pass
1057
1463
  * for a complete one. */
1058
1464
  async exportRunsCSV(params = {}) {
1465
+ return this.exportCSVAt("/runs/export", params);
1466
+ }
1467
+ async exportCSVAt(base, params) {
1059
1468
  this.requireAPIKey();
1060
1469
  const { text, headers } = await this.requestRaw(
1061
- this.exportPath(params, "csv")
1470
+ this.exportPath(base, params, "csv")
1062
1471
  );
1063
1472
  const total = Number.parseInt(headers.get(HEADER_EXPORT_TOTAL) ?? "", 10);
1064
1473
  const truncated = headers.get(HEADER_EXPORT_TRUNCATED);
@@ -1069,13 +1478,13 @@ var VerdifaxClient = class {
1069
1478
  }
1070
1479
  return { csv: text, total, truncated: truncated === "true" };
1071
1480
  }
1072
- exportPath(params, format) {
1481
+ exportPath(base, params, format) {
1073
1482
  const q = new URLSearchParams();
1074
1483
  if (params.from) q.set("from", params.from);
1075
1484
  if (params.to) q.set("to", params.to);
1076
1485
  if (params.job_id) q.set("job_id", params.job_id);
1077
1486
  q.set("format", format);
1078
- return `/runs/export?${q.toString()}`;
1487
+ return `${base}?${q.toString()}`;
1079
1488
  }
1080
1489
  // ── Attestation receipts (auth) ──────────────────────────────
1081
1490
  async listAttestations(params = {}) {
@@ -1213,14 +1622,48 @@ var VerdifaxClient = class {
1213
1622
  return this.request("POST", "/dcae/verify", bundle);
1214
1623
  }
1215
1624
  // ── Admin: API keys (auth) ───────────────────────────────────
1216
- async adminCreateKey(name) {
1625
+ /** Creates a customer key. With `accountId` the key joins that existing
1626
+ * account; without it the key gets an account of its own. A key never
1627
+ * changes account. */
1628
+ async adminCreateKey(name, options = {}) {
1217
1629
  this.requireAPIKey();
1218
1630
  if (!name) {
1219
1631
  throw new VerdifaxError("verdifax: name is required for adminCreateKey");
1220
1632
  }
1221
- return this.request("POST", "/admin/keys", {
1222
- name
1223
- });
1633
+ const body = { name };
1634
+ if (options.accountId !== void 0) {
1635
+ requirePositiveId(options.accountId, "accountId");
1636
+ body.account_id = options.accountId;
1637
+ }
1638
+ return this.request("POST", "/admin/keys", body);
1639
+ }
1640
+ /** Creates an account and its first owner key, whose secret is in the
1641
+ * result this once. The owner key creates the member keys. The account
1642
+ * number is a label Verdifax assigned, not a verified company identity. */
1643
+ async adminCreateAccount(name, options = {}) {
1644
+ this.requireAPIKey();
1645
+ if (typeof name !== "string" || name.trim() === "") {
1646
+ throw new VerdifaxError(
1647
+ "verdifax: name is required for adminCreateAccount"
1648
+ );
1649
+ }
1650
+ const body = { name };
1651
+ if (options.tier !== void 0) body.tier = options.tier;
1652
+ if (options.expiresAt !== void 0) body.expires_at = options.expiresAt;
1653
+ return this.request(
1654
+ "POST",
1655
+ "/admin/accounts",
1656
+ body
1657
+ );
1658
+ }
1659
+ /** Every account, with its key counts. */
1660
+ async adminListAccounts() {
1661
+ this.requireAPIKey();
1662
+ const out = await this.request(
1663
+ "GET",
1664
+ "/admin/accounts"
1665
+ );
1666
+ return out?.accounts ?? [];
1224
1667
  }
1225
1668
  async adminListKeys() {
1226
1669
  this.requireAPIKey();
@@ -1399,6 +1842,57 @@ var VerdifaxClient = class {
1399
1842
  return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
1400
1843
  }
1401
1844
  };
1845
+ function signOffsBase(options, method) {
1846
+ return asOwnerOf(options, method) ? ACCOUNT_SIGNOFFS_PATH : SIGNOFFS_PATH;
1847
+ }
1848
+ function asOwnerOf(options, method) {
1849
+ const asOwner = options?.asOwner;
1850
+ if (asOwner !== void 0 && typeof asOwner !== "boolean") {
1851
+ throw new VerdifaxError(`verdifax: ${method} asOwner must be true or false`);
1852
+ }
1853
+ return asOwner === true;
1854
+ }
1855
+ function signOffTargetOf(target) {
1856
+ if (typeof target !== "object" || target === null || Array.isArray(target)) {
1857
+ throw new VerdifaxError("verdifax: signOff needs a target object");
1858
+ }
1859
+ for (const name of Object.keys(target)) {
1860
+ if (!["kind", "run_id", "digest", "job_id"].includes(name)) {
1861
+ throw new VerdifaxError(`verdifax: signOff target has an unknown field: ${name}`);
1862
+ }
1863
+ }
1864
+ if (!SIGNOFF_TARGET_KINDS.has(target.kind)) {
1865
+ throw new VerdifaxError(
1866
+ "verdifax: signOff target kind must be record, receipt or job"
1867
+ );
1868
+ }
1869
+ requirePositiveId(target.run_id, "target.run_id");
1870
+ validateHex64(target.digest, "target.digest");
1871
+ if (target.kind === "job") {
1872
+ validateJobName(target.job_id, "target.job_id");
1873
+ return {
1874
+ kind: target.kind,
1875
+ run_id: target.run_id,
1876
+ digest: target.digest,
1877
+ job_id: target.job_id
1878
+ };
1879
+ }
1880
+ if (target.job_id !== void 0) {
1881
+ throw new VerdifaxError(
1882
+ "verdifax: signOff target job_id is for a job target only"
1883
+ );
1884
+ }
1885
+ return { kind: target.kind, run_id: target.run_id, digest: target.digest };
1886
+ }
1887
+ function recordMatchesSent(record, target, decision, previous, signed) {
1888
+ const got = record?.target;
1889
+ return typeof record === "object" && record !== null && record.signature === signed.signature && record.approver_key === signed.approver_key && record.decision === decision && record.previous_signoff_hash === previous && typeof got === "object" && got !== null && got.kind === target.kind && got.run_id === target.run_id && got.digest === target.digest && (got.job_id ?? "") === (target.job_id ?? "");
1890
+ }
1891
+ function requirePositiveId(id, name) {
1892
+ if (!Number.isSafeInteger(id) || id <= 0) {
1893
+ throw new VerdifaxError(`verdifax: ${name} must be a whole number > 0`);
1894
+ }
1895
+ }
1402
1896
  function withSignedText(result, sent) {
1403
1897
  return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
1404
1898
  }
@@ -1599,6 +2093,7 @@ export {
1599
2093
  VerdifaxClient,
1600
2094
  VerdifaxError,
1601
2095
  VerdifaxJob,
2096
+ approverKeyProof,
1602
2097
  captureEnvironment,
1603
2098
  cresReceiptHash,
1604
2099
  dcaeClosureHash,
@@ -1613,6 +2108,7 @@ export {
1613
2108
  requestKeyRegistrationBody,
1614
2109
  requestSignaturePreimage,
1615
2110
  requestSignaturePreimageV2,
2111
+ signOffPreimage,
1616
2112
  signRequest,
1617
2113
  signRequestV2,
1618
2114
  validateHex64,