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/README.md +121 -3
- package/dist/index.d.mts +415 -4
- package/dist/index.d.ts +415 -4
- package/dist/index.js +554 -57
- package/dist/index.mjs +552 -56
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
909
|
+
`verdifax: ${method} needs the signingKey client option`
|
|
827
910
|
);
|
|
828
911
|
}
|
|
829
912
|
return this.request(
|
|
830
913
|
"POST",
|
|
831
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
`${
|
|
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 ?
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1222
|
-
|
|
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,
|