verdifax 1.4.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 +76 -1
- package/dist/index.d.mts +273 -1
- package/dist/index.d.ts +273 -1
- package/dist/index.js +459 -21
- package/dist/index.mjs +453 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
MAX_BATCH_ROWS: () => MAX_BATCH_ROWS,
|
|
32
32
|
NotFoundError: () => NotFoundError,
|
|
33
33
|
RUN_STATUSES: () => RUN_STATUSES,
|
|
34
|
+
SignalFeedError: () => SignalFeedError,
|
|
34
35
|
VerdifaxClient: () => VerdifaxClient,
|
|
35
36
|
VerdifaxError: () => VerdifaxError,
|
|
36
37
|
VerdifaxJob: () => VerdifaxJob,
|
|
@@ -52,9 +53,11 @@ __export(index_exports, {
|
|
|
52
53
|
signOffPreimage: () => signOffPreimage,
|
|
53
54
|
signRequest: () => signRequest,
|
|
54
55
|
signRequestV2: () => signRequestV2,
|
|
56
|
+
signalEventHash: () => signalEventHash,
|
|
55
57
|
validateHex64: () => validateHex64,
|
|
56
58
|
validateJobName: () => validateJobName,
|
|
57
|
-
validateRouteId: () => validateRouteId
|
|
59
|
+
validateRouteId: () => validateRouteId,
|
|
60
|
+
verifyWebhook: () => verifyWebhook
|
|
58
61
|
});
|
|
59
62
|
module.exports = __toCommonJS(index_exports);
|
|
60
63
|
|
|
@@ -108,6 +111,17 @@ var APIKeyRequiredError = class extends VerdifaxError {
|
|
|
108
111
|
super("verdifax: apiKey is required for this endpoint");
|
|
109
112
|
}
|
|
110
113
|
};
|
|
114
|
+
var SignalFeedError = class extends VerdifaxError {
|
|
115
|
+
constructor(lastSequence, reason, options) {
|
|
116
|
+
super(
|
|
117
|
+
`verdifax: the signal feed stopped after sequence ${lastSequence}: ${reason}; follow again with after: ${lastSequence}`,
|
|
118
|
+
options
|
|
119
|
+
);
|
|
120
|
+
this.lastSequence = lastSequence;
|
|
121
|
+
}
|
|
122
|
+
lastSequence;
|
|
123
|
+
name = "VerdifaxSignalFeedError";
|
|
124
|
+
};
|
|
111
125
|
|
|
112
126
|
// src/signing.ts
|
|
113
127
|
var import_node_crypto = require("crypto");
|
|
@@ -147,6 +161,18 @@ function validateJobName(value, fieldName) {
|
|
|
147
161
|
);
|
|
148
162
|
}
|
|
149
163
|
}
|
|
164
|
+
function requirePositiveId(id, name) {
|
|
165
|
+
if (!Number.isSafeInteger(id) || id <= 0) {
|
|
166
|
+
throw new VerdifaxError(`verdifax: ${name} must be a whole number > 0`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function asOwnerOf(options, method) {
|
|
170
|
+
const asOwner = options?.asOwner;
|
|
171
|
+
if (asOwner !== void 0 && typeof asOwner !== "boolean") {
|
|
172
|
+
throw new VerdifaxError(`verdifax: ${method} asOwner must be true or false`);
|
|
173
|
+
}
|
|
174
|
+
return asOwner === true;
|
|
175
|
+
}
|
|
150
176
|
|
|
151
177
|
// src/helpers.ts
|
|
152
178
|
function formatProviderPayload(provider, prompt, response) {
|
|
@@ -743,11 +769,327 @@ var VerdifaxJob = class {
|
|
|
743
769
|
}
|
|
744
770
|
};
|
|
745
771
|
|
|
772
|
+
// src/signals.ts
|
|
773
|
+
var import_node_crypto2 = require("crypto");
|
|
774
|
+
var SIGNAL_FORM = "verdifax.signal.v1";
|
|
775
|
+
var SIGNALS_PATH = "/signals";
|
|
776
|
+
var ACCOUNT_SIGNALS_PATH = "/account/signals";
|
|
777
|
+
var FEED_KEYS_PATH = "/account/feed-keys";
|
|
778
|
+
var WEBHOOKS_PATH = "/account/webhooks";
|
|
779
|
+
var MAX_SIGNAL_LIMIT = 500;
|
|
780
|
+
var MAX_SIGNAL_WAIT_SECONDS = 25;
|
|
781
|
+
var DEFAULT_FOLLOW_WAIT_SECONDS = 25;
|
|
782
|
+
var SIGNAL_TIMEOUT_MARGIN_MS = 3e4;
|
|
783
|
+
var EMPTY_PAGE_PAUSE_MS = 1e3;
|
|
784
|
+
var MAX_DELIVERY_LIMIT = 500;
|
|
785
|
+
var MAX_FEED_KEY_NAME_BYTES = 200;
|
|
786
|
+
var WEBHOOK_SECRET_PREFIX = "whsec_";
|
|
787
|
+
var WEBHOOK_SIGNATURE_VERSION = "v1";
|
|
788
|
+
var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
789
|
+
var HMAC_SHA256_BYTES = 32;
|
|
790
|
+
var SIGNAL_KINDS = Object.freeze([
|
|
791
|
+
"run.refused",
|
|
792
|
+
"run.halted",
|
|
793
|
+
"run.failed",
|
|
794
|
+
"policy.changed",
|
|
795
|
+
"signoff.recorded",
|
|
796
|
+
"approver.registered",
|
|
797
|
+
"approver.revoked",
|
|
798
|
+
"key.created",
|
|
799
|
+
"key.revoked"
|
|
800
|
+
]);
|
|
801
|
+
function signalsRead(options, method, clientTimeoutMs) {
|
|
802
|
+
const asOwner = asOwnerOf(options, method);
|
|
803
|
+
const after = given(options?.after, 0);
|
|
804
|
+
const limit = options?.limit;
|
|
805
|
+
const wait = given(options?.wait, 0);
|
|
806
|
+
requireAfter(after, method);
|
|
807
|
+
if (limit !== void 0) requireLimit(limit, method, MAX_SIGNAL_LIMIT);
|
|
808
|
+
requireWait(wait, method);
|
|
809
|
+
const q = new URLSearchParams();
|
|
810
|
+
q.set("after", String(after));
|
|
811
|
+
if (limit !== void 0) q.set("limit", String(limit));
|
|
812
|
+
q.set("wait", String(wait));
|
|
813
|
+
return {
|
|
814
|
+
path: `${asOwner ? ACCOUNT_SIGNALS_PATH : SIGNALS_PATH}?${q.toString()}`,
|
|
815
|
+
timeoutMs: Math.max(clientTimeoutMs, wait * 1e3 + SIGNAL_TIMEOUT_MARGIN_MS)
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function followStart(options) {
|
|
819
|
+
const method = "followSignals";
|
|
820
|
+
const asOwner = asOwnerOf(options, method);
|
|
821
|
+
const after = given(options?.after, 0);
|
|
822
|
+
const wait = given(options?.wait, DEFAULT_FOLLOW_WAIT_SECONDS);
|
|
823
|
+
requireAfter(after, method);
|
|
824
|
+
requireWait(wait, method);
|
|
825
|
+
return { after, wait, asOwner };
|
|
826
|
+
}
|
|
827
|
+
async function* followSignalFeed(read, start) {
|
|
828
|
+
let last = start.after;
|
|
829
|
+
for (; ; ) {
|
|
830
|
+
let page;
|
|
831
|
+
try {
|
|
832
|
+
page = await read(last);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
throw new SignalFeedError(
|
|
835
|
+
last,
|
|
836
|
+
err instanceof Error ? err.message : String(err),
|
|
837
|
+
{ cause: err }
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
const events = pageEvents(page, last);
|
|
841
|
+
for (const event of events) {
|
|
842
|
+
yield event;
|
|
843
|
+
last = event.data.sequence;
|
|
844
|
+
}
|
|
845
|
+
if (events.length === 0 && start.wait === 0) {
|
|
846
|
+
await new Promise((r) => setTimeout(r, EMPTY_PAGE_PAUSE_MS));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function pageEvents(page, after) {
|
|
851
|
+
const events = page?.events ?? [];
|
|
852
|
+
if (!Array.isArray(events)) {
|
|
853
|
+
throw new SignalFeedError(after, "the server's page has no events list");
|
|
854
|
+
}
|
|
855
|
+
let previous = after;
|
|
856
|
+
for (const event of events) {
|
|
857
|
+
const sequence = event?.data?.sequence;
|
|
858
|
+
if (!Number.isSafeInteger(sequence) || sequence <= previous) {
|
|
859
|
+
throw new SignalFeedError(
|
|
860
|
+
after,
|
|
861
|
+
`the server's page holds an event out of order (sequence ${String(sequence)} after ${previous})`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
previous = sequence;
|
|
865
|
+
}
|
|
866
|
+
if (page?.next_after !== previous) {
|
|
867
|
+
throw new SignalFeedError(
|
|
868
|
+
after,
|
|
869
|
+
`the server's next_after ${String(page?.next_after)} is not the last sequence on its page (${previous})`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
return events;
|
|
873
|
+
}
|
|
874
|
+
function feedKeyBody(name, expiresAt) {
|
|
875
|
+
if (typeof name !== "string" || name.trim() === "" || Buffer.byteLength(name.trim(), "utf8") > MAX_FEED_KEY_NAME_BYTES) {
|
|
876
|
+
throw new VerdifaxError(
|
|
877
|
+
"verdifax: createFeedKey needs a name of at most 200 bytes"
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
if (expiresAt !== void 0 && typeof expiresAt !== "string") {
|
|
881
|
+
throw new VerdifaxError(
|
|
882
|
+
"verdifax: createFeedKey expiresAt must be an RFC 3339 time or a YYYY-MM-DD date, as a string"
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return expiresAt === void 0 ? { name } : { name, expires_at: expiresAt };
|
|
886
|
+
}
|
|
887
|
+
function webhookBody(url, options) {
|
|
888
|
+
if (typeof url !== "string" || !url.startsWith("https://") || url.length <= 8) {
|
|
889
|
+
throw new VerdifaxError(
|
|
890
|
+
"verdifax: createWebhook needs a url that starts with https://"
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
const types = options?.types;
|
|
894
|
+
if (types === void 0) return { url };
|
|
895
|
+
if (!Array.isArray(types)) {
|
|
896
|
+
throw new VerdifaxError(
|
|
897
|
+
"verdifax: createWebhook types must be a list of signal kinds"
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
const seen = /* @__PURE__ */ new Set();
|
|
901
|
+
for (const kind of types) {
|
|
902
|
+
if (!SIGNAL_KINDS.includes(kind) || seen.has(kind)) {
|
|
903
|
+
throw new VerdifaxError(
|
|
904
|
+
`verdifax: createWebhook types must be distinct signal kinds (${SIGNAL_KINDS.join(", ")}); an empty list takes every kind`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
seen.add(kind);
|
|
908
|
+
}
|
|
909
|
+
return { url, types: [...types] };
|
|
910
|
+
}
|
|
911
|
+
function deliveriesPath(id, options) {
|
|
912
|
+
const limit = options?.limit;
|
|
913
|
+
if (limit === void 0) return `${WEBHOOKS_PATH}/${id}/deliveries`;
|
|
914
|
+
requireLimit(limit, "listWebhookDeliveries", MAX_DELIVERY_LIMIT);
|
|
915
|
+
return `${WEBHOOKS_PATH}/${id}/deliveries?limit=${limit}`;
|
|
916
|
+
}
|
|
917
|
+
function verifyWebhook(headers, body, secret, options = {}) {
|
|
918
|
+
const key = webhookSecretBytes(secret);
|
|
919
|
+
const tolerance = options?.toleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
|
|
920
|
+
if (typeof tolerance !== "number" || !Number.isFinite(tolerance) || tolerance < 0) {
|
|
921
|
+
throw new VerdifaxError(
|
|
922
|
+
"verdifax: verifyWebhook toleranceSeconds must be a number of seconds of 0 or more"
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
const now = options?.now ?? /* @__PURE__ */ new Date();
|
|
926
|
+
if (!(now instanceof Date) || Number.isNaN(now.getTime())) {
|
|
927
|
+
throw new VerdifaxError("verdifax: verifyWebhook now must be a valid Date");
|
|
928
|
+
}
|
|
929
|
+
let raw;
|
|
930
|
+
if (typeof body === "string") {
|
|
931
|
+
raw = Buffer.from(body, "utf8");
|
|
932
|
+
} else if (body instanceof Uint8Array) {
|
|
933
|
+
raw = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
934
|
+
} else {
|
|
935
|
+
throw new VerdifaxError(
|
|
936
|
+
"verdifax: verifyWebhook needs the body as the raw bytes received (a Buffer, Uint8Array or string), not parsed JSON"
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const id = singleHeader(headers, "webhook-id");
|
|
940
|
+
const timestamp = singleHeader(headers, "webhook-timestamp");
|
|
941
|
+
const signatures = headerValues(headers, "webhook-signature").join(" ");
|
|
942
|
+
if (!signatures) {
|
|
943
|
+
throw new VerdifaxError("verdifax: webhook check failed: the webhook-signature header is missing");
|
|
944
|
+
}
|
|
945
|
+
if (!/^[0-9]{1,15}$/.test(timestamp)) {
|
|
946
|
+
throw new VerdifaxError(
|
|
947
|
+
"verdifax: webhook check failed: webhook-timestamp is not a whole number of seconds"
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const age = Math.abs(now.getTime() / 1e3 - Number(timestamp));
|
|
951
|
+
if (age > tolerance) {
|
|
952
|
+
throw new VerdifaxError(
|
|
953
|
+
`verdifax: webhook check failed: webhook-timestamp is more than ${tolerance} seconds from now`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const expected = (0, import_node_crypto2.createHmac)("sha256", key).update(`${id}.${timestamp}.`, "utf8").update(raw).digest();
|
|
957
|
+
let v1Seen = false;
|
|
958
|
+
let matched = false;
|
|
959
|
+
for (const entry of signatures.split(" ")) {
|
|
960
|
+
const comma = entry.indexOf(",");
|
|
961
|
+
if (comma < 0 || entry.slice(0, comma) !== WEBHOOK_SIGNATURE_VERSION) continue;
|
|
962
|
+
v1Seen = true;
|
|
963
|
+
const given2 = Buffer.from(entry.slice(comma + 1), "base64");
|
|
964
|
+
if (given2.length === HMAC_SHA256_BYTES && (0, import_node_crypto2.timingSafeEqual)(given2, expected)) {
|
|
965
|
+
matched = true;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (!v1Seen) {
|
|
969
|
+
throw new VerdifaxError(
|
|
970
|
+
"verdifax: webhook check failed: webhook-signature holds no v1 signature"
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
if (!matched) {
|
|
974
|
+
throw new VerdifaxError(
|
|
975
|
+
"verdifax: webhook check failed: no v1 signature in webhook-signature matches the id, timestamp and body under this secret"
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
let event;
|
|
979
|
+
try {
|
|
980
|
+
event = JSON.parse(raw.toString("utf8"));
|
|
981
|
+
} catch {
|
|
982
|
+
throw new VerdifaxError(
|
|
983
|
+
"verdifax: webhook check failed: the signature matches but the body is not JSON"
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
if (typeof event !== "object" || event === null || Array.isArray(event)) {
|
|
987
|
+
throw new VerdifaxError(
|
|
988
|
+
"verdifax: webhook check failed: the signature matches but the body is not a CloudEvent object"
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (event.id !== id) {
|
|
992
|
+
throw new VerdifaxError(
|
|
993
|
+
"verdifax: webhook check failed: the event id in the body does not match webhook-id"
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
return event;
|
|
997
|
+
}
|
|
998
|
+
function signalEventHash(kind, entryHash, fields) {
|
|
999
|
+
const { account, sequence, previousSignalHash } = fields ?? {};
|
|
1000
|
+
for (const [name, value] of [
|
|
1001
|
+
["kind", kind],
|
|
1002
|
+
["entryHash", entryHash],
|
|
1003
|
+
["account", account],
|
|
1004
|
+
["previousSignalHash", previousSignalHash]
|
|
1005
|
+
]) {
|
|
1006
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
1007
|
+
throw new VerdifaxError(
|
|
1008
|
+
`verdifax: signalEventHash ${name} must be text without a zero byte`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
1013
|
+
throw new VerdifaxError(
|
|
1014
|
+
"verdifax: signalEventHash sequence must be a whole number of 0 or more"
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
return (0, import_node_crypto2.createHash)("sha256").update(
|
|
1018
|
+
[SIGNAL_FORM, kind, entryHash, account, String(sequence), previousSignalHash].join("\0"),
|
|
1019
|
+
"utf8"
|
|
1020
|
+
).digest("hex");
|
|
1021
|
+
}
|
|
1022
|
+
function given(value, fallback) {
|
|
1023
|
+
return value === void 0 ? fallback : value;
|
|
1024
|
+
}
|
|
1025
|
+
function requireAfter(after, method) {
|
|
1026
|
+
if (!Number.isSafeInteger(after) || after < 0) {
|
|
1027
|
+
throw new VerdifaxError(
|
|
1028
|
+
`verdifax: ${method} after must be a whole number of 0 or more, the last sequence read`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function requireLimit(limit, method, max) {
|
|
1033
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > max) {
|
|
1034
|
+
throw new VerdifaxError(
|
|
1035
|
+
`verdifax: ${method} limit must be a whole number from 1 to ${max}`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function requireWait(wait, method) {
|
|
1040
|
+
if (!Number.isSafeInteger(wait) || wait < 0 || wait > MAX_SIGNAL_WAIT_SECONDS) {
|
|
1041
|
+
throw new VerdifaxError(
|
|
1042
|
+
`verdifax: ${method} wait must be a whole number of seconds from 0 to ${MAX_SIGNAL_WAIT_SECONDS}`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function webhookSecretBytes(secret) {
|
|
1047
|
+
if (typeof secret !== "string" || !secret.startsWith(WEBHOOK_SECRET_PREFIX)) {
|
|
1048
|
+
throw new VerdifaxError(
|
|
1049
|
+
"verdifax: verifyWebhook needs the subscription's secret, which starts with whsec_"
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
const encoded = secret.slice(WEBHOOK_SECRET_PREFIX.length);
|
|
1053
|
+
const key = Buffer.from(encoded, "base64");
|
|
1054
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded) || key.length === 0 || key.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
|
|
1055
|
+
throw new VerdifaxError(
|
|
1056
|
+
"verdifax: verifyWebhook needs the subscription's secret: whsec_ and then base64"
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return key;
|
|
1060
|
+
}
|
|
1061
|
+
function headerValues(headers, name) {
|
|
1062
|
+
if (typeof headers !== "object" || headers === null) {
|
|
1063
|
+
throw new VerdifaxError("verdifax: verifyWebhook needs the delivery's headers");
|
|
1064
|
+
}
|
|
1065
|
+
if (typeof headers.get === "function") {
|
|
1066
|
+
const value = headers.get(name);
|
|
1067
|
+
return value === null || value === void 0 ? [] : [String(value)];
|
|
1068
|
+
}
|
|
1069
|
+
const out = [];
|
|
1070
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1071
|
+
if (key.toLowerCase() !== name) continue;
|
|
1072
|
+
if (Array.isArray(value)) out.push(...value.map(String));
|
|
1073
|
+
else if (value !== void 0) out.push(String(value));
|
|
1074
|
+
}
|
|
1075
|
+
return out;
|
|
1076
|
+
}
|
|
1077
|
+
function singleHeader(headers, name) {
|
|
1078
|
+
const values = headerValues(headers, name);
|
|
1079
|
+
if (values.length === 0 || values[0] === "") {
|
|
1080
|
+
throw new VerdifaxError(`verdifax: webhook check failed: the ${name} header is missing`);
|
|
1081
|
+
}
|
|
1082
|
+
if (values.length > 1) {
|
|
1083
|
+
throw new VerdifaxError(`verdifax: webhook check failed: the ${name} header appears more than once`);
|
|
1084
|
+
}
|
|
1085
|
+
return values[0];
|
|
1086
|
+
}
|
|
1087
|
+
|
|
746
1088
|
// src/version.ts
|
|
747
|
-
var SDK_VERSION = "1.
|
|
1089
|
+
var SDK_VERSION = "1.5.0";
|
|
748
1090
|
|
|
749
1091
|
// src/client.ts
|
|
750
|
-
var
|
|
1092
|
+
var import_node_crypto3 = require("crypto");
|
|
751
1093
|
var DEFAULT_BASE_URL = "https://api.verdifax.com";
|
|
752
1094
|
var DEFAULT_TIMEOUT_MS = 9e4;
|
|
753
1095
|
var DEFAULT_MAX_RETRIES = 3;
|
|
@@ -1295,7 +1637,7 @@ var VerdifaxClient = class {
|
|
|
1295
1637
|
"verdifax: registerApprover needs a name of at most 200 bytes"
|
|
1296
1638
|
);
|
|
1297
1639
|
}
|
|
1298
|
-
const body = approver instanceof Uint8Array || approver instanceof
|
|
1640
|
+
const body = approver instanceof Uint8Array || approver instanceof import_node_crypto3.KeyObject ? approverKeyProof(approver) : { public_key: approver?.public_key, proof: approver?.proof };
|
|
1299
1641
|
if (!approverKeyProofVerifies(body)) {
|
|
1300
1642
|
throw new VerdifaxError(
|
|
1301
1643
|
"verdifax: registerApprover needs the approver's signing key, or a public_key and proof from approverKeyProof that verify"
|
|
@@ -1440,6 +1782,103 @@ var VerdifaxClient = class {
|
|
|
1440
1782
|
`${ACCOUNT_SIGNOFFS_PATH}/history`
|
|
1441
1783
|
) ?? [];
|
|
1442
1784
|
}
|
|
1785
|
+
// ── Signal feed ──────────────────────────────────────────────
|
|
1786
|
+
//
|
|
1787
|
+
// Every control signal Verdifax recorded for the account, in order, each
|
|
1788
|
+
// with its sealed entry. The feed cannot show a signal Verdifax never
|
|
1789
|
+
// recorded. A feed key reads it on GET /signals and does nothing else;
|
|
1790
|
+
// the owner key reads it with asOwner and manages feed keys and webhooks.
|
|
1791
|
+
// The feed and webhook logic is in signals.ts.
|
|
1792
|
+
/** One page of the account's feed: the events after `after`, oldest
|
|
1793
|
+
* first, with `next_after` to read from next. With `wait` the server
|
|
1794
|
+
* holds the request open up to that many seconds for the next event, and
|
|
1795
|
+
* this call's timeout is at least the wait plus 30 seconds. With
|
|
1796
|
+
* `asOwner` it reads GET /account/signals with the owner key. Bad
|
|
1797
|
+
* options are refused before sending. Save the events and check them
|
|
1798
|
+
* with `verdifax-verify --signals`. */
|
|
1799
|
+
async getSignals(options = {}) {
|
|
1800
|
+
this.requireAPIKey();
|
|
1801
|
+
const read = signalsRead(options, "getSignals", this.timeoutMs);
|
|
1802
|
+
const out = await this.request(
|
|
1803
|
+
"GET",
|
|
1804
|
+
read.path,
|
|
1805
|
+
void 0,
|
|
1806
|
+
false,
|
|
1807
|
+
read.timeoutMs
|
|
1808
|
+
);
|
|
1809
|
+
return { ...out, events: out?.events ?? [] };
|
|
1810
|
+
}
|
|
1811
|
+
/** Follows the feed from `after`, yielding each event once, in order,
|
|
1812
|
+
* until you stop with `break` or `return`. Each read starts at the page
|
|
1813
|
+
* before's `next_after`, waits up to `wait` seconds (default 25) when
|
|
1814
|
+
* there is no event, and is retried like any GET. A read that still
|
|
1815
|
+
* fails throws {@link SignalFeedError}, whose `lastSequence` is the last
|
|
1816
|
+
* sequence yielded: follow again from there and nothing is lost. Bad
|
|
1817
|
+
* options are refused here, before anything is sent. */
|
|
1818
|
+
followSignals(options = {}) {
|
|
1819
|
+
this.requireAPIKey();
|
|
1820
|
+
const start = followStart(options);
|
|
1821
|
+
return followSignalFeed(
|
|
1822
|
+
(after) => this.getSignals({ after, wait: start.wait, asOwner: start.asOwner }),
|
|
1823
|
+
start
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
/** Creates a feed key in the owner's account. A feed key reads the
|
|
1827
|
+
* account's signal feed, with {@link getSignals} and
|
|
1828
|
+
* {@link followSignals}, and nothing else. The secret is in the result
|
|
1829
|
+
* this once. */
|
|
1830
|
+
async createFeedKey(name, options = {}) {
|
|
1831
|
+
this.requireAPIKey();
|
|
1832
|
+
return this.request(
|
|
1833
|
+
"POST",
|
|
1834
|
+
FEED_KEYS_PATH,
|
|
1835
|
+
feedKeyBody(name, options?.expiresAt)
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
/** Revokes one of the account's feed keys. An id that is not a feed key
|
|
1839
|
+
* of the account throws {@link NotFoundError}. */
|
|
1840
|
+
async revokeFeedKey(id) {
|
|
1841
|
+
this.requireAPIKey();
|
|
1842
|
+
requirePositiveId(id, "id");
|
|
1843
|
+
return this.request("DELETE", `${FEED_KEYS_PATH}/${id}`);
|
|
1844
|
+
}
|
|
1845
|
+
/** Subscribes an https address to the account's signals. `types` are the
|
|
1846
|
+
* kinds to deliver, each once; empty or left out takes every kind. The
|
|
1847
|
+
* secret, `whsec_` and base64, is in the result this once: check each
|
|
1848
|
+
* delivery with {@link verifyWebhook}. A delivery is a copy: the pull
|
|
1849
|
+
* feed and the public log are the record. The server checks the address
|
|
1850
|
+
* too, and refuses an eleventh subscription with HTTP 409. */
|
|
1851
|
+
async createWebhook(url, options = {}) {
|
|
1852
|
+
this.requireAPIKey();
|
|
1853
|
+
return this.request(
|
|
1854
|
+
"POST",
|
|
1855
|
+
WEBHOOKS_PATH,
|
|
1856
|
+
webhookBody(url, options)
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
/** The account's webhook subscriptions, without their secrets. */
|
|
1860
|
+
async listWebhooks() {
|
|
1861
|
+
this.requireAPIKey();
|
|
1862
|
+
const out = await this.request("GET", WEBHOOKS_PATH);
|
|
1863
|
+
return { ...out, webhooks: out?.webhooks ?? [] };
|
|
1864
|
+
}
|
|
1865
|
+
/** Ends a webhook subscription. Its events stay in the pull feed. */
|
|
1866
|
+
async deleteWebhook(id) {
|
|
1867
|
+
this.requireAPIKey();
|
|
1868
|
+
requirePositiveId(id, "id");
|
|
1869
|
+
return this.request("DELETE", `${WEBHOOKS_PATH}/${id}`);
|
|
1870
|
+
}
|
|
1871
|
+
/** A webhook's newest deliveries first, up to `limit` (1 to 500; the
|
|
1872
|
+
* server's default is 50). */
|
|
1873
|
+
async listWebhookDeliveries(id, options = {}) {
|
|
1874
|
+
this.requireAPIKey();
|
|
1875
|
+
requirePositiveId(id, "id");
|
|
1876
|
+
const out = await this.request(
|
|
1877
|
+
"GET",
|
|
1878
|
+
deliveriesPath(id, options)
|
|
1879
|
+
);
|
|
1880
|
+
return { ...out, deliveries: out?.deliveries ?? [] };
|
|
1881
|
+
}
|
|
1443
1882
|
// ── Runs ──────────────────────────────────────────────────────
|
|
1444
1883
|
async getRun(runId) {
|
|
1445
1884
|
this.requireAPIKey();
|
|
@@ -1585,6 +2024,14 @@ var VerdifaxClient = class {
|
|
|
1585
2024
|
async getCESAuditRecord(runId) {
|
|
1586
2025
|
return this.getRunArtifact(runId, "ces-audit-record");
|
|
1587
2026
|
}
|
|
2027
|
+
/** A failed run's sealed failure receipt, in the form `verdifax-verify`
|
|
2028
|
+
* reads. A run that failed before failure receipts were sealed has none
|
|
2029
|
+
* and throws {@link NotFoundError}. */
|
|
2030
|
+
async getFailureReceipt(runId) {
|
|
2031
|
+
this.requireAPIKey();
|
|
2032
|
+
requirePositiveId(runId, "runId");
|
|
2033
|
+
return this.getRunArtifact(runId, "failure-receipt");
|
|
2034
|
+
}
|
|
1588
2035
|
/** The audit report for a run, as JSON. */
|
|
1589
2036
|
async getRunReport(runId) {
|
|
1590
2037
|
const out = await this.getRunArtifact(runId, "report");
|
|
@@ -1782,7 +2229,7 @@ var VerdifaxClient = class {
|
|
|
1782
2229
|
});
|
|
1783
2230
|
}
|
|
1784
2231
|
// ── HTTP plumbing, retry-aware ──────────────────────────────
|
|
1785
|
-
async request(method, path, body, rawText = false) {
|
|
2232
|
+
async request(method, path, body, rawText = false, timeoutMs = this.timeoutMs) {
|
|
1786
2233
|
const url = this.baseURL + path;
|
|
1787
2234
|
const headers = {
|
|
1788
2235
|
"User-Agent": this.userAgent
|
|
@@ -1799,7 +2246,7 @@ var VerdifaxClient = class {
|
|
|
1799
2246
|
await new Promise((r) => setTimeout(r, sleep));
|
|
1800
2247
|
}
|
|
1801
2248
|
const controller = new AbortController();
|
|
1802
|
-
const timer = setTimeout(() => controller.abort(),
|
|
2249
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1803
2250
|
let resp;
|
|
1804
2251
|
try {
|
|
1805
2252
|
resp = await fetch(url, {
|
|
@@ -1896,13 +2343,6 @@ var VerdifaxClient = class {
|
|
|
1896
2343
|
function signOffsBase(options, method) {
|
|
1897
2344
|
return asOwnerOf(options, method) ? ACCOUNT_SIGNOFFS_PATH : SIGNOFFS_PATH;
|
|
1898
2345
|
}
|
|
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
2346
|
function signOffTargetOf(target) {
|
|
1907
2347
|
if (typeof target !== "object" || target === null || Array.isArray(target)) {
|
|
1908
2348
|
throw new VerdifaxError("verdifax: signOff needs a target object");
|
|
@@ -1939,11 +2379,6 @@ function recordMatchesSent(record, target, decision, previous, signed) {
|
|
|
1939
2379
|
const got = record?.target;
|
|
1940
2380
|
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
2381
|
}
|
|
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
|
-
}
|
|
1947
2382
|
function withSignedText(result, sent) {
|
|
1948
2383
|
return "signed_request" in sent ? { ...result, signed_request: sent.signed_request } : result;
|
|
1949
2384
|
}
|
|
@@ -2069,13 +2504,13 @@ var RUN_STATUSES = [
|
|
|
2069
2504
|
];
|
|
2070
2505
|
|
|
2071
2506
|
// src/section0.ts
|
|
2072
|
-
var
|
|
2507
|
+
var import_node_crypto4 = require("crypto");
|
|
2073
2508
|
var DSE_FORMULA_VERSION = "dse.tcu.dispatch.v1";
|
|
2074
2509
|
var DCAE_PREIMAGE_VERSION = "verdifax.dcae.v1";
|
|
2075
2510
|
var CRES_PREIMAGE_VERSION = "verdifax.cres.v1";
|
|
2076
2511
|
var CRES_ENGINE_VERSION = "verdifax-cres/1.0.0";
|
|
2077
2512
|
function sha256Hex2(preimage) {
|
|
2078
|
-
return (0,
|
|
2513
|
+
return (0, import_node_crypto4.createHash)("sha256").update(Buffer.from(preimage, "utf8")).digest("hex");
|
|
2079
2514
|
}
|
|
2080
2515
|
function requireHex64(value, name) {
|
|
2081
2516
|
try {
|
|
@@ -2142,6 +2577,7 @@ function cresReceiptHash(fields) {
|
|
|
2142
2577
|
MAX_BATCH_ROWS,
|
|
2143
2578
|
NotFoundError,
|
|
2144
2579
|
RUN_STATUSES,
|
|
2580
|
+
SignalFeedError,
|
|
2145
2581
|
VerdifaxClient,
|
|
2146
2582
|
VerdifaxError,
|
|
2147
2583
|
VerdifaxJob,
|
|
@@ -2163,7 +2599,9 @@ function cresReceiptHash(fields) {
|
|
|
2163
2599
|
signOffPreimage,
|
|
2164
2600
|
signRequest,
|
|
2165
2601
|
signRequestV2,
|
|
2602
|
+
signalEventHash,
|
|
2166
2603
|
validateHex64,
|
|
2167
2604
|
validateJobName,
|
|
2168
|
-
validateRouteId
|
|
2605
|
+
validateRouteId,
|
|
2606
|
+
verifyWebhook
|
|
2169
2607
|
});
|