@paybond/kit 0.3.0 → 0.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 +77 -24
- package/dist/index.d.ts +656 -2
- package/dist/index.js +665 -2
- package/dist/mcp-server.d.ts +50 -0
- package/dist/mcp-server.js +932 -0
- package/package.json +20 -3
package/dist/index.js
CHANGED
|
@@ -47,6 +47,60 @@ export class SignalHttpError extends Error {
|
|
|
47
47
|
this.bodyText = init.bodyText;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
const agentRecognitionProofHeader = "x-paybond-agent-recognition-proof";
|
|
51
|
+
export class A2AHttpError extends Error {
|
|
52
|
+
statusCode;
|
|
53
|
+
url;
|
|
54
|
+
bodyText;
|
|
55
|
+
constructor(message, init) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "A2AHttpError";
|
|
58
|
+
this.statusCode = init.statusCode;
|
|
59
|
+
this.url = init.url;
|
|
60
|
+
this.bodyText = init.bodyText;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class ProtocolHttpError extends Error {
|
|
64
|
+
statusCode;
|
|
65
|
+
url;
|
|
66
|
+
bodyText;
|
|
67
|
+
errorCode;
|
|
68
|
+
errorMessage;
|
|
69
|
+
constructor(message, init) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "ProtocolHttpError";
|
|
72
|
+
this.statusCode = init.statusCode;
|
|
73
|
+
this.url = init.url;
|
|
74
|
+
this.bodyText = init.bodyText;
|
|
75
|
+
const parsed = parseGatewayErrorEnvelope(init.bodyText);
|
|
76
|
+
this.errorCode = init.errorCode ?? parsed.errorCode;
|
|
77
|
+
this.errorMessage = init.errorMessage ?? parsed.errorMessage;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function parseGatewayErrorEnvelope(text) {
|
|
81
|
+
if (!text.trim().startsWith("{")) {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const body = JSON.parse(text);
|
|
86
|
+
if (body === null || Array.isArray(body) || typeof body !== "object") {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
const errorCode = typeof body.error === "string" && body.error.trim() ? body.error.trim() : undefined;
|
|
90
|
+
const errorMessage = typeof body.message === "string" && body.message.trim() ? body.message.trim() : undefined;
|
|
91
|
+
return { errorCode, errorMessage };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function protocolHTTPErrorMessage(prefix, statusCode, bodyText) {
|
|
98
|
+
const parsed = parseGatewayErrorEnvelope(bodyText);
|
|
99
|
+
if (parsed.errorCode) {
|
|
100
|
+
return `${prefix} HTTP ${statusCode} (${parsed.errorCode}): ${parsed.errorMessage ?? bodyText}`;
|
|
101
|
+
}
|
|
102
|
+
return `${prefix} HTTP ${statusCode}: ${bodyText}`;
|
|
103
|
+
}
|
|
50
104
|
function normalizeBase(url) {
|
|
51
105
|
return url.trim().replace(/\/+$/, "");
|
|
52
106
|
}
|
|
@@ -605,6 +659,16 @@ export class HarborClient {
|
|
|
605
659
|
}
|
|
606
660
|
const DEFAULT_PRINCIPAL_PATH = "/v1/auth/principal";
|
|
607
661
|
const SETTLEMENT_RAIL_VALUES = new Set(["stripe_connect", "x402_usdc_base"]);
|
|
662
|
+
const FRAUD_REVIEW_EVENT_TYPES = new Set([
|
|
663
|
+
"review_open_requested",
|
|
664
|
+
"appeal_requested",
|
|
665
|
+
"replay_requested",
|
|
666
|
+
"review_outcome_recorded",
|
|
667
|
+
"confirmed_risk",
|
|
668
|
+
"false_positive",
|
|
669
|
+
"needs_more_evidence",
|
|
670
|
+
]);
|
|
671
|
+
const FRAUD_REVIEW_OUTCOMES = new Set(["confirmed_risk", "false_positive", "needs_more_evidence"]);
|
|
608
672
|
function assertJSONObject(value) {
|
|
609
673
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
610
674
|
throw new Error("expected JSON object");
|
|
@@ -770,6 +834,21 @@ export class GatewaySignalClient {
|
|
|
770
834
|
this.assertTenant(body, url);
|
|
771
835
|
return body;
|
|
772
836
|
}
|
|
837
|
+
async getSignedPortfolioArtifact(scoreVersion) {
|
|
838
|
+
const url = `${this.base}signal/v1/portfolio/signed-export${this.scoreQuery(scoreVersion)}`;
|
|
839
|
+
const res = await this.fetchGetWithRetries(url);
|
|
840
|
+
const text = await res.text();
|
|
841
|
+
if (!res.ok) {
|
|
842
|
+
throw new SignalHttpError(`Signal signed portfolio artifact HTTP ${res.status}: ${text}`, {
|
|
843
|
+
statusCode: res.status,
|
|
844
|
+
url,
|
|
845
|
+
bodyText: text,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
849
|
+
this.assertTenant(body, url);
|
|
850
|
+
return body;
|
|
851
|
+
}
|
|
773
852
|
async getOperatorExplanation(operatorDid, scoreVersion) {
|
|
774
853
|
const enc = encodeURIComponent(operatorDid);
|
|
775
854
|
const url = `${this.base}signal/v1/operators/${enc}/explanation${this.scoreQuery(scoreVersion)}`;
|
|
@@ -815,6 +894,553 @@ export class GatewaySignalClient {
|
|
|
815
894
|
return body;
|
|
816
895
|
}
|
|
817
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* Tenant-bound client for gateway fraud review and metrics routes.
|
|
899
|
+
*/
|
|
900
|
+
export class GatewayFraudClient {
|
|
901
|
+
base;
|
|
902
|
+
tenantId;
|
|
903
|
+
bearerToken;
|
|
904
|
+
maxRetries;
|
|
905
|
+
constructor(gatewayBaseUrl, tenantId, options) {
|
|
906
|
+
this.base = normalizeBase(gatewayBaseUrl) + "/";
|
|
907
|
+
this.tenantId = tenantId.trim();
|
|
908
|
+
this.bearerToken = options.staticGatewayBearerToken.trim();
|
|
909
|
+
this.maxRetries = Math.max(1, options.maxRetries ?? 3);
|
|
910
|
+
}
|
|
911
|
+
async fetchGetWithRetries(url) {
|
|
912
|
+
let lastErr;
|
|
913
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
914
|
+
let res;
|
|
915
|
+
try {
|
|
916
|
+
res = await fetch(url, {
|
|
917
|
+
method: "GET",
|
|
918
|
+
headers: {
|
|
919
|
+
accept: "application/json",
|
|
920
|
+
authorization: `Bearer ${this.bearerToken}`,
|
|
921
|
+
},
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
catch (e) {
|
|
925
|
+
lastErr = e;
|
|
926
|
+
if (attempt + 1 >= this.maxRetries)
|
|
927
|
+
throw e;
|
|
928
|
+
await new Promise((r) => setTimeout(r, backoffMs(attempt)));
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if ([429, 500, 502, 503, 504].includes(res.status)) {
|
|
932
|
+
if (attempt + 1 >= this.maxRetries) {
|
|
933
|
+
return res;
|
|
934
|
+
}
|
|
935
|
+
const raSec = parseRetryAfterSeconds(res.headers.get("retry-after"));
|
|
936
|
+
const delayMs = raSec != null ? raSec * 1000 : backoffMs(attempt);
|
|
937
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
return res;
|
|
941
|
+
}
|
|
942
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
943
|
+
}
|
|
944
|
+
async fetchPostJSON(url, payload) {
|
|
945
|
+
return fetch(url, {
|
|
946
|
+
method: "POST",
|
|
947
|
+
headers: {
|
|
948
|
+
accept: "application/json",
|
|
949
|
+
authorization: `Bearer ${this.bearerToken}`,
|
|
950
|
+
"content-type": "application/json",
|
|
951
|
+
},
|
|
952
|
+
body: JSON.stringify(payload),
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
async fetchPutJSON(url, payload) {
|
|
956
|
+
return fetch(url, {
|
|
957
|
+
method: "PUT",
|
|
958
|
+
headers: {
|
|
959
|
+
accept: "application/json",
|
|
960
|
+
authorization: `Bearer ${this.bearerToken}`,
|
|
961
|
+
"content-type": "application/json",
|
|
962
|
+
},
|
|
963
|
+
body: JSON.stringify(payload),
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
assertTenant(body, url) {
|
|
967
|
+
const tid = String(body.tenant_id ?? "");
|
|
968
|
+
if (tid !== this.tenantId) {
|
|
969
|
+
throw new Error(`fraud tenant mismatch: client=${this.tenantId} gateway=${tid} url=${url}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
assertOperator(body, operatorDid, label) {
|
|
973
|
+
const echoedOperator = String(body.operator_did ?? "");
|
|
974
|
+
if (echoedOperator !== operatorDid) {
|
|
975
|
+
throw new Error(`fraud ${label} operator mismatch: requested=${operatorDid} gateway=${echoedOperator}`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
query(params) {
|
|
979
|
+
const qs = new URLSearchParams();
|
|
980
|
+
for (const [key, value] of Object.entries(params)) {
|
|
981
|
+
if (typeof value === "number") {
|
|
982
|
+
qs.set(key, String(value));
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (typeof value === "string" && value.trim()) {
|
|
986
|
+
qs.set(key, value.trim());
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const raw = qs.toString();
|
|
990
|
+
return raw ? `?${raw}` : "";
|
|
991
|
+
}
|
|
992
|
+
normalizedSeverity(severity) {
|
|
993
|
+
const normalized = severity?.trim();
|
|
994
|
+
if (!normalized) {
|
|
995
|
+
return undefined;
|
|
996
|
+
}
|
|
997
|
+
if (!["elevated", "high", "critical"].includes(normalized)) {
|
|
998
|
+
throw new Error("fraud severity must be one of elevated, high, or critical");
|
|
999
|
+
}
|
|
1000
|
+
return normalized;
|
|
1001
|
+
}
|
|
1002
|
+
normalizedWindow(window) {
|
|
1003
|
+
const normalized = window?.trim();
|
|
1004
|
+
if (!normalized) {
|
|
1005
|
+
return undefined;
|
|
1006
|
+
}
|
|
1007
|
+
if (!["24h", "7d", "30d"].includes(normalized)) {
|
|
1008
|
+
throw new Error("fraud metrics window must be one of 24h, 7d, or 30d");
|
|
1009
|
+
}
|
|
1010
|
+
return normalized;
|
|
1011
|
+
}
|
|
1012
|
+
normalizedReleaseGateMode(mode) {
|
|
1013
|
+
const normalized = mode.trim();
|
|
1014
|
+
if (!["review_only", "critical_hold"].includes(normalized)) {
|
|
1015
|
+
throw new Error("fraud release gate mode must be one of review_only or critical_hold");
|
|
1016
|
+
}
|
|
1017
|
+
return normalized;
|
|
1018
|
+
}
|
|
1019
|
+
normalizedReviewEventType(eventType) {
|
|
1020
|
+
const normalized = eventType.trim();
|
|
1021
|
+
if (!FRAUD_REVIEW_EVENT_TYPES.has(normalized)) {
|
|
1022
|
+
throw new Error("fraud review eventType must be one of review_open_requested, appeal_requested, replay_requested, review_outcome_recorded, confirmed_risk, false_positive, or needs_more_evidence");
|
|
1023
|
+
}
|
|
1024
|
+
return normalized;
|
|
1025
|
+
}
|
|
1026
|
+
normalizedReviewOutcome(outcome) {
|
|
1027
|
+
const normalized = outcome.trim();
|
|
1028
|
+
if (!FRAUD_REVIEW_OUTCOMES.has(normalized)) {
|
|
1029
|
+
throw new Error("fraud review outcome must be one of confirmed_risk, false_positive, or needs_more_evidence");
|
|
1030
|
+
}
|
|
1031
|
+
return normalized;
|
|
1032
|
+
}
|
|
1033
|
+
optionalReviewContext(value) {
|
|
1034
|
+
const normalized = value?.trim();
|
|
1035
|
+
return normalized || undefined;
|
|
1036
|
+
}
|
|
1037
|
+
async getFraudAssessment(operatorDid, scoreVersion) {
|
|
1038
|
+
const enc = encodeURIComponent(operatorDid);
|
|
1039
|
+
const url = `${this.base}signal/v1/operators/${enc}/review-status${this.query({
|
|
1040
|
+
score_version: scoreVersion,
|
|
1041
|
+
})}`;
|
|
1042
|
+
const res = await this.fetchGetWithRetries(url);
|
|
1043
|
+
const text = await res.text();
|
|
1044
|
+
if (res.status === 404) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
if (!res.ok) {
|
|
1048
|
+
throw new SignalHttpError(`Fraud assessment HTTP ${res.status}: ${text}`, {
|
|
1049
|
+
statusCode: res.status,
|
|
1050
|
+
url,
|
|
1051
|
+
bodyText: text,
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
1055
|
+
this.assertTenant(body, url);
|
|
1056
|
+
this.assertOperator(body, operatorDid, "assessment");
|
|
1057
|
+
return body;
|
|
1058
|
+
}
|
|
1059
|
+
async listFraudReviewQueue(options = {}) {
|
|
1060
|
+
let rawLimit;
|
|
1061
|
+
if (options.limit !== undefined) {
|
|
1062
|
+
if (!Number.isFinite(options.limit)) {
|
|
1063
|
+
throw new Error("fraud review queue limit must be a finite number");
|
|
1064
|
+
}
|
|
1065
|
+
rawLimit = Math.max(1, Math.min(Math.floor(options.limit), 500));
|
|
1066
|
+
}
|
|
1067
|
+
const url = `${this.base}signal/v1/review-queue${this.query({
|
|
1068
|
+
state: options.state,
|
|
1069
|
+
fraud_severity: this.normalizedSeverity(options.severity),
|
|
1070
|
+
limit: rawLimit,
|
|
1071
|
+
score_version: options.scoreVersion,
|
|
1072
|
+
})}`;
|
|
1073
|
+
const res = await this.fetchGetWithRetries(url);
|
|
1074
|
+
const text = await res.text();
|
|
1075
|
+
if (!res.ok) {
|
|
1076
|
+
throw new SignalHttpError(`Fraud review queue HTTP ${res.status}: ${text}`, {
|
|
1077
|
+
statusCode: res.status,
|
|
1078
|
+
url,
|
|
1079
|
+
bodyText: text,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
1083
|
+
this.assertTenant(body, url);
|
|
1084
|
+
return body;
|
|
1085
|
+
}
|
|
1086
|
+
async getFraudMetrics(options = {}) {
|
|
1087
|
+
const url = `${this.base}signal/v1/fraud/metrics${this.query({
|
|
1088
|
+
window: this.normalizedWindow(options.window),
|
|
1089
|
+
score_version: options.scoreVersion,
|
|
1090
|
+
})}`;
|
|
1091
|
+
const res = await this.fetchGetWithRetries(url);
|
|
1092
|
+
const text = await res.text();
|
|
1093
|
+
if (!res.ok) {
|
|
1094
|
+
throw new SignalHttpError(`Fraud metrics HTTP ${res.status}: ${text}`, {
|
|
1095
|
+
statusCode: res.status,
|
|
1096
|
+
url,
|
|
1097
|
+
bodyText: text,
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
1101
|
+
this.assertTenant(body, url);
|
|
1102
|
+
return body;
|
|
1103
|
+
}
|
|
1104
|
+
async getFraudReleaseGateConfig(scoreVersion) {
|
|
1105
|
+
const url = `${this.base}signal/v1/fraud/release-gate${this.query({
|
|
1106
|
+
score_version: scoreVersion,
|
|
1107
|
+
})}`;
|
|
1108
|
+
const res = await this.fetchGetWithRetries(url);
|
|
1109
|
+
const text = await res.text();
|
|
1110
|
+
if (!res.ok) {
|
|
1111
|
+
throw new SignalHttpError(`Fraud release gate HTTP ${res.status}: ${text}`, {
|
|
1112
|
+
statusCode: res.status,
|
|
1113
|
+
url,
|
|
1114
|
+
bodyText: text,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
1118
|
+
this.assertTenant(body, url);
|
|
1119
|
+
return body;
|
|
1120
|
+
}
|
|
1121
|
+
async setFraudReleaseGateMode(mode) {
|
|
1122
|
+
const normalized = this.normalizedReleaseGateMode(mode);
|
|
1123
|
+
const url = `${this.base}signal/v1/fraud/release-gate`;
|
|
1124
|
+
const res = await this.fetchPutJSON(url, { mode: normalized });
|
|
1125
|
+
const text = await res.text();
|
|
1126
|
+
if (!res.ok) {
|
|
1127
|
+
throw new SignalHttpError(`Fraud release gate update HTTP ${res.status}: ${text}`, {
|
|
1128
|
+
statusCode: res.status,
|
|
1129
|
+
url,
|
|
1130
|
+
bodyText: text,
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
1134
|
+
this.assertTenant(body, url);
|
|
1135
|
+
return body;
|
|
1136
|
+
}
|
|
1137
|
+
async recordFraudReviewEvent(operatorDid, event, scoreVersion) {
|
|
1138
|
+
let eventType = this.normalizedReviewEventType(event.eventType);
|
|
1139
|
+
let reviewOutcome = event.reviewOutcome ?? event.review_outcome;
|
|
1140
|
+
if (FRAUD_REVIEW_OUTCOMES.has(eventType)) {
|
|
1141
|
+
reviewOutcome = eventType;
|
|
1142
|
+
eventType = "review_outcome_recorded";
|
|
1143
|
+
}
|
|
1144
|
+
const normalizedOutcome = reviewOutcome === undefined ? undefined : this.normalizedReviewOutcome(reviewOutcome);
|
|
1145
|
+
if (eventType === "review_outcome_recorded" && normalizedOutcome === undefined) {
|
|
1146
|
+
throw new Error("fraud review outcome must be one of confirmed_risk, false_positive, or needs_more_evidence");
|
|
1147
|
+
}
|
|
1148
|
+
const signalCode = this.optionalReviewContext(event.signalCode ?? event.signal_code);
|
|
1149
|
+
const intentId = this.optionalReviewContext(event.intentId ?? event.intent_id);
|
|
1150
|
+
const providerEventId = this.optionalReviewContext(event.providerEventId ?? event.provider_event_id);
|
|
1151
|
+
const enc = encodeURIComponent(operatorDid);
|
|
1152
|
+
const url = `${this.base}signal/v1/operators/${enc}/review-events${this.query({
|
|
1153
|
+
score_version: scoreVersion,
|
|
1154
|
+
})}`;
|
|
1155
|
+
const res = await this.fetchPostJSON(url, {
|
|
1156
|
+
event_type: eventType,
|
|
1157
|
+
...(normalizedOutcome ? { review_outcome: normalizedOutcome } : {}),
|
|
1158
|
+
...(signalCode ? { signal_code: signalCode } : {}),
|
|
1159
|
+
...(intentId ? { intent_id: intentId } : {}),
|
|
1160
|
+
...(providerEventId ? { provider_event_id: providerEventId } : {}),
|
|
1161
|
+
summary: event.summary,
|
|
1162
|
+
});
|
|
1163
|
+
const text = await res.text();
|
|
1164
|
+
if (!res.ok && res.status !== 429) {
|
|
1165
|
+
throw new SignalHttpError(`Fraud review event HTTP ${res.status}: ${text}`, {
|
|
1166
|
+
statusCode: res.status,
|
|
1167
|
+
url,
|
|
1168
|
+
bodyText: text,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
1172
|
+
this.assertTenant(body, url);
|
|
1173
|
+
this.assertOperator(body, operatorDid, "review event");
|
|
1174
|
+
return body;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Public or optionally authenticated reader for the gateway's A2A discovery surface.
|
|
1179
|
+
*/
|
|
1180
|
+
export class GatewayA2AClient {
|
|
1181
|
+
base;
|
|
1182
|
+
bearerToken;
|
|
1183
|
+
maxRetries;
|
|
1184
|
+
constructor(gatewayBaseUrl, options) {
|
|
1185
|
+
this.base = normalizeBase(gatewayBaseUrl) + "/";
|
|
1186
|
+
this.bearerToken = options?.staticGatewayBearerToken?.trim() || undefined;
|
|
1187
|
+
this.maxRetries = Math.max(1, options?.maxRetries ?? 3);
|
|
1188
|
+
}
|
|
1189
|
+
async fetchGetWithRetries(url) {
|
|
1190
|
+
let lastErr;
|
|
1191
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
1192
|
+
let res;
|
|
1193
|
+
try {
|
|
1194
|
+
const headers = new Headers({
|
|
1195
|
+
accept: "application/json",
|
|
1196
|
+
});
|
|
1197
|
+
if (this.bearerToken) {
|
|
1198
|
+
headers.set("authorization", `Bearer ${this.bearerToken}`);
|
|
1199
|
+
}
|
|
1200
|
+
res = await fetch(url, {
|
|
1201
|
+
method: "GET",
|
|
1202
|
+
headers,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
catch (e) {
|
|
1206
|
+
lastErr = e;
|
|
1207
|
+
if (attempt + 1 >= this.maxRetries)
|
|
1208
|
+
throw e;
|
|
1209
|
+
await new Promise((r) => setTimeout(r, backoffMs(attempt)));
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if ([429, 500, 502, 503, 504].includes(res.status)) {
|
|
1213
|
+
if (attempt + 1 >= this.maxRetries) {
|
|
1214
|
+
return res;
|
|
1215
|
+
}
|
|
1216
|
+
const raSec = parseRetryAfterSeconds(res.headers.get("retry-after"));
|
|
1217
|
+
const delayMs = raSec != null ? raSec * 1000 : backoffMs(attempt);
|
|
1218
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
return res;
|
|
1222
|
+
}
|
|
1223
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
1224
|
+
}
|
|
1225
|
+
async getAgentCard() {
|
|
1226
|
+
const url = `${this.base}.well-known/agent-card.json`;
|
|
1227
|
+
const res = await this.fetchGetWithRetries(url);
|
|
1228
|
+
const text = await res.text();
|
|
1229
|
+
if (!res.ok) {
|
|
1230
|
+
throw new A2AHttpError(`A2A agent card HTTP ${res.status}: ${text}`, {
|
|
1231
|
+
statusCode: res.status,
|
|
1232
|
+
url,
|
|
1233
|
+
bodyText: text,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
return assertJSONObject(JSON.parse(text));
|
|
1237
|
+
}
|
|
1238
|
+
async getTaskContracts() {
|
|
1239
|
+
const url = `${this.base}protocol/v2/a2a/task-contracts`;
|
|
1240
|
+
const res = await this.fetchGetWithRetries(url);
|
|
1241
|
+
const text = await res.text();
|
|
1242
|
+
if (!res.ok) {
|
|
1243
|
+
throw new A2AHttpError(`A2A task contracts HTTP ${res.status}: ${text}`, {
|
|
1244
|
+
statusCode: res.status,
|
|
1245
|
+
url,
|
|
1246
|
+
bodyText: text,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
return assertJSONObject(JSON.parse(text));
|
|
1250
|
+
}
|
|
1251
|
+
async getTaskContract(contractId) {
|
|
1252
|
+
const enc = encodeURIComponent(contractId);
|
|
1253
|
+
const url = `${this.base}protocol/v2/a2a/task-contracts/${enc}`;
|
|
1254
|
+
const res = await this.fetchGetWithRetries(url);
|
|
1255
|
+
const text = await res.text();
|
|
1256
|
+
if (!res.ok) {
|
|
1257
|
+
throw new A2AHttpError(`A2A task contract HTTP ${res.status}: ${text}`, {
|
|
1258
|
+
statusCode: res.status,
|
|
1259
|
+
url,
|
|
1260
|
+
bodyText: text,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
return assertJSONObject(JSON.parse(text));
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
export class GatewayProtocolClient {
|
|
1267
|
+
tenantId;
|
|
1268
|
+
base;
|
|
1269
|
+
staticGatewayBearerToken;
|
|
1270
|
+
maxRetries;
|
|
1271
|
+
constructor(gatewayBaseUrl, tenantId, init) {
|
|
1272
|
+
this.base = `${normalizeBase(gatewayBaseUrl)}/`;
|
|
1273
|
+
this.tenantId = tenantId.trim();
|
|
1274
|
+
this.staticGatewayBearerToken = init?.staticGatewayBearerToken?.trim() || null;
|
|
1275
|
+
this.maxRetries = Math.max(1, init?.maxRetries ?? 3);
|
|
1276
|
+
}
|
|
1277
|
+
async fetchWithRetries(url, init) {
|
|
1278
|
+
let lastErr;
|
|
1279
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
1280
|
+
let res;
|
|
1281
|
+
try {
|
|
1282
|
+
res = await fetch(url, init);
|
|
1283
|
+
}
|
|
1284
|
+
catch (e) {
|
|
1285
|
+
lastErr = e;
|
|
1286
|
+
if (attempt + 1 >= this.maxRetries) {
|
|
1287
|
+
throw e;
|
|
1288
|
+
}
|
|
1289
|
+
await new Promise((r) => setTimeout(r, backoffMs(attempt)));
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
if ([429, 500, 502, 503, 504].includes(res.status) && attempt + 1 < this.maxRetries) {
|
|
1293
|
+
const raSec = parseRetryAfterSeconds(res.headers.get("retry-after"));
|
|
1294
|
+
const delayMs = raSec != null ? raSec * 1000 : backoffMs(attempt);
|
|
1295
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
return res;
|
|
1299
|
+
}
|
|
1300
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
1301
|
+
}
|
|
1302
|
+
headers(extra) {
|
|
1303
|
+
const headers = new Headers(extra);
|
|
1304
|
+
headers.set("accept", "application/json");
|
|
1305
|
+
headers.set("x-tenant-id", this.tenantId);
|
|
1306
|
+
if (this.staticGatewayBearerToken) {
|
|
1307
|
+
headers.set("authorization", `Bearer ${this.staticGatewayBearerToken}`);
|
|
1308
|
+
}
|
|
1309
|
+
return headers;
|
|
1310
|
+
}
|
|
1311
|
+
async postJSON(path, payload, extraHeaders) {
|
|
1312
|
+
const url = `${this.base}${path.replace(/^\/+/, "")}`;
|
|
1313
|
+
const res = await this.fetchWithRetries(url, {
|
|
1314
|
+
method: "POST",
|
|
1315
|
+
headers: this.headers({
|
|
1316
|
+
"content-type": "application/json",
|
|
1317
|
+
...(extraHeaders ?? {}),
|
|
1318
|
+
}),
|
|
1319
|
+
body: JSON.stringify(payload),
|
|
1320
|
+
});
|
|
1321
|
+
const text = await res.text();
|
|
1322
|
+
if (!res.ok) {
|
|
1323
|
+
const parsed = parseGatewayErrorEnvelope(text);
|
|
1324
|
+
throw new ProtocolHttpError(protocolHTTPErrorMessage(`gateway POST ${path}`, res.status, text), {
|
|
1325
|
+
statusCode: res.status,
|
|
1326
|
+
url,
|
|
1327
|
+
bodyText: text,
|
|
1328
|
+
errorCode: parsed.errorCode,
|
|
1329
|
+
errorMessage: parsed.errorMessage,
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
return assertJSONObject(JSON.parse(text));
|
|
1333
|
+
}
|
|
1334
|
+
async importAgentMandateV1(init) {
|
|
1335
|
+
const url = `${this.base}protocol/v2/mandates`;
|
|
1336
|
+
const res = await this.fetchWithRetries(url, {
|
|
1337
|
+
method: "POST",
|
|
1338
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
1339
|
+
body: JSON.stringify({
|
|
1340
|
+
signed_mandate: init.signedMandate,
|
|
1341
|
+
intent_id: init.intentId,
|
|
1342
|
+
transport_binding: init.transportBinding ?? {},
|
|
1343
|
+
recognition_proof: init.recognitionProof,
|
|
1344
|
+
}),
|
|
1345
|
+
});
|
|
1346
|
+
const text = await res.text();
|
|
1347
|
+
if (!res.ok) {
|
|
1348
|
+
const parsed = parseGatewayErrorEnvelope(text);
|
|
1349
|
+
throw new ProtocolHttpError(protocolHTTPErrorMessage("protocol mandate import", res.status, text), {
|
|
1350
|
+
statusCode: res.status,
|
|
1351
|
+
url,
|
|
1352
|
+
bodyText: text,
|
|
1353
|
+
errorCode: parsed.errorCode,
|
|
1354
|
+
errorMessage: parsed.errorMessage,
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
1358
|
+
if (String(body.intent_id ?? "").trim() !== init.intentId) {
|
|
1359
|
+
throw new Error(`protocol intent mismatch: requested=${init.intentId} gateway=${String(body.intent_id ?? "")}`);
|
|
1360
|
+
}
|
|
1361
|
+
if (String(body.mandate?.authorization?.tenant_id ?? "").trim() !== this.tenantId) {
|
|
1362
|
+
throw new Error(`protocol mandate tenant mismatch: client=${this.tenantId} gateway=${String(body.mandate?.authorization?.tenant_id ?? "")}`);
|
|
1363
|
+
}
|
|
1364
|
+
return body;
|
|
1365
|
+
}
|
|
1366
|
+
async getSettlementReceiptV1(receiptId) {
|
|
1367
|
+
const enc = encodeURIComponent(receiptId);
|
|
1368
|
+
const url = `${this.base}protocol/v2/receipts/${enc}`;
|
|
1369
|
+
const res = await this.fetchWithRetries(url, {
|
|
1370
|
+
method: "GET",
|
|
1371
|
+
headers: this.headers(),
|
|
1372
|
+
});
|
|
1373
|
+
const text = await res.text();
|
|
1374
|
+
if (!res.ok) {
|
|
1375
|
+
const parsed = parseGatewayErrorEnvelope(text);
|
|
1376
|
+
throw new ProtocolHttpError(protocolHTTPErrorMessage("protocol settlement receipt", res.status, text), {
|
|
1377
|
+
statusCode: res.status,
|
|
1378
|
+
url,
|
|
1379
|
+
bodyText: text,
|
|
1380
|
+
errorCode: parsed.errorCode,
|
|
1381
|
+
errorMessage: parsed.errorMessage,
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
const body = assertJSONObject(JSON.parse(text));
|
|
1385
|
+
if (String(body.receipt_id ?? "").trim() !== receiptId) {
|
|
1386
|
+
throw new Error(`protocol receipt mismatch: requested=${receiptId} gateway=${String(body.receipt_id ?? "")}`);
|
|
1387
|
+
}
|
|
1388
|
+
if (String(body.tenant_id ?? "").trim() !== this.tenantId) {
|
|
1389
|
+
throw new Error(`protocol receipt tenant mismatch: client=${this.tenantId} gateway=${String(body.tenant_id ?? "")}`);
|
|
1390
|
+
}
|
|
1391
|
+
return body;
|
|
1392
|
+
}
|
|
1393
|
+
async verifyProtocolReceiptV1(receipt) {
|
|
1394
|
+
const url = `${this.base}protocol/v2/receipts/verify`;
|
|
1395
|
+
const res = await this.fetchWithRetries(url, {
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
1398
|
+
body: JSON.stringify(receipt),
|
|
1399
|
+
});
|
|
1400
|
+
const text = await res.text();
|
|
1401
|
+
if (!res.ok) {
|
|
1402
|
+
const parsed = parseGatewayErrorEnvelope(text);
|
|
1403
|
+
throw new ProtocolHttpError(protocolHTTPErrorMessage("protocol receipt verify", res.status, text), {
|
|
1404
|
+
statusCode: res.status,
|
|
1405
|
+
url,
|
|
1406
|
+
bodyText: text,
|
|
1407
|
+
errorCode: parsed.errorCode,
|
|
1408
|
+
errorMessage: parsed.errorMessage,
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
return assertJSONObject(JSON.parse(text));
|
|
1412
|
+
}
|
|
1413
|
+
async createHarborIntent(init) {
|
|
1414
|
+
return this.postJSON("/harbor/intents", init.body, gatewayMutationHeaders(init.recognitionProof, {
|
|
1415
|
+
...(init.idempotencyKey?.trim() ? { "idempotency-key": init.idempotencyKey.trim() } : {}),
|
|
1416
|
+
}));
|
|
1417
|
+
}
|
|
1418
|
+
async fundHarborIntent(init) {
|
|
1419
|
+
return this.postJSON(`/harbor/intents/${encodeURIComponent(init.intentId)}/fund`, {}, gatewayMutationHeaders(init.recognitionProof, {
|
|
1420
|
+
...(init.paymentSignature?.trim() ? { "payment-signature": init.paymentSignature.trim() } : {}),
|
|
1421
|
+
...(init.idempotencyKey?.trim() ? { "idempotency-key": init.idempotencyKey.trim() } : {}),
|
|
1422
|
+
}));
|
|
1423
|
+
}
|
|
1424
|
+
async submitHarborEvidence(init) {
|
|
1425
|
+
return this.postJSON(`/harbor/intents/${encodeURIComponent(init.intentId)}/evidence`, init.body, gatewayMutationHeaders(init.recognitionProof, {
|
|
1426
|
+
...(init.idempotencyKey?.trim() ? { "idempotency-key": init.idempotencyKey.trim() } : {}),
|
|
1427
|
+
}));
|
|
1428
|
+
}
|
|
1429
|
+
async confirmHarborSettlement(init) {
|
|
1430
|
+
return this.postJSON(`/harbor/intents/${encodeURIComponent(init.intentId)}/settlement/confirm`, init.body, gatewayMutationHeaders(init.recognitionProof, {
|
|
1431
|
+
...(init.idempotencyKey?.trim() ? { "idempotency-key": init.idempotencyKey.trim() } : {}),
|
|
1432
|
+
}));
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function gatewayMutationHeaders(recognitionProof, headers) {
|
|
1436
|
+
return {
|
|
1437
|
+
...(headers ?? {}),
|
|
1438
|
+
[agentRecognitionProofHeader]: encodeRecognitionProofHeader(recognitionProof),
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
function encodeRecognitionProofHeader(proof) {
|
|
1442
|
+
return Buffer.from(JSON.stringify(proof), "utf8").toString("base64url");
|
|
1443
|
+
}
|
|
818
1444
|
async function resolveGatewayTenantId(gatewayBaseUrl, apiKey, principalPath, maxRetries) {
|
|
819
1445
|
const base = normalizeBase(gatewayBaseUrl);
|
|
820
1446
|
const path = principalPath.startsWith("/") ? principalPath : `/${principalPath}`;
|
|
@@ -882,6 +1508,25 @@ export class ServiceAccountSignalSession {
|
|
|
882
1508
|
await Promise.resolve();
|
|
883
1509
|
}
|
|
884
1510
|
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Tenant-bound fraud review and metrics session for one service-account API key.
|
|
1513
|
+
*/
|
|
1514
|
+
export class ServiceAccountFraudSession {
|
|
1515
|
+
fraud;
|
|
1516
|
+
constructor(fraud) {
|
|
1517
|
+
this.fraud = fraud;
|
|
1518
|
+
}
|
|
1519
|
+
static async open(init) {
|
|
1520
|
+
const tenantId = await resolveGatewayTenantId(init.gatewayBaseUrl, init.apiKey, init.principalPath ?? DEFAULT_PRINCIPAL_PATH, Math.max(1, init.maxRetries ?? 3));
|
|
1521
|
+
return new ServiceAccountFraudSession(new GatewayFraudClient(init.gatewayBaseUrl, tenantId, {
|
|
1522
|
+
staticGatewayBearerToken: init.apiKey,
|
|
1523
|
+
maxRetries: init.maxRetries ?? 3,
|
|
1524
|
+
}));
|
|
1525
|
+
}
|
|
1526
|
+
async aclose() {
|
|
1527
|
+
await Promise.resolve();
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
885
1530
|
/**
|
|
886
1531
|
* Ergonomic intent helpers: principal-signed intent create, x402 funding, and payee-signed evidence.
|
|
887
1532
|
*/
|
|
@@ -931,12 +1576,18 @@ export class PaybondIntents {
|
|
|
931
1576
|
export class Paybond {
|
|
932
1577
|
harbor;
|
|
933
1578
|
signal;
|
|
1579
|
+
fraud;
|
|
1580
|
+
a2a;
|
|
1581
|
+
protocol;
|
|
934
1582
|
intents;
|
|
935
1583
|
session;
|
|
936
|
-
constructor(session, signal) {
|
|
1584
|
+
constructor(session, signal, fraud, a2a, protocol) {
|
|
937
1585
|
this.session = session;
|
|
938
1586
|
this.harbor = session.harbor;
|
|
939
1587
|
this.signal = signal;
|
|
1588
|
+
this.fraud = fraud;
|
|
1589
|
+
this.a2a = a2a;
|
|
1590
|
+
this.protocol = protocol;
|
|
940
1591
|
this.intents = new PaybondIntents(session.harbor);
|
|
941
1592
|
}
|
|
942
1593
|
/** Open a tenant-bound session via gateway `harbor-access` exchange. */
|
|
@@ -946,7 +1597,19 @@ export class Paybond {
|
|
|
946
1597
|
staticGatewayBearerToken: init.apiKey,
|
|
947
1598
|
maxRetries: init.maxRetries ?? 3,
|
|
948
1599
|
});
|
|
949
|
-
|
|
1600
|
+
const fraud = new GatewayFraudClient(init.gatewayBaseUrl, session.harbor.tenantId, {
|
|
1601
|
+
staticGatewayBearerToken: init.apiKey,
|
|
1602
|
+
maxRetries: init.maxRetries ?? 3,
|
|
1603
|
+
});
|
|
1604
|
+
const a2a = new GatewayA2AClient(init.gatewayBaseUrl, {
|
|
1605
|
+
staticGatewayBearerToken: init.apiKey,
|
|
1606
|
+
maxRetries: init.maxRetries ?? 3,
|
|
1607
|
+
});
|
|
1608
|
+
const protocol = new GatewayProtocolClient(init.gatewayBaseUrl, session.harbor.tenantId, {
|
|
1609
|
+
staticGatewayBearerToken: init.apiKey,
|
|
1610
|
+
maxRetries: init.maxRetries ?? 3,
|
|
1611
|
+
});
|
|
1612
|
+
return new Paybond(session, signal, fraud, a2a, protocol);
|
|
950
1613
|
}
|
|
951
1614
|
async rotateHarborToken() {
|
|
952
1615
|
await this.session.rotateHarborToken();
|