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.js CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  VerdifaxClient: () => VerdifaxClient,
35
35
  VerdifaxError: () => VerdifaxError,
36
36
  VerdifaxJob: () => VerdifaxJob,
37
+ approverKeyProof: () => approverKeyProof,
37
38
  captureEnvironment: () => captureEnvironment,
38
39
  cresReceiptHash: () => cresReceiptHash,
39
40
  dcaeClosureHash: () => dcaeClosureHash,
@@ -48,6 +49,7 @@ __export(index_exports, {
48
49
  requestKeyRegistrationBody: () => requestKeyRegistrationBody,
49
50
  requestSignaturePreimage: () => requestSignaturePreimage,
50
51
  requestSignaturePreimageV2: () => requestSignaturePreimageV2,
52
+ signOffPreimage: () => signOffPreimage,
51
53
  signRequest: () => signRequest,
52
54
  signRequestV2: () => signRequestV2,
53
55
  validateHex64: () => validateHex64,
@@ -389,7 +391,10 @@ var JOB_FORM = "verdifax.job.v1";
389
391
  var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
390
392
  var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
391
393
  var POLICY_ACTIVATION_FORM = "verdifax.policy-activation.v1";
394
+ var SIGNOFF_FORM = "verdifax.signoff.v1";
395
+ var APPROVER_KEY_PROOF_FORM = "verdifax.approver-key.v1";
392
396
  var SIGNING_KEY_BYTES = 32;
397
+ var SPKI_ED25519_HEADER = Buffer.from("302a300506032b6570032100", "hex");
393
398
  var PKCS8_ED25519_HEADER = Buffer.from(
394
399
  "302e020100300506032b657004220420",
395
400
  "hex"
@@ -573,6 +578,71 @@ function policyActivationApproval(signingKey, fields, options = {}) {
573
578
  nonce
574
579
  };
575
580
  }
581
+ function signOffPreimage(fields) {
582
+ const { target } = fields;
583
+ return Buffer.from(
584
+ [
585
+ SIGNOFF_FORM,
586
+ fields.chain_id,
587
+ fields.previous_signoff_hash,
588
+ target.kind,
589
+ String(target.run_id),
590
+ target.digest,
591
+ target.job_id ?? "",
592
+ fields.decision,
593
+ fields.note_hash,
594
+ fields.signed_at,
595
+ fields.nonce
596
+ ].join("\0"),
597
+ "utf8"
598
+ );
599
+ }
600
+ function signOffSignature(approverKey, fields, options = {}) {
601
+ const key = loadSigningKey(approverKey);
602
+ const signed_at = options.signedAt ?? signedAtNow();
603
+ const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
604
+ const preimage = signOffPreimage({ ...fields, signed_at, nonce });
605
+ return {
606
+ approver_key: rawPublicKey((0, import_node_crypto.createPublicKey)(key)).toString("base64"),
607
+ signature: (0, import_node_crypto.sign)(null, preimage, key).toString("base64"),
608
+ signed_at,
609
+ nonce
610
+ };
611
+ }
612
+ function approverKeyProof(approverKey) {
613
+ const key = loadSigningKey(approverKey);
614
+ const public_key = rawPublicKey((0, import_node_crypto.createPublicKey)(key)).toString("base64");
615
+ const proof = (0, import_node_crypto.sign)(
616
+ null,
617
+ Buffer.from(`${APPROVER_KEY_PROOF_FORM}\0${public_key}`, "utf8"),
618
+ key
619
+ ).toString("base64");
620
+ return { public_key, proof };
621
+ }
622
+ function approverKeyProofVerifies(proof) {
623
+ if (typeof proof?.public_key !== "string" || typeof proof?.proof !== "string") {
624
+ return false;
625
+ }
626
+ const raw = Buffer.from(proof.public_key, "base64");
627
+ const signature = Buffer.from(proof.proof, "base64");
628
+ if (raw.length !== 32 || raw.toString("base64") !== proof.public_key || signature.length !== 64 || signature.toString("base64") !== proof.proof) {
629
+ return false;
630
+ }
631
+ const publicKey = (0, import_node_crypto.createPublicKey)({
632
+ key: Buffer.concat([SPKI_ED25519_HEADER, raw]),
633
+ format: "der",
634
+ type: "spki"
635
+ });
636
+ return (0, import_node_crypto.verify)(
637
+ null,
638
+ Buffer.from(`${APPROVER_KEY_PROOF_FORM}\0${proof.public_key}`, "utf8"),
639
+ publicKey,
640
+ signature
641
+ );
642
+ }
643
+ function publicKeyOf(key) {
644
+ return rawPublicKey((0, import_node_crypto.createPublicKey)(key)).toString("base64");
645
+ }
576
646
 
577
647
  // src/job.ts
578
648
  var STEP_OPTIONS = {
@@ -674,9 +744,10 @@ var VerdifaxJob = class {
674
744
  };
675
745
 
676
746
  // src/version.ts
677
- var SDK_VERSION = "1.2.0";
747
+ var SDK_VERSION = "1.4.0";
678
748
 
679
749
  // src/client.ts
750
+ var import_node_crypto2 = require("crypto");
680
751
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
681
752
  var DEFAULT_TIMEOUT_MS = 9e4;
682
753
  var DEFAULT_MAX_RETRIES = 3;
@@ -685,6 +756,16 @@ var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
685
756
  var HEADER_API_KEY = "X-Verdifax-Key";
686
757
  var REQUEST_KEY_PATH = "/keys/request-key";
687
758
  var POLICIES_PATH = "/policies";
759
+ var ACCOUNT_PATH = "/account";
760
+ var ACCOUNT_REQUEST_KEY_PATH = "/account/request-key";
761
+ var ACCOUNT_POLICIES_PATH = "/account/policies";
762
+ var ACCOUNT_CHAIN_ID = /^account-[1-9][0-9]*$/;
763
+ var SIGNOFFS_PATH = "/signoffs";
764
+ var ACCOUNT_SIGNOFFS_PATH = "/account/signoffs";
765
+ var APPROVERS_PATH = "/account/approvers";
766
+ var SIGNOFF_TARGET_KINDS = /* @__PURE__ */ new Set(["record", "receipt", "job"]);
767
+ var SIGNOFF_DECISIONS = /* @__PURE__ */ new Set(["approve", "reject"]);
768
+ var MAX_APPROVER_NAME_BYTES = 200;
688
769
  var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
689
770
  var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
690
771
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
@@ -870,15 +951,18 @@ var VerdifaxClient = class {
870
951
  * that the client holds the private key. From then on the server
871
952
  * refuses a run on this API key without a request signature. */
872
953
  async registerRequestKey() {
954
+ return this.registerRequestKeyAt(REQUEST_KEY_PATH, "registerRequestKey");
955
+ }
956
+ async registerRequestKeyAt(path, method) {
873
957
  this.requireAPIKey();
874
958
  if (!this.signingKey) {
875
959
  throw new VerdifaxError(
876
- "verdifax: registerRequestKey needs the signingKey client option"
960
+ `verdifax: ${method} needs the signingKey client option`
877
961
  );
878
962
  }
879
963
  return this.request(
880
964
  "POST",
881
- REQUEST_KEY_PATH,
965
+ path,
882
966
  requestKeyRegistrationBody(this.signingKey)
883
967
  );
884
968
  }
@@ -917,10 +1001,13 @@ var VerdifaxClient = class {
917
1001
  * unsigned requests, because the server refuses a signed run on an API
918
1002
  * key with no registered key. If the call fails it keeps the key. */
919
1003
  async clearRequestKey() {
1004
+ await this.clearRequestKeyAt(REQUEST_KEY_PATH);
1005
+ }
1006
+ async clearRequestKeyAt(path) {
920
1007
  this.requireAPIKey();
921
1008
  await this.request(
922
1009
  "DELETE",
923
- REQUEST_KEY_PATH,
1010
+ path,
924
1011
  this.signingKey ? { change: requestKeyChangeApproval(this.signingKey, "") } : void 0
925
1012
  );
926
1013
  this.signingKey = void 0;
@@ -932,13 +1019,7 @@ var VerdifaxClient = class {
932
1019
  * not a string: serializing an object here would change the hash.
933
1020
  * Verdifax does not write, review or approve a customer's policy. */
934
1021
  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 });
1022
+ return this.uploadPolicyAt(POLICIES_PATH, document);
942
1023
  }
943
1024
  /** This key's stored documents, with the chain its changes belong to,
944
1025
  * the newest change in it (64 zeros when there is none yet) and the
@@ -949,44 +1030,13 @@ var VerdifaxClient = class {
949
1030
  }
950
1031
  /** One stored document, with its exact text as `document`. */
951
1032
  async getPolicy(documentHash) {
952
- this.requireAPIKey();
953
- validateHex64(documentHash, "documentHash");
954
- return this.request(
955
- "GET",
956
- `${POLICIES_PATH}/${documentHash}`
957
- );
1033
+ return this.getPolicyAt(POLICIES_PATH, documentHash);
958
1034
  }
959
1035
  /** Shows which rule would fire on sample inputs, for a document given as
960
1036
  * text or one already stored, named by its hash: exactly one of the two.
961
1037
  * It seals nothing. */
962
1038
  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
- );
1039
+ return this.testPolicyAt(POLICIES_PATH, req, "testPolicy");
990
1040
  }
991
1041
  /** This key's policy changes, oldest first, as the list the server sends.
992
1042
  * Save it as JSON, for example with
@@ -1012,7 +1062,54 @@ var VerdifaxClient = class {
1012
1062
  async activatePolicy(documentHash) {
1013
1063
  this.requireAPIKey();
1014
1064
  validateHex64(documentHash, "documentHash");
1015
- const chain = await this.listPolicies();
1065
+ return this.activatePolicyAt(
1066
+ POLICIES_PATH,
1067
+ documentHash,
1068
+ await this.listPolicies()
1069
+ );
1070
+ }
1071
+ async uploadPolicyAt(base, document) {
1072
+ this.requireAPIKey();
1073
+ if (typeof document !== "string") {
1074
+ throw new VerdifaxError(
1075
+ "verdifax: document must be the policy's exact text, as a string"
1076
+ );
1077
+ }
1078
+ return this.request("POST", base, { document });
1079
+ }
1080
+ async getPolicyAt(base, documentHash) {
1081
+ this.requireAPIKey();
1082
+ validateHex64(documentHash, "documentHash");
1083
+ return this.request("GET", `${base}/${documentHash}`);
1084
+ }
1085
+ async testPolicyAt(base, req, method) {
1086
+ this.requireAPIKey();
1087
+ const { inputs, document, documentHash } = req ?? {};
1088
+ if (document === void 0 === (documentHash === void 0)) {
1089
+ throw new VerdifaxError(
1090
+ `verdifax: ${method} needs exactly one of document or documentHash`
1091
+ );
1092
+ }
1093
+ if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) {
1094
+ throw new VerdifaxError(`verdifax: ${method} needs inputs as an object`);
1095
+ }
1096
+ const body = { inputs };
1097
+ if (document !== void 0) {
1098
+ if (typeof document !== "string") {
1099
+ throw new VerdifaxError(
1100
+ "verdifax: document must be the policy's exact text, as a string"
1101
+ );
1102
+ }
1103
+ body.document = document;
1104
+ } else {
1105
+ validateHex64(documentHash, "documentHash");
1106
+ body.document_hash = documentHash;
1107
+ }
1108
+ return this.request("POST", `${base}/test`, body);
1109
+ }
1110
+ /** Posts the change that follows the newest one in `chain`, signed by
1111
+ * the request key when the client has one. */
1112
+ async activatePolicyAt(base, documentHash, chain) {
1016
1113
  const previous = chain?.newest_activation_hash;
1017
1114
  if (typeof previous !== "string") {
1018
1115
  throw new VerdifaxError(
@@ -1034,11 +1131,315 @@ var VerdifaxClient = class {
1034
1131
  }
1035
1132
  const out = await this.request(
1036
1133
  "POST",
1037
- `${POLICIES_PATH}/${documentHash}/activate`,
1134
+ `${base}/${documentHash}/activate`,
1038
1135
  body
1039
1136
  );
1040
1137
  return out.activation;
1041
1138
  }
1139
+ // ── Account, with its owner key (auth) ───────────────────────
1140
+ //
1141
+ // An account is a company on Verdifax. Every key belongs to one account
1142
+ // for life. These methods need the account's owner key, which manages the
1143
+ // account and cannot run anything: the server refuses it on every other
1144
+ // route. The account number is a label Verdifax assigned, not a verified
1145
+ // company identity.
1146
+ /** The owner's account: its number, name, tier, key counts and the
1147
+ * chain of its policy changes. */
1148
+ async getAccount() {
1149
+ this.requireAPIKey();
1150
+ return this.request("GET", ACCOUNT_PATH);
1151
+ }
1152
+ /** Creates a member key in the account. It takes the account's tier and
1153
+ * stays in the account for life. The secret is in the result this once.
1154
+ * On a trial account the key needs a request key before it can run. */
1155
+ async createAccountKey(name, options = {}) {
1156
+ this.requireAPIKey();
1157
+ if (typeof name !== "string" || name.trim() === "") {
1158
+ throw new VerdifaxError(
1159
+ "verdifax: name is required for createAccountKey"
1160
+ );
1161
+ }
1162
+ const body = { name };
1163
+ if (options.expiresAt !== void 0) body.expires_at = options.expiresAt;
1164
+ return this.request(
1165
+ "POST",
1166
+ `${ACCOUNT_PATH}/keys`,
1167
+ body
1168
+ );
1169
+ }
1170
+ /** Every key in the account, owner keys included, without secrets. */
1171
+ async listAccountKeys() {
1172
+ this.requireAPIKey();
1173
+ const out = await this.request(
1174
+ "GET",
1175
+ `${ACCOUNT_PATH}/keys`
1176
+ );
1177
+ return { ...out, keys: out?.keys ?? [] };
1178
+ }
1179
+ /** Revokes one of the account's member keys. A key in another account,
1180
+ * or an owner key, throws {@link NotFoundError}. */
1181
+ async revokeAccountKey(keyId) {
1182
+ this.requireAPIKey();
1183
+ requirePositiveId(keyId, "keyId");
1184
+ return this.request(
1185
+ "DELETE",
1186
+ `${ACCOUNT_PATH}/keys/${keyId}`
1187
+ );
1188
+ }
1189
+ /** Registers this client's public key as the owner key's request key,
1190
+ * which must sign every change to the account's policy. It works as
1191
+ * {@link registerRequestKey} does. */
1192
+ async registerAccountRequestKey() {
1193
+ return this.registerRequestKeyAt(
1194
+ ACCOUNT_REQUEST_KEY_PATH,
1195
+ "registerAccountRequestKey"
1196
+ );
1197
+ }
1198
+ /** Clears the owner key's request key, as {@link clearRequestKey} does
1199
+ * for a key's own. With none registered the account's policy cannot be
1200
+ * changed. A trial owner key's request key cannot be cleared. */
1201
+ async clearAccountRequestKey() {
1202
+ await this.clearRequestKeyAt(ACCOUNT_REQUEST_KEY_PATH);
1203
+ }
1204
+ /** The runs of every key in the account, paged as {@link listRuns}. */
1205
+ async listAccountRuns(params = {}) {
1206
+ return this.listRunsAt(`${ACCOUNT_PATH}/runs`, params);
1207
+ }
1208
+ /** The runs of every key in the account in a period, as
1209
+ * {@link exportRuns} lists a key's, with the account's number, its
1210
+ * policy documents and its chain of changes. The account's view covers
1211
+ * the runs its keys made through Verdifax, not runs made elsewhere. */
1212
+ async exportAccountRuns(params = {}) {
1213
+ this.requireAPIKey();
1214
+ return this.request(
1215
+ "GET",
1216
+ this.exportPath(`${ACCOUNT_PATH}/runs/export`, params, "json")
1217
+ );
1218
+ }
1219
+ /** The same rows as {@link exportAccountRuns} as raw CSV text, checked
1220
+ * as {@link exportRunsCSV} checks a key's. */
1221
+ async exportAccountRunsCSV(params = {}) {
1222
+ return this.exportCSVAt(`${ACCOUNT_PATH}/runs/export`, params);
1223
+ }
1224
+ /** Stores an account policy document, as {@link uploadPolicy} stores a
1225
+ * key's. Every key in the account must pass the account's policy on top
1226
+ * of its own. Verdifax does not write, review or approve it. */
1227
+ async uploadAccountPolicy(document) {
1228
+ return this.uploadPolicyAt(ACCOUNT_POLICIES_PATH, document);
1229
+ }
1230
+ /** The account's stored documents, its chain `account-N`, the newest
1231
+ * change in it and the document in force. */
1232
+ async listAccountPolicies() {
1233
+ this.requireAPIKey();
1234
+ return this.request("GET", ACCOUNT_POLICIES_PATH);
1235
+ }
1236
+ /** One of the account's documents, with its exact text. */
1237
+ async getAccountPolicy(documentHash) {
1238
+ return this.getPolicyAt(ACCOUNT_POLICIES_PATH, documentHash);
1239
+ }
1240
+ /** {@link testPolicy} for the account's documents. It seals nothing. */
1241
+ async testAccountPolicy(req) {
1242
+ return this.testPolicyAt(ACCOUNT_POLICIES_PATH, req, "testAccountPolicy");
1243
+ }
1244
+ /** The account's policy changes, oldest first, in the form
1245
+ * `verdifax-verify --policy-history FILE` reads. */
1246
+ async getAccountPolicyHistory() {
1247
+ this.requireAPIKey();
1248
+ return this.request(
1249
+ "GET",
1250
+ `${ACCOUNT_POLICIES_PATH}/history`
1251
+ );
1252
+ }
1253
+ /** Makes a stored document the account's policy in force, as
1254
+ * {@link activatePolicy} does for a key's. It reads
1255
+ * {@link listAccountPolicies} for the chain and its newest change, and
1256
+ * the owner key's request key signs the change over that chain,
1257
+ * `account-N`. The server refuses an unsigned account change, so this
1258
+ * needs the `signingKey` option and refuses to send without it. */
1259
+ async activateAccountPolicy(documentHash) {
1260
+ this.requireAPIKey();
1261
+ validateHex64(documentHash, "documentHash");
1262
+ if (!this.signingKey) {
1263
+ throw new VerdifaxError(
1264
+ "verdifax: activateAccountPolicy needs the owner key's request key in the signingKey client option, because the server refuses an unsigned account policy change"
1265
+ );
1266
+ }
1267
+ const chain = await this.listAccountPolicies();
1268
+ if (typeof chain?.chain_id !== "string" || !ACCOUNT_CHAIN_ID.test(chain.chain_id)) {
1269
+ throw new VerdifaxError(
1270
+ "verdifax: the server did not report an account chain_id (account-N), so the change cannot be signed"
1271
+ );
1272
+ }
1273
+ return this.activatePolicyAt(ACCOUNT_POLICIES_PATH, documentHash, chain);
1274
+ }
1275
+ // ── Sign-off ─────────────────────────────────────────────────
1276
+ //
1277
+ // An account's owner registers approver keys. An approver signs a
1278
+ // decision, approve or reject, on one exact result on their own machine,
1279
+ // and any key in the account, or the owner key with asOwner, submits it.
1280
+ // A sign-off shows that the holder of a registered approver key signed
1281
+ // that decision on that exact result and that Verdifax logged it on the
1282
+ // public log. It does not show the review was careful, the result right,
1283
+ // or who the person is, and it gates nothing on its own.
1284
+ /** Registers an approver key to the owner's account. `approver` is the
1285
+ * approver's signing key, from which the proof is made on this machine,
1286
+ * or the `{ public_key, proof }` that {@link approverKeyProof} made on
1287
+ * the approver's own machine, so the owner need not hold the private
1288
+ * key. A proof that does not verify is refused before sending. The
1289
+ * server refuses a key that is a request key in the same account.
1290
+ * `name` is a label, not an identity. */
1291
+ async registerApprover(name, approver) {
1292
+ this.requireAPIKey();
1293
+ if (typeof name !== "string" || name.trim() === "" || Buffer.byteLength(name.trim(), "utf8") > MAX_APPROVER_NAME_BYTES) {
1294
+ throw new VerdifaxError(
1295
+ "verdifax: registerApprover needs a name of at most 200 bytes"
1296
+ );
1297
+ }
1298
+ const body = approver instanceof Uint8Array || approver instanceof import_node_crypto2.KeyObject ? approverKeyProof(approver) : { public_key: approver?.public_key, proof: approver?.proof };
1299
+ if (!approverKeyProofVerifies(body)) {
1300
+ throw new VerdifaxError(
1301
+ "verdifax: registerApprover needs the approver's signing key, or a public_key and proof from approverKeyProof that verify"
1302
+ );
1303
+ }
1304
+ if (this.signingKey && publicKeyOf(this.signingKey) === body.public_key) {
1305
+ throw new VerdifaxError(
1306
+ "verdifax: an approver key must not be a request key in the same account, and this is the client's signingKey"
1307
+ );
1308
+ }
1309
+ return this.request("POST", APPROVERS_PATH, {
1310
+ public_key: body.public_key,
1311
+ name,
1312
+ proof: body.proof
1313
+ });
1314
+ }
1315
+ /** The account's approver keys, revoked ones included. */
1316
+ async listApprovers() {
1317
+ this.requireAPIKey();
1318
+ const out = await this.request("GET", APPROVERS_PATH);
1319
+ return { ...out, approvers: out?.approvers ?? [] };
1320
+ }
1321
+ /** Revokes one of the account's approver keys. Its earlier sign-offs
1322
+ * stay on the record. A key that is not an active approver in the
1323
+ * account throws {@link NotFoundError}. */
1324
+ async revokeApprover(approverId) {
1325
+ this.requireAPIKey();
1326
+ requirePositiveId(approverId, "approverId");
1327
+ return this.request(
1328
+ "DELETE",
1329
+ `${APPROVERS_PATH}/${approverId}`
1330
+ );
1331
+ }
1332
+ /** What an approver signs against: the account's chain `account-N` and
1333
+ * its newest sign-off (64 zeros when there is none yet). With
1334
+ * `asOwner` it reads the owner key's route. */
1335
+ async getNewestSignOff(options = {}) {
1336
+ this.requireAPIKey();
1337
+ return this.request(
1338
+ "GET",
1339
+ `${signOffsBase(options, "getNewestSignOff")}/newest`
1340
+ );
1341
+ }
1342
+ /** Signs an approver's decision, approve or reject, on one exact result
1343
+ * and submits it. It first reads {@link getNewestSignOff} for the
1344
+ * account's chain and its newest sign-off, then signs over them with
1345
+ * `approverKey` on this machine. The approver's private key is never
1346
+ * sent.
1347
+ *
1348
+ * `target.digest` is what the result's public-log entry covers: for a
1349
+ * record, the `bundle_anchor.anchored_digest` of the bundle
1350
+ * {@link getRunArtifacts} returns; for a receipt, its hash; for a job,
1351
+ * the final step's record digest, read as a record's, with `job_id`.
1352
+ *
1353
+ * `note` is hashed here and only its SHA-256 is sent and signed: the
1354
+ * text never reaches Verdifax. With `asOwner` it uses the owner key's
1355
+ * routes. Bad input is refused before anything is sent.
1356
+ *
1357
+ * The record the server returns must carry the signature,
1358
+ * approver_key, decision, previous_signoff_hash and target that were
1359
+ * sent, or this throws {@link VerdifaxError}.
1360
+ *
1361
+ * A later sign-off on the same result supersedes this one, and neither
1362
+ * is deleted. An HTTP 409 means the chain moved or the target is not
1363
+ * this account's result: it throws the usual {@link APIError} and is
1364
+ * not retried. */
1365
+ async signOff(target, decision, approverKey, options = {}) {
1366
+ this.requireAPIKey();
1367
+ const base = signOffsBase(options, "signOff");
1368
+ const exact = signOffTargetOf(target);
1369
+ if (!SIGNOFF_DECISIONS.has(decision)) {
1370
+ throw new VerdifaxError(
1371
+ "verdifax: signOff decision must be approve or reject"
1372
+ );
1373
+ }
1374
+ const note = options?.note;
1375
+ if (note !== void 0 && (typeof note !== "string" || note === "")) {
1376
+ throw new VerdifaxError(
1377
+ "verdifax: signOff note must be non-empty text when given"
1378
+ );
1379
+ }
1380
+ const key = loadSigningKey(approverKey);
1381
+ const note_hash = note === void 0 ? "" : sha256Hex(note);
1382
+ const newest = await this.getNewestSignOff(options);
1383
+ if (typeof newest?.chain_id !== "string" || !ACCOUNT_CHAIN_ID.test(newest.chain_id)) {
1384
+ throw new VerdifaxError(
1385
+ "verdifax: the server did not report an account chain_id (account-N), so the sign-off cannot be signed"
1386
+ );
1387
+ }
1388
+ const previous = newest.newest_signoff_hash;
1389
+ if (typeof previous !== "string" || !/^[0-9a-f]{64}$/.test(previous)) {
1390
+ throw new VerdifaxError(
1391
+ "verdifax: the server did not report newest_signoff_hash, so the sign-off cannot name the one it follows"
1392
+ );
1393
+ }
1394
+ const signed = signOffSignature(key, {
1395
+ chain_id: newest.chain_id,
1396
+ previous_signoff_hash: previous,
1397
+ target: exact,
1398
+ decision,
1399
+ note_hash
1400
+ });
1401
+ const body = {
1402
+ previous_signoff_hash: previous,
1403
+ target: exact,
1404
+ decision,
1405
+ ...note_hash ? { note_hash } : {},
1406
+ ...signed
1407
+ };
1408
+ const out = await this.request(
1409
+ "POST",
1410
+ base,
1411
+ body
1412
+ );
1413
+ const record = out?.signoff;
1414
+ if (!recordMatchesSent(record, exact, decision, previous, signed)) {
1415
+ throw new VerdifaxError(
1416
+ "verdifax: the sign-off the server returned does not carry the signature, approver_key, decision, previous_signoff_hash and target that were sent"
1417
+ );
1418
+ }
1419
+ return record;
1420
+ }
1421
+ /** The sign-offs on one run, oldest first, in the form
1422
+ * `verdifax-verify --signoff FILE` reads: for a member key, a run it
1423
+ * made; with `asOwner`, any run of the account. The latest is the
1424
+ * decision in force. */
1425
+ async getSignOffs(runId, options = {}) {
1426
+ this.requireAPIKey();
1427
+ requirePositiveId(runId, "runId");
1428
+ const base = asOwnerOf(options, "getSignOffs") ? `${ACCOUNT_PATH}/runs` : "/runs";
1429
+ return await this.request(
1430
+ "GET",
1431
+ `${base}/${runId}/signoffs`
1432
+ ) ?? [];
1433
+ }
1434
+ /** The account's whole chain of sign-offs, oldest first, in the form
1435
+ * `verdifax-verify --signoff-history FILE` reads. Needs the owner key. */
1436
+ async getSignOffHistory() {
1437
+ this.requireAPIKey();
1438
+ return await this.request(
1439
+ "GET",
1440
+ `${ACCOUNT_SIGNOFFS_PATH}/history`
1441
+ ) ?? [];
1442
+ }
1042
1443
  // ── Runs ──────────────────────────────────────────────────────
1043
1444
  async getRun(runId) {
1044
1445
  this.requireAPIKey();
@@ -1051,6 +1452,9 @@ var VerdifaxClient = class {
1051
1452
  return this.request("GET", `/runs/${runId}/verify`);
1052
1453
  }
1053
1454
  async listRuns(params = {}) {
1455
+ return this.listRunsAt("/runs", params);
1456
+ }
1457
+ async listRunsAt(base, params) {
1054
1458
  this.requireAPIKey();
1055
1459
  const q = new URLSearchParams();
1056
1460
  if (params.limit && params.limit > 0) {
@@ -1060,7 +1464,7 @@ var VerdifaxClient = class {
1060
1464
  q.set("offset", String(params.offset));
1061
1465
  }
1062
1466
  const qs = q.toString();
1063
- const path = qs ? `/runs?${qs}` : "/runs";
1467
+ const path = qs ? `${base}?${qs}` : base;
1064
1468
  const out = await this.request("GET", path);
1065
1469
  return { runs: out.runs ?? [], total: out.total ?? 0 };
1066
1470
  }
@@ -1098,7 +1502,10 @@ var VerdifaxClient = class {
1098
1502
  * call that never reached Verdifax cannot appear in it. */
1099
1503
  async exportRuns(params = {}) {
1100
1504
  this.requireAPIKey();
1101
- return this.request("GET", this.exportPath(params, "json"));
1505
+ return this.request(
1506
+ "GET",
1507
+ this.exportPath("/runs/export", params, "json")
1508
+ );
1102
1509
  }
1103
1510
  /** The same rows as {@link exportRuns}: the raw CSV text with `total`
1104
1511
  * and `truncated` from the response headers. Check `truncated`: when
@@ -1106,9 +1513,12 @@ var VerdifaxClient = class {
1106
1513
  * does not send those headers, so that a cut-off export cannot pass
1107
1514
  * for a complete one. */
1108
1515
  async exportRunsCSV(params = {}) {
1516
+ return this.exportCSVAt("/runs/export", params);
1517
+ }
1518
+ async exportCSVAt(base, params) {
1109
1519
  this.requireAPIKey();
1110
1520
  const { text, headers } = await this.requestRaw(
1111
- this.exportPath(params, "csv")
1521
+ this.exportPath(base, params, "csv")
1112
1522
  );
1113
1523
  const total = Number.parseInt(headers.get(HEADER_EXPORT_TOTAL) ?? "", 10);
1114
1524
  const truncated = headers.get(HEADER_EXPORT_TRUNCATED);
@@ -1119,13 +1529,13 @@ var VerdifaxClient = class {
1119
1529
  }
1120
1530
  return { csv: text, total, truncated: truncated === "true" };
1121
1531
  }
1122
- exportPath(params, format) {
1532
+ exportPath(base, params, format) {
1123
1533
  const q = new URLSearchParams();
1124
1534
  if (params.from) q.set("from", params.from);
1125
1535
  if (params.to) q.set("to", params.to);
1126
1536
  if (params.job_id) q.set("job_id", params.job_id);
1127
1537
  q.set("format", format);
1128
- return `/runs/export?${q.toString()}`;
1538
+ return `${base}?${q.toString()}`;
1129
1539
  }
1130
1540
  // ── Attestation receipts (auth) ──────────────────────────────
1131
1541
  async listAttestations(params = {}) {
@@ -1263,14 +1673,48 @@ var VerdifaxClient = class {
1263
1673
  return this.request("POST", "/dcae/verify", bundle);
1264
1674
  }
1265
1675
  // ── Admin: API keys (auth) ───────────────────────────────────
1266
- async adminCreateKey(name) {
1676
+ /** Creates a customer key. With `accountId` the key joins that existing
1677
+ * account; without it the key gets an account of its own. A key never
1678
+ * changes account. */
1679
+ async adminCreateKey(name, options = {}) {
1267
1680
  this.requireAPIKey();
1268
1681
  if (!name) {
1269
1682
  throw new VerdifaxError("verdifax: name is required for adminCreateKey");
1270
1683
  }
1271
- return this.request("POST", "/admin/keys", {
1272
- name
1273
- });
1684
+ const body = { name };
1685
+ if (options.accountId !== void 0) {
1686
+ requirePositiveId(options.accountId, "accountId");
1687
+ body.account_id = options.accountId;
1688
+ }
1689
+ return this.request("POST", "/admin/keys", body);
1690
+ }
1691
+ /** Creates an account and its first owner key, whose secret is in the
1692
+ * result this once. The owner key creates the member keys. The account
1693
+ * number is a label Verdifax assigned, not a verified company identity. */
1694
+ async adminCreateAccount(name, options = {}) {
1695
+ this.requireAPIKey();
1696
+ if (typeof name !== "string" || name.trim() === "") {
1697
+ throw new VerdifaxError(
1698
+ "verdifax: name is required for adminCreateAccount"
1699
+ );
1700
+ }
1701
+ const body = { name };
1702
+ if (options.tier !== void 0) body.tier = options.tier;
1703
+ if (options.expiresAt !== void 0) body.expires_at = options.expiresAt;
1704
+ return this.request(
1705
+ "POST",
1706
+ "/admin/accounts",
1707
+ body
1708
+ );
1709
+ }
1710
+ /** Every account, with its key counts. */
1711
+ async adminListAccounts() {
1712
+ this.requireAPIKey();
1713
+ const out = await this.request(
1714
+ "GET",
1715
+ "/admin/accounts"
1716
+ );
1717
+ return out?.accounts ?? [];
1274
1718
  }
1275
1719
  async adminListKeys() {
1276
1720
  this.requireAPIKey();
@@ -1449,6 +1893,57 @@ var VerdifaxClient = class {
1449
1893
  return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
1450
1894
  }
1451
1895
  };
1896
+ function signOffsBase(options, method) {
1897
+ return asOwnerOf(options, method) ? ACCOUNT_SIGNOFFS_PATH : SIGNOFFS_PATH;
1898
+ }
1899
+ function asOwnerOf(options, method) {
1900
+ const asOwner = options?.asOwner;
1901
+ if (asOwner !== void 0 && typeof asOwner !== "boolean") {
1902
+ throw new VerdifaxError(`verdifax: ${method} asOwner must be true or false`);
1903
+ }
1904
+ return asOwner === true;
1905
+ }
1906
+ function signOffTargetOf(target) {
1907
+ if (typeof target !== "object" || target === null || Array.isArray(target)) {
1908
+ throw new VerdifaxError("verdifax: signOff needs a target object");
1909
+ }
1910
+ for (const name of Object.keys(target)) {
1911
+ if (!["kind", "run_id", "digest", "job_id"].includes(name)) {
1912
+ throw new VerdifaxError(`verdifax: signOff target has an unknown field: ${name}`);
1913
+ }
1914
+ }
1915
+ if (!SIGNOFF_TARGET_KINDS.has(target.kind)) {
1916
+ throw new VerdifaxError(
1917
+ "verdifax: signOff target kind must be record, receipt or job"
1918
+ );
1919
+ }
1920
+ requirePositiveId(target.run_id, "target.run_id");
1921
+ validateHex64(target.digest, "target.digest");
1922
+ if (target.kind === "job") {
1923
+ validateJobName(target.job_id, "target.job_id");
1924
+ return {
1925
+ kind: target.kind,
1926
+ run_id: target.run_id,
1927
+ digest: target.digest,
1928
+ job_id: target.job_id
1929
+ };
1930
+ }
1931
+ if (target.job_id !== void 0) {
1932
+ throw new VerdifaxError(
1933
+ "verdifax: signOff target job_id is for a job target only"
1934
+ );
1935
+ }
1936
+ return { kind: target.kind, run_id: target.run_id, digest: target.digest };
1937
+ }
1938
+ function recordMatchesSent(record, target, decision, previous, signed) {
1939
+ const got = record?.target;
1940
+ 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 ?? "");
1941
+ }
1942
+ function requirePositiveId(id, name) {
1943
+ if (!Number.isSafeInteger(id) || id <= 0) {
1944
+ throw new VerdifaxError(`verdifax: ${name} must be a whole number > 0`);
1945
+ }
1946
+ }
1452
1947
  function withSignedText(result, sent) {
1453
1948
  return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
1454
1949
  }
@@ -1574,13 +2069,13 @@ var RUN_STATUSES = [
1574
2069
  ];
1575
2070
 
1576
2071
  // src/section0.ts
1577
- var import_node_crypto2 = require("crypto");
2072
+ var import_node_crypto3 = require("crypto");
1578
2073
  var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
1579
2074
  var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
1580
2075
  var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
1581
2076
  var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
1582
2077
  function sha256Hex2(preimage) {
1583
- return (0, import_node_crypto2.createHash)("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
2078
+ return (0, import_node_crypto3.createHash)("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
1584
2079
  }
1585
2080
  function requireHex64(value, name) {
1586
2081
  try {
@@ -1650,6 +2145,7 @@ function cresReceiptHash(fields) {
1650
2145
  VerdifaxClient,
1651
2146
  VerdifaxError,
1652
2147
  VerdifaxJob,
2148
+ approverKeyProof,
1653
2149
  captureEnvironment,
1654
2150
  cresReceiptHash,
1655
2151
  dcaeClosureHash,
@@ -1664,6 +2160,7 @@ function cresReceiptHash(fields) {
1664
2160
  requestKeyRegistrationBody,
1665
2161
  requestSignaturePreimage,
1666
2162
  requestSignaturePreimageV2,
2163
+ signOffPreimage,
1667
2164
  signRequest,
1668
2165
  signRequestV2,
1669
2166
  validateHex64,