@routedock/routedock 0.2.0 → 0.3.1
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 +305 -0
- package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
- package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
- package/dist/{ProviderRegistry-BJ9oJLVz.d.cts → ProviderRegistry-CyZ5eryO.d.cts} +2 -2
- package/dist/{ProviderRegistry-C50nyhaF.d.ts → ProviderRegistry-w4_hkI_1.d.ts} +2 -2
- package/dist/{RouteDockClient-BbkqjhMG.d.ts → RouteDockClient-CXvWXix5.d.cts} +25 -9
- package/dist/{RouteDockClient-B1roCa8h.d.cts → RouteDockClient-CjM2VyOd.d.ts} +25 -9
- package/dist/{SessionReconciler-Dscw43F3.d.ts → SessionReconciler-Bw-PoBVl.d.ts} +14 -5
- package/dist/{SessionReconciler-bi0z2qtN.d.cts → SessionReconciler-Cz2nuOlJ.d.cts} +14 -5
- package/dist/{SessionStore-DvkZv8-E.d.ts → SessionStore-B4-Hi4H8.d.cts} +34 -2
- package/dist/{SessionStore-BpK8vv0L.d.cts → SessionStore-BaYiegP0.d.ts} +34 -2
- package/dist/client.cjs +432 -125
- package/dist/client.d.cts +9 -8
- package/dist/client.d.ts +9 -8
- package/dist/client.js +424 -127
- package/dist/index.cjs +655 -228
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +646 -229
- package/dist/{types-DmhmfLzP.d.cts → logger-D8_Q2ytp.d.cts} +58 -1
- package/dist/{types-DmhmfLzP.d.ts → logger-D8_Q2ytp.d.ts} +58 -1
- package/dist/provider/fastify.cjs +306 -89
- package/dist/provider/fastify.d.cts +9 -4
- package/dist/provider/fastify.d.ts +9 -4
- package/dist/provider/fastify.js +305 -88
- package/dist/provider/hono.cjs +286 -82
- package/dist/provider/hono.d.cts +9 -7
- package/dist/provider/hono.d.ts +9 -7
- package/dist/provider/hono.js +282 -78
- package/dist/provider.cjs +326 -98
- package/dist/provider.d.cts +14 -10
- package/dist/provider.d.ts +14 -10
- package/dist/provider.js +318 -98
- package/dist/react.cjs +410 -125
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +411 -126
- package/dist/{routedockMiddleware-BlDsHuPl.d.ts → routedockMiddleware-57gUaEMf.d.ts} +9 -4
- package/dist/{routedockMiddleware-D97A5ptQ.d.cts → routedockMiddleware-DlyLrxp7.d.cts} +9 -4
- package/dist/schema.cjs +40 -0
- package/dist/schema.js +40 -0
- package/dist/{sign-Dpj2Mp7k.d.ts → sign-AKH67sft.d.cts} +1 -1
- package/dist/{sign-CUf3rhTx.d.cts → sign-BEq-OsSq.d.ts} +1 -1
- package/dist/testing.cjs +6 -4
- package/dist/testing.d.cts +32 -13
- package/dist/testing.d.ts +32 -13
- package/dist/testing.js +6 -4
- package/package.json +2 -2
- package/dist/MppSessionHandler-B4TyD7Xg.d.cts +0 -121
- package/dist/MppSessionHandler-CLB5SzgN.d.ts +0 -121
package/dist/index.cjs
CHANGED
|
@@ -18,9 +18,9 @@ var path = require('path');
|
|
|
18
18
|
var facilitator = require('@x402/stellar/exact/facilitator');
|
|
19
19
|
var server$1 = require('@x402/stellar/exact/server');
|
|
20
20
|
var server = require('@x402/core/server');
|
|
21
|
-
var server$
|
|
21
|
+
var server$4 = require('@stellar/mpp/charge/server');
|
|
22
22
|
var server$2 = require('mppx/server');
|
|
23
|
-
var server$
|
|
23
|
+
var server$3 = require('@stellar/mpp/channel/server');
|
|
24
24
|
|
|
25
25
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
26
26
|
|
|
@@ -171,10 +171,35 @@ function wrapFetchError(err, context) {
|
|
|
171
171
|
if (err instanceof RouteDockError) return err;
|
|
172
172
|
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
173
173
|
}
|
|
174
|
+
function wrapMppError(err, context) {
|
|
175
|
+
if (err instanceof RouteDockError) return err;
|
|
176
|
+
if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
|
|
177
|
+
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
178
|
+
}
|
|
179
|
+
return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
|
|
180
|
+
}
|
|
174
181
|
|
|
175
182
|
// src/types.ts
|
|
176
183
|
var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
177
184
|
|
|
185
|
+
// src/internal/logger.ts
|
|
186
|
+
function createConsoleLogger() {
|
|
187
|
+
return (level, message, fields) => {
|
|
188
|
+
const values = fields ? Object.values(fields) : [];
|
|
189
|
+
if (values.length > 0) {
|
|
190
|
+
console[level](message, ...values);
|
|
191
|
+
} else {
|
|
192
|
+
console[level](message);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
var consoleLogger = createConsoleLogger();
|
|
197
|
+
var noopLogger = () => {
|
|
198
|
+
};
|
|
199
|
+
function resolveLogger(logger, fallback = consoleLogger) {
|
|
200
|
+
return logger ?? fallback;
|
|
201
|
+
}
|
|
202
|
+
|
|
178
203
|
// src/internal/retry.ts
|
|
179
204
|
var DEFAULT_RETRY_POLICY = {
|
|
180
205
|
maxAttempts: 4,
|
|
@@ -189,10 +214,11 @@ function sleep(ms) {
|
|
|
189
214
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
190
215
|
}
|
|
191
216
|
async function withRetry(fn, policy = {}) {
|
|
192
|
-
const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
|
|
217
|
+
const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
|
|
193
218
|
...DEFAULT_RETRY_POLICY,
|
|
194
219
|
...policy
|
|
195
220
|
};
|
|
221
|
+
const log = resolveLogger(logger);
|
|
196
222
|
let lastError;
|
|
197
223
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
198
224
|
try {
|
|
@@ -210,7 +236,7 @@ async function withRetry(fn, policy = {}) {
|
|
|
210
236
|
try {
|
|
211
237
|
await onRetry(attempt, err, delay);
|
|
212
238
|
} catch (cbErr) {
|
|
213
|
-
|
|
239
|
+
log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
|
|
214
240
|
}
|
|
215
241
|
}
|
|
216
242
|
await sleep(delay);
|
|
@@ -224,6 +250,84 @@ var USDC_ISSUERS = {
|
|
|
224
250
|
};
|
|
225
251
|
var usdcToUnits = nulthSdk.usdcToStroops;
|
|
226
252
|
|
|
253
|
+
// src/internal/assetUtils.ts
|
|
254
|
+
function normalizeManifestAssets(manifest) {
|
|
255
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
256
|
+
return manifest.assets;
|
|
257
|
+
}
|
|
258
|
+
if (manifest.asset && manifest.asset_contract) {
|
|
259
|
+
return [
|
|
260
|
+
{
|
|
261
|
+
asset: manifest.asset,
|
|
262
|
+
asset_contract: manifest.asset_contract
|
|
263
|
+
}
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
throw new RouteDockManifestError(
|
|
267
|
+
"Manifest must define either assets[] or root asset/asset_contract fields"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
function getEligibleAssets(manifest, mode, endpoint) {
|
|
271
|
+
const assets = normalizeManifestAssets(manifest);
|
|
272
|
+
return assets.filter((asset) => {
|
|
273
|
+
if (asset.modes && !asset.modes.includes(mode)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
|
|
277
|
+
const trimmedEndpoint = endpoint.replace(/^\//, "");
|
|
278
|
+
if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (manifest.endpoints) {
|
|
282
|
+
let pathname = endpoint;
|
|
283
|
+
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
|
|
284
|
+
try {
|
|
285
|
+
pathname = new URL(endpoint).pathname;
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const trimmedPathname = pathname.replace(/^\//, "");
|
|
290
|
+
const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
|
|
291
|
+
const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
|
|
292
|
+
const keyMatches = key === endpoint || key === trimmedEndpoint;
|
|
293
|
+
if (pathMatches || keyMatches) {
|
|
294
|
+
return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
});
|
|
298
|
+
if (!matchesEndpoint) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
|
|
309
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
310
|
+
return selectAsset(manifest, mode, endpoint).asset_contract;
|
|
311
|
+
}
|
|
312
|
+
return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
|
|
313
|
+
}
|
|
314
|
+
function selectAsset(manifest, mode, endpoint) {
|
|
315
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
316
|
+
if (eligible.length === 0) {
|
|
317
|
+
const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
|
|
318
|
+
throw new RouteDockManifestError(
|
|
319
|
+
`No eligible assets found for mode '${mode}'${endpointMsg}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return eligible[0];
|
|
323
|
+
}
|
|
324
|
+
function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
325
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
326
|
+
return eligible.some(
|
|
327
|
+
(asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
227
331
|
// src/schemas/routedock.schema.json
|
|
228
332
|
var routedock_schema_default = {
|
|
229
333
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -303,6 +407,14 @@ var routedock_schema_default = {
|
|
|
303
407
|
type: "string",
|
|
304
408
|
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
305
409
|
},
|
|
410
|
+
assets: {
|
|
411
|
+
type: "array",
|
|
412
|
+
minItems: 1,
|
|
413
|
+
items: {
|
|
414
|
+
$ref: "#/definitions/AssetConfig"
|
|
415
|
+
},
|
|
416
|
+
description: "Payment assets accepted by this provider. Each asset can be scoped to specific modes and endpoints. When present, assets[0] must match the root-level asset and asset_contract."
|
|
417
|
+
},
|
|
306
418
|
payee: {
|
|
307
419
|
type: "string",
|
|
308
420
|
description: "Stellar address (G...) that receives payments"
|
|
@@ -435,6 +547,38 @@ var routedock_schema_default = {
|
|
|
435
547
|
enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
|
|
436
548
|
description: "A supported RouteDock payment mode"
|
|
437
549
|
},
|
|
550
|
+
AssetConfig: {
|
|
551
|
+
type: "object",
|
|
552
|
+
description: "Payment asset configuration with optional mode and endpoint scoping",
|
|
553
|
+
required: ["asset", "asset_contract"],
|
|
554
|
+
additionalProperties: false,
|
|
555
|
+
properties: {
|
|
556
|
+
asset: {
|
|
557
|
+
type: "string",
|
|
558
|
+
description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
|
|
559
|
+
},
|
|
560
|
+
asset_contract: {
|
|
561
|
+
type: "string",
|
|
562
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
563
|
+
},
|
|
564
|
+
modes: {
|
|
565
|
+
type: "array",
|
|
566
|
+
items: {
|
|
567
|
+
$ref: "#/definitions/PaymentMode"
|
|
568
|
+
},
|
|
569
|
+
uniqueItems: true,
|
|
570
|
+
description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
|
|
571
|
+
},
|
|
572
|
+
endpoints: {
|
|
573
|
+
type: "array",
|
|
574
|
+
items: {
|
|
575
|
+
type: "string"
|
|
576
|
+
},
|
|
577
|
+
uniqueItems: true,
|
|
578
|
+
description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
},
|
|
438
582
|
PricingConfig: {
|
|
439
583
|
type: "object",
|
|
440
584
|
description: "Per-request pricing for x402 or mpp-charge modes",
|
|
@@ -580,7 +724,7 @@ var routedock_schema_default = {
|
|
|
580
724
|
|
|
581
725
|
// package.json
|
|
582
726
|
var package_default = {
|
|
583
|
-
version: "0.
|
|
727
|
+
version: "0.3.1"};
|
|
584
728
|
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
585
729
|
function canonicalize(value) {
|
|
586
730
|
if (Array.isArray(value)) {
|
|
@@ -704,6 +848,7 @@ function assertEndpointActive(manifest, url, logger) {
|
|
|
704
848
|
const [key, descriptor] = deprecated;
|
|
705
849
|
const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
|
|
706
850
|
logger(
|
|
851
|
+
"warn",
|
|
707
852
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
|
|
708
853
|
);
|
|
709
854
|
}
|
|
@@ -725,6 +870,27 @@ function assertManifestValid(manifest, baseUrl) {
|
|
|
725
870
|
}
|
|
726
871
|
}
|
|
727
872
|
}
|
|
873
|
+
if (manifest.assets !== void 0) {
|
|
874
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
875
|
+
throw new RouteDockManifestError(
|
|
876
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
880
|
+
const a = manifest.assets[i];
|
|
881
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
882
|
+
throw new RouteDockManifestError(
|
|
883
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const first = manifest.assets[0];
|
|
888
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
889
|
+
throw new RouteDockManifestError(
|
|
890
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
728
894
|
}
|
|
729
895
|
var CACHE_TTL_MS = 6e4;
|
|
730
896
|
var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
|
|
@@ -904,16 +1070,16 @@ function logSelection(manifest, selection, log, deprecated) {
|
|
|
904
1070
|
const reason = selection.reason ? ` (${selection.reason})` : "";
|
|
905
1071
|
if (deprecated) {
|
|
906
1072
|
log(
|
|
1073
|
+
"warn",
|
|
907
1074
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
|
|
908
1075
|
);
|
|
909
1076
|
return;
|
|
910
1077
|
}
|
|
911
|
-
log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
|
|
1078
|
+
log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
|
|
912
1079
|
}
|
|
913
1080
|
function selectMode(manifest, options = {}) {
|
|
914
1081
|
const modes = manifest.modes;
|
|
915
|
-
const log = options.logger ??
|
|
916
|
-
});
|
|
1082
|
+
const log = options.logger ?? noopLogger;
|
|
917
1083
|
const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
|
|
918
1084
|
if (options.forceMode) {
|
|
919
1085
|
if (!modes.includes(options.forceMode)) {
|
|
@@ -923,10 +1089,11 @@ function selectMode(manifest, options = {}) {
|
|
|
923
1089
|
}
|
|
924
1090
|
if (deprecatedSet.has(options.forceMode)) {
|
|
925
1091
|
log(
|
|
1092
|
+
"warn",
|
|
926
1093
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
|
|
927
1094
|
);
|
|
928
1095
|
} else {
|
|
929
|
-
log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
|
|
1096
|
+
log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
|
|
930
1097
|
}
|
|
931
1098
|
return options.forceMode;
|
|
932
1099
|
}
|
|
@@ -962,6 +1129,69 @@ function rankProvidersByLatency(manifests, targetRegion) {
|
|
|
962
1129
|
return 0;
|
|
963
1130
|
});
|
|
964
1131
|
}
|
|
1132
|
+
|
|
1133
|
+
// src/internal/payee.ts
|
|
1134
|
+
function resolvePayee(manifest, mode) {
|
|
1135
|
+
const override = manifest.pricing[mode]?.payee;
|
|
1136
|
+
return override ?? manifest.payee;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/client/challenge.ts
|
|
1140
|
+
function isRecord(value) {
|
|
1141
|
+
return typeof value === "object" && value !== null;
|
|
1142
|
+
}
|
|
1143
|
+
function isPositiveIntegerString(value) {
|
|
1144
|
+
return typeof value === "string" && /^[1-9]\d*$/.test(value);
|
|
1145
|
+
}
|
|
1146
|
+
function checkAmount(amount, maxAmount) {
|
|
1147
|
+
if (!isPositiveIntegerString(amount)) {
|
|
1148
|
+
return new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1149
|
+
}
|
|
1150
|
+
let max;
|
|
1151
|
+
try {
|
|
1152
|
+
max = nulthSdk.usdcToStroops(maxAmount);
|
|
1153
|
+
} catch {
|
|
1154
|
+
return new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1155
|
+
}
|
|
1156
|
+
if (BigInt(amount) > max) {
|
|
1157
|
+
return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
|
|
1158
|
+
}
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
function checkX402Accept(accept, manifest, network, maxAmount) {
|
|
1162
|
+
if (accept.network !== network) {
|
|
1163
|
+
return new RouteDockPolicyRejectError("challenge_network_mismatch");
|
|
1164
|
+
}
|
|
1165
|
+
if (accept.payTo !== resolvePayee(manifest, "x402")) {
|
|
1166
|
+
return new RouteDockPolicyRejectError("challenge_payee_mismatch");
|
|
1167
|
+
}
|
|
1168
|
+
if (accept.asset !== manifest.asset_contract) {
|
|
1169
|
+
return new RouteDockPolicyRejectError("challenge_asset_mismatch");
|
|
1170
|
+
}
|
|
1171
|
+
return checkAmount(accept.amount, maxAmount);
|
|
1172
|
+
}
|
|
1173
|
+
function filterX402Accepts(accepts, manifest, network, maxAmount) {
|
|
1174
|
+
return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
|
|
1175
|
+
}
|
|
1176
|
+
function checkChargeChallenge(request, manifest, network, maxAmount) {
|
|
1177
|
+
const methodDetails = request["methodDetails"];
|
|
1178
|
+
const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
|
|
1179
|
+
const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
|
|
1180
|
+
if (challengeNetwork !== network) {
|
|
1181
|
+
return new RouteDockPolicyRejectError("challenge_network_mismatch");
|
|
1182
|
+
}
|
|
1183
|
+
const recipient = request["recipient"];
|
|
1184
|
+
if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
|
|
1185
|
+
return new RouteDockPolicyRejectError("challenge_payee_mismatch");
|
|
1186
|
+
}
|
|
1187
|
+
const currency = request["currency"];
|
|
1188
|
+
if (typeof currency !== "string" || currency !== manifest.asset_contract) {
|
|
1189
|
+
return new RouteDockPolicyRejectError("challenge_asset_mismatch");
|
|
1190
|
+
}
|
|
1191
|
+
return checkAmount(request["amount"], maxAmount);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/client/x402Client.ts
|
|
965
1195
|
var CAIP2 = {
|
|
966
1196
|
testnet: "stellar:testnet",
|
|
967
1197
|
mainnet: "stellar:pubnet"
|
|
@@ -993,83 +1223,106 @@ var X402Client = class _X402Client {
|
|
|
993
1223
|
if (!pricing.facilitator) {
|
|
994
1224
|
throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
|
|
995
1225
|
}
|
|
996
|
-
|
|
997
|
-
|
|
1226
|
+
const caip2 = CAIP2[this.network];
|
|
1227
|
+
const probe = await withRetry(async () => {
|
|
1228
|
+
let init2;
|
|
998
1229
|
try {
|
|
999
|
-
|
|
1230
|
+
init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
|
|
1000
1231
|
} catch (err) {
|
|
1001
1232
|
throw wrapFetchError(err, "x402 initial request");
|
|
1002
1233
|
}
|
|
1003
|
-
if (
|
|
1004
|
-
if (!
|
|
1234
|
+
if (init2.status !== 402) {
|
|
1235
|
+
if (!init2.ok) {
|
|
1005
1236
|
throw httpStatusToError(
|
|
1006
|
-
`x402 initial request failed: HTTP ${
|
|
1007
|
-
|
|
1008
|
-
|
|
1237
|
+
`x402 initial request failed: HTTP ${init2.status}`,
|
|
1238
|
+
init2.status,
|
|
1239
|
+
init2
|
|
1009
1240
|
);
|
|
1010
1241
|
}
|
|
1011
1242
|
let data2;
|
|
1012
1243
|
try {
|
|
1013
|
-
data2 = await
|
|
1244
|
+
data2 = await init2.json();
|
|
1014
1245
|
} catch {
|
|
1015
|
-
throw new RouteDockManifestError(
|
|
1246
|
+
throw new RouteDockManifestError(
|
|
1247
|
+
`Failed to parse JSON from non-402 response (HTTP ${init2.status})`
|
|
1248
|
+
);
|
|
1016
1249
|
}
|
|
1017
|
-
return {
|
|
1018
|
-
}
|
|
1019
|
-
const reqHeader = init.headers.get("X-Payment-Requirements");
|
|
1020
|
-
if (!reqHeader) {
|
|
1021
|
-
throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
|
|
1250
|
+
return { kind: "free", data: data2 };
|
|
1022
1251
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1252
|
+
return { kind: "payment-required", init: init2 };
|
|
1253
|
+
}, this.retryPolicy);
|
|
1254
|
+
if (probe.kind === "free") {
|
|
1255
|
+
return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
|
|
1256
|
+
}
|
|
1257
|
+
const init = probe.init;
|
|
1258
|
+
const reqHeader = init.headers.get("X-Payment-Requirements");
|
|
1259
|
+
if (!reqHeader) {
|
|
1260
|
+
throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
|
|
1261
|
+
}
|
|
1262
|
+
let paymentRequired;
|
|
1263
|
+
try {
|
|
1264
|
+
paymentRequired = http.decodePaymentRequiredHeader(reqHeader);
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
|
|
1267
|
+
cause: err
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
const firstAccept = paymentRequired.accepts[0];
|
|
1271
|
+
if (!firstAccept) {
|
|
1272
|
+
throw new RouteDockManifestError("402 response carries no payment accepts");
|
|
1273
|
+
}
|
|
1274
|
+
const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
|
|
1275
|
+
if (allowed.length === 0) {
|
|
1276
|
+
throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
|
|
1277
|
+
}
|
|
1278
|
+
let paymentPayload;
|
|
1279
|
+
try {
|
|
1280
|
+
paymentPayload = await this.httpClient.createPaymentPayload({
|
|
1281
|
+
...paymentRequired,
|
|
1282
|
+
accepts: allowed
|
|
1283
|
+
});
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
|
|
1286
|
+
cause: err
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
1290
|
+
const signedAmount = nulthSdk.stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
|
|
1291
|
+
const settled = await withRetry(async () => {
|
|
1292
|
+
let response;
|
|
1041
1293
|
try {
|
|
1042
|
-
|
|
1294
|
+
response = await fetch(url, { headers: paymentHeaders });
|
|
1043
1295
|
} catch (err) {
|
|
1044
1296
|
throw wrapFetchError(err, "x402 settlement request");
|
|
1045
1297
|
}
|
|
1046
|
-
if (!
|
|
1047
|
-
if (
|
|
1298
|
+
if (!response.ok) {
|
|
1299
|
+
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
1048
1300
|
throw httpStatusToError(
|
|
1049
|
-
`x402 payment failed: HTTP ${
|
|
1050
|
-
|
|
1051
|
-
|
|
1301
|
+
`x402 payment failed: HTTP ${response.status}`,
|
|
1302
|
+
response.status,
|
|
1303
|
+
response
|
|
1052
1304
|
);
|
|
1053
1305
|
}
|
|
1054
|
-
throw new RouteDockManifestError(`x402 payment failed: HTTP ${
|
|
1306
|
+
throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
|
|
1055
1307
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
txHash = settleResponse.transaction ?? null;
|
|
1062
|
-
} catch {
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
let data;
|
|
1308
|
+
return response;
|
|
1309
|
+
}, this.retryPolicy);
|
|
1310
|
+
let txHash = null;
|
|
1311
|
+
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1312
|
+
if (responseHeader) {
|
|
1066
1313
|
try {
|
|
1067
|
-
|
|
1314
|
+
const settleResponse = http.decodePaymentResponseHeader(responseHeader);
|
|
1315
|
+
txHash = settleResponse.transaction ?? null;
|
|
1068
1316
|
} catch {
|
|
1069
|
-
throw new RouteDockManifestError("Failed to parse JSON from settled response");
|
|
1070
1317
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1318
|
+
}
|
|
1319
|
+
let data;
|
|
1320
|
+
try {
|
|
1321
|
+
data = await settled.json();
|
|
1322
|
+
} catch {
|
|
1323
|
+
throw new RouteDockManifestError("Failed to parse JSON from settled response");
|
|
1324
|
+
}
|
|
1325
|
+
return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
|
|
1073
1326
|
}
|
|
1074
1327
|
};
|
|
1075
1328
|
var MppChargeClient = class {
|
|
@@ -1086,27 +1339,42 @@ var MppChargeClient = class {
|
|
|
1086
1339
|
if (!pricing) {
|
|
1087
1340
|
throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
|
|
1088
1341
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1342
|
+
const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
|
|
1343
|
+
let txHash = null;
|
|
1344
|
+
let signedAmount;
|
|
1345
|
+
let credential;
|
|
1346
|
+
const mppx = client$2.Mppx.create({
|
|
1347
|
+
polyfill: false,
|
|
1348
|
+
methods: [
|
|
1349
|
+
client$3.stellar.charge({
|
|
1350
|
+
keypair: this.keypair,
|
|
1351
|
+
mode: "pull",
|
|
1352
|
+
onProgress(event) {
|
|
1353
|
+
if (event.type === "paid") {
|
|
1354
|
+
txHash = event.hash;
|
|
1101
1355
|
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1356
|
+
}
|
|
1357
|
+
})
|
|
1358
|
+
],
|
|
1359
|
+
onChallenge: async (challenge, { createCredential }) => {
|
|
1360
|
+
const request = challenge.request;
|
|
1361
|
+
const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
|
|
1362
|
+
if (rejection) throw rejection;
|
|
1363
|
+
const amount = request["amount"];
|
|
1364
|
+
if (typeof amount !== "string") {
|
|
1365
|
+
throw new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1366
|
+
}
|
|
1367
|
+
signedAmount = nulthSdk.stroopsToUsdc(BigInt(amount));
|
|
1368
|
+
credential ??= await createCredential();
|
|
1369
|
+
return credential;
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
return withRetry(async () => {
|
|
1105
1373
|
let response;
|
|
1106
1374
|
try {
|
|
1107
|
-
response = await mppx.fetch(url);
|
|
1375
|
+
response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
|
|
1108
1376
|
} catch (err) {
|
|
1109
|
-
throw
|
|
1377
|
+
throw wrapMppError(err, "MPP charge request");
|
|
1110
1378
|
}
|
|
1111
1379
|
if (!response.ok) {
|
|
1112
1380
|
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
@@ -1127,7 +1395,13 @@ var MppChargeClient = class {
|
|
|
1127
1395
|
{ cause }
|
|
1128
1396
|
);
|
|
1129
1397
|
}
|
|
1130
|
-
return {
|
|
1398
|
+
return {
|
|
1399
|
+
data,
|
|
1400
|
+
txHash,
|
|
1401
|
+
mode: "mpp-charge",
|
|
1402
|
+
amount: signedAmount ?? pricing.amount,
|
|
1403
|
+
timestamp: Date.now()
|
|
1404
|
+
};
|
|
1131
1405
|
}, this.retryPolicy);
|
|
1132
1406
|
}
|
|
1133
1407
|
};
|
|
@@ -1166,16 +1440,18 @@ function resolveStoreNetworkId(network) {
|
|
|
1166
1440
|
return network === mpp.STELLAR_PUBNET ? mpp.STELLAR_PUBNET : mpp.STELLAR_TESTNET;
|
|
1167
1441
|
}
|
|
1168
1442
|
var MppSessionClient = class {
|
|
1169
|
-
constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
|
|
1443
|
+
constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
|
|
1170
1444
|
this.keypair = keypair;
|
|
1171
1445
|
this.network = network;
|
|
1172
1446
|
this.retryPolicy = retryPolicy;
|
|
1173
1447
|
this.webSocketFactory = webSocketFactory;
|
|
1448
|
+
this.logger = logger;
|
|
1174
1449
|
}
|
|
1175
1450
|
keypair;
|
|
1176
1451
|
network;
|
|
1177
1452
|
retryPolicy;
|
|
1178
1453
|
webSocketFactory;
|
|
1454
|
+
logger;
|
|
1179
1455
|
async openSession(url, manifest, commitmentSecret, options, onSpend) {
|
|
1180
1456
|
const mode = options?.mode ?? "mpp-session";
|
|
1181
1457
|
const pricing = manifest.pricing[mode];
|
|
@@ -1400,7 +1676,7 @@ var MppSessionClient = class {
|
|
|
1400
1676
|
if (abortController.signal.aborted) {
|
|
1401
1677
|
throw err;
|
|
1402
1678
|
}
|
|
1403
|
-
throw
|
|
1679
|
+
throw wrapMppError(err, "Voucher request");
|
|
1404
1680
|
}
|
|
1405
1681
|
if (!resp.ok) {
|
|
1406
1682
|
if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
|
|
@@ -1693,9 +1969,10 @@ var MppSessionClient = class {
|
|
|
1693
1969
|
emit("session:timeout", { maxDurationMs });
|
|
1694
1970
|
void handle.close().catch((error) => {
|
|
1695
1971
|
emit("session:close-failed", { maxDurationMs, error });
|
|
1696
|
-
|
|
1972
|
+
this.logger(
|
|
1973
|
+
"warn",
|
|
1697
1974
|
`RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
|
|
1698
|
-
error
|
|
1975
|
+
{ error }
|
|
1699
1976
|
);
|
|
1700
1977
|
});
|
|
1701
1978
|
}, maxDurationMs);
|
|
@@ -1745,7 +2022,7 @@ var MppSessionClient = class {
|
|
|
1745
2022
|
);
|
|
1746
2023
|
onSigned?.();
|
|
1747
2024
|
} catch (err) {
|
|
1748
|
-
throw
|
|
2025
|
+
throw wrapMppError(err, "Voucher credential");
|
|
1749
2026
|
}
|
|
1750
2027
|
const wsUrl = url.replace(/^http/, "ws");
|
|
1751
2028
|
const queue = [];
|
|
@@ -1922,11 +2199,6 @@ function encodeAuthSignature(proof) {
|
|
|
1922
2199
|
const payload = { nulth: "zk-v1", proof };
|
|
1923
2200
|
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
1924
2201
|
}
|
|
1925
|
-
function usdcToStroops2(amount) {
|
|
1926
|
-
const [whole = "0", frac = ""] = amount.split(".");
|
|
1927
|
-
const padded = (frac + "0000000").slice(0, 7);
|
|
1928
|
-
return BigInt(whole) * 10000000n + BigInt(padded);
|
|
1929
|
-
}
|
|
1930
2202
|
var NulthClient = class {
|
|
1931
2203
|
constructor(config) {
|
|
1932
2204
|
this.config = config;
|
|
@@ -1942,7 +2214,10 @@ var NulthClient = class {
|
|
|
1942
2214
|
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
1943
2215
|
);
|
|
1944
2216
|
}
|
|
1945
|
-
|
|
2217
|
+
resolveLogger(config.logger)(
|
|
2218
|
+
"warn",
|
|
2219
|
+
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2220
|
+
);
|
|
1946
2221
|
}
|
|
1947
2222
|
config;
|
|
1948
2223
|
policy;
|
|
@@ -1994,6 +2269,12 @@ var NulthClient = class {
|
|
|
1994
2269
|
return proof;
|
|
1995
2270
|
}
|
|
1996
2271
|
enforcePolicy(context) {
|
|
2272
|
+
if (context.amountStroops < 0n) {
|
|
2273
|
+
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2274
|
+
}
|
|
2275
|
+
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2276
|
+
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2277
|
+
}
|
|
1997
2278
|
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
1998
2279
|
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
1999
2280
|
}
|
|
@@ -2028,14 +2309,14 @@ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
|
2028
2309
|
const pricing = manifest.pricing[mode];
|
|
2029
2310
|
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
2030
2311
|
return {
|
|
2031
|
-
payee: manifest.payee,
|
|
2032
|
-
amountStroops:
|
|
2312
|
+
payee: pricing.payee ?? manifest.payee,
|
|
2313
|
+
amountStroops: nulthSdk.usdcToStroops(pricing.amount),
|
|
2033
2314
|
assetContract: manifest.asset_contract,
|
|
2034
2315
|
ledgerSequence
|
|
2035
2316
|
};
|
|
2036
2317
|
}
|
|
2037
2318
|
function createPolicyState(input) {
|
|
2038
|
-
const dailyCapStroops =
|
|
2319
|
+
const dailyCapStroops = nulthSdk.usdcToStroops(input.dailyCapUsdc);
|
|
2039
2320
|
return {
|
|
2040
2321
|
dailyCapStroops,
|
|
2041
2322
|
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
@@ -2137,7 +2418,8 @@ var InMemorySpendStore = class {
|
|
|
2137
2418
|
state = null;
|
|
2138
2419
|
constructor(options = {}) {
|
|
2139
2420
|
if (options.warn !== false) {
|
|
2140
|
-
|
|
2421
|
+
resolveLogger(options.logger)(
|
|
2422
|
+
"warn",
|
|
2141
2423
|
"[RouteDock] Using in-memory SpendStore: the daily spend cap is NOT durable and resets on every process restart. Inject a persistent SpendStore via RouteDockClientConfig.spendStore for production safety."
|
|
2142
2424
|
);
|
|
2143
2425
|
}
|
|
@@ -2227,7 +2509,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2227
2509
|
this.network = config.network;
|
|
2228
2510
|
this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
|
|
2229
2511
|
this.retryPolicy = config.retryPolicy;
|
|
2230
|
-
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2512
|
+
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2513
|
+
warn: !!config.spendCap,
|
|
2514
|
+
...config.logger && { logger: config.logger }
|
|
2515
|
+
});
|
|
2231
2516
|
this.logger = config.logger;
|
|
2232
2517
|
this.manifestTimeoutMs = config.manifestTimeoutMs;
|
|
2233
2518
|
this.expectedPayee = config.expectedPayee;
|
|
@@ -2239,9 +2524,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2239
2524
|
_secrets.set(this, config.commitmentSecret);
|
|
2240
2525
|
}
|
|
2241
2526
|
const secretKey = this.keypair.secret();
|
|
2242
|
-
this.
|
|
2243
|
-
this.
|
|
2244
|
-
this.
|
|
2527
|
+
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2528
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2529
|
+
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2530
|
+
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2245
2531
|
}
|
|
2246
2532
|
/** Fetch manifest and select mode — shared by pay() and estimateCost(). */
|
|
2247
2533
|
async _resolveManifest(url, options) {
|
|
@@ -2270,12 +2556,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2270
2556
|
* call before committing to a payment — for approval gates and manual
|
|
2271
2557
|
* trustline remediation.
|
|
2272
2558
|
*/
|
|
2273
|
-
async preflight(manifest) {
|
|
2559
|
+
async preflight(manifest, mode, endpoint) {
|
|
2274
2560
|
assertManifestValid(manifest);
|
|
2275
|
-
|
|
2561
|
+
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2562
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2276
2563
|
return {
|
|
2277
2564
|
hasTrustline: true,
|
|
2278
|
-
asset:
|
|
2565
|
+
asset: selectedAsset,
|
|
2279
2566
|
modes: manifest.modes,
|
|
2280
2567
|
...manifest.regions && { regions: manifest.regions },
|
|
2281
2568
|
...manifest.latency_hints && { latency_hints: manifest.latency_hints },
|
|
@@ -2289,8 +2576,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2289
2576
|
* trustline is missing. Results are cached per (account, asset) so the
|
|
2290
2577
|
* per-payment overhead is one RPC call amortized to zero after the first.
|
|
2291
2578
|
*/
|
|
2292
|
-
async _checkTrustline(manifest) {
|
|
2293
|
-
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${
|
|
2579
|
+
async _checkTrustline(manifest, targetAsset = manifest.asset) {
|
|
2580
|
+
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2294
2581
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2295
2582
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2296
2583
|
const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
|
|
@@ -2298,13 +2585,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2298
2585
|
try {
|
|
2299
2586
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2300
2587
|
const balances = account.balances;
|
|
2301
|
-
const expectedIssuer = getAssetIssuer(
|
|
2588
|
+
const expectedIssuer = getAssetIssuer(targetAsset, this.network);
|
|
2302
2589
|
const hasTrustline = balances.some(
|
|
2303
|
-
(b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code ===
|
|
2590
|
+
(b) => typeof b === "object" && b !== null && (targetAsset === "XLM" && b.asset_type === "native" || "asset_code" in b && b.asset_code === targetAsset && (!expectedIssuer || b.asset_issuer === expectedIssuer))
|
|
2304
2591
|
);
|
|
2305
2592
|
if (!hasTrustline) {
|
|
2306
|
-
const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${
|
|
2307
|
-
throw new RouteDockTrustlineError(
|
|
2593
|
+
const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${targetAsset}:${expectedIssuer} --limit 100000` : `Establish a trustline for ${targetAsset} with the appropriate issuer on ${this.network}`;
|
|
2594
|
+
throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
|
|
2308
2595
|
}
|
|
2309
2596
|
_RouteDockClient._trustlineCache.set(cacheKey, {
|
|
2310
2597
|
exists: true,
|
|
@@ -2313,7 +2600,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2313
2600
|
} catch (err) {
|
|
2314
2601
|
if (err instanceof RouteDockTrustlineError) throw err;
|
|
2315
2602
|
this.logger?.(
|
|
2316
|
-
|
|
2603
|
+
"warn",
|
|
2604
|
+
`[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
|
|
2317
2605
|
);
|
|
2318
2606
|
}
|
|
2319
2607
|
}
|
|
@@ -2321,18 +2609,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2321
2609
|
* Pay for one request at `url`. Fetches manifest, selects payment mode,
|
|
2322
2610
|
* runs trustline preflight, reserves local spend cap BEFORE executing the
|
|
2323
2611
|
* payment, then commits the spend record on success. Rolls back the
|
|
2324
|
-
* reservation if the on-chain payment fails
|
|
2325
|
-
*
|
|
2612
|
+
* reservation if the on-chain payment fails, and also when the call
|
|
2613
|
+
* completes without paying anything (an x402 endpoint answering a plain
|
|
2614
|
+
* 200 with no challenge). Concurrent pay() calls are serialized through a
|
|
2615
|
+
* per-instance mutex to prevent spend-cap overrun.
|
|
2326
2616
|
*/
|
|
2327
2617
|
async pay(url, options) {
|
|
2328
2618
|
const baseUrl = new URL(url).origin;
|
|
2329
2619
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2330
2620
|
assertEndpointActive(manifest, url, this.logger);
|
|
2331
2621
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
return this._payWithNulthVault(url, manifest, mode);
|
|
2335
|
-
}
|
|
2622
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2623
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2336
2624
|
let amount;
|
|
2337
2625
|
switch (mode) {
|
|
2338
2626
|
case "x402":
|
|
@@ -2352,22 +2640,30 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2352
2640
|
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2353
2641
|
let result;
|
|
2354
2642
|
try {
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2643
|
+
if (this.vault?.mode === "nulth") {
|
|
2644
|
+
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2645
|
+
} else {
|
|
2646
|
+
switch (mode) {
|
|
2647
|
+
case "x402":
|
|
2648
|
+
result = await this.x402.pay(url, manifest);
|
|
2649
|
+
break;
|
|
2650
|
+
case "mpp-charge":
|
|
2651
|
+
result = await this.charge.pay(url, manifest);
|
|
2652
|
+
break;
|
|
2653
|
+
default:
|
|
2654
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2655
|
+
}
|
|
2364
2656
|
}
|
|
2365
2657
|
} catch (err) {
|
|
2366
2658
|
await this._rollbackSpend(reserveId).catch(() => {
|
|
2367
2659
|
});
|
|
2368
2660
|
throw err;
|
|
2369
2661
|
}
|
|
2370
|
-
|
|
2662
|
+
if (result.txHash === null && result.amount === "0") {
|
|
2663
|
+
await this._rollbackSpend(reserveId);
|
|
2664
|
+
} else {
|
|
2665
|
+
await this._commitSpend(reserveId);
|
|
2666
|
+
}
|
|
2371
2667
|
return result;
|
|
2372
2668
|
}
|
|
2373
2669
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
@@ -2417,9 +2713,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2417
2713
|
default:
|
|
2418
2714
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2419
2715
|
}
|
|
2716
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2420
2717
|
return {
|
|
2421
2718
|
amount,
|
|
2422
|
-
asset:
|
|
2719
|
+
asset: selectedAsset,
|
|
2423
2720
|
mode,
|
|
2424
2721
|
manifest,
|
|
2425
2722
|
...manifest.regions && { regions: manifest.regions },
|
|
@@ -2656,7 +2953,7 @@ var SupabaseSeenTxStore = class {
|
|
|
2656
2953
|
}
|
|
2657
2954
|
supabase;
|
|
2658
2955
|
async get(key) {
|
|
2659
|
-
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
|
|
2956
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
|
|
2660
2957
|
if (error) {
|
|
2661
2958
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
2662
2959
|
}
|
|
@@ -2664,7 +2961,8 @@ var SupabaseSeenTxStore = class {
|
|
|
2664
2961
|
const headers = data.headers;
|
|
2665
2962
|
return {
|
|
2666
2963
|
txHash: data.tx_hash ?? null,
|
|
2667
|
-
...headers ? { headers } : {}
|
|
2964
|
+
...headers ? { headers } : {},
|
|
2965
|
+
createdAt: Date.parse(data.created_at)
|
|
2668
2966
|
};
|
|
2669
2967
|
}
|
|
2670
2968
|
async set(key, record) {
|
|
@@ -2672,7 +2970,8 @@ var SupabaseSeenTxStore = class {
|
|
|
2672
2970
|
{
|
|
2673
2971
|
key,
|
|
2674
2972
|
tx_hash: record.txHash,
|
|
2675
|
-
headers: record.headers ?? null
|
|
2973
|
+
headers: record.headers ?? null,
|
|
2974
|
+
created_at: new Date(record.createdAt ?? Date.now()).toISOString()
|
|
2676
2975
|
},
|
|
2677
2976
|
{ onConflict: "key" }
|
|
2678
2977
|
);
|
|
@@ -2680,21 +2979,47 @@ var SupabaseSeenTxStore = class {
|
|
|
2680
2979
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
2681
2980
|
}
|
|
2682
2981
|
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Atomically increment the settlement's replay counter through the
|
|
2984
|
+
* `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
|
|
2985
|
+
* row lock gives concurrent claims distinct counts. Returns `Infinity` when
|
|
2986
|
+
* the key no longer exists, so the caller treats it as spent.
|
|
2987
|
+
*/
|
|
2988
|
+
async claimReplay(key) {
|
|
2989
|
+
const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
|
|
2990
|
+
p_key: key
|
|
2991
|
+
});
|
|
2992
|
+
if (error) {
|
|
2993
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
|
|
2994
|
+
}
|
|
2995
|
+
if (data == null) return Number.POSITIVE_INFINITY;
|
|
2996
|
+
return data;
|
|
2997
|
+
}
|
|
2683
2998
|
};
|
|
2684
|
-
|
|
2685
|
-
// src/provider/payee.ts
|
|
2686
|
-
function resolvePayee(manifest, mode) {
|
|
2687
|
-
const override = manifest.pricing[mode]?.payee;
|
|
2688
|
-
return override ?? manifest.payee;
|
|
2689
|
-
}
|
|
2690
2999
|
function extractPayerAddress(key) {
|
|
2691
3000
|
if (typeof key !== "string") return null;
|
|
2692
3001
|
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
2693
3002
|
}
|
|
2694
3003
|
|
|
2695
3004
|
// src/provider/SeenTxStore.ts
|
|
3005
|
+
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
3006
|
+
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
3007
|
+
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
3008
|
+
const record = await store.get(key);
|
|
3009
|
+
if (!record) return { kind: "miss" };
|
|
3010
|
+
const { createdAt } = record;
|
|
3011
|
+
if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
|
|
3012
|
+
return { kind: "spent" };
|
|
3013
|
+
}
|
|
3014
|
+
if (store.claimReplay) {
|
|
3015
|
+
const replayed = await store.claimReplay(key);
|
|
3016
|
+
if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
|
|
3017
|
+
}
|
|
3018
|
+
return { kind: "replay", record };
|
|
3019
|
+
}
|
|
2696
3020
|
var InMemorySeenTxStore = class {
|
|
2697
3021
|
map = /* @__PURE__ */ new Map();
|
|
3022
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
2698
3023
|
order = [];
|
|
2699
3024
|
constructor(maxEntriesOrOptions = 1e4) {
|
|
2700
3025
|
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
@@ -2702,7 +3027,8 @@ var InMemorySeenTxStore = class {
|
|
|
2702
3027
|
if (options.warn !== false) {
|
|
2703
3028
|
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
2704
3029
|
if (isServerless) {
|
|
2705
|
-
|
|
3030
|
+
resolveLogger(options.logger)(
|
|
3031
|
+
"warn",
|
|
2706
3032
|
"[RouteDock] Using in-memory SeenTxStore: settlement deduplication is NOT durable and resets on every isolate restart. Supply a SupabaseSeenTxStore (or other durable SeenTxStore) for production safety."
|
|
2707
3033
|
);
|
|
2708
3034
|
}
|
|
@@ -2717,21 +3043,43 @@ var InMemorySeenTxStore = class {
|
|
|
2717
3043
|
this.order.push(key);
|
|
2718
3044
|
if (this.order.length > this.maxEntries) {
|
|
2719
3045
|
const evicted = this.order.shift();
|
|
2720
|
-
if (evicted !== void 0)
|
|
3046
|
+
if (evicted !== void 0) {
|
|
3047
|
+
this.map.delete(evicted);
|
|
3048
|
+
this.replayCounts.delete(evicted);
|
|
3049
|
+
}
|
|
2721
3050
|
}
|
|
2722
3051
|
}
|
|
2723
3052
|
this.map.set(key, record);
|
|
2724
3053
|
}
|
|
3054
|
+
claimReplay(key) {
|
|
3055
|
+
if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
|
|
3056
|
+
const next = (this.replayCounts.get(key) ?? 0) + 1;
|
|
3057
|
+
this.replayCounts.set(key, next);
|
|
3058
|
+
return next;
|
|
3059
|
+
}
|
|
2725
3060
|
};
|
|
2726
3061
|
async function sha256Hex2(input) {
|
|
2727
3062
|
const buf = new TextEncoder().encode(input);
|
|
2728
3063
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
2729
3064
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2730
3065
|
}
|
|
2731
|
-
async function paymentIdempotencyKey(getHeader) {
|
|
3066
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
2732
3067
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
2733
3068
|
if (!material) return null;
|
|
2734
|
-
return sha256Hex2(
|
|
3069
|
+
return sha256Hex2(
|
|
3070
|
+
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// src/provider/settleResult.ts
|
|
3075
|
+
function readSettleResult(result) {
|
|
3076
|
+
const settle = result;
|
|
3077
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
3078
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
3079
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
3080
|
+
return failed(settle.errorReason);
|
|
3081
|
+
}
|
|
3082
|
+
return { ok: true, txHash: settle.transaction };
|
|
2735
3083
|
}
|
|
2736
3084
|
|
|
2737
3085
|
// src/provider/x402Handler.ts
|
|
@@ -2744,7 +3092,8 @@ function createX402Handler(opts) {
|
|
|
2744
3092
|
const caip2 = CAIP22[opts.network];
|
|
2745
3093
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
2746
3094
|
const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
2747
|
-
const
|
|
3095
|
+
const logger = resolveLogger(opts.logger);
|
|
3096
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
2748
3097
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
2749
3098
|
const localFacilitator = new facilitator.ExactStellarScheme([signer], {
|
|
2750
3099
|
areFeesSponsored: true
|
|
@@ -2765,22 +3114,29 @@ function createX402Handler(opts) {
|
|
|
2765
3114
|
}
|
|
2766
3115
|
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
2767
3116
|
const payTo = resolvePayee(opts.manifest, "x402");
|
|
2768
|
-
const requirements = {
|
|
2769
|
-
scheme: "exact",
|
|
2770
|
-
network: caip2,
|
|
2771
|
-
asset: opts.assetContract,
|
|
2772
|
-
amount: amountInBaseUnits,
|
|
2773
|
-
payTo,
|
|
2774
|
-
maxTimeoutSeconds: 60,
|
|
2775
|
-
extra: {
|
|
2776
|
-
areFeesSponsored: true,
|
|
2777
|
-
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
2778
|
-
}
|
|
2779
|
-
};
|
|
2780
3117
|
return async (req, res, next) => {
|
|
2781
3118
|
try {
|
|
2782
|
-
const
|
|
2783
|
-
|
|
3119
|
+
const endpoint = req.path || req.originalUrl;
|
|
3120
|
+
const assetContract = resolveAssetContract(
|
|
3121
|
+
opts.manifest,
|
|
3122
|
+
"x402",
|
|
3123
|
+
endpoint,
|
|
3124
|
+
opts.assetContract
|
|
3125
|
+
);
|
|
3126
|
+
const requirements = {
|
|
3127
|
+
scheme: "exact",
|
|
3128
|
+
network: caip2,
|
|
3129
|
+
asset: assetContract,
|
|
3130
|
+
amount: amountInBaseUnits,
|
|
3131
|
+
payTo,
|
|
3132
|
+
maxTimeoutSeconds: 60,
|
|
3133
|
+
extra: {
|
|
3134
|
+
areFeesSponsored: true,
|
|
3135
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
const respondPaymentRequired = async (error, reason) => {
|
|
3139
|
+
const body = { error, ...reason ? { reason } : {} };
|
|
2784
3140
|
if (ozServer) {
|
|
2785
3141
|
const resourceInfo = {
|
|
2786
3142
|
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
@@ -2790,7 +3146,7 @@ function createX402Handler(opts) {
|
|
|
2790
3146
|
[requirements],
|
|
2791
3147
|
resourceInfo
|
|
2792
3148
|
);
|
|
2793
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(
|
|
3149
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
2794
3150
|
} else {
|
|
2795
3151
|
const x402Response = {
|
|
2796
3152
|
x402Version: 2,
|
|
@@ -2800,19 +3156,35 @@ function createX402Handler(opts) {
|
|
|
2800
3156
|
},
|
|
2801
3157
|
accepts: [requirements]
|
|
2802
3158
|
};
|
|
2803
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(
|
|
3159
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
|
|
2804
3160
|
}
|
|
3161
|
+
};
|
|
3162
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
3163
|
+
if (!paymentHeader) {
|
|
3164
|
+
await respondPaymentRequired("Payment Required");
|
|
2805
3165
|
return;
|
|
2806
3166
|
}
|
|
2807
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3167
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
3168
|
+
(name) => {
|
|
3169
|
+
const v = req.headers[name.toLowerCase()];
|
|
3170
|
+
return Array.isArray(v) ? v[0] : v;
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
method: req.method,
|
|
3174
|
+
path: req.originalUrl.split("?")[0],
|
|
3175
|
+
amount: requirements.amount,
|
|
3176
|
+
payTo: requirements.payTo
|
|
3177
|
+
}
|
|
3178
|
+
);
|
|
2811
3179
|
if (idempotencyKey) {
|
|
2812
|
-
const
|
|
2813
|
-
if (
|
|
2814
|
-
|
|
2815
|
-
|
|
3180
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
3181
|
+
if (replayCheck.kind === "spent") {
|
|
3182
|
+
res.status(402).json({ error: "Payment already used" });
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
if (replayCheck.kind === "replay") {
|
|
3186
|
+
if (replayCheck.record.headers) {
|
|
3187
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
2816
3188
|
res.setHeader(k, val);
|
|
2817
3189
|
}
|
|
2818
3190
|
}
|
|
@@ -2831,15 +3203,18 @@ function createX402Handler(opts) {
|
|
|
2831
3203
|
}
|
|
2832
3204
|
if (ozServer) {
|
|
2833
3205
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
2834
|
-
|
|
2835
|
-
if (
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
http.encodePaymentResponseHeader(
|
|
2839
|
-
settleResult
|
|
2840
|
-
)
|
|
2841
|
-
);
|
|
3206
|
+
const outcome = readSettleResult(settleResult);
|
|
3207
|
+
if (!outcome.ok) {
|
|
3208
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
3209
|
+
return;
|
|
2842
3210
|
}
|
|
3211
|
+
txHash = outcome.txHash;
|
|
3212
|
+
res.setHeader(
|
|
3213
|
+
"X-Payment-Response",
|
|
3214
|
+
http.encodePaymentResponseHeader(
|
|
3215
|
+
settleResult
|
|
3216
|
+
)
|
|
3217
|
+
);
|
|
2843
3218
|
} else {
|
|
2844
3219
|
const verifyResult = await localFacilitator.verify(
|
|
2845
3220
|
payload,
|
|
@@ -2856,15 +3231,18 @@ function createX402Handler(opts) {
|
|
|
2856
3231
|
payload,
|
|
2857
3232
|
requirements
|
|
2858
3233
|
);
|
|
2859
|
-
|
|
2860
|
-
if (
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
http.encodePaymentResponseHeader(
|
|
2864
|
-
settleResult
|
|
2865
|
-
)
|
|
2866
|
-
);
|
|
3234
|
+
const outcome = readSettleResult(settleResult);
|
|
3235
|
+
if (!outcome.ok) {
|
|
3236
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
3237
|
+
return;
|
|
2867
3238
|
}
|
|
3239
|
+
txHash = outcome.txHash;
|
|
3240
|
+
res.setHeader(
|
|
3241
|
+
"X-Payment-Response",
|
|
3242
|
+
http.encodePaymentResponseHeader(
|
|
3243
|
+
settleResult
|
|
3244
|
+
)
|
|
3245
|
+
);
|
|
2868
3246
|
}
|
|
2869
3247
|
if (idempotencyKey) {
|
|
2870
3248
|
const headers = {};
|
|
@@ -2872,17 +3250,21 @@ function createX402Handler(opts) {
|
|
|
2872
3250
|
if (typeof paymentResponse === "string") {
|
|
2873
3251
|
headers["X-Payment-Response"] = paymentResponse;
|
|
2874
3252
|
}
|
|
2875
|
-
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
3253
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
2876
3254
|
}
|
|
2877
3255
|
if (txHash && opts.onSettled) {
|
|
2878
3256
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
2879
|
-
|
|
3257
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
2880
3258
|
opts.onCallbackError?.(err, "onSettled");
|
|
2881
3259
|
});
|
|
2882
3260
|
}
|
|
2883
3261
|
next();
|
|
2884
3262
|
} catch (err) {
|
|
2885
|
-
|
|
3263
|
+
if (err instanceof RouteDockManifestError) {
|
|
3264
|
+
next(err);
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
2886
3268
|
res.status(500).json({ error: "Payment settlement failed" });
|
|
2887
3269
|
}
|
|
2888
3270
|
};
|
|
@@ -2895,20 +3277,37 @@ function createMppChargeHandler(opts) {
|
|
|
2895
3277
|
const networkId = MPP_NETWORK[opts.network];
|
|
2896
3278
|
const amountHumanReadable = opts.amount;
|
|
2897
3279
|
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
2898
|
-
const
|
|
2899
|
-
const
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
3280
|
+
const logger = resolveLogger(opts.logger);
|
|
3281
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
3282
|
+
const mppxInstances = /* @__PURE__ */ new Map();
|
|
3283
|
+
function getMppx(contract) {
|
|
3284
|
+
let instance = mppxInstances.get(contract);
|
|
3285
|
+
if (!instance) {
|
|
3286
|
+
instance = server$2.Mppx.create({
|
|
3287
|
+
secretKey: opts.payeeSecretKey,
|
|
3288
|
+
methods: [
|
|
3289
|
+
server$4.stellar.charge({
|
|
3290
|
+
recipient,
|
|
3291
|
+
currency: contract,
|
|
3292
|
+
network: networkId,
|
|
3293
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
3294
|
+
})
|
|
3295
|
+
]
|
|
3296
|
+
});
|
|
3297
|
+
mppxInstances.set(contract, instance);
|
|
3298
|
+
}
|
|
3299
|
+
return instance;
|
|
3300
|
+
}
|
|
2910
3301
|
return async (req, res, next) => {
|
|
2911
3302
|
try {
|
|
3303
|
+
const endpoint = req.path || req.originalUrl;
|
|
3304
|
+
const assetContract = resolveAssetContract(
|
|
3305
|
+
opts.manifest,
|
|
3306
|
+
"mpp-charge",
|
|
3307
|
+
endpoint,
|
|
3308
|
+
opts.assetContract
|
|
3309
|
+
);
|
|
3310
|
+
const mppx = getMppx(assetContract);
|
|
2912
3311
|
let payerAddress = null;
|
|
2913
3312
|
try {
|
|
2914
3313
|
const authHeader = req.headers["authorization"];
|
|
@@ -2924,15 +3323,27 @@ function createMppChargeHandler(opts) {
|
|
|
2924
3323
|
}
|
|
2925
3324
|
} catch {
|
|
2926
3325
|
}
|
|
2927
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
3326
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
3327
|
+
(name) => {
|
|
3328
|
+
const v = req.headers[name.toLowerCase()];
|
|
3329
|
+
return Array.isArray(v) ? v[0] : v;
|
|
3330
|
+
},
|
|
3331
|
+
{
|
|
3332
|
+
method: req.method,
|
|
3333
|
+
path: req.originalUrl.split("?")[0],
|
|
3334
|
+
amount: amountHumanReadable,
|
|
3335
|
+
payTo: recipient
|
|
3336
|
+
}
|
|
3337
|
+
);
|
|
2931
3338
|
if (idempotencyKey) {
|
|
2932
|
-
const
|
|
2933
|
-
if (
|
|
2934
|
-
|
|
2935
|
-
|
|
3339
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
3340
|
+
if (replayCheck.kind === "spent") {
|
|
3341
|
+
res.status(402).json({ error: "Payment already used" });
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
if (replayCheck.kind === "replay") {
|
|
3345
|
+
if (replayCheck.record.headers) {
|
|
3346
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
2936
3347
|
res.setHeader(k, val);
|
|
2937
3348
|
}
|
|
2938
3349
|
}
|
|
@@ -2944,7 +3355,7 @@ function createMppChargeHandler(opts) {
|
|
|
2944
3355
|
const handler = mppx["stellar/charge"];
|
|
2945
3356
|
const result = await handler({
|
|
2946
3357
|
amount: amountHumanReadable,
|
|
2947
|
-
currency:
|
|
3358
|
+
currency: assetContract,
|
|
2948
3359
|
recipient,
|
|
2949
3360
|
description: opts.manifest.name
|
|
2950
3361
|
})(fetchReq);
|
|
@@ -2975,12 +3386,13 @@ function createMppChargeHandler(opts) {
|
|
|
2975
3386
|
if (idempotencyKey) {
|
|
2976
3387
|
await seenTxStore.set(idempotencyKey, {
|
|
2977
3388
|
txHash: reference ?? null,
|
|
2978
|
-
headers: receiptHeaders
|
|
3389
|
+
headers: receiptHeaders,
|
|
3390
|
+
createdAt: Date.now()
|
|
2979
3391
|
});
|
|
2980
3392
|
}
|
|
2981
3393
|
if (reference && opts.onSettled) {
|
|
2982
3394
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
2983
|
-
|
|
3395
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
2984
3396
|
opts.onCallbackError?.(err, "onSettled");
|
|
2985
3397
|
});
|
|
2986
3398
|
}
|
|
@@ -3081,8 +3493,9 @@ function createMppSessionHandler(opts) {
|
|
|
3081
3493
|
const networkId = MPP_NETWORK2[opts.network];
|
|
3082
3494
|
const rateHuman = opts.rate;
|
|
3083
3495
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
3496
|
+
const logger = resolveLogger(opts.logger);
|
|
3084
3497
|
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
3085
|
-
const innerStore = server$
|
|
3498
|
+
const innerStore = server$3.Store.memory();
|
|
3086
3499
|
let record = null;
|
|
3087
3500
|
let voucherCount = 0;
|
|
3088
3501
|
let sessionOpened = false;
|
|
@@ -3132,7 +3545,7 @@ function createMppSessionHandler(opts) {
|
|
|
3132
3545
|
reason
|
|
3133
3546
|
});
|
|
3134
3547
|
} catch (err) {
|
|
3135
|
-
|
|
3548
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
3136
3549
|
}
|
|
3137
3550
|
}
|
|
3138
3551
|
}
|
|
@@ -3179,7 +3592,7 @@ function createMppSessionHandler(opts) {
|
|
|
3179
3592
|
sessionOpened = true;
|
|
3180
3593
|
if (opts.onSessionOpen) {
|
|
3181
3594
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
3182
|
-
|
|
3595
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
3183
3596
|
opts.onCallbackError?.(err, "onSessionOpen");
|
|
3184
3597
|
});
|
|
3185
3598
|
}
|
|
@@ -3187,7 +3600,7 @@ function createMppSessionHandler(opts) {
|
|
|
3187
3600
|
if (opts.onVoucher) {
|
|
3188
3601
|
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
3189
3602
|
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
3190
|
-
|
|
3603
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
3191
3604
|
opts.onCallbackError?.(err, "onVoucher");
|
|
3192
3605
|
});
|
|
3193
3606
|
}
|
|
@@ -3197,7 +3610,7 @@ function createMppSessionHandler(opts) {
|
|
|
3197
3610
|
methods: [
|
|
3198
3611
|
withTypedChannelErrors(
|
|
3199
3612
|
onVerifiedCredential(
|
|
3200
|
-
server$
|
|
3613
|
+
server$3.stellar.channel({
|
|
3201
3614
|
channel: opts.channelFactory,
|
|
3202
3615
|
commitmentKey: opts.commitmentPublicKey,
|
|
3203
3616
|
network: networkId,
|
|
@@ -3228,7 +3641,7 @@ function createMppSessionHandler(opts) {
|
|
|
3228
3641
|
}
|
|
3229
3642
|
if (closeAmount > 0n && closeSig) {
|
|
3230
3643
|
const closePayer = record?.payer ?? null;
|
|
3231
|
-
const closeTxHash = await server$
|
|
3644
|
+
const closeTxHash = await server$3.close({
|
|
3232
3645
|
channel: opts.channelFactory,
|
|
3233
3646
|
amount: closeAmount,
|
|
3234
3647
|
signature: Buffer.from(closeSig, "hex"),
|
|
@@ -3240,7 +3653,7 @@ function createMppSessionHandler(opts) {
|
|
|
3240
3653
|
if (opts.onSettled) {
|
|
3241
3654
|
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
3242
3655
|
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
3243
|
-
|
|
3656
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
3244
3657
|
opts.onCallbackError?.(err, "onSettled");
|
|
3245
3658
|
});
|
|
3246
3659
|
}
|
|
@@ -3251,7 +3664,7 @@ function createMppSessionHandler(opts) {
|
|
|
3251
3664
|
payeeKeypair.publicKey()
|
|
3252
3665
|
);
|
|
3253
3666
|
if (!settlementAddresses) {
|
|
3254
|
-
|
|
3667
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
3255
3668
|
} else {
|
|
3256
3669
|
try {
|
|
3257
3670
|
const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
|
|
@@ -3274,7 +3687,7 @@ function createMppSessionHandler(opts) {
|
|
|
3274
3687
|
preparedTx.sign(payeeKeypair);
|
|
3275
3688
|
await server.sendTransaction(preparedTx);
|
|
3276
3689
|
} catch (recordErr) {
|
|
3277
|
-
|
|
3690
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
3278
3691
|
}
|
|
3279
3692
|
}
|
|
3280
3693
|
}
|
|
@@ -3331,12 +3744,13 @@ function routedock(opts) {
|
|
|
3331
3744
|
payeeSecretKey: opts.payeeSecretKey,
|
|
3332
3745
|
network: opts.network,
|
|
3333
3746
|
amount: x402Price,
|
|
3334
|
-
assetContract: opts.assetContract,
|
|
3747
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3335
3748
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
3336
3749
|
manifest: signedManifest,
|
|
3337
3750
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
3338
3751
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3339
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
3752
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
3753
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3340
3754
|
})
|
|
3341
3755
|
);
|
|
3342
3756
|
}
|
|
@@ -3350,11 +3764,12 @@ function routedock(opts) {
|
|
|
3350
3764
|
payeeSecretKey: opts.payeeSecretKey,
|
|
3351
3765
|
network: opts.network,
|
|
3352
3766
|
amount: chargePrice,
|
|
3353
|
-
assetContract: opts.assetContract,
|
|
3767
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3354
3768
|
manifest: signedManifest,
|
|
3355
3769
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
3356
3770
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3357
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
3771
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
3772
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3358
3773
|
})
|
|
3359
3774
|
);
|
|
3360
3775
|
}
|
|
@@ -3372,7 +3787,7 @@ function routedock(opts) {
|
|
|
3372
3787
|
network: opts.network,
|
|
3373
3788
|
channelFactory: sessionPricing.channelFactory,
|
|
3374
3789
|
rate: sessionPricing.rate,
|
|
3375
|
-
assetContract: opts.assetContract,
|
|
3790
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3376
3791
|
manifest: signedManifest,
|
|
3377
3792
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
3378
3793
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
@@ -3380,7 +3795,8 @@ function routedock(opts) {
|
|
|
3380
3795
|
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
3381
3796
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3382
3797
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
3383
|
-
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
|
|
3798
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
3799
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3384
3800
|
})
|
|
3385
3801
|
);
|
|
3386
3802
|
}
|
|
@@ -3544,25 +3960,26 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3544
3960
|
}
|
|
3545
3961
|
}
|
|
3546
3962
|
async function runStartupReconciliation(opts, logger) {
|
|
3547
|
-
const log = logger
|
|
3548
|
-
log("[SessionReconciler] Running startup reconciliation...");
|
|
3963
|
+
const log = resolveLogger(logger);
|
|
3964
|
+
log("info", "[SessionReconciler] Running startup reconciliation...");
|
|
3549
3965
|
try {
|
|
3550
3966
|
const stats = await reconcileAbandonedSessions(opts);
|
|
3551
3967
|
if (stats.orphanedCount === 0) {
|
|
3552
|
-
log("[SessionReconciler] No orphaned sessions found");
|
|
3968
|
+
log("info", "[SessionReconciler] No orphaned sessions found");
|
|
3553
3969
|
return;
|
|
3554
3970
|
}
|
|
3555
3971
|
log(
|
|
3972
|
+
"info",
|
|
3556
3973
|
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
3557
3974
|
);
|
|
3558
3975
|
if (stats.errors.length > 0) {
|
|
3559
|
-
log("[SessionReconciler] Reconciliation errors:");
|
|
3976
|
+
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
3560
3977
|
for (const err of stats.errors) {
|
|
3561
|
-
log(` - ${err.channelId}: ${err.reason}`);
|
|
3978
|
+
log("warn", ` - ${err.channelId}: ${err.reason}`);
|
|
3562
3979
|
}
|
|
3563
3980
|
}
|
|
3564
3981
|
} catch (err) {
|
|
3565
|
-
log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
3982
|
+
log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
3566
3983
|
}
|
|
3567
3984
|
}
|
|
3568
3985
|
function tryDecodeBase64(value) {
|
|
@@ -3749,23 +4166,32 @@ exports.assertManifestValid = assertManifestValid;
|
|
|
3749
4166
|
exports.assertNulthVaultManifest = assertNulthVaultManifest;
|
|
3750
4167
|
exports.backoffDelayMs = backoffDelayMs;
|
|
3751
4168
|
exports.configureManifestCache = configureManifestCache;
|
|
4169
|
+
exports.consoleLogger = consoleLogger;
|
|
4170
|
+
exports.createConsoleLogger = createConsoleLogger;
|
|
3752
4171
|
exports.createNulthSigner = createNulthSigner;
|
|
3753
4172
|
exports.createPolicyState = createPolicyState;
|
|
3754
4173
|
exports.decimalToStroops = decimalToStroops;
|
|
3755
4174
|
exports.decodeAuthSignature = decodeAuthSignature;
|
|
3756
4175
|
exports.fetchLedgerSequence = fetchLedgerSequence;
|
|
3757
4176
|
exports.fetchManifest = fetchManifest;
|
|
4177
|
+
exports.getEligibleAssets = getEligibleAssets;
|
|
3758
4178
|
exports.httpStatusToError = httpStatusToError;
|
|
3759
4179
|
exports.invalidateManifest = invalidateManifest;
|
|
4180
|
+
exports.isAssetEligible = isAssetEligible;
|
|
3760
4181
|
exports.manifestDigest = manifestDigest;
|
|
4182
|
+
exports.noopLogger = noopLogger;
|
|
4183
|
+
exports.normalizeManifestAssets = normalizeManifestAssets;
|
|
3761
4184
|
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
3762
4185
|
exports.paymentContextFromManifest = paymentContextFromManifest;
|
|
3763
4186
|
exports.prepareNulthSigner = prepareNulthSigner;
|
|
3764
4187
|
exports.rankProvidersByLatency = rankProvidersByLatency;
|
|
3765
4188
|
exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
|
|
3766
4189
|
exports.registerProvider = registerProvider;
|
|
4190
|
+
exports.resolveAssetContract = resolveAssetContract;
|
|
4191
|
+
exports.resolveLogger = resolveLogger;
|
|
3767
4192
|
exports.routedock = routedock;
|
|
3768
4193
|
exports.runStartupReconciliation = runStartupReconciliation;
|
|
4194
|
+
exports.selectAsset = selectAsset;
|
|
3769
4195
|
exports.selectMode = selectMode;
|
|
3770
4196
|
exports.setNulthPaymentContext = setNulthPaymentContext;
|
|
3771
4197
|
exports.signManifest = signManifest;
|
|
@@ -3774,3 +4200,4 @@ exports.usdcToUnits = usdcToUnits;
|
|
|
3774
4200
|
exports.verifyManifestSignature = verifyManifestSignature;
|
|
3775
4201
|
exports.withRetry = withRetry;
|
|
3776
4202
|
exports.wrapFetchError = wrapFetchError;
|
|
4203
|
+
exports.wrapMppError = wrapMppError;
|