@routedock/routedock 0.2.0 → 0.3.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 +305 -0
- package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
- package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
- package/dist/{ProviderRegistry-C50nyhaF.d.ts → ProviderRegistry-CA_TdREG.d.ts} +2 -2
- package/dist/{ProviderRegistry-BJ9oJLVz.d.cts → ProviderRegistry-D-0rqK0N.d.cts} +2 -2
- package/dist/{RouteDockClient-B1roCa8h.d.cts → RouteDockClient-Bh86bVly.d.ts} +23 -9
- package/dist/{RouteDockClient-BbkqjhMG.d.ts → RouteDockClient-go9aopfF.d.cts} +23 -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 +429 -117
- package/dist/client.d.cts +9 -8
- package/dist/client.d.ts +9 -8
- package/dist/client.js +421 -119
- package/dist/index.cjs +652 -220
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +643 -221
- 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 +407 -117
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +408 -118
- 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.0"};
|
|
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 = [];
|
|
@@ -1942,7 +2219,10 @@ var NulthClient = class {
|
|
|
1942
2219
|
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
1943
2220
|
);
|
|
1944
2221
|
}
|
|
1945
|
-
|
|
2222
|
+
resolveLogger(config.logger)(
|
|
2223
|
+
"warn",
|
|
2224
|
+
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2225
|
+
);
|
|
1946
2226
|
}
|
|
1947
2227
|
config;
|
|
1948
2228
|
policy;
|
|
@@ -1994,6 +2274,12 @@ var NulthClient = class {
|
|
|
1994
2274
|
return proof;
|
|
1995
2275
|
}
|
|
1996
2276
|
enforcePolicy(context) {
|
|
2277
|
+
if (context.amountStroops < 0n) {
|
|
2278
|
+
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2279
|
+
}
|
|
2280
|
+
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2281
|
+
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2282
|
+
}
|
|
1997
2283
|
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
1998
2284
|
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
1999
2285
|
}
|
|
@@ -2137,7 +2423,8 @@ var InMemorySpendStore = class {
|
|
|
2137
2423
|
state = null;
|
|
2138
2424
|
constructor(options = {}) {
|
|
2139
2425
|
if (options.warn !== false) {
|
|
2140
|
-
|
|
2426
|
+
resolveLogger(options.logger)(
|
|
2427
|
+
"warn",
|
|
2141
2428
|
"[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
2429
|
);
|
|
2143
2430
|
}
|
|
@@ -2227,7 +2514,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2227
2514
|
this.network = config.network;
|
|
2228
2515
|
this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
|
|
2229
2516
|
this.retryPolicy = config.retryPolicy;
|
|
2230
|
-
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2517
|
+
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2518
|
+
warn: !!config.spendCap,
|
|
2519
|
+
...config.logger && { logger: config.logger }
|
|
2520
|
+
});
|
|
2231
2521
|
this.logger = config.logger;
|
|
2232
2522
|
this.manifestTimeoutMs = config.manifestTimeoutMs;
|
|
2233
2523
|
this.expectedPayee = config.expectedPayee;
|
|
@@ -2239,9 +2529,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2239
2529
|
_secrets.set(this, config.commitmentSecret);
|
|
2240
2530
|
}
|
|
2241
2531
|
const secretKey = this.keypair.secret();
|
|
2242
|
-
this.
|
|
2243
|
-
this.
|
|
2244
|
-
this.
|
|
2532
|
+
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2533
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2534
|
+
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2535
|
+
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2245
2536
|
}
|
|
2246
2537
|
/** Fetch manifest and select mode — shared by pay() and estimateCost(). */
|
|
2247
2538
|
async _resolveManifest(url, options) {
|
|
@@ -2270,12 +2561,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2270
2561
|
* call before committing to a payment — for approval gates and manual
|
|
2271
2562
|
* trustline remediation.
|
|
2272
2563
|
*/
|
|
2273
|
-
async preflight(manifest) {
|
|
2564
|
+
async preflight(manifest, mode, endpoint) {
|
|
2274
2565
|
assertManifestValid(manifest);
|
|
2275
|
-
|
|
2566
|
+
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2567
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2276
2568
|
return {
|
|
2277
2569
|
hasTrustline: true,
|
|
2278
|
-
asset:
|
|
2570
|
+
asset: selectedAsset,
|
|
2279
2571
|
modes: manifest.modes,
|
|
2280
2572
|
...manifest.regions && { regions: manifest.regions },
|
|
2281
2573
|
...manifest.latency_hints && { latency_hints: manifest.latency_hints },
|
|
@@ -2289,8 +2581,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2289
2581
|
* trustline is missing. Results are cached per (account, asset) so the
|
|
2290
2582
|
* per-payment overhead is one RPC call amortized to zero after the first.
|
|
2291
2583
|
*/
|
|
2292
|
-
async _checkTrustline(manifest) {
|
|
2293
|
-
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${
|
|
2584
|
+
async _checkTrustline(manifest, targetAsset = manifest.asset) {
|
|
2585
|
+
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2294
2586
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2295
2587
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2296
2588
|
const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
|
|
@@ -2298,13 +2590,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2298
2590
|
try {
|
|
2299
2591
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2300
2592
|
const balances = account.balances;
|
|
2301
|
-
const expectedIssuer = getAssetIssuer(
|
|
2593
|
+
const expectedIssuer = getAssetIssuer(targetAsset, this.network);
|
|
2302
2594
|
const hasTrustline = balances.some(
|
|
2303
|
-
(b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code ===
|
|
2595
|
+
(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
2596
|
);
|
|
2305
2597
|
if (!hasTrustline) {
|
|
2306
|
-
const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${
|
|
2307
|
-
throw new RouteDockTrustlineError(
|
|
2598
|
+
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}`;
|
|
2599
|
+
throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
|
|
2308
2600
|
}
|
|
2309
2601
|
_RouteDockClient._trustlineCache.set(cacheKey, {
|
|
2310
2602
|
exists: true,
|
|
@@ -2313,7 +2605,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2313
2605
|
} catch (err) {
|
|
2314
2606
|
if (err instanceof RouteDockTrustlineError) throw err;
|
|
2315
2607
|
this.logger?.(
|
|
2316
|
-
|
|
2608
|
+
"warn",
|
|
2609
|
+
`[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
|
|
2317
2610
|
);
|
|
2318
2611
|
}
|
|
2319
2612
|
}
|
|
@@ -2321,18 +2614,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2321
2614
|
* Pay for one request at `url`. Fetches manifest, selects payment mode,
|
|
2322
2615
|
* runs trustline preflight, reserves local spend cap BEFORE executing the
|
|
2323
2616
|
* payment, then commits the spend record on success. Rolls back the
|
|
2324
|
-
* reservation if the on-chain payment fails
|
|
2325
|
-
*
|
|
2617
|
+
* reservation if the on-chain payment fails, and also when the call
|
|
2618
|
+
* completes without paying anything (an x402 endpoint answering a plain
|
|
2619
|
+
* 200 with no challenge). Concurrent pay() calls are serialized through a
|
|
2620
|
+
* per-instance mutex to prevent spend-cap overrun.
|
|
2326
2621
|
*/
|
|
2327
2622
|
async pay(url, options) {
|
|
2328
2623
|
const baseUrl = new URL(url).origin;
|
|
2329
2624
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2330
2625
|
assertEndpointActive(manifest, url, this.logger);
|
|
2331
2626
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
return this._payWithNulthVault(url, manifest, mode);
|
|
2335
|
-
}
|
|
2627
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2628
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2336
2629
|
let amount;
|
|
2337
2630
|
switch (mode) {
|
|
2338
2631
|
case "x402":
|
|
@@ -2352,22 +2645,30 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2352
2645
|
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2353
2646
|
let result;
|
|
2354
2647
|
try {
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2648
|
+
if (this.vault?.mode === "nulth") {
|
|
2649
|
+
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2650
|
+
} else {
|
|
2651
|
+
switch (mode) {
|
|
2652
|
+
case "x402":
|
|
2653
|
+
result = await this.x402.pay(url, manifest);
|
|
2654
|
+
break;
|
|
2655
|
+
case "mpp-charge":
|
|
2656
|
+
result = await this.charge.pay(url, manifest);
|
|
2657
|
+
break;
|
|
2658
|
+
default:
|
|
2659
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2660
|
+
}
|
|
2364
2661
|
}
|
|
2365
2662
|
} catch (err) {
|
|
2366
2663
|
await this._rollbackSpend(reserveId).catch(() => {
|
|
2367
2664
|
});
|
|
2368
2665
|
throw err;
|
|
2369
2666
|
}
|
|
2370
|
-
|
|
2667
|
+
if (result.txHash === null && result.amount === "0") {
|
|
2668
|
+
await this._rollbackSpend(reserveId);
|
|
2669
|
+
} else {
|
|
2670
|
+
await this._commitSpend(reserveId);
|
|
2671
|
+
}
|
|
2371
2672
|
return result;
|
|
2372
2673
|
}
|
|
2373
2674
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
@@ -2417,9 +2718,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2417
2718
|
default:
|
|
2418
2719
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2419
2720
|
}
|
|
2721
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2420
2722
|
return {
|
|
2421
2723
|
amount,
|
|
2422
|
-
asset:
|
|
2724
|
+
asset: selectedAsset,
|
|
2423
2725
|
mode,
|
|
2424
2726
|
manifest,
|
|
2425
2727
|
...manifest.regions && { regions: manifest.regions },
|
|
@@ -2656,7 +2958,7 @@ var SupabaseSeenTxStore = class {
|
|
|
2656
2958
|
}
|
|
2657
2959
|
supabase;
|
|
2658
2960
|
async get(key) {
|
|
2659
|
-
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
|
|
2961
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
|
|
2660
2962
|
if (error) {
|
|
2661
2963
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
2662
2964
|
}
|
|
@@ -2664,7 +2966,8 @@ var SupabaseSeenTxStore = class {
|
|
|
2664
2966
|
const headers = data.headers;
|
|
2665
2967
|
return {
|
|
2666
2968
|
txHash: data.tx_hash ?? null,
|
|
2667
|
-
...headers ? { headers } : {}
|
|
2969
|
+
...headers ? { headers } : {},
|
|
2970
|
+
createdAt: Date.parse(data.created_at)
|
|
2668
2971
|
};
|
|
2669
2972
|
}
|
|
2670
2973
|
async set(key, record) {
|
|
@@ -2672,7 +2975,8 @@ var SupabaseSeenTxStore = class {
|
|
|
2672
2975
|
{
|
|
2673
2976
|
key,
|
|
2674
2977
|
tx_hash: record.txHash,
|
|
2675
|
-
headers: record.headers ?? null
|
|
2978
|
+
headers: record.headers ?? null,
|
|
2979
|
+
created_at: new Date(record.createdAt ?? Date.now()).toISOString()
|
|
2676
2980
|
},
|
|
2677
2981
|
{ onConflict: "key" }
|
|
2678
2982
|
);
|
|
@@ -2680,21 +2984,47 @@ var SupabaseSeenTxStore = class {
|
|
|
2680
2984
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
2681
2985
|
}
|
|
2682
2986
|
}
|
|
2987
|
+
/**
|
|
2988
|
+
* Atomically increment the settlement's replay counter through the
|
|
2989
|
+
* `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
|
|
2990
|
+
* row lock gives concurrent claims distinct counts. Returns `Infinity` when
|
|
2991
|
+
* the key no longer exists, so the caller treats it as spent.
|
|
2992
|
+
*/
|
|
2993
|
+
async claimReplay(key) {
|
|
2994
|
+
const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
|
|
2995
|
+
p_key: key
|
|
2996
|
+
});
|
|
2997
|
+
if (error) {
|
|
2998
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
|
|
2999
|
+
}
|
|
3000
|
+
if (data == null) return Number.POSITIVE_INFINITY;
|
|
3001
|
+
return data;
|
|
3002
|
+
}
|
|
2683
3003
|
};
|
|
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
3004
|
function extractPayerAddress(key) {
|
|
2691
3005
|
if (typeof key !== "string") return null;
|
|
2692
3006
|
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
2693
3007
|
}
|
|
2694
3008
|
|
|
2695
3009
|
// src/provider/SeenTxStore.ts
|
|
3010
|
+
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
3011
|
+
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
3012
|
+
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
3013
|
+
const record = await store.get(key);
|
|
3014
|
+
if (!record) return { kind: "miss" };
|
|
3015
|
+
const { createdAt } = record;
|
|
3016
|
+
if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
|
|
3017
|
+
return { kind: "spent" };
|
|
3018
|
+
}
|
|
3019
|
+
if (store.claimReplay) {
|
|
3020
|
+
const replayed = await store.claimReplay(key);
|
|
3021
|
+
if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
|
|
3022
|
+
}
|
|
3023
|
+
return { kind: "replay", record };
|
|
3024
|
+
}
|
|
2696
3025
|
var InMemorySeenTxStore = class {
|
|
2697
3026
|
map = /* @__PURE__ */ new Map();
|
|
3027
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
2698
3028
|
order = [];
|
|
2699
3029
|
constructor(maxEntriesOrOptions = 1e4) {
|
|
2700
3030
|
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
@@ -2702,7 +3032,8 @@ var InMemorySeenTxStore = class {
|
|
|
2702
3032
|
if (options.warn !== false) {
|
|
2703
3033
|
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
2704
3034
|
if (isServerless) {
|
|
2705
|
-
|
|
3035
|
+
resolveLogger(options.logger)(
|
|
3036
|
+
"warn",
|
|
2706
3037
|
"[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
3038
|
);
|
|
2708
3039
|
}
|
|
@@ -2717,21 +3048,43 @@ var InMemorySeenTxStore = class {
|
|
|
2717
3048
|
this.order.push(key);
|
|
2718
3049
|
if (this.order.length > this.maxEntries) {
|
|
2719
3050
|
const evicted = this.order.shift();
|
|
2720
|
-
if (evicted !== void 0)
|
|
3051
|
+
if (evicted !== void 0) {
|
|
3052
|
+
this.map.delete(evicted);
|
|
3053
|
+
this.replayCounts.delete(evicted);
|
|
3054
|
+
}
|
|
2721
3055
|
}
|
|
2722
3056
|
}
|
|
2723
3057
|
this.map.set(key, record);
|
|
2724
3058
|
}
|
|
3059
|
+
claimReplay(key) {
|
|
3060
|
+
if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
|
|
3061
|
+
const next = (this.replayCounts.get(key) ?? 0) + 1;
|
|
3062
|
+
this.replayCounts.set(key, next);
|
|
3063
|
+
return next;
|
|
3064
|
+
}
|
|
2725
3065
|
};
|
|
2726
3066
|
async function sha256Hex2(input) {
|
|
2727
3067
|
const buf = new TextEncoder().encode(input);
|
|
2728
3068
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
2729
3069
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2730
3070
|
}
|
|
2731
|
-
async function paymentIdempotencyKey(getHeader) {
|
|
3071
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
2732
3072
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
2733
3073
|
if (!material) return null;
|
|
2734
|
-
return sha256Hex2(
|
|
3074
|
+
return sha256Hex2(
|
|
3075
|
+
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// src/provider/settleResult.ts
|
|
3080
|
+
function readSettleResult(result) {
|
|
3081
|
+
const settle = result;
|
|
3082
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
3083
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
3084
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
3085
|
+
return failed(settle.errorReason);
|
|
3086
|
+
}
|
|
3087
|
+
return { ok: true, txHash: settle.transaction };
|
|
2735
3088
|
}
|
|
2736
3089
|
|
|
2737
3090
|
// src/provider/x402Handler.ts
|
|
@@ -2744,7 +3097,8 @@ function createX402Handler(opts) {
|
|
|
2744
3097
|
const caip2 = CAIP22[opts.network];
|
|
2745
3098
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
2746
3099
|
const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
2747
|
-
const
|
|
3100
|
+
const logger = resolveLogger(opts.logger);
|
|
3101
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
2748
3102
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
2749
3103
|
const localFacilitator = new facilitator.ExactStellarScheme([signer], {
|
|
2750
3104
|
areFeesSponsored: true
|
|
@@ -2765,22 +3119,29 @@ function createX402Handler(opts) {
|
|
|
2765
3119
|
}
|
|
2766
3120
|
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
2767
3121
|
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
3122
|
return async (req, res, next) => {
|
|
2781
3123
|
try {
|
|
2782
|
-
const
|
|
2783
|
-
|
|
3124
|
+
const endpoint = req.path || req.originalUrl;
|
|
3125
|
+
const assetContract = resolveAssetContract(
|
|
3126
|
+
opts.manifest,
|
|
3127
|
+
"x402",
|
|
3128
|
+
endpoint,
|
|
3129
|
+
opts.assetContract
|
|
3130
|
+
);
|
|
3131
|
+
const requirements = {
|
|
3132
|
+
scheme: "exact",
|
|
3133
|
+
network: caip2,
|
|
3134
|
+
asset: assetContract,
|
|
3135
|
+
amount: amountInBaseUnits,
|
|
3136
|
+
payTo,
|
|
3137
|
+
maxTimeoutSeconds: 60,
|
|
3138
|
+
extra: {
|
|
3139
|
+
areFeesSponsored: true,
|
|
3140
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
const respondPaymentRequired = async (error, reason) => {
|
|
3144
|
+
const body = { error, ...reason ? { reason } : {} };
|
|
2784
3145
|
if (ozServer) {
|
|
2785
3146
|
const resourceInfo = {
|
|
2786
3147
|
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
@@ -2790,7 +3151,7 @@ function createX402Handler(opts) {
|
|
|
2790
3151
|
[requirements],
|
|
2791
3152
|
resourceInfo
|
|
2792
3153
|
);
|
|
2793
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(
|
|
3154
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
2794
3155
|
} else {
|
|
2795
3156
|
const x402Response = {
|
|
2796
3157
|
x402Version: 2,
|
|
@@ -2800,19 +3161,35 @@ function createX402Handler(opts) {
|
|
|
2800
3161
|
},
|
|
2801
3162
|
accepts: [requirements]
|
|
2802
3163
|
};
|
|
2803
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(
|
|
3164
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
|
|
2804
3165
|
}
|
|
3166
|
+
};
|
|
3167
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
3168
|
+
if (!paymentHeader) {
|
|
3169
|
+
await respondPaymentRequired("Payment Required");
|
|
2805
3170
|
return;
|
|
2806
3171
|
}
|
|
2807
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3172
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
3173
|
+
(name) => {
|
|
3174
|
+
const v = req.headers[name.toLowerCase()];
|
|
3175
|
+
return Array.isArray(v) ? v[0] : v;
|
|
3176
|
+
},
|
|
3177
|
+
{
|
|
3178
|
+
method: req.method,
|
|
3179
|
+
path: req.originalUrl.split("?")[0],
|
|
3180
|
+
amount: requirements.amount,
|
|
3181
|
+
payTo: requirements.payTo
|
|
3182
|
+
}
|
|
3183
|
+
);
|
|
2811
3184
|
if (idempotencyKey) {
|
|
2812
|
-
const
|
|
2813
|
-
if (
|
|
2814
|
-
|
|
2815
|
-
|
|
3185
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
3186
|
+
if (replayCheck.kind === "spent") {
|
|
3187
|
+
res.status(402).json({ error: "Payment already used" });
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
if (replayCheck.kind === "replay") {
|
|
3191
|
+
if (replayCheck.record.headers) {
|
|
3192
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
2816
3193
|
res.setHeader(k, val);
|
|
2817
3194
|
}
|
|
2818
3195
|
}
|
|
@@ -2831,15 +3208,18 @@ function createX402Handler(opts) {
|
|
|
2831
3208
|
}
|
|
2832
3209
|
if (ozServer) {
|
|
2833
3210
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
2834
|
-
|
|
2835
|
-
if (
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
http.encodePaymentResponseHeader(
|
|
2839
|
-
settleResult
|
|
2840
|
-
)
|
|
2841
|
-
);
|
|
3211
|
+
const outcome = readSettleResult(settleResult);
|
|
3212
|
+
if (!outcome.ok) {
|
|
3213
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
3214
|
+
return;
|
|
2842
3215
|
}
|
|
3216
|
+
txHash = outcome.txHash;
|
|
3217
|
+
res.setHeader(
|
|
3218
|
+
"X-Payment-Response",
|
|
3219
|
+
http.encodePaymentResponseHeader(
|
|
3220
|
+
settleResult
|
|
3221
|
+
)
|
|
3222
|
+
);
|
|
2843
3223
|
} else {
|
|
2844
3224
|
const verifyResult = await localFacilitator.verify(
|
|
2845
3225
|
payload,
|
|
@@ -2856,15 +3236,18 @@ function createX402Handler(opts) {
|
|
|
2856
3236
|
payload,
|
|
2857
3237
|
requirements
|
|
2858
3238
|
);
|
|
2859
|
-
|
|
2860
|
-
if (
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
http.encodePaymentResponseHeader(
|
|
2864
|
-
settleResult
|
|
2865
|
-
)
|
|
2866
|
-
);
|
|
3239
|
+
const outcome = readSettleResult(settleResult);
|
|
3240
|
+
if (!outcome.ok) {
|
|
3241
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
3242
|
+
return;
|
|
2867
3243
|
}
|
|
3244
|
+
txHash = outcome.txHash;
|
|
3245
|
+
res.setHeader(
|
|
3246
|
+
"X-Payment-Response",
|
|
3247
|
+
http.encodePaymentResponseHeader(
|
|
3248
|
+
settleResult
|
|
3249
|
+
)
|
|
3250
|
+
);
|
|
2868
3251
|
}
|
|
2869
3252
|
if (idempotencyKey) {
|
|
2870
3253
|
const headers = {};
|
|
@@ -2872,17 +3255,21 @@ function createX402Handler(opts) {
|
|
|
2872
3255
|
if (typeof paymentResponse === "string") {
|
|
2873
3256
|
headers["X-Payment-Response"] = paymentResponse;
|
|
2874
3257
|
}
|
|
2875
|
-
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
3258
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
2876
3259
|
}
|
|
2877
3260
|
if (txHash && opts.onSettled) {
|
|
2878
3261
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
2879
|
-
|
|
3262
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
2880
3263
|
opts.onCallbackError?.(err, "onSettled");
|
|
2881
3264
|
});
|
|
2882
3265
|
}
|
|
2883
3266
|
next();
|
|
2884
3267
|
} catch (err) {
|
|
2885
|
-
|
|
3268
|
+
if (err instanceof RouteDockManifestError) {
|
|
3269
|
+
next(err);
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
2886
3273
|
res.status(500).json({ error: "Payment settlement failed" });
|
|
2887
3274
|
}
|
|
2888
3275
|
};
|
|
@@ -2895,20 +3282,37 @@ function createMppChargeHandler(opts) {
|
|
|
2895
3282
|
const networkId = MPP_NETWORK[opts.network];
|
|
2896
3283
|
const amountHumanReadable = opts.amount;
|
|
2897
3284
|
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
2898
|
-
const
|
|
2899
|
-
const
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
3285
|
+
const logger = resolveLogger(opts.logger);
|
|
3286
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
3287
|
+
const mppxInstances = /* @__PURE__ */ new Map();
|
|
3288
|
+
function getMppx(contract) {
|
|
3289
|
+
let instance = mppxInstances.get(contract);
|
|
3290
|
+
if (!instance) {
|
|
3291
|
+
instance = server$2.Mppx.create({
|
|
3292
|
+
secretKey: opts.payeeSecretKey,
|
|
3293
|
+
methods: [
|
|
3294
|
+
server$4.stellar.charge({
|
|
3295
|
+
recipient,
|
|
3296
|
+
currency: contract,
|
|
3297
|
+
network: networkId,
|
|
3298
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
3299
|
+
})
|
|
3300
|
+
]
|
|
3301
|
+
});
|
|
3302
|
+
mppxInstances.set(contract, instance);
|
|
3303
|
+
}
|
|
3304
|
+
return instance;
|
|
3305
|
+
}
|
|
2910
3306
|
return async (req, res, next) => {
|
|
2911
3307
|
try {
|
|
3308
|
+
const endpoint = req.path || req.originalUrl;
|
|
3309
|
+
const assetContract = resolveAssetContract(
|
|
3310
|
+
opts.manifest,
|
|
3311
|
+
"mpp-charge",
|
|
3312
|
+
endpoint,
|
|
3313
|
+
opts.assetContract
|
|
3314
|
+
);
|
|
3315
|
+
const mppx = getMppx(assetContract);
|
|
2912
3316
|
let payerAddress = null;
|
|
2913
3317
|
try {
|
|
2914
3318
|
const authHeader = req.headers["authorization"];
|
|
@@ -2924,15 +3328,27 @@ function createMppChargeHandler(opts) {
|
|
|
2924
3328
|
}
|
|
2925
3329
|
} catch {
|
|
2926
3330
|
}
|
|
2927
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
3331
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
3332
|
+
(name) => {
|
|
3333
|
+
const v = req.headers[name.toLowerCase()];
|
|
3334
|
+
return Array.isArray(v) ? v[0] : v;
|
|
3335
|
+
},
|
|
3336
|
+
{
|
|
3337
|
+
method: req.method,
|
|
3338
|
+
path: req.originalUrl.split("?")[0],
|
|
3339
|
+
amount: amountHumanReadable,
|
|
3340
|
+
payTo: recipient
|
|
3341
|
+
}
|
|
3342
|
+
);
|
|
2931
3343
|
if (idempotencyKey) {
|
|
2932
|
-
const
|
|
2933
|
-
if (
|
|
2934
|
-
|
|
2935
|
-
|
|
3344
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
3345
|
+
if (replayCheck.kind === "spent") {
|
|
3346
|
+
res.status(402).json({ error: "Payment already used" });
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
if (replayCheck.kind === "replay") {
|
|
3350
|
+
if (replayCheck.record.headers) {
|
|
3351
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
2936
3352
|
res.setHeader(k, val);
|
|
2937
3353
|
}
|
|
2938
3354
|
}
|
|
@@ -2944,7 +3360,7 @@ function createMppChargeHandler(opts) {
|
|
|
2944
3360
|
const handler = mppx["stellar/charge"];
|
|
2945
3361
|
const result = await handler({
|
|
2946
3362
|
amount: amountHumanReadable,
|
|
2947
|
-
currency:
|
|
3363
|
+
currency: assetContract,
|
|
2948
3364
|
recipient,
|
|
2949
3365
|
description: opts.manifest.name
|
|
2950
3366
|
})(fetchReq);
|
|
@@ -2975,12 +3391,13 @@ function createMppChargeHandler(opts) {
|
|
|
2975
3391
|
if (idempotencyKey) {
|
|
2976
3392
|
await seenTxStore.set(idempotencyKey, {
|
|
2977
3393
|
txHash: reference ?? null,
|
|
2978
|
-
headers: receiptHeaders
|
|
3394
|
+
headers: receiptHeaders,
|
|
3395
|
+
createdAt: Date.now()
|
|
2979
3396
|
});
|
|
2980
3397
|
}
|
|
2981
3398
|
if (reference && opts.onSettled) {
|
|
2982
3399
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
2983
|
-
|
|
3400
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
2984
3401
|
opts.onCallbackError?.(err, "onSettled");
|
|
2985
3402
|
});
|
|
2986
3403
|
}
|
|
@@ -3081,8 +3498,9 @@ function createMppSessionHandler(opts) {
|
|
|
3081
3498
|
const networkId = MPP_NETWORK2[opts.network];
|
|
3082
3499
|
const rateHuman = opts.rate;
|
|
3083
3500
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
3501
|
+
const logger = resolveLogger(opts.logger);
|
|
3084
3502
|
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
3085
|
-
const innerStore = server$
|
|
3503
|
+
const innerStore = server$3.Store.memory();
|
|
3086
3504
|
let record = null;
|
|
3087
3505
|
let voucherCount = 0;
|
|
3088
3506
|
let sessionOpened = false;
|
|
@@ -3132,7 +3550,7 @@ function createMppSessionHandler(opts) {
|
|
|
3132
3550
|
reason
|
|
3133
3551
|
});
|
|
3134
3552
|
} catch (err) {
|
|
3135
|
-
|
|
3553
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
3136
3554
|
}
|
|
3137
3555
|
}
|
|
3138
3556
|
}
|
|
@@ -3179,7 +3597,7 @@ function createMppSessionHandler(opts) {
|
|
|
3179
3597
|
sessionOpened = true;
|
|
3180
3598
|
if (opts.onSessionOpen) {
|
|
3181
3599
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
3182
|
-
|
|
3600
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
3183
3601
|
opts.onCallbackError?.(err, "onSessionOpen");
|
|
3184
3602
|
});
|
|
3185
3603
|
}
|
|
@@ -3187,7 +3605,7 @@ function createMppSessionHandler(opts) {
|
|
|
3187
3605
|
if (opts.onVoucher) {
|
|
3188
3606
|
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
3189
3607
|
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
3190
|
-
|
|
3608
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
3191
3609
|
opts.onCallbackError?.(err, "onVoucher");
|
|
3192
3610
|
});
|
|
3193
3611
|
}
|
|
@@ -3197,7 +3615,7 @@ function createMppSessionHandler(opts) {
|
|
|
3197
3615
|
methods: [
|
|
3198
3616
|
withTypedChannelErrors(
|
|
3199
3617
|
onVerifiedCredential(
|
|
3200
|
-
server$
|
|
3618
|
+
server$3.stellar.channel({
|
|
3201
3619
|
channel: opts.channelFactory,
|
|
3202
3620
|
commitmentKey: opts.commitmentPublicKey,
|
|
3203
3621
|
network: networkId,
|
|
@@ -3228,7 +3646,7 @@ function createMppSessionHandler(opts) {
|
|
|
3228
3646
|
}
|
|
3229
3647
|
if (closeAmount > 0n && closeSig) {
|
|
3230
3648
|
const closePayer = record?.payer ?? null;
|
|
3231
|
-
const closeTxHash = await server$
|
|
3649
|
+
const closeTxHash = await server$3.close({
|
|
3232
3650
|
channel: opts.channelFactory,
|
|
3233
3651
|
amount: closeAmount,
|
|
3234
3652
|
signature: Buffer.from(closeSig, "hex"),
|
|
@@ -3240,7 +3658,7 @@ function createMppSessionHandler(opts) {
|
|
|
3240
3658
|
if (opts.onSettled) {
|
|
3241
3659
|
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
3242
3660
|
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
3243
|
-
|
|
3661
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
3244
3662
|
opts.onCallbackError?.(err, "onSettled");
|
|
3245
3663
|
});
|
|
3246
3664
|
}
|
|
@@ -3251,7 +3669,7 @@ function createMppSessionHandler(opts) {
|
|
|
3251
3669
|
payeeKeypair.publicKey()
|
|
3252
3670
|
);
|
|
3253
3671
|
if (!settlementAddresses) {
|
|
3254
|
-
|
|
3672
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
3255
3673
|
} else {
|
|
3256
3674
|
try {
|
|
3257
3675
|
const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
|
|
@@ -3274,7 +3692,7 @@ function createMppSessionHandler(opts) {
|
|
|
3274
3692
|
preparedTx.sign(payeeKeypair);
|
|
3275
3693
|
await server.sendTransaction(preparedTx);
|
|
3276
3694
|
} catch (recordErr) {
|
|
3277
|
-
|
|
3695
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
3278
3696
|
}
|
|
3279
3697
|
}
|
|
3280
3698
|
}
|
|
@@ -3331,12 +3749,13 @@ function routedock(opts) {
|
|
|
3331
3749
|
payeeSecretKey: opts.payeeSecretKey,
|
|
3332
3750
|
network: opts.network,
|
|
3333
3751
|
amount: x402Price,
|
|
3334
|
-
assetContract: opts.assetContract,
|
|
3752
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3335
3753
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
3336
3754
|
manifest: signedManifest,
|
|
3337
3755
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
3338
3756
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3339
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
3757
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
3758
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3340
3759
|
})
|
|
3341
3760
|
);
|
|
3342
3761
|
}
|
|
@@ -3350,11 +3769,12 @@ function routedock(opts) {
|
|
|
3350
3769
|
payeeSecretKey: opts.payeeSecretKey,
|
|
3351
3770
|
network: opts.network,
|
|
3352
3771
|
amount: chargePrice,
|
|
3353
|
-
assetContract: opts.assetContract,
|
|
3772
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3354
3773
|
manifest: signedManifest,
|
|
3355
3774
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
3356
3775
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3357
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
3776
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
3777
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3358
3778
|
})
|
|
3359
3779
|
);
|
|
3360
3780
|
}
|
|
@@ -3372,7 +3792,7 @@ function routedock(opts) {
|
|
|
3372
3792
|
network: opts.network,
|
|
3373
3793
|
channelFactory: sessionPricing.channelFactory,
|
|
3374
3794
|
rate: sessionPricing.rate,
|
|
3375
|
-
assetContract: opts.assetContract,
|
|
3795
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3376
3796
|
manifest: signedManifest,
|
|
3377
3797
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
3378
3798
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
@@ -3380,7 +3800,8 @@ function routedock(opts) {
|
|
|
3380
3800
|
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
3381
3801
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3382
3802
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
3383
|
-
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
|
|
3803
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
3804
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3384
3805
|
})
|
|
3385
3806
|
);
|
|
3386
3807
|
}
|
|
@@ -3544,25 +3965,26 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3544
3965
|
}
|
|
3545
3966
|
}
|
|
3546
3967
|
async function runStartupReconciliation(opts, logger) {
|
|
3547
|
-
const log = logger
|
|
3548
|
-
log("[SessionReconciler] Running startup reconciliation...");
|
|
3968
|
+
const log = resolveLogger(logger);
|
|
3969
|
+
log("info", "[SessionReconciler] Running startup reconciliation...");
|
|
3549
3970
|
try {
|
|
3550
3971
|
const stats = await reconcileAbandonedSessions(opts);
|
|
3551
3972
|
if (stats.orphanedCount === 0) {
|
|
3552
|
-
log("[SessionReconciler] No orphaned sessions found");
|
|
3973
|
+
log("info", "[SessionReconciler] No orphaned sessions found");
|
|
3553
3974
|
return;
|
|
3554
3975
|
}
|
|
3555
3976
|
log(
|
|
3977
|
+
"info",
|
|
3556
3978
|
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
3557
3979
|
);
|
|
3558
3980
|
if (stats.errors.length > 0) {
|
|
3559
|
-
log("[SessionReconciler] Reconciliation errors:");
|
|
3981
|
+
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
3560
3982
|
for (const err of stats.errors) {
|
|
3561
|
-
log(` - ${err.channelId}: ${err.reason}`);
|
|
3983
|
+
log("warn", ` - ${err.channelId}: ${err.reason}`);
|
|
3562
3984
|
}
|
|
3563
3985
|
}
|
|
3564
3986
|
} catch (err) {
|
|
3565
|
-
log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
3987
|
+
log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
3566
3988
|
}
|
|
3567
3989
|
}
|
|
3568
3990
|
function tryDecodeBase64(value) {
|
|
@@ -3749,23 +4171,32 @@ exports.assertManifestValid = assertManifestValid;
|
|
|
3749
4171
|
exports.assertNulthVaultManifest = assertNulthVaultManifest;
|
|
3750
4172
|
exports.backoffDelayMs = backoffDelayMs;
|
|
3751
4173
|
exports.configureManifestCache = configureManifestCache;
|
|
4174
|
+
exports.consoleLogger = consoleLogger;
|
|
4175
|
+
exports.createConsoleLogger = createConsoleLogger;
|
|
3752
4176
|
exports.createNulthSigner = createNulthSigner;
|
|
3753
4177
|
exports.createPolicyState = createPolicyState;
|
|
3754
4178
|
exports.decimalToStroops = decimalToStroops;
|
|
3755
4179
|
exports.decodeAuthSignature = decodeAuthSignature;
|
|
3756
4180
|
exports.fetchLedgerSequence = fetchLedgerSequence;
|
|
3757
4181
|
exports.fetchManifest = fetchManifest;
|
|
4182
|
+
exports.getEligibleAssets = getEligibleAssets;
|
|
3758
4183
|
exports.httpStatusToError = httpStatusToError;
|
|
3759
4184
|
exports.invalidateManifest = invalidateManifest;
|
|
4185
|
+
exports.isAssetEligible = isAssetEligible;
|
|
3760
4186
|
exports.manifestDigest = manifestDigest;
|
|
4187
|
+
exports.noopLogger = noopLogger;
|
|
4188
|
+
exports.normalizeManifestAssets = normalizeManifestAssets;
|
|
3761
4189
|
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
3762
4190
|
exports.paymentContextFromManifest = paymentContextFromManifest;
|
|
3763
4191
|
exports.prepareNulthSigner = prepareNulthSigner;
|
|
3764
4192
|
exports.rankProvidersByLatency = rankProvidersByLatency;
|
|
3765
4193
|
exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
|
|
3766
4194
|
exports.registerProvider = registerProvider;
|
|
4195
|
+
exports.resolveAssetContract = resolveAssetContract;
|
|
4196
|
+
exports.resolveLogger = resolveLogger;
|
|
3767
4197
|
exports.routedock = routedock;
|
|
3768
4198
|
exports.runStartupReconciliation = runStartupReconciliation;
|
|
4199
|
+
exports.selectAsset = selectAsset;
|
|
3769
4200
|
exports.selectMode = selectMode;
|
|
3770
4201
|
exports.setNulthPaymentContext = setNulthPaymentContext;
|
|
3771
4202
|
exports.signManifest = signManifest;
|
|
@@ -3774,3 +4205,4 @@ exports.usdcToUnits = usdcToUnits;
|
|
|
3774
4205
|
exports.verifyManifestSignature = verifyManifestSignature;
|
|
3775
4206
|
exports.withRetry = withRetry;
|
|
3776
4207
|
exports.wrapFetchError = wrapFetchError;
|
|
4208
|
+
exports.wrapMppError = wrapMppError;
|