verdifax 1.3.0 → 1.5.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 +133 -1
- package/dist/index.d.mts +479 -1
- package/dist/index.d.ts +479 -1
- package/dist/index.js +741 -10
- package/dist/index.mjs +737 -10
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -48,6 +48,17 @@ var APIKeyRequiredError = class extends VerdifaxError {
|
|
|
48
48
|
super("verdifax: apiKey is required for this endpoint");
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
+
var SignalFeedError = class extends VerdifaxError {
|
|
52
|
+
constructor(lastSequence, reason, options) {
|
|
53
|
+
super(
|
|
54
|
+
`verdifax: the signal feed stopped after sequence ${lastSequence}: ${reason}; follow again with after: ${lastSequence}`,
|
|
55
|
+
options
|
|
56
|
+
);
|
|
57
|
+
this.lastSequence = lastSequence;
|
|
58
|
+
}
|
|
59
|
+
lastSequence;
|
|
60
|
+
name = "VerdifaxSignalFeedError";
|
|
61
|
+
};
|
|
51
62
|
|
|
52
63
|
// src/signing.ts
|
|
53
64
|
import {
|
|
@@ -57,7 +68,8 @@ import {
|
|
|
57
68
|
generateKeyPairSync,
|
|
58
69
|
KeyObject,
|
|
59
70
|
randomBytes,
|
|
60
|
-
sign
|
|
71
|
+
sign,
|
|
72
|
+
verify
|
|
61
73
|
} from "crypto";
|
|
62
74
|
|
|
63
75
|
// src/validation.ts
|
|
@@ -95,6 +107,18 @@ function validateJobName(value, fieldName) {
|
|
|
95
107
|
);
|
|
96
108
|
}
|
|
97
109
|
}
|
|
110
|
+
function requirePositiveId(id, name) {
|
|
111
|
+
if (!Number.isSafeInteger(id) || id <= 0) {
|
|
112
|
+
throw new VerdifaxError(`verdifax: ${name} must be a whole number > 0`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function asOwnerOf(options, method) {
|
|
116
|
+
const asOwner = options?.asOwner;
|
|
117
|
+
if (asOwner !== void 0 && typeof asOwner !== "boolean") {
|
|
118
|
+
throw new VerdifaxError(`verdifax: ${method} asOwner must be true or false`);
|
|
119
|
+
}
|
|
120
|
+
return asOwner === true;
|
|
121
|
+
}
|
|
98
122
|
|
|
99
123
|
// src/helpers.ts
|
|
100
124
|
function formatProviderPayload(provider, prompt, response) {
|
|
@@ -339,7 +363,10 @@ var JOB_FORM = "verdifax.job.v1";
|
|
|
339
363
|
var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
|
|
340
364
|
var REQUEST_KEY_CHANGE_FORM = "verdifax.request-key-change.v1";
|
|
341
365
|
var POLICY_ACTIVATION_FORM = "verdifax.policy-activation.v1";
|
|
366
|
+
var SIGNOFF_FORM = "verdifax.signoff.v1";
|
|
367
|
+
var APPROVER_KEY_PROOF_FORM = "verdifax.approver-key.v1";
|
|
342
368
|
var SIGNING_KEY_BYTES = 32;
|
|
369
|
+
var SPKI_ED25519_HEADER = Buffer.from("302a300506032b6570032100", "hex");
|
|
343
370
|
var PKCS8_ED25519_HEADER = Buffer.from(
|
|
344
371
|
"302e020100300506032b657004220420",
|
|
345
372
|
"hex"
|
|
@@ -523,6 +550,71 @@ function policyActivationApproval(signingKey, fields, options = {}) {
|
|
|
523
550
|
nonce
|
|
524
551
|
};
|
|
525
552
|
}
|
|
553
|
+
function signOffPreimage(fields) {
|
|
554
|
+
const { target } = fields;
|
|
555
|
+
return Buffer.from(
|
|
556
|
+
[
|
|
557
|
+
SIGNOFF_FORM,
|
|
558
|
+
fields.chain_id,
|
|
559
|
+
fields.previous_signoff_hash,
|
|
560
|
+
target.kind,
|
|
561
|
+
String(target.run_id),
|
|
562
|
+
target.digest,
|
|
563
|
+
target.job_id ?? "",
|
|
564
|
+
fields.decision,
|
|
565
|
+
fields.note_hash,
|
|
566
|
+
fields.signed_at,
|
|
567
|
+
fields.nonce
|
|
568
|
+
].join("\0"),
|
|
569
|
+
"utf8"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
function signOffSignature(approverKey, fields, options = {}) {
|
|
573
|
+
const key = loadSigningKey(approverKey);
|
|
574
|
+
const signed_at = options.signedAt ?? signedAtNow();
|
|
575
|
+
const nonce = options.nonce ?? randomBytes(16).toString("hex");
|
|
576
|
+
const preimage = signOffPreimage({ ...fields, signed_at, nonce });
|
|
577
|
+
return {
|
|
578
|
+
approver_key: rawPublicKey(createPublicKey(key)).toString("base64"),
|
|
579
|
+
signature: sign(null, preimage, key).toString("base64"),
|
|
580
|
+
signed_at,
|
|
581
|
+
nonce
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function approverKeyProof(approverKey) {
|
|
585
|
+
const key = loadSigningKey(approverKey);
|
|
586
|
+
const public_key = rawPublicKey(createPublicKey(key)).toString("base64");
|
|
587
|
+
const proof = sign(
|
|
588
|
+
null,
|
|
589
|
+
Buffer.from(`${APPROVER_KEY_PROOF_FORM}\0${public_key}`, "utf8"),
|
|
590
|
+
key
|
|
591
|
+
).toString("base64");
|
|
592
|
+
return { public_key, proof };
|
|
593
|
+
}
|
|
594
|
+
function approverKeyProofVerifies(proof) {
|
|
595
|
+
if (typeof proof?.public_key !== "string" || typeof proof?.proof !== "string") {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
const raw = Buffer.from(proof.public_key, "base64");
|
|
599
|
+
const signature = Buffer.from(proof.proof, "base64");
|
|
600
|
+
if (raw.length !== 32 || raw.toString("base64") !== proof.public_key || signature.length !== 64 || signature.toString("base64") !== proof.proof) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
const publicKey = createPublicKey({
|
|
604
|
+
key: Buffer.concat([SPKI_ED25519_HEADER, raw]),
|
|
605
|
+
format: "der",
|
|
606
|
+
type: "spki"
|
|
607
|
+
});
|
|
608
|
+
return verify(
|
|
609
|
+
null,
|
|
610
|
+
Buffer.from(`${APPROVER_KEY_PROOF_FORM}\0${proof.public_key}`, "utf8"),
|
|
611
|
+
publicKey,
|
|
612
|
+
signature
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
function publicKeyOf(key) {
|
|
616
|
+
return rawPublicKey(createPublicKey(key)).toString("base64");
|
|
617
|
+
}
|
|
526
618
|
|
|
527
619
|
// src/job.ts
|
|
528
620
|
var STEP_OPTIONS = {
|
|
@@ -623,10 +715,327 @@ var VerdifaxJob = class {
|
|
|
623
715
|
}
|
|
624
716
|
};
|
|
625
717
|
|
|
718
|
+
// src/signals.ts
|
|
719
|
+
import { createHash as createHash2, createHmac, timingSafeEqual } from "crypto";
|
|
720
|
+
var SIGNAL_FORM = "verdifax.signal.v1";
|
|
721
|
+
var SIGNALS_PATH = "/signals";
|
|
722
|
+
var ACCOUNT_SIGNALS_PATH = "/account/signals";
|
|
723
|
+
var FEED_KEYS_PATH = "/account/feed-keys";
|
|
724
|
+
var WEBHOOKS_PATH = "/account/webhooks";
|
|
725
|
+
var MAX_SIGNAL_LIMIT = 500;
|
|
726
|
+
var MAX_SIGNAL_WAIT_SECONDS = 25;
|
|
727
|
+
var DEFAULT_FOLLOW_WAIT_SECONDS = 25;
|
|
728
|
+
var SIGNAL_TIMEOUT_MARGIN_MS = 3e4;
|
|
729
|
+
var EMPTY_PAGE_PAUSE_MS = 1e3;
|
|
730
|
+
var MAX_DELIVERY_LIMIT = 500;
|
|
731
|
+
var MAX_FEED_KEY_NAME_BYTES = 200;
|
|
732
|
+
var WEBHOOK_SECRET_PREFIX = "whsec_";
|
|
733
|
+
var WEBHOOK_SIGNATURE_VERSION = "v1";
|
|
734
|
+
var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
735
|
+
var HMAC_SHA256_BYTES = 32;
|
|
736
|
+
var SIGNAL_KINDS = Object.freeze([
|
|
737
|
+
"run.refused",
|
|
738
|
+
"run.halted",
|
|
739
|
+
"run.failed",
|
|
740
|
+
"policy.changed",
|
|
741
|
+
"signoff.recorded",
|
|
742
|
+
"approver.registered",
|
|
743
|
+
"approver.revoked",
|
|
744
|
+
"key.created",
|
|
745
|
+
"key.revoked"
|
|
746
|
+
]);
|
|
747
|
+
function signalsRead(options, method, clientTimeoutMs) {
|
|
748
|
+
const asOwner = asOwnerOf(options, method);
|
|
749
|
+
const after = given(options?.after, 0);
|
|
750
|
+
const limit = options?.limit;
|
|
751
|
+
const wait = given(options?.wait, 0);
|
|
752
|
+
requireAfter(after, method);
|
|
753
|
+
if (limit !== void 0) requireLimit(limit, method, MAX_SIGNAL_LIMIT);
|
|
754
|
+
requireWait(wait, method);
|
|
755
|
+
const q = new URLSearchParams();
|
|
756
|
+
q.set("after", String(after));
|
|
757
|
+
if (limit !== void 0) q.set("limit", String(limit));
|
|
758
|
+
q.set("wait", String(wait));
|
|
759
|
+
return {
|
|
760
|
+
path: `${asOwner ? ACCOUNT_SIGNALS_PATH : SIGNALS_PATH}?${q.toString()}`,
|
|
761
|
+
timeoutMs: Math.max(clientTimeoutMs, wait * 1e3 + SIGNAL_TIMEOUT_MARGIN_MS)
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function followStart(options) {
|
|
765
|
+
const method = "followSignals";
|
|
766
|
+
const asOwner = asOwnerOf(options, method);
|
|
767
|
+
const after = given(options?.after, 0);
|
|
768
|
+
const wait = given(options?.wait, DEFAULT_FOLLOW_WAIT_SECONDS);
|
|
769
|
+
requireAfter(after, method);
|
|
770
|
+
requireWait(wait, method);
|
|
771
|
+
return { after, wait, asOwner };
|
|
772
|
+
}
|
|
773
|
+
async function* followSignalFeed(read, start) {
|
|
774
|
+
let last = start.after;
|
|
775
|
+
for (; ; ) {
|
|
776
|
+
let page;
|
|
777
|
+
try {
|
|
778
|
+
page = await read(last);
|
|
779
|
+
} catch (err) {
|
|
780
|
+
throw new SignalFeedError(
|
|
781
|
+
last,
|
|
782
|
+
err instanceof Error ? err.message : String(err),
|
|
783
|
+
{ cause: err }
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
const events = pageEvents(page, last);
|
|
787
|
+
for (const event of events) {
|
|
788
|
+
yield event;
|
|
789
|
+
last = event.data.sequence;
|
|
790
|
+
}
|
|
791
|
+
if (events.length === 0 && start.wait === 0) {
|
|
792
|
+
await new Promise((r) => setTimeout(r, EMPTY_PAGE_PAUSE_MS));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function pageEvents(page, after) {
|
|
797
|
+
const events = page?.events ?? [];
|
|
798
|
+
if (!Array.isArray(events)) {
|
|
799
|
+
throw new SignalFeedError(after, "the server's page has no events list");
|
|
800
|
+
}
|
|
801
|
+
let previous = after;
|
|
802
|
+
for (const event of events) {
|
|
803
|
+
const sequence = event?.data?.sequence;
|
|
804
|
+
if (!Number.isSafeInteger(sequence) || sequence <= previous) {
|
|
805
|
+
throw new SignalFeedError(
|
|
806
|
+
after,
|
|
807
|
+
`the server's page holds an event out of order (sequence ${String(sequence)} after ${previous})`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
previous = sequence;
|
|
811
|
+
}
|
|
812
|
+
if (page?.next_after !== previous) {
|
|
813
|
+
throw new SignalFeedError(
|
|
814
|
+
after,
|
|
815
|
+
`the server's next_after ${String(page?.next_after)} is not the last sequence on its page (${previous})`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return events;
|
|
819
|
+
}
|
|
820
|
+
function feedKeyBody(name, expiresAt) {
|
|
821
|
+
if (typeof name !== "string" || name.trim() === "" || Buffer.byteLength(name.trim(), "utf8") > MAX_FEED_KEY_NAME_BYTES) {
|
|
822
|
+
throw new VerdifaxError(
|
|
823
|
+
"verdifax: createFeedKey needs a name of at most 200 bytes"
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if (expiresAt !== void 0 && typeof expiresAt !== "string") {
|
|
827
|
+
throw new VerdifaxError(
|
|
828
|
+
"verdifax: createFeedKey expiresAt must be an RFC 3339 time or a YYYY-MM-DD date, as a string"
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
return expiresAt === void 0 ? { name } : { name, expires_at: expiresAt };
|
|
832
|
+
}
|
|
833
|
+
function webhookBody(url, options) {
|
|
834
|
+
if (typeof url !== "string" || !url.startsWith("https://") || url.length <= 8) {
|
|
835
|
+
throw new VerdifaxError(
|
|
836
|
+
"verdifax: createWebhook needs a url that starts with https://"
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const types = options?.types;
|
|
840
|
+
if (types === void 0) return { url };
|
|
841
|
+
if (!Array.isArray(types)) {
|
|
842
|
+
throw new VerdifaxError(
|
|
843
|
+
"verdifax: createWebhook types must be a list of signal kinds"
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const seen = /* @__PURE__ */ new Set();
|
|
847
|
+
for (const kind of types) {
|
|
848
|
+
if (!SIGNAL_KINDS.includes(kind) || seen.has(kind)) {
|
|
849
|
+
throw new VerdifaxError(
|
|
850
|
+
`verdifax: createWebhook types must be distinct signal kinds (${SIGNAL_KINDS.join(", ")}); an empty list takes every kind`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
seen.add(kind);
|
|
854
|
+
}
|
|
855
|
+
return { url, types: [...types] };
|
|
856
|
+
}
|
|
857
|
+
function deliveriesPath(id, options) {
|
|
858
|
+
const limit = options?.limit;
|
|
859
|
+
if (limit === void 0) return `${WEBHOOKS_PATH}/${id}/deliveries`;
|
|
860
|
+
requireLimit(limit, "listWebhookDeliveries", MAX_DELIVERY_LIMIT);
|
|
861
|
+
return `${WEBHOOKS_PATH}/${id}/deliveries?limit=${limit}`;
|
|
862
|
+
}
|
|
863
|
+
function verifyWebhook(headers, body, secret, options = {}) {
|
|
864
|
+
const key = webhookSecretBytes(secret);
|
|
865
|
+
const tolerance = options?.toleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
|
|
866
|
+
if (typeof tolerance !== "number" || !Number.isFinite(tolerance) || tolerance < 0) {
|
|
867
|
+
throw new VerdifaxError(
|
|
868
|
+
"verdifax: verifyWebhook toleranceSeconds must be a number of seconds of 0 or more"
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
const now = options?.now ?? /* @__PURE__ */ new Date();
|
|
872
|
+
if (!(now instanceof Date) || Number.isNaN(now.getTime())) {
|
|
873
|
+
throw new VerdifaxError("verdifax: verifyWebhook now must be a valid Date");
|
|
874
|
+
}
|
|
875
|
+
let raw;
|
|
876
|
+
if (typeof body === "string") {
|
|
877
|
+
raw = Buffer.from(body, "utf8");
|
|
878
|
+
} else if (body instanceof Uint8Array) {
|
|
879
|
+
raw = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
880
|
+
} else {
|
|
881
|
+
throw new VerdifaxError(
|
|
882
|
+
"verdifax: verifyWebhook needs the body as the raw bytes received (a Buffer, Uint8Array or string), not parsed JSON"
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const id = singleHeader(headers, "webhook-id");
|
|
886
|
+
const timestamp = singleHeader(headers, "webhook-timestamp");
|
|
887
|
+
const signatures = headerValues(headers, "webhook-signature").join(" ");
|
|
888
|
+
if (!signatures) {
|
|
889
|
+
throw new VerdifaxError("verdifax: webhook check failed: the webhook-signature header is missing");
|
|
890
|
+
}
|
|
891
|
+
if (!/^[0-9]{1,15}$/.test(timestamp)) {
|
|
892
|
+
throw new VerdifaxError(
|
|
893
|
+
"verdifax: webhook check failed: webhook-timestamp is not a whole number of seconds"
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
const age = Math.abs(now.getTime() / 1e3 - Number(timestamp));
|
|
897
|
+
if (age > tolerance) {
|
|
898
|
+
throw new VerdifaxError(
|
|
899
|
+
`verdifax: webhook check failed: webhook-timestamp is more than ${tolerance} seconds from now`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.`, "utf8").update(raw).digest();
|
|
903
|
+
let v1Seen = false;
|
|
904
|
+
let matched = false;
|
|
905
|
+
for (const entry of signatures.split(" ")) {
|
|
906
|
+
const comma = entry.indexOf(",");
|
|
907
|
+
if (comma < 0 || entry.slice(0, comma) !== WEBHOOK_SIGNATURE_VERSION) continue;
|
|
908
|
+
v1Seen = true;
|
|
909
|
+
const given2 = Buffer.from(entry.slice(comma + 1), "base64");
|
|
910
|
+
if (given2.length === HMAC_SHA256_BYTES && timingSafeEqual(given2, expected)) {
|
|
911
|
+
matched = true;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (!v1Seen) {
|
|
915
|
+
throw new VerdifaxError(
|
|
916
|
+
"verdifax: webhook check failed: webhook-signature holds no v1 signature"
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (!matched) {
|
|
920
|
+
throw new VerdifaxError(
|
|
921
|
+
"verdifax: webhook check failed: no v1 signature in webhook-signature matches the id, timestamp and body under this secret"
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
let event;
|
|
925
|
+
try {
|
|
926
|
+
event = JSON.parse(raw.toString("utf8"));
|
|
927
|
+
} catch {
|
|
928
|
+
throw new VerdifaxError(
|
|
929
|
+
"verdifax: webhook check failed: the signature matches but the body is not JSON"
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
if (typeof event !== "object" || event === null || Array.isArray(event)) {
|
|
933
|
+
throw new VerdifaxError(
|
|
934
|
+
"verdifax: webhook check failed: the signature matches but the body is not a CloudEvent object"
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
if (event.id !== id) {
|
|
938
|
+
throw new VerdifaxError(
|
|
939
|
+
"verdifax: webhook check failed: the event id in the body does not match webhook-id"
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return event;
|
|
943
|
+
}
|
|
944
|
+
function signalEventHash(kind, entryHash, fields) {
|
|
945
|
+
const { account, sequence, previousSignalHash } = fields ?? {};
|
|
946
|
+
for (const [name, value] of [
|
|
947
|
+
["kind", kind],
|
|
948
|
+
["entryHash", entryHash],
|
|
949
|
+
["account", account],
|
|
950
|
+
["previousSignalHash", previousSignalHash]
|
|
951
|
+
]) {
|
|
952
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
953
|
+
throw new VerdifaxError(
|
|
954
|
+
`verdifax: signalEventHash ${name} must be text without a zero byte`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
959
|
+
throw new VerdifaxError(
|
|
960
|
+
"verdifax: signalEventHash sequence must be a whole number of 0 or more"
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
return createHash2("sha256").update(
|
|
964
|
+
[SIGNAL_FORM, kind, entryHash, account, String(sequence), previousSignalHash].join("\0"),
|
|
965
|
+
"utf8"
|
|
966
|
+
).digest("hex");
|
|
967
|
+
}
|
|
968
|
+
function given(value, fallback) {
|
|
969
|
+
return value === void 0 ? fallback : value;
|
|
970
|
+
}
|
|
971
|
+
function requireAfter(after, method) {
|
|
972
|
+
if (!Number.isSafeInteger(after) || after < 0) {
|
|
973
|
+
throw new VerdifaxError(
|
|
974
|
+
`verdifax: ${method} after must be a whole number of 0 or more, the last sequence read`
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function requireLimit(limit, method, max) {
|
|
979
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) {
|
|
980
|
+
throw new VerdifaxError(
|
|
981
|
+
`verdifax: ${method} limit must be a whole number from 1 to ${max}`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function requireWait(wait, method) {
|
|
986
|
+
if (!Number.isSafeInteger(wait) || wait < 0 || wait > MAX_SIGNAL_WAIT_SECONDS) {
|
|
987
|
+
throw new VerdifaxError(
|
|
988
|
+
`verdifax: ${method} wait must be a whole number of seconds from 0 to ${MAX_SIGNAL_WAIT_SECONDS}`
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function webhookSecretBytes(secret) {
|
|
993
|
+
if (typeof secret !== "string" || !secret.startsWith(WEBHOOK_SECRET_PREFIX)) {
|
|
994
|
+
throw new VerdifaxError(
|
|
995
|
+
"verdifax: verifyWebhook needs the subscription's secret, which starts with whsec_"
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const encoded = secret.slice(WEBHOOK_SECRET_PREFIX.length);
|
|
999
|
+
const key = Buffer.from(encoded, "base64");
|
|
1000
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded) || key.length === 0 || key.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
|
|
1001
|
+
throw new VerdifaxError(
|
|
1002
|
+
"verdifax: verifyWebhook needs the subscription's secret: whsec_ and then base64"
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
return key;
|
|
1006
|
+
}
|
|
1007
|
+
function headerValues(headers, name) {
|
|
1008
|
+
if (typeof headers !== "object" || headers === null) {
|
|
1009
|
+
throw new VerdifaxError("verdifax: verifyWebhook needs the delivery's headers");
|
|
1010
|
+
}
|
|
1011
|
+
if (typeof headers.get === "function") {
|
|
1012
|
+
const value = headers.get(name);
|
|
1013
|
+
return value === null || value === void 0 ? [] : [String(value)];
|
|
1014
|
+
}
|
|
1015
|
+
const out = [];
|
|
1016
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1017
|
+
if (key.toLowerCase() !== name) continue;
|
|
1018
|
+
if (Array.isArray(value)) out.push(...value.map(String));
|
|
1019
|
+
else if (value !== void 0) out.push(String(value));
|
|
1020
|
+
}
|
|
1021
|
+
return out;
|
|
1022
|
+
}
|
|
1023
|
+
function singleHeader(headers, name) {
|
|
1024
|
+
const values = headerValues(headers, name);
|
|
1025
|
+
if (values.length === 0 || values[0] === "") {
|
|
1026
|
+
throw new VerdifaxError(`verdifax: webhook check failed: the ${name} header is missing`);
|
|
1027
|
+
}
|
|
1028
|
+
if (values.length > 1) {
|
|
1029
|
+
throw new VerdifaxError(`verdifax: webhook check failed: the ${name} header appears more than once`);
|
|
1030
|
+
}
|
|
1031
|
+
return values[0];
|
|
1032
|
+
}
|
|
1033
|
+
|
|
626
1034
|
// src/version.ts
|
|
627
|
-
var SDK_VERSION = "1.
|
|
1035
|
+
var SDK_VERSION = "1.5.0";
|
|
628
1036
|
|
|
629
1037
|
// src/client.ts
|
|
1038
|
+
import { KeyObject as KeyObject2 } from "crypto";
|
|
630
1039
|
var DEFAULT_BASE_URL = "https://api.verdifax.com";
|
|
631
1040
|
var DEFAULT_TIMEOUT_MS = 9e4;
|
|
632
1041
|
var DEFAULT_MAX_RETRIES = 3;
|
|
@@ -639,6 +1048,12 @@ var ACCOUNT_PATH = "/account";
|
|
|
639
1048
|
var ACCOUNT_REQUEST_KEY_PATH = "/account/request-key";
|
|
640
1049
|
var ACCOUNT_POLICIES_PATH = "/account/policies";
|
|
641
1050
|
var ACCOUNT_CHAIN_ID = /^account-[1-9][0-9]*$/;
|
|
1051
|
+
var SIGNOFFS_PATH = "/signoffs";
|
|
1052
|
+
var ACCOUNT_SIGNOFFS_PATH = "/account/signoffs";
|
|
1053
|
+
var APPROVERS_PATH = "/account/approvers";
|
|
1054
|
+
var SIGNOFF_TARGET_KINDS = /* @__PURE__ */ new Set(["record", "receipt", "job"]);
|
|
1055
|
+
var SIGNOFF_DECISIONS = /* @__PURE__ */ new Set(["approve", "reject"]);
|
|
1056
|
+
var MAX_APPROVER_NAME_BYTES = 200;
|
|
642
1057
|
var HEADER_EXPORT_TOTAL = "X-Verdifax-Total";
|
|
643
1058
|
var HEADER_EXPORT_TRUNCATED = "X-Verdifax-Truncated";
|
|
644
1059
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
@@ -1145,6 +1560,271 @@ var VerdifaxClient = class {
|
|
|
1145
1560
|
}
|
|
1146
1561
|
return this.activatePolicyAt(ACCOUNT_POLICIES_PATH, documentHash, chain);
|
|
1147
1562
|
}
|
|
1563
|
+
// ── Sign-off ─────────────────────────────────────────────────
|
|
1564
|
+
//
|
|
1565
|
+
// An account's owner registers approver keys. An approver signs a
|
|
1566
|
+
// decision, approve or reject, on one exact result on their own machine,
|
|
1567
|
+
// and any key in the account, or the owner key with asOwner, submits it.
|
|
1568
|
+
// A sign-off shows that the holder of a registered approver key signed
|
|
1569
|
+
// that decision on that exact result and that Verdifax logged it on the
|
|
1570
|
+
// public log. It does not show the review was careful, the result right,
|
|
1571
|
+
// or who the person is, and it gates nothing on its own.
|
|
1572
|
+
/** Registers an approver key to the owner's account. `approver` is the
|
|
1573
|
+
* approver's signing key, from which the proof is made on this machine,
|
|
1574
|
+
* or the `{ public_key, proof }` that {@link approverKeyProof} made on
|
|
1575
|
+
* the approver's own machine, so the owner need not hold the private
|
|
1576
|
+
* key. A proof that does not verify is refused before sending. The
|
|
1577
|
+
* server refuses a key that is a request key in the same account.
|
|
1578
|
+
* `name` is a label, not an identity. */
|
|
1579
|
+
async registerApprover(name, approver) {
|
|
1580
|
+
this.requireAPIKey();
|
|
1581
|
+
if (typeof name !== "string" || name.trim() === "" || Buffer.byteLength(name.trim(), "utf8") > MAX_APPROVER_NAME_BYTES) {
|
|
1582
|
+
throw new VerdifaxError(
|
|
1583
|
+
"verdifax: registerApprover needs a name of at most 200 bytes"
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
const body = approver instanceof Uint8Array || approver instanceof KeyObject2 ? approverKeyProof(approver) : { public_key: approver?.public_key, proof: approver?.proof };
|
|
1587
|
+
if (!approverKeyProofVerifies(body)) {
|
|
1588
|
+
throw new VerdifaxError(
|
|
1589
|
+
"verdifax: registerApprover needs the approver's signing key, or a public_key and proof from approverKeyProof that verify"
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
if (this.signingKey && publicKeyOf(this.signingKey) === body.public_key) {
|
|
1593
|
+
throw new VerdifaxError(
|
|
1594
|
+
"verdifax: an approver key must not be a request key in the same account, and this is the client's signingKey"
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
return this.request("POST", APPROVERS_PATH, {
|
|
1598
|
+
public_key: body.public_key,
|
|
1599
|
+
name,
|
|
1600
|
+
proof: body.proof
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
/** The account's approver keys, revoked ones included. */
|
|
1604
|
+
async listApprovers() {
|
|
1605
|
+
this.requireAPIKey();
|
|
1606
|
+
const out = await this.request("GET", APPROVERS_PATH);
|
|
1607
|
+
return { ...out, approvers: out?.approvers ?? [] };
|
|
1608
|
+
}
|
|
1609
|
+
/** Revokes one of the account's approver keys. Its earlier sign-offs
|
|
1610
|
+
* stay on the record. A key that is not an active approver in the
|
|
1611
|
+
* account throws {@link NotFoundError}. */
|
|
1612
|
+
async revokeApprover(approverId) {
|
|
1613
|
+
this.requireAPIKey();
|
|
1614
|
+
requirePositiveId(approverId, "approverId");
|
|
1615
|
+
return this.request(
|
|
1616
|
+
"DELETE",
|
|
1617
|
+
`${APPROVERS_PATH}/${approverId}`
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
/** What an approver signs against: the account's chain `account-N` and
|
|
1621
|
+
* its newest sign-off (64 zeros when there is none yet). With
|
|
1622
|
+
* `asOwner` it reads the owner key's route. */
|
|
1623
|
+
async getNewestSignOff(options = {}) {
|
|
1624
|
+
this.requireAPIKey();
|
|
1625
|
+
return this.request(
|
|
1626
|
+
"GET",
|
|
1627
|
+
`${signOffsBase(options, "getNewestSignOff")}/newest`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
/** Signs an approver's decision, approve or reject, on one exact result
|
|
1631
|
+
* and submits it. It first reads {@link getNewestSignOff} for the
|
|
1632
|
+
* account's chain and its newest sign-off, then signs over them with
|
|
1633
|
+
* `approverKey` on this machine. The approver's private key is never
|
|
1634
|
+
* sent.
|
|
1635
|
+
*
|
|
1636
|
+
* `target.digest` is what the result's public-log entry covers: for a
|
|
1637
|
+
* record, the `bundle_anchor.anchored_digest` of the bundle
|
|
1638
|
+
* {@link getRunArtifacts} returns; for a receipt, its hash; for a job,
|
|
1639
|
+
* the final step's record digest, read as a record's, with `job_id`.
|
|
1640
|
+
*
|
|
1641
|
+
* `note` is hashed here and only its SHA-256 is sent and signed: the
|
|
1642
|
+
* text never reaches Verdifax. With `asOwner` it uses the owner key's
|
|
1643
|
+
* routes. Bad input is refused before anything is sent.
|
|
1644
|
+
*
|
|
1645
|
+
* The record the server returns must carry the signature,
|
|
1646
|
+
* approver_key, decision, previous_signoff_hash and target that were
|
|
1647
|
+
* sent, or this throws {@link VerdifaxError}.
|
|
1648
|
+
*
|
|
1649
|
+
* A later sign-off on the same result supersedes this one, and neither
|
|
1650
|
+
* is deleted. An HTTP 409 means the chain moved or the target is not
|
|
1651
|
+
* this account's result: it throws the usual {@link APIError} and is
|
|
1652
|
+
* not retried. */
|
|
1653
|
+
async signOff(target, decision, approverKey, options = {}) {
|
|
1654
|
+
this.requireAPIKey();
|
|
1655
|
+
const base = signOffsBase(options, "signOff");
|
|
1656
|
+
const exact = signOffTargetOf(target);
|
|
1657
|
+
if (!SIGNOFF_DECISIONS.has(decision)) {
|
|
1658
|
+
throw new VerdifaxError(
|
|
1659
|
+
"verdifax: signOff decision must be approve or reject"
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const note = options?.note;
|
|
1663
|
+
if (note !== void 0 && (typeof note !== "string" || note === "")) {
|
|
1664
|
+
throw new VerdifaxError(
|
|
1665
|
+
"verdifax: signOff note must be non-empty text when given"
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
const key = loadSigningKey(approverKey);
|
|
1669
|
+
const note_hash = note === void 0 ? "" : sha256Hex(note);
|
|
1670
|
+
const newest = await this.getNewestSignOff(options);
|
|
1671
|
+
if (typeof newest?.chain_id !== "string" || !ACCOUNT_CHAIN_ID.test(newest.chain_id)) {
|
|
1672
|
+
throw new VerdifaxError(
|
|
1673
|
+
"verdifax: the server did not report an account chain_id (account-N), so the sign-off cannot be signed"
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
const previous = newest.newest_signoff_hash;
|
|
1677
|
+
if (typeof previous !== "string" || !/^[0-9a-f]{64}$/.test(previous)) {
|
|
1678
|
+
throw new VerdifaxError(
|
|
1679
|
+
"verdifax: the server did not report newest_signoff_hash, so the sign-off cannot name the one it follows"
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
const signed = signOffSignature(key, {
|
|
1683
|
+
chain_id: newest.chain_id,
|
|
1684
|
+
previous_signoff_hash: previous,
|
|
1685
|
+
target: exact,
|
|
1686
|
+
decision,
|
|
1687
|
+
note_hash
|
|
1688
|
+
});
|
|
1689
|
+
const body = {
|
|
1690
|
+
previous_signoff_hash: previous,
|
|
1691
|
+
target: exact,
|
|
1692
|
+
decision,
|
|
1693
|
+
...note_hash ? { note_hash } : {},
|
|
1694
|
+
...signed
|
|
1695
|
+
};
|
|
1696
|
+
const out = await this.request(
|
|
1697
|
+
"POST",
|
|
1698
|
+
base,
|
|
1699
|
+
body
|
|
1700
|
+
);
|
|
1701
|
+
const record = out?.signoff;
|
|
1702
|
+
if (!recordMatchesSent(record, exact, decision, previous, signed)) {
|
|
1703
|
+
throw new VerdifaxError(
|
|
1704
|
+
"verdifax: the sign-off the server returned does not carry the signature, approver_key, decision, previous_signoff_hash and target that were sent"
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
return record;
|
|
1708
|
+
}
|
|
1709
|
+
/** The sign-offs on one run, oldest first, in the form
|
|
1710
|
+
* `verdifax-verify --signoff FILE` reads: for a member key, a run it
|
|
1711
|
+
* made; with `asOwner`, any run of the account. The latest is the
|
|
1712
|
+
* decision in force. */
|
|
1713
|
+
async getSignOffs(runId, options = {}) {
|
|
1714
|
+
this.requireAPIKey();
|
|
1715
|
+
requirePositiveId(runId, "runId");
|
|
1716
|
+
const base = asOwnerOf(options, "getSignOffs") ? `${ACCOUNT_PATH}/runs` : "/runs";
|
|
1717
|
+
return await this.request(
|
|
1718
|
+
"GET",
|
|
1719
|
+
`${base}/${runId}/signoffs`
|
|
1720
|
+
) ?? [];
|
|
1721
|
+
}
|
|
1722
|
+
/** The account's whole chain of sign-offs, oldest first, in the form
|
|
1723
|
+
* `verdifax-verify --signoff-history FILE` reads. Needs the owner key. */
|
|
1724
|
+
async getSignOffHistory() {
|
|
1725
|
+
this.requireAPIKey();
|
|
1726
|
+
return await this.request(
|
|
1727
|
+
"GET",
|
|
1728
|
+
`${ACCOUNT_SIGNOFFS_PATH}/history`
|
|
1729
|
+
) ?? [];
|
|
1730
|
+
}
|
|
1731
|
+
// ── Signal feed ──────────────────────────────────────────────
|
|
1732
|
+
//
|
|
1733
|
+
// Every control signal Verdifax recorded for the account, in order, each
|
|
1734
|
+
// with its sealed entry. The feed cannot show a signal Verdifax never
|
|
1735
|
+
// recorded. A feed key reads it on GET /signals and does nothing else;
|
|
1736
|
+
// the owner key reads it with asOwner and manages feed keys and webhooks.
|
|
1737
|
+
// The feed and webhook logic is in signals.ts.
|
|
1738
|
+
/** One page of the account's feed: the events after `after`, oldest
|
|
1739
|
+
* first, with `next_after` to read from next. With `wait` the server
|
|
1740
|
+
* holds the request open up to that many seconds for the next event, and
|
|
1741
|
+
* this call's timeout is at least the wait plus 30 seconds. With
|
|
1742
|
+
* `asOwner` it reads GET /account/signals with the owner key. Bad
|
|
1743
|
+
* options are refused before sending. Save the events and check them
|
|
1744
|
+
* with `verdifax-verify --signals`. */
|
|
1745
|
+
async getSignals(options = {}) {
|
|
1746
|
+
this.requireAPIKey();
|
|
1747
|
+
const read = signalsRead(options, "getSignals", this.timeoutMs);
|
|
1748
|
+
const out = await this.request(
|
|
1749
|
+
"GET",
|
|
1750
|
+
read.path,
|
|
1751
|
+
void 0,
|
|
1752
|
+
false,
|
|
1753
|
+
read.timeoutMs
|
|
1754
|
+
);
|
|
1755
|
+
return { ...out, events: out?.events ?? [] };
|
|
1756
|
+
}
|
|
1757
|
+
/** Follows the feed from `after`, yielding each event once, in order,
|
|
1758
|
+
* until you stop with `break` or `return`. Each read starts at the page
|
|
1759
|
+
* before's `next_after`, waits up to `wait` seconds (default 25) when
|
|
1760
|
+
* there is no event, and is retried like any GET. A read that still
|
|
1761
|
+
* fails throws {@link SignalFeedError}, whose `lastSequence` is the last
|
|
1762
|
+
* sequence yielded: follow again from there and nothing is lost. Bad
|
|
1763
|
+
* options are refused here, before anything is sent. */
|
|
1764
|
+
followSignals(options = {}) {
|
|
1765
|
+
this.requireAPIKey();
|
|
1766
|
+
const start = followStart(options);
|
|
1767
|
+
return followSignalFeed(
|
|
1768
|
+
(after) => this.getSignals({ after, wait: start.wait, asOwner: start.asOwner }),
|
|
1769
|
+
start
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
/** Creates a feed key in the owner's account. A feed key reads the
|
|
1773
|
+
* account's signal feed, with {@link getSignals} and
|
|
1774
|
+
* {@link followSignals}, and nothing else. The secret is in the result
|
|
1775
|
+
* this once. */
|
|
1776
|
+
async createFeedKey(name, options = {}) {
|
|
1777
|
+
this.requireAPIKey();
|
|
1778
|
+
return this.request(
|
|
1779
|
+
"POST",
|
|
1780
|
+
FEED_KEYS_PATH,
|
|
1781
|
+
feedKeyBody(name, options?.expiresAt)
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
/** Revokes one of the account's feed keys. An id that is not a feed key
|
|
1785
|
+
* of the account throws {@link NotFoundError}. */
|
|
1786
|
+
async revokeFeedKey(id) {
|
|
1787
|
+
this.requireAPIKey();
|
|
1788
|
+
requirePositiveId(id, "id");
|
|
1789
|
+
return this.request("DELETE", `${FEED_KEYS_PATH}/${id}`);
|
|
1790
|
+
}
|
|
1791
|
+
/** Subscribes an https address to the account's signals. `types` are the
|
|
1792
|
+
* kinds to deliver, each once; empty or left out takes every kind. The
|
|
1793
|
+
* secret, `whsec_` and base64, is in the result this once: check each
|
|
1794
|
+
* delivery with {@link verifyWebhook}. A delivery is a copy: the pull
|
|
1795
|
+
* feed and the public log are the record. The server checks the address
|
|
1796
|
+
* too, and refuses an eleventh subscription with HTTP 409. */
|
|
1797
|
+
async createWebhook(url, options = {}) {
|
|
1798
|
+
this.requireAPIKey();
|
|
1799
|
+
return this.request(
|
|
1800
|
+
"POST",
|
|
1801
|
+
WEBHOOKS_PATH,
|
|
1802
|
+
webhookBody(url, options)
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
/** The account's webhook subscriptions, without their secrets. */
|
|
1806
|
+
async listWebhooks() {
|
|
1807
|
+
this.requireAPIKey();
|
|
1808
|
+
const out = await this.request("GET", WEBHOOKS_PATH);
|
|
1809
|
+
return { ...out, webhooks: out?.webhooks ?? [] };
|
|
1810
|
+
}
|
|
1811
|
+
/** Ends a webhook subscription. Its events stay in the pull feed. */
|
|
1812
|
+
async deleteWebhook(id) {
|
|
1813
|
+
this.requireAPIKey();
|
|
1814
|
+
requirePositiveId(id, "id");
|
|
1815
|
+
return this.request("DELETE", `${WEBHOOKS_PATH}/${id}`);
|
|
1816
|
+
}
|
|
1817
|
+
/** A webhook's newest deliveries first, up to `limit` (1 to 500; the
|
|
1818
|
+
* server's default is 50). */
|
|
1819
|
+
async listWebhookDeliveries(id, options = {}) {
|
|
1820
|
+
this.requireAPIKey();
|
|
1821
|
+
requirePositiveId(id, "id");
|
|
1822
|
+
const out = await this.request(
|
|
1823
|
+
"GET",
|
|
1824
|
+
deliveriesPath(id, options)
|
|
1825
|
+
);
|
|
1826
|
+
return { ...out, deliveries: out?.deliveries ?? [] };
|
|
1827
|
+
}
|
|
1148
1828
|
// ── Runs ──────────────────────────────────────────────────────
|
|
1149
1829
|
async getRun(runId) {
|
|
1150
1830
|
this.requireAPIKey();
|
|
@@ -1290,6 +1970,14 @@ var VerdifaxClient = class {
|
|
|
1290
1970
|
async getCESAuditRecord(runId) {
|
|
1291
1971
|
return this.getRunArtifact(runId, "ces-audit-record");
|
|
1292
1972
|
}
|
|
1973
|
+
/** A failed run's sealed failure receipt, in the form `verdifax-verify`
|
|
1974
|
+
* reads. A run that failed before failure receipts were sealed has none
|
|
1975
|
+
* and throws {@link NotFoundError}. */
|
|
1976
|
+
async getFailureReceipt(runId) {
|
|
1977
|
+
this.requireAPIKey();
|
|
1978
|
+
requirePositiveId(runId, "runId");
|
|
1979
|
+
return this.getRunArtifact(runId, "failure-receipt");
|
|
1980
|
+
}
|
|
1293
1981
|
/** The audit report for a run, as JSON. */
|
|
1294
1982
|
async getRunReport(runId) {
|
|
1295
1983
|
const out = await this.getRunArtifact(runId, "report");
|
|
@@ -1487,7 +2175,7 @@ var VerdifaxClient = class {
|
|
|
1487
2175
|
});
|
|
1488
2176
|
}
|
|
1489
2177
|
// ── HTTP plumbing, retry-aware ──────────────────────────────
|
|
1490
|
-
async request(method, path, body, rawText = false) {
|
|
2178
|
+
async request(method, path, body, rawText = false, timeoutMs = this.timeoutMs) {
|
|
1491
2179
|
const url = this.baseURL + path;
|
|
1492
2180
|
const headers = {
|
|
1493
2181
|
"User-Agent": this.userAgent
|
|
@@ -1504,7 +2192,7 @@ var VerdifaxClient = class {
|
|
|
1504
2192
|
await new Promise((r) => setTimeout(r, sleep));
|
|
1505
2193
|
}
|
|
1506
2194
|
const controller = new AbortController();
|
|
1507
|
-
const timer = setTimeout(() => controller.abort(),
|
|
2195
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1508
2196
|
let resp;
|
|
1509
2197
|
try {
|
|
1510
2198
|
resp = await fetch(url, {
|
|
@@ -1598,10 +2286,44 @@ var VerdifaxClient = class {
|
|
|
1598
2286
|
return this.signingKey ? signRequestV2(this.signingKey, req) : withExecuteFlags(req);
|
|
1599
2287
|
}
|
|
1600
2288
|
};
|
|
1601
|
-
function
|
|
1602
|
-
|
|
1603
|
-
|
|
2289
|
+
function signOffsBase(options, method) {
|
|
2290
|
+
return asOwnerOf(options, method) ? ACCOUNT_SIGNOFFS_PATH : SIGNOFFS_PATH;
|
|
2291
|
+
}
|
|
2292
|
+
function signOffTargetOf(target) {
|
|
2293
|
+
if (typeof target !== "object" || target === null || Array.isArray(target)) {
|
|
2294
|
+
throw new VerdifaxError("verdifax: signOff needs a target object");
|
|
2295
|
+
}
|
|
2296
|
+
for (const name of Object.keys(target)) {
|
|
2297
|
+
if (!["kind", "run_id", "digest", "job_id"].includes(name)) {
|
|
2298
|
+
throw new VerdifaxError(`verdifax: signOff target has an unknown field: ${name}`);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (!SIGNOFF_TARGET_KINDS.has(target.kind)) {
|
|
2302
|
+
throw new VerdifaxError(
|
|
2303
|
+
"verdifax: signOff target kind must be record, receipt or job"
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
requirePositiveId(target.run_id, "target.run_id");
|
|
2307
|
+
validateHex64(target.digest, "target.digest");
|
|
2308
|
+
if (target.kind === "job") {
|
|
2309
|
+
validateJobName(target.job_id, "target.job_id");
|
|
2310
|
+
return {
|
|
2311
|
+
kind: target.kind,
|
|
2312
|
+
run_id: target.run_id,
|
|
2313
|
+
digest: target.digest,
|
|
2314
|
+
job_id: target.job_id
|
|
2315
|
+
};
|
|
1604
2316
|
}
|
|
2317
|
+
if (target.job_id !== void 0) {
|
|
2318
|
+
throw new VerdifaxError(
|
|
2319
|
+
"verdifax: signOff target job_id is for a job target only"
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
return { kind: target.kind, run_id: target.run_id, digest: target.digest };
|
|
2323
|
+
}
|
|
2324
|
+
function recordMatchesSent(record, target, decision, previous, signed) {
|
|
2325
|
+
const got = record?.target;
|
|
2326
|
+
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 ?? "");
|
|
1605
2327
|
}
|
|
1606
2328
|
function withSignedText(result, sent) {
|
|
1607
2329
|
return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
|
|
@@ -1728,13 +2450,13 @@ var RUN_STATUSES = [
|
|
|
1728
2450
|
];
|
|
1729
2451
|
|
|
1730
2452
|
// src/section0.ts
|
|
1731
|
-
import { createHash as
|
|
2453
|
+
import { createHash as createHash3 } from "crypto";
|
|
1732
2454
|
var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
|
|
1733
2455
|
var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
|
|
1734
2456
|
var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
|
|
1735
2457
|
var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
|
|
1736
2458
|
function sha256Hex2(preimage) {
|
|
1737
|
-
return
|
|
2459
|
+
return createHash3("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
|
|
1738
2460
|
}
|
|
1739
2461
|
function requireHex64(value, name) {
|
|
1740
2462
|
try {
|
|
@@ -1800,9 +2522,11 @@ export {
|
|
|
1800
2522
|
MAX_BATCH_ROWS,
|
|
1801
2523
|
NotFoundError,
|
|
1802
2524
|
RUN_STATUSES,
|
|
2525
|
+
SignalFeedError,
|
|
1803
2526
|
VerdifaxClient,
|
|
1804
2527
|
VerdifaxError,
|
|
1805
2528
|
VerdifaxJob,
|
|
2529
|
+
approverKeyProof,
|
|
1806
2530
|
captureEnvironment,
|
|
1807
2531
|
cresReceiptHash,
|
|
1808
2532
|
dcaeClosureHash,
|
|
@@ -1817,9 +2541,12 @@ export {
|
|
|
1817
2541
|
requestKeyRegistrationBody,
|
|
1818
2542
|
requestSignaturePreimage,
|
|
1819
2543
|
requestSignaturePreimageV2,
|
|
2544
|
+
signOffPreimage,
|
|
1820
2545
|
signRequest,
|
|
1821
2546
|
signRequestV2,
|
|
2547
|
+
signalEventHash,
|
|
1822
2548
|
validateHex64,
|
|
1823
2549
|
validateJobName,
|
|
1824
|
-
validateRouteId
|
|
2550
|
+
validateRouteId,
|
|
2551
|
+
verifyWebhook
|
|
1825
2552
|
};
|