@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { usdcToStroops } from '@routedock/nulth-sdk';
|
|
1
|
+
import { usdcToStroops, stroopsToUsdc } from '@routedock/nulth-sdk';
|
|
2
2
|
export { USDC_DECIMALS, usdcToStroops } from '@routedock/nulth-sdk';
|
|
3
3
|
import { Keypair, rpc, Horizon, xdr, Address, scValToNative, hash, Networks, StrKey, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
|
|
4
4
|
import { Validator } from '@cfworker/json-schema';
|
|
@@ -17,9 +17,9 @@ import path from 'path';
|
|
|
17
17
|
import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/facilitator';
|
|
18
18
|
import { ExactStellarScheme as ExactStellarScheme$2 } from '@x402/stellar/exact/server';
|
|
19
19
|
import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
|
|
20
|
-
import { stellar as stellar$
|
|
21
|
-
import { Mppx as Mppx$1
|
|
22
|
-
import { Store, stellar as stellar$
|
|
20
|
+
import { stellar as stellar$3 } from '@stellar/mpp/charge/server';
|
|
21
|
+
import { Request, Mppx as Mppx$1 } from 'mppx/server';
|
|
22
|
+
import { Store, stellar as stellar$2, close } from '@stellar/mpp/channel/server';
|
|
23
23
|
|
|
24
24
|
// src/errors.ts
|
|
25
25
|
var RouteDockError = class extends Error {
|
|
@@ -165,10 +165,35 @@ function wrapFetchError(err, context) {
|
|
|
165
165
|
if (err instanceof RouteDockError) return err;
|
|
166
166
|
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
167
167
|
}
|
|
168
|
+
function wrapMppError(err, context) {
|
|
169
|
+
if (err instanceof RouteDockError) return err;
|
|
170
|
+
if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
|
|
171
|
+
return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
|
|
172
|
+
}
|
|
173
|
+
return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
|
|
174
|
+
}
|
|
168
175
|
|
|
169
176
|
// src/types.ts
|
|
170
177
|
var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
171
178
|
|
|
179
|
+
// src/internal/logger.ts
|
|
180
|
+
function createConsoleLogger() {
|
|
181
|
+
return (level, message, fields) => {
|
|
182
|
+
const values = fields ? Object.values(fields) : [];
|
|
183
|
+
if (values.length > 0) {
|
|
184
|
+
console[level](message, ...values);
|
|
185
|
+
} else {
|
|
186
|
+
console[level](message);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
var consoleLogger = createConsoleLogger();
|
|
191
|
+
var noopLogger = () => {
|
|
192
|
+
};
|
|
193
|
+
function resolveLogger(logger, fallback = consoleLogger) {
|
|
194
|
+
return logger ?? fallback;
|
|
195
|
+
}
|
|
196
|
+
|
|
172
197
|
// src/internal/retry.ts
|
|
173
198
|
var DEFAULT_RETRY_POLICY = {
|
|
174
199
|
maxAttempts: 4,
|
|
@@ -183,10 +208,11 @@ function sleep(ms) {
|
|
|
183
208
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
184
209
|
}
|
|
185
210
|
async function withRetry(fn, policy = {}) {
|
|
186
|
-
const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
|
|
211
|
+
const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
|
|
187
212
|
...DEFAULT_RETRY_POLICY,
|
|
188
213
|
...policy
|
|
189
214
|
};
|
|
215
|
+
const log = resolveLogger(logger);
|
|
190
216
|
let lastError;
|
|
191
217
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
192
218
|
try {
|
|
@@ -204,7 +230,7 @@ async function withRetry(fn, policy = {}) {
|
|
|
204
230
|
try {
|
|
205
231
|
await onRetry(attempt, err, delay);
|
|
206
232
|
} catch (cbErr) {
|
|
207
|
-
|
|
233
|
+
log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
|
|
208
234
|
}
|
|
209
235
|
}
|
|
210
236
|
await sleep(delay);
|
|
@@ -218,6 +244,84 @@ var USDC_ISSUERS = {
|
|
|
218
244
|
};
|
|
219
245
|
var usdcToUnits = usdcToStroops;
|
|
220
246
|
|
|
247
|
+
// src/internal/assetUtils.ts
|
|
248
|
+
function normalizeManifestAssets(manifest) {
|
|
249
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
250
|
+
return manifest.assets;
|
|
251
|
+
}
|
|
252
|
+
if (manifest.asset && manifest.asset_contract) {
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
asset: manifest.asset,
|
|
256
|
+
asset_contract: manifest.asset_contract
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
throw new RouteDockManifestError(
|
|
261
|
+
"Manifest must define either assets[] or root asset/asset_contract fields"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
function getEligibleAssets(manifest, mode, endpoint) {
|
|
265
|
+
const assets = normalizeManifestAssets(manifest);
|
|
266
|
+
return assets.filter((asset) => {
|
|
267
|
+
if (asset.modes && !asset.modes.includes(mode)) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
|
|
271
|
+
const trimmedEndpoint = endpoint.replace(/^\//, "");
|
|
272
|
+
if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if (manifest.endpoints) {
|
|
276
|
+
let pathname = endpoint;
|
|
277
|
+
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
|
|
278
|
+
try {
|
|
279
|
+
pathname = new URL(endpoint).pathname;
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const trimmedPathname = pathname.replace(/^\//, "");
|
|
284
|
+
const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
|
|
285
|
+
const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
|
|
286
|
+
const keyMatches = key === endpoint || key === trimmedEndpoint;
|
|
287
|
+
if (pathMatches || keyMatches) {
|
|
288
|
+
return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
});
|
|
292
|
+
if (!matchesEndpoint) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
|
|
303
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
304
|
+
return selectAsset(manifest, mode, endpoint).asset_contract;
|
|
305
|
+
}
|
|
306
|
+
return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
|
|
307
|
+
}
|
|
308
|
+
function selectAsset(manifest, mode, endpoint) {
|
|
309
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
310
|
+
if (eligible.length === 0) {
|
|
311
|
+
const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
|
|
312
|
+
throw new RouteDockManifestError(
|
|
313
|
+
`No eligible assets found for mode '${mode}'${endpointMsg}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return eligible[0];
|
|
317
|
+
}
|
|
318
|
+
function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
319
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
320
|
+
return eligible.some(
|
|
321
|
+
(asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
221
325
|
// src/schemas/routedock.schema.json
|
|
222
326
|
var routedock_schema_default = {
|
|
223
327
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -297,6 +401,14 @@ var routedock_schema_default = {
|
|
|
297
401
|
type: "string",
|
|
298
402
|
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
299
403
|
},
|
|
404
|
+
assets: {
|
|
405
|
+
type: "array",
|
|
406
|
+
minItems: 1,
|
|
407
|
+
items: {
|
|
408
|
+
$ref: "#/definitions/AssetConfig"
|
|
409
|
+
},
|
|
410
|
+
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."
|
|
411
|
+
},
|
|
300
412
|
payee: {
|
|
301
413
|
type: "string",
|
|
302
414
|
description: "Stellar address (G...) that receives payments"
|
|
@@ -429,6 +541,38 @@ var routedock_schema_default = {
|
|
|
429
541
|
enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
|
|
430
542
|
description: "A supported RouteDock payment mode"
|
|
431
543
|
},
|
|
544
|
+
AssetConfig: {
|
|
545
|
+
type: "object",
|
|
546
|
+
description: "Payment asset configuration with optional mode and endpoint scoping",
|
|
547
|
+
required: ["asset", "asset_contract"],
|
|
548
|
+
additionalProperties: false,
|
|
549
|
+
properties: {
|
|
550
|
+
asset: {
|
|
551
|
+
type: "string",
|
|
552
|
+
description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
|
|
553
|
+
},
|
|
554
|
+
asset_contract: {
|
|
555
|
+
type: "string",
|
|
556
|
+
description: "Stellar Asset Contract (SAC) address for the payment asset"
|
|
557
|
+
},
|
|
558
|
+
modes: {
|
|
559
|
+
type: "array",
|
|
560
|
+
items: {
|
|
561
|
+
$ref: "#/definitions/PaymentMode"
|
|
562
|
+
},
|
|
563
|
+
uniqueItems: true,
|
|
564
|
+
description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
|
|
565
|
+
},
|
|
566
|
+
endpoints: {
|
|
567
|
+
type: "array",
|
|
568
|
+
items: {
|
|
569
|
+
type: "string"
|
|
570
|
+
},
|
|
571
|
+
uniqueItems: true,
|
|
572
|
+
description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
},
|
|
432
576
|
PricingConfig: {
|
|
433
577
|
type: "object",
|
|
434
578
|
description: "Per-request pricing for x402 or mpp-charge modes",
|
|
@@ -574,7 +718,7 @@ var routedock_schema_default = {
|
|
|
574
718
|
|
|
575
719
|
// package.json
|
|
576
720
|
var package_default = {
|
|
577
|
-
version: "0.
|
|
721
|
+
version: "0.3.0"};
|
|
578
722
|
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
579
723
|
function canonicalize(value) {
|
|
580
724
|
if (Array.isArray(value)) {
|
|
@@ -698,6 +842,7 @@ function assertEndpointActive(manifest, url, logger) {
|
|
|
698
842
|
const [key, descriptor] = deprecated;
|
|
699
843
|
const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
|
|
700
844
|
logger(
|
|
845
|
+
"warn",
|
|
701
846
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
|
|
702
847
|
);
|
|
703
848
|
}
|
|
@@ -719,6 +864,27 @@ function assertManifestValid(manifest, baseUrl) {
|
|
|
719
864
|
}
|
|
720
865
|
}
|
|
721
866
|
}
|
|
867
|
+
if (manifest.assets !== void 0) {
|
|
868
|
+
if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
|
|
869
|
+
throw new RouteDockManifestError(
|
|
870
|
+
`Invalid manifest${context}: assets must be a non-empty array when defined`
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
for (let i = 0; i < manifest.assets.length; i++) {
|
|
874
|
+
const a = manifest.assets[i];
|
|
875
|
+
if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
|
|
876
|
+
throw new RouteDockManifestError(
|
|
877
|
+
`Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const first = manifest.assets[0];
|
|
882
|
+
if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
|
|
883
|
+
throw new RouteDockManifestError(
|
|
884
|
+
`Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
722
888
|
}
|
|
723
889
|
var CACHE_TTL_MS = 6e4;
|
|
724
890
|
var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
|
|
@@ -898,16 +1064,16 @@ function logSelection(manifest, selection, log, deprecated) {
|
|
|
898
1064
|
const reason = selection.reason ? ` (${selection.reason})` : "";
|
|
899
1065
|
if (deprecated) {
|
|
900
1066
|
log(
|
|
1067
|
+
"warn",
|
|
901
1068
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
|
|
902
1069
|
);
|
|
903
1070
|
return;
|
|
904
1071
|
}
|
|
905
|
-
log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
|
|
1072
|
+
log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
|
|
906
1073
|
}
|
|
907
1074
|
function selectMode(manifest, options = {}) {
|
|
908
1075
|
const modes = manifest.modes;
|
|
909
|
-
const log = options.logger ??
|
|
910
|
-
});
|
|
1076
|
+
const log = options.logger ?? noopLogger;
|
|
911
1077
|
const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
|
|
912
1078
|
if (options.forceMode) {
|
|
913
1079
|
if (!modes.includes(options.forceMode)) {
|
|
@@ -917,10 +1083,11 @@ function selectMode(manifest, options = {}) {
|
|
|
917
1083
|
}
|
|
918
1084
|
if (deprecatedSet.has(options.forceMode)) {
|
|
919
1085
|
log(
|
|
1086
|
+
"warn",
|
|
920
1087
|
`[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
|
|
921
1088
|
);
|
|
922
1089
|
} else {
|
|
923
|
-
log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
|
|
1090
|
+
log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
|
|
924
1091
|
}
|
|
925
1092
|
return options.forceMode;
|
|
926
1093
|
}
|
|
@@ -956,6 +1123,69 @@ function rankProvidersByLatency(manifests, targetRegion) {
|
|
|
956
1123
|
return 0;
|
|
957
1124
|
});
|
|
958
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
// src/internal/payee.ts
|
|
1128
|
+
function resolvePayee(manifest, mode) {
|
|
1129
|
+
const override = manifest.pricing[mode]?.payee;
|
|
1130
|
+
return override ?? manifest.payee;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/client/challenge.ts
|
|
1134
|
+
function isRecord(value) {
|
|
1135
|
+
return typeof value === "object" && value !== null;
|
|
1136
|
+
}
|
|
1137
|
+
function isPositiveIntegerString(value) {
|
|
1138
|
+
return typeof value === "string" && /^[1-9]\d*$/.test(value);
|
|
1139
|
+
}
|
|
1140
|
+
function checkAmount(amount, maxAmount) {
|
|
1141
|
+
if (!isPositiveIntegerString(amount)) {
|
|
1142
|
+
return new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1143
|
+
}
|
|
1144
|
+
let max;
|
|
1145
|
+
try {
|
|
1146
|
+
max = usdcToStroops(maxAmount);
|
|
1147
|
+
} catch {
|
|
1148
|
+
return new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1149
|
+
}
|
|
1150
|
+
if (BigInt(amount) > max) {
|
|
1151
|
+
return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
function checkX402Accept(accept, manifest, network, maxAmount) {
|
|
1156
|
+
if (accept.network !== network) {
|
|
1157
|
+
return new RouteDockPolicyRejectError("challenge_network_mismatch");
|
|
1158
|
+
}
|
|
1159
|
+
if (accept.payTo !== resolvePayee(manifest, "x402")) {
|
|
1160
|
+
return new RouteDockPolicyRejectError("challenge_payee_mismatch");
|
|
1161
|
+
}
|
|
1162
|
+
if (accept.asset !== manifest.asset_contract) {
|
|
1163
|
+
return new RouteDockPolicyRejectError("challenge_asset_mismatch");
|
|
1164
|
+
}
|
|
1165
|
+
return checkAmount(accept.amount, maxAmount);
|
|
1166
|
+
}
|
|
1167
|
+
function filterX402Accepts(accepts, manifest, network, maxAmount) {
|
|
1168
|
+
return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
|
|
1169
|
+
}
|
|
1170
|
+
function checkChargeChallenge(request, manifest, network, maxAmount) {
|
|
1171
|
+
const methodDetails = request["methodDetails"];
|
|
1172
|
+
const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
|
|
1173
|
+
const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
|
|
1174
|
+
if (challengeNetwork !== network) {
|
|
1175
|
+
return new RouteDockPolicyRejectError("challenge_network_mismatch");
|
|
1176
|
+
}
|
|
1177
|
+
const recipient = request["recipient"];
|
|
1178
|
+
if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
|
|
1179
|
+
return new RouteDockPolicyRejectError("challenge_payee_mismatch");
|
|
1180
|
+
}
|
|
1181
|
+
const currency = request["currency"];
|
|
1182
|
+
if (typeof currency !== "string" || currency !== manifest.asset_contract) {
|
|
1183
|
+
return new RouteDockPolicyRejectError("challenge_asset_mismatch");
|
|
1184
|
+
}
|
|
1185
|
+
return checkAmount(request["amount"], maxAmount);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/client/x402Client.ts
|
|
959
1189
|
var CAIP2 = {
|
|
960
1190
|
testnet: "stellar:testnet",
|
|
961
1191
|
mainnet: "stellar:pubnet"
|
|
@@ -987,83 +1217,106 @@ var X402Client = class _X402Client {
|
|
|
987
1217
|
if (!pricing.facilitator) {
|
|
988
1218
|
throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
|
|
989
1219
|
}
|
|
990
|
-
|
|
991
|
-
|
|
1220
|
+
const caip2 = CAIP2[this.network];
|
|
1221
|
+
const probe = await withRetry(async () => {
|
|
1222
|
+
let init2;
|
|
992
1223
|
try {
|
|
993
|
-
|
|
1224
|
+
init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
|
|
994
1225
|
} catch (err) {
|
|
995
1226
|
throw wrapFetchError(err, "x402 initial request");
|
|
996
1227
|
}
|
|
997
|
-
if (
|
|
998
|
-
if (!
|
|
1228
|
+
if (init2.status !== 402) {
|
|
1229
|
+
if (!init2.ok) {
|
|
999
1230
|
throw httpStatusToError(
|
|
1000
|
-
`x402 initial request failed: HTTP ${
|
|
1001
|
-
|
|
1002
|
-
|
|
1231
|
+
`x402 initial request failed: HTTP ${init2.status}`,
|
|
1232
|
+
init2.status,
|
|
1233
|
+
init2
|
|
1003
1234
|
);
|
|
1004
1235
|
}
|
|
1005
1236
|
let data2;
|
|
1006
1237
|
try {
|
|
1007
|
-
data2 = await
|
|
1238
|
+
data2 = await init2.json();
|
|
1008
1239
|
} catch {
|
|
1009
|
-
throw new RouteDockManifestError(
|
|
1240
|
+
throw new RouteDockManifestError(
|
|
1241
|
+
`Failed to parse JSON from non-402 response (HTTP ${init2.status})`
|
|
1242
|
+
);
|
|
1010
1243
|
}
|
|
1011
|
-
return {
|
|
1012
|
-
}
|
|
1013
|
-
const reqHeader = init.headers.get("X-Payment-Requirements");
|
|
1014
|
-
if (!reqHeader) {
|
|
1015
|
-
throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
|
|
1244
|
+
return { kind: "free", data: data2 };
|
|
1016
1245
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1246
|
+
return { kind: "payment-required", init: init2 };
|
|
1247
|
+
}, this.retryPolicy);
|
|
1248
|
+
if (probe.kind === "free") {
|
|
1249
|
+
return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
|
|
1250
|
+
}
|
|
1251
|
+
const init = probe.init;
|
|
1252
|
+
const reqHeader = init.headers.get("X-Payment-Requirements");
|
|
1253
|
+
if (!reqHeader) {
|
|
1254
|
+
throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
|
|
1255
|
+
}
|
|
1256
|
+
let paymentRequired;
|
|
1257
|
+
try {
|
|
1258
|
+
paymentRequired = decodePaymentRequiredHeader(reqHeader);
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
|
|
1261
|
+
cause: err
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
const firstAccept = paymentRequired.accepts[0];
|
|
1265
|
+
if (!firstAccept) {
|
|
1266
|
+
throw new RouteDockManifestError("402 response carries no payment accepts");
|
|
1267
|
+
}
|
|
1268
|
+
const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
|
|
1269
|
+
if (allowed.length === 0) {
|
|
1270
|
+
throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
|
|
1271
|
+
}
|
|
1272
|
+
let paymentPayload;
|
|
1273
|
+
try {
|
|
1274
|
+
paymentPayload = await this.httpClient.createPaymentPayload({
|
|
1275
|
+
...paymentRequired,
|
|
1276
|
+
accepts: allowed
|
|
1277
|
+
});
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
|
|
1280
|
+
cause: err
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
1284
|
+
const signedAmount = stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
|
|
1285
|
+
const settled = await withRetry(async () => {
|
|
1286
|
+
let response;
|
|
1035
1287
|
try {
|
|
1036
|
-
|
|
1288
|
+
response = await fetch(url, { headers: paymentHeaders });
|
|
1037
1289
|
} catch (err) {
|
|
1038
1290
|
throw wrapFetchError(err, "x402 settlement request");
|
|
1039
1291
|
}
|
|
1040
|
-
if (!
|
|
1041
|
-
if (
|
|
1292
|
+
if (!response.ok) {
|
|
1293
|
+
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
1042
1294
|
throw httpStatusToError(
|
|
1043
|
-
`x402 payment failed: HTTP ${
|
|
1044
|
-
|
|
1045
|
-
|
|
1295
|
+
`x402 payment failed: HTTP ${response.status}`,
|
|
1296
|
+
response.status,
|
|
1297
|
+
response
|
|
1046
1298
|
);
|
|
1047
1299
|
}
|
|
1048
|
-
throw new RouteDockManifestError(`x402 payment failed: HTTP ${
|
|
1300
|
+
throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
|
|
1049
1301
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
txHash = settleResponse.transaction ?? null;
|
|
1056
|
-
} catch {
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
let data;
|
|
1302
|
+
return response;
|
|
1303
|
+
}, this.retryPolicy);
|
|
1304
|
+
let txHash = null;
|
|
1305
|
+
const responseHeader = settled.headers.get("X-Payment-Response");
|
|
1306
|
+
if (responseHeader) {
|
|
1060
1307
|
try {
|
|
1061
|
-
|
|
1308
|
+
const settleResponse = decodePaymentResponseHeader(responseHeader);
|
|
1309
|
+
txHash = settleResponse.transaction ?? null;
|
|
1062
1310
|
} catch {
|
|
1063
|
-
throw new RouteDockManifestError("Failed to parse JSON from settled response");
|
|
1064
1311
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1312
|
+
}
|
|
1313
|
+
let data;
|
|
1314
|
+
try {
|
|
1315
|
+
data = await settled.json();
|
|
1316
|
+
} catch {
|
|
1317
|
+
throw new RouteDockManifestError("Failed to parse JSON from settled response");
|
|
1318
|
+
}
|
|
1319
|
+
return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
|
|
1067
1320
|
}
|
|
1068
1321
|
};
|
|
1069
1322
|
var MppChargeClient = class {
|
|
@@ -1080,27 +1333,42 @@ var MppChargeClient = class {
|
|
|
1080
1333
|
if (!pricing) {
|
|
1081
1334
|
throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
|
|
1082
1335
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1336
|
+
const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
|
|
1337
|
+
let txHash = null;
|
|
1338
|
+
let signedAmount;
|
|
1339
|
+
let credential;
|
|
1340
|
+
const mppx = Mppx.create({
|
|
1341
|
+
polyfill: false,
|
|
1342
|
+
methods: [
|
|
1343
|
+
stellar.charge({
|
|
1344
|
+
keypair: this.keypair,
|
|
1345
|
+
mode: "pull",
|
|
1346
|
+
onProgress(event) {
|
|
1347
|
+
if (event.type === "paid") {
|
|
1348
|
+
txHash = event.hash;
|
|
1095
1349
|
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1350
|
+
}
|
|
1351
|
+
})
|
|
1352
|
+
],
|
|
1353
|
+
onChallenge: async (challenge, { createCredential }) => {
|
|
1354
|
+
const request = challenge.request;
|
|
1355
|
+
const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
|
|
1356
|
+
if (rejection) throw rejection;
|
|
1357
|
+
const amount = request["amount"];
|
|
1358
|
+
if (typeof amount !== "string") {
|
|
1359
|
+
throw new RouteDockPolicyRejectError("challenge_amount_invalid");
|
|
1360
|
+
}
|
|
1361
|
+
signedAmount = stroopsToUsdc(BigInt(amount));
|
|
1362
|
+
credential ??= await createCredential();
|
|
1363
|
+
return credential;
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
return withRetry(async () => {
|
|
1099
1367
|
let response;
|
|
1100
1368
|
try {
|
|
1101
|
-
response = await mppx.fetch(url);
|
|
1369
|
+
response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
|
|
1102
1370
|
} catch (err) {
|
|
1103
|
-
throw
|
|
1371
|
+
throw wrapMppError(err, "MPP charge request");
|
|
1104
1372
|
}
|
|
1105
1373
|
if (!response.ok) {
|
|
1106
1374
|
if (response.status >= 500 || response.status === 429 || response.status === 503) {
|
|
@@ -1121,7 +1389,13 @@ var MppChargeClient = class {
|
|
|
1121
1389
|
{ cause }
|
|
1122
1390
|
);
|
|
1123
1391
|
}
|
|
1124
|
-
return {
|
|
1392
|
+
return {
|
|
1393
|
+
data,
|
|
1394
|
+
txHash,
|
|
1395
|
+
mode: "mpp-charge",
|
|
1396
|
+
amount: signedAmount ?? pricing.amount,
|
|
1397
|
+
timestamp: Date.now()
|
|
1398
|
+
};
|
|
1125
1399
|
}, this.retryPolicy);
|
|
1126
1400
|
}
|
|
1127
1401
|
};
|
|
@@ -1160,16 +1434,18 @@ function resolveStoreNetworkId(network) {
|
|
|
1160
1434
|
return network === STELLAR_PUBNET ? STELLAR_PUBNET : STELLAR_TESTNET;
|
|
1161
1435
|
}
|
|
1162
1436
|
var MppSessionClient = class {
|
|
1163
|
-
constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
|
|
1437
|
+
constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
|
|
1164
1438
|
this.keypair = keypair;
|
|
1165
1439
|
this.network = network;
|
|
1166
1440
|
this.retryPolicy = retryPolicy;
|
|
1167
1441
|
this.webSocketFactory = webSocketFactory;
|
|
1442
|
+
this.logger = logger;
|
|
1168
1443
|
}
|
|
1169
1444
|
keypair;
|
|
1170
1445
|
network;
|
|
1171
1446
|
retryPolicy;
|
|
1172
1447
|
webSocketFactory;
|
|
1448
|
+
logger;
|
|
1173
1449
|
async openSession(url, manifest, commitmentSecret, options, onSpend) {
|
|
1174
1450
|
const mode = options?.mode ?? "mpp-session";
|
|
1175
1451
|
const pricing = manifest.pricing[mode];
|
|
@@ -1394,7 +1670,7 @@ var MppSessionClient = class {
|
|
|
1394
1670
|
if (abortController.signal.aborted) {
|
|
1395
1671
|
throw err;
|
|
1396
1672
|
}
|
|
1397
|
-
throw
|
|
1673
|
+
throw wrapMppError(err, "Voucher request");
|
|
1398
1674
|
}
|
|
1399
1675
|
if (!resp.ok) {
|
|
1400
1676
|
if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
|
|
@@ -1687,9 +1963,10 @@ var MppSessionClient = class {
|
|
|
1687
1963
|
emit("session:timeout", { maxDurationMs });
|
|
1688
1964
|
void handle.close().catch((error) => {
|
|
1689
1965
|
emit("session:close-failed", { maxDurationMs, error });
|
|
1690
|
-
|
|
1966
|
+
this.logger(
|
|
1967
|
+
"warn",
|
|
1691
1968
|
`RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
|
|
1692
|
-
error
|
|
1969
|
+
{ error }
|
|
1693
1970
|
);
|
|
1694
1971
|
});
|
|
1695
1972
|
}, maxDurationMs);
|
|
@@ -1739,7 +2016,7 @@ var MppSessionClient = class {
|
|
|
1739
2016
|
);
|
|
1740
2017
|
onSigned?.();
|
|
1741
2018
|
} catch (err) {
|
|
1742
|
-
throw
|
|
2019
|
+
throw wrapMppError(err, "Voucher credential");
|
|
1743
2020
|
}
|
|
1744
2021
|
const wsUrl = url.replace(/^http/, "ws");
|
|
1745
2022
|
const queue = [];
|
|
@@ -1936,7 +2213,10 @@ var NulthClient = class {
|
|
|
1936
2213
|
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
1937
2214
|
);
|
|
1938
2215
|
}
|
|
1939
|
-
|
|
2216
|
+
resolveLogger(config.logger)(
|
|
2217
|
+
"warn",
|
|
2218
|
+
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2219
|
+
);
|
|
1940
2220
|
}
|
|
1941
2221
|
config;
|
|
1942
2222
|
policy;
|
|
@@ -1988,6 +2268,12 @@ var NulthClient = class {
|
|
|
1988
2268
|
return proof;
|
|
1989
2269
|
}
|
|
1990
2270
|
enforcePolicy(context) {
|
|
2271
|
+
if (context.amountStroops < 0n) {
|
|
2272
|
+
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2273
|
+
}
|
|
2274
|
+
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2275
|
+
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2276
|
+
}
|
|
1991
2277
|
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
1992
2278
|
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
1993
2279
|
}
|
|
@@ -2131,7 +2417,8 @@ var InMemorySpendStore = class {
|
|
|
2131
2417
|
state = null;
|
|
2132
2418
|
constructor(options = {}) {
|
|
2133
2419
|
if (options.warn !== false) {
|
|
2134
|
-
|
|
2420
|
+
resolveLogger(options.logger)(
|
|
2421
|
+
"warn",
|
|
2135
2422
|
"[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."
|
|
2136
2423
|
);
|
|
2137
2424
|
}
|
|
@@ -2221,7 +2508,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2221
2508
|
this.network = config.network;
|
|
2222
2509
|
this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
|
|
2223
2510
|
this.retryPolicy = config.retryPolicy;
|
|
2224
|
-
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2511
|
+
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2512
|
+
warn: !!config.spendCap,
|
|
2513
|
+
...config.logger && { logger: config.logger }
|
|
2514
|
+
});
|
|
2225
2515
|
this.logger = config.logger;
|
|
2226
2516
|
this.manifestTimeoutMs = config.manifestTimeoutMs;
|
|
2227
2517
|
this.expectedPayee = config.expectedPayee;
|
|
@@ -2233,9 +2523,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2233
2523
|
_secrets.set(this, config.commitmentSecret);
|
|
2234
2524
|
}
|
|
2235
2525
|
const secretKey = this.keypair.secret();
|
|
2236
|
-
this.
|
|
2237
|
-
this.
|
|
2238
|
-
this.
|
|
2526
|
+
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2527
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2528
|
+
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2529
|
+
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2239
2530
|
}
|
|
2240
2531
|
/** Fetch manifest and select mode — shared by pay() and estimateCost(). */
|
|
2241
2532
|
async _resolveManifest(url, options) {
|
|
@@ -2264,12 +2555,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2264
2555
|
* call before committing to a payment — for approval gates and manual
|
|
2265
2556
|
* trustline remediation.
|
|
2266
2557
|
*/
|
|
2267
|
-
async preflight(manifest) {
|
|
2558
|
+
async preflight(manifest, mode, endpoint) {
|
|
2268
2559
|
assertManifestValid(manifest);
|
|
2269
|
-
|
|
2560
|
+
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2561
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2270
2562
|
return {
|
|
2271
2563
|
hasTrustline: true,
|
|
2272
|
-
asset:
|
|
2564
|
+
asset: selectedAsset,
|
|
2273
2565
|
modes: manifest.modes,
|
|
2274
2566
|
...manifest.regions && { regions: manifest.regions },
|
|
2275
2567
|
...manifest.latency_hints && { latency_hints: manifest.latency_hints },
|
|
@@ -2283,8 +2575,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2283
2575
|
* trustline is missing. Results are cached per (account, asset) so the
|
|
2284
2576
|
* per-payment overhead is one RPC call amortized to zero after the first.
|
|
2285
2577
|
*/
|
|
2286
|
-
async _checkTrustline(manifest) {
|
|
2287
|
-
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${
|
|
2578
|
+
async _checkTrustline(manifest, targetAsset = manifest.asset) {
|
|
2579
|
+
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2288
2580
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2289
2581
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2290
2582
|
const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
|
|
@@ -2292,13 +2584,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2292
2584
|
try {
|
|
2293
2585
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2294
2586
|
const balances = account.balances;
|
|
2295
|
-
const expectedIssuer = getAssetIssuer(
|
|
2587
|
+
const expectedIssuer = getAssetIssuer(targetAsset, this.network);
|
|
2296
2588
|
const hasTrustline = balances.some(
|
|
2297
|
-
(b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code ===
|
|
2589
|
+
(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))
|
|
2298
2590
|
);
|
|
2299
2591
|
if (!hasTrustline) {
|
|
2300
|
-
const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${
|
|
2301
|
-
throw new RouteDockTrustlineError(
|
|
2592
|
+
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}`;
|
|
2593
|
+
throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
|
|
2302
2594
|
}
|
|
2303
2595
|
_RouteDockClient._trustlineCache.set(cacheKey, {
|
|
2304
2596
|
exists: true,
|
|
@@ -2307,7 +2599,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2307
2599
|
} catch (err) {
|
|
2308
2600
|
if (err instanceof RouteDockTrustlineError) throw err;
|
|
2309
2601
|
this.logger?.(
|
|
2310
|
-
|
|
2602
|
+
"warn",
|
|
2603
|
+
`[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
|
|
2311
2604
|
);
|
|
2312
2605
|
}
|
|
2313
2606
|
}
|
|
@@ -2315,18 +2608,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2315
2608
|
* Pay for one request at `url`. Fetches manifest, selects payment mode,
|
|
2316
2609
|
* runs trustline preflight, reserves local spend cap BEFORE executing the
|
|
2317
2610
|
* payment, then commits the spend record on success. Rolls back the
|
|
2318
|
-
* reservation if the on-chain payment fails
|
|
2319
|
-
*
|
|
2611
|
+
* reservation if the on-chain payment fails, and also when the call
|
|
2612
|
+
* completes without paying anything (an x402 endpoint answering a plain
|
|
2613
|
+
* 200 with no challenge). Concurrent pay() calls are serialized through a
|
|
2614
|
+
* per-instance mutex to prevent spend-cap overrun.
|
|
2320
2615
|
*/
|
|
2321
2616
|
async pay(url, options) {
|
|
2322
2617
|
const baseUrl = new URL(url).origin;
|
|
2323
2618
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2324
2619
|
assertEndpointActive(manifest, url, this.logger);
|
|
2325
2620
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
return this._payWithNulthVault(url, manifest, mode);
|
|
2329
|
-
}
|
|
2621
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2622
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2330
2623
|
let amount;
|
|
2331
2624
|
switch (mode) {
|
|
2332
2625
|
case "x402":
|
|
@@ -2346,22 +2639,30 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2346
2639
|
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2347
2640
|
let result;
|
|
2348
2641
|
try {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2642
|
+
if (this.vault?.mode === "nulth") {
|
|
2643
|
+
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2644
|
+
} else {
|
|
2645
|
+
switch (mode) {
|
|
2646
|
+
case "x402":
|
|
2647
|
+
result = await this.x402.pay(url, manifest);
|
|
2648
|
+
break;
|
|
2649
|
+
case "mpp-charge":
|
|
2650
|
+
result = await this.charge.pay(url, manifest);
|
|
2651
|
+
break;
|
|
2652
|
+
default:
|
|
2653
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2654
|
+
}
|
|
2358
2655
|
}
|
|
2359
2656
|
} catch (err) {
|
|
2360
2657
|
await this._rollbackSpend(reserveId).catch(() => {
|
|
2361
2658
|
});
|
|
2362
2659
|
throw err;
|
|
2363
2660
|
}
|
|
2364
|
-
|
|
2661
|
+
if (result.txHash === null && result.amount === "0") {
|
|
2662
|
+
await this._rollbackSpend(reserveId);
|
|
2663
|
+
} else {
|
|
2664
|
+
await this._commitSpend(reserveId);
|
|
2665
|
+
}
|
|
2365
2666
|
return result;
|
|
2366
2667
|
}
|
|
2367
2668
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
@@ -2411,9 +2712,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2411
2712
|
default:
|
|
2412
2713
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2413
2714
|
}
|
|
2715
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2414
2716
|
return {
|
|
2415
2717
|
amount,
|
|
2416
|
-
asset:
|
|
2718
|
+
asset: selectedAsset,
|
|
2417
2719
|
mode,
|
|
2418
2720
|
manifest,
|
|
2419
2721
|
...manifest.regions && { regions: manifest.regions },
|
|
@@ -2650,7 +2952,7 @@ var SupabaseSeenTxStore = class {
|
|
|
2650
2952
|
}
|
|
2651
2953
|
supabase;
|
|
2652
2954
|
async get(key) {
|
|
2653
|
-
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
|
|
2955
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
|
|
2654
2956
|
if (error) {
|
|
2655
2957
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
2656
2958
|
}
|
|
@@ -2658,7 +2960,8 @@ var SupabaseSeenTxStore = class {
|
|
|
2658
2960
|
const headers = data.headers;
|
|
2659
2961
|
return {
|
|
2660
2962
|
txHash: data.tx_hash ?? null,
|
|
2661
|
-
...headers ? { headers } : {}
|
|
2963
|
+
...headers ? { headers } : {},
|
|
2964
|
+
createdAt: Date.parse(data.created_at)
|
|
2662
2965
|
};
|
|
2663
2966
|
}
|
|
2664
2967
|
async set(key, record) {
|
|
@@ -2666,7 +2969,8 @@ var SupabaseSeenTxStore = class {
|
|
|
2666
2969
|
{
|
|
2667
2970
|
key,
|
|
2668
2971
|
tx_hash: record.txHash,
|
|
2669
|
-
headers: record.headers ?? null
|
|
2972
|
+
headers: record.headers ?? null,
|
|
2973
|
+
created_at: new Date(record.createdAt ?? Date.now()).toISOString()
|
|
2670
2974
|
},
|
|
2671
2975
|
{ onConflict: "key" }
|
|
2672
2976
|
);
|
|
@@ -2674,21 +2978,47 @@ var SupabaseSeenTxStore = class {
|
|
|
2674
2978
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
2675
2979
|
}
|
|
2676
2980
|
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Atomically increment the settlement's replay counter through the
|
|
2983
|
+
* `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
|
|
2984
|
+
* row lock gives concurrent claims distinct counts. Returns `Infinity` when
|
|
2985
|
+
* the key no longer exists, so the caller treats it as spent.
|
|
2986
|
+
*/
|
|
2987
|
+
async claimReplay(key) {
|
|
2988
|
+
const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
|
|
2989
|
+
p_key: key
|
|
2990
|
+
});
|
|
2991
|
+
if (error) {
|
|
2992
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
|
|
2993
|
+
}
|
|
2994
|
+
if (data == null) return Number.POSITIVE_INFINITY;
|
|
2995
|
+
return data;
|
|
2996
|
+
}
|
|
2677
2997
|
};
|
|
2678
|
-
|
|
2679
|
-
// src/provider/payee.ts
|
|
2680
|
-
function resolvePayee(manifest, mode) {
|
|
2681
|
-
const override = manifest.pricing[mode]?.payee;
|
|
2682
|
-
return override ?? manifest.payee;
|
|
2683
|
-
}
|
|
2684
2998
|
function extractPayerAddress(key) {
|
|
2685
2999
|
if (typeof key !== "string") return null;
|
|
2686
3000
|
return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
2687
3001
|
}
|
|
2688
3002
|
|
|
2689
3003
|
// src/provider/SeenTxStore.ts
|
|
3004
|
+
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
3005
|
+
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
3006
|
+
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
3007
|
+
const record = await store.get(key);
|
|
3008
|
+
if (!record) return { kind: "miss" };
|
|
3009
|
+
const { createdAt } = record;
|
|
3010
|
+
if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
|
|
3011
|
+
return { kind: "spent" };
|
|
3012
|
+
}
|
|
3013
|
+
if (store.claimReplay) {
|
|
3014
|
+
const replayed = await store.claimReplay(key);
|
|
3015
|
+
if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
|
|
3016
|
+
}
|
|
3017
|
+
return { kind: "replay", record };
|
|
3018
|
+
}
|
|
2690
3019
|
var InMemorySeenTxStore = class {
|
|
2691
3020
|
map = /* @__PURE__ */ new Map();
|
|
3021
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
2692
3022
|
order = [];
|
|
2693
3023
|
constructor(maxEntriesOrOptions = 1e4) {
|
|
2694
3024
|
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
@@ -2696,7 +3026,8 @@ var InMemorySeenTxStore = class {
|
|
|
2696
3026
|
if (options.warn !== false) {
|
|
2697
3027
|
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
2698
3028
|
if (isServerless) {
|
|
2699
|
-
|
|
3029
|
+
resolveLogger(options.logger)(
|
|
3030
|
+
"warn",
|
|
2700
3031
|
"[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."
|
|
2701
3032
|
);
|
|
2702
3033
|
}
|
|
@@ -2711,21 +3042,43 @@ var InMemorySeenTxStore = class {
|
|
|
2711
3042
|
this.order.push(key);
|
|
2712
3043
|
if (this.order.length > this.maxEntries) {
|
|
2713
3044
|
const evicted = this.order.shift();
|
|
2714
|
-
if (evicted !== void 0)
|
|
3045
|
+
if (evicted !== void 0) {
|
|
3046
|
+
this.map.delete(evicted);
|
|
3047
|
+
this.replayCounts.delete(evicted);
|
|
3048
|
+
}
|
|
2715
3049
|
}
|
|
2716
3050
|
}
|
|
2717
3051
|
this.map.set(key, record);
|
|
2718
3052
|
}
|
|
3053
|
+
claimReplay(key) {
|
|
3054
|
+
if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
|
|
3055
|
+
const next = (this.replayCounts.get(key) ?? 0) + 1;
|
|
3056
|
+
this.replayCounts.set(key, next);
|
|
3057
|
+
return next;
|
|
3058
|
+
}
|
|
2719
3059
|
};
|
|
2720
3060
|
async function sha256Hex2(input) {
|
|
2721
3061
|
const buf = new TextEncoder().encode(input);
|
|
2722
3062
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
2723
3063
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2724
3064
|
}
|
|
2725
|
-
async function paymentIdempotencyKey(getHeader) {
|
|
3065
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
2726
3066
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
2727
3067
|
if (!material) return null;
|
|
2728
|
-
return sha256Hex2(
|
|
3068
|
+
return sha256Hex2(
|
|
3069
|
+
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/provider/settleResult.ts
|
|
3074
|
+
function readSettleResult(result) {
|
|
3075
|
+
const settle = result;
|
|
3076
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
3077
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
3078
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
3079
|
+
return failed(settle.errorReason);
|
|
3080
|
+
}
|
|
3081
|
+
return { ok: true, txHash: settle.transaction };
|
|
2729
3082
|
}
|
|
2730
3083
|
|
|
2731
3084
|
// src/provider/x402Handler.ts
|
|
@@ -2738,7 +3091,8 @@ function createX402Handler(opts) {
|
|
|
2738
3091
|
const caip2 = CAIP22[opts.network];
|
|
2739
3092
|
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
2740
3093
|
const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
2741
|
-
const
|
|
3094
|
+
const logger = resolveLogger(opts.logger);
|
|
3095
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
2742
3096
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
2743
3097
|
const localFacilitator = new ExactStellarScheme$1([signer], {
|
|
2744
3098
|
areFeesSponsored: true
|
|
@@ -2759,22 +3113,29 @@ function createX402Handler(opts) {
|
|
|
2759
3113
|
}
|
|
2760
3114
|
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
2761
3115
|
const payTo = resolvePayee(opts.manifest, "x402");
|
|
2762
|
-
const requirements = {
|
|
2763
|
-
scheme: "exact",
|
|
2764
|
-
network: caip2,
|
|
2765
|
-
asset: opts.assetContract,
|
|
2766
|
-
amount: amountInBaseUnits,
|
|
2767
|
-
payTo,
|
|
2768
|
-
maxTimeoutSeconds: 60,
|
|
2769
|
-
extra: {
|
|
2770
|
-
areFeesSponsored: true,
|
|
2771
|
-
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
2772
|
-
}
|
|
2773
|
-
};
|
|
2774
3116
|
return async (req, res, next) => {
|
|
2775
3117
|
try {
|
|
2776
|
-
const
|
|
2777
|
-
|
|
3118
|
+
const endpoint = req.path || req.originalUrl;
|
|
3119
|
+
const assetContract = resolveAssetContract(
|
|
3120
|
+
opts.manifest,
|
|
3121
|
+
"x402",
|
|
3122
|
+
endpoint,
|
|
3123
|
+
opts.assetContract
|
|
3124
|
+
);
|
|
3125
|
+
const requirements = {
|
|
3126
|
+
scheme: "exact",
|
|
3127
|
+
network: caip2,
|
|
3128
|
+
asset: assetContract,
|
|
3129
|
+
amount: amountInBaseUnits,
|
|
3130
|
+
payTo,
|
|
3131
|
+
maxTimeoutSeconds: 60,
|
|
3132
|
+
extra: {
|
|
3133
|
+
areFeesSponsored: true,
|
|
3134
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
3135
|
+
}
|
|
3136
|
+
};
|
|
3137
|
+
const respondPaymentRequired = async (error, reason) => {
|
|
3138
|
+
const body = { error, ...reason ? { reason } : {} };
|
|
2778
3139
|
if (ozServer) {
|
|
2779
3140
|
const resourceInfo = {
|
|
2780
3141
|
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
@@ -2784,7 +3145,7 @@ function createX402Handler(opts) {
|
|
|
2784
3145
|
[requirements],
|
|
2785
3146
|
resourceInfo
|
|
2786
3147
|
);
|
|
2787
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(
|
|
3148
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
2788
3149
|
} else {
|
|
2789
3150
|
const x402Response = {
|
|
2790
3151
|
x402Version: 2,
|
|
@@ -2794,19 +3155,35 @@ function createX402Handler(opts) {
|
|
|
2794
3155
|
},
|
|
2795
3156
|
accepts: [requirements]
|
|
2796
3157
|
};
|
|
2797
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(
|
|
3158
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(body);
|
|
2798
3159
|
}
|
|
3160
|
+
};
|
|
3161
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
3162
|
+
if (!paymentHeader) {
|
|
3163
|
+
await respondPaymentRequired("Payment Required");
|
|
2799
3164
|
return;
|
|
2800
3165
|
}
|
|
2801
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3166
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
3167
|
+
(name) => {
|
|
3168
|
+
const v = req.headers[name.toLowerCase()];
|
|
3169
|
+
return Array.isArray(v) ? v[0] : v;
|
|
3170
|
+
},
|
|
3171
|
+
{
|
|
3172
|
+
method: req.method,
|
|
3173
|
+
path: req.originalUrl.split("?")[0],
|
|
3174
|
+
amount: requirements.amount,
|
|
3175
|
+
payTo: requirements.payTo
|
|
3176
|
+
}
|
|
3177
|
+
);
|
|
2805
3178
|
if (idempotencyKey) {
|
|
2806
|
-
const
|
|
2807
|
-
if (
|
|
2808
|
-
|
|
2809
|
-
|
|
3179
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
3180
|
+
if (replayCheck.kind === "spent") {
|
|
3181
|
+
res.status(402).json({ error: "Payment already used" });
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
if (replayCheck.kind === "replay") {
|
|
3185
|
+
if (replayCheck.record.headers) {
|
|
3186
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
2810
3187
|
res.setHeader(k, val);
|
|
2811
3188
|
}
|
|
2812
3189
|
}
|
|
@@ -2825,15 +3202,18 @@ function createX402Handler(opts) {
|
|
|
2825
3202
|
}
|
|
2826
3203
|
if (ozServer) {
|
|
2827
3204
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
2828
|
-
|
|
2829
|
-
if (
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
encodePaymentResponseHeader(
|
|
2833
|
-
settleResult
|
|
2834
|
-
)
|
|
2835
|
-
);
|
|
3205
|
+
const outcome = readSettleResult(settleResult);
|
|
3206
|
+
if (!outcome.ok) {
|
|
3207
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
3208
|
+
return;
|
|
2836
3209
|
}
|
|
3210
|
+
txHash = outcome.txHash;
|
|
3211
|
+
res.setHeader(
|
|
3212
|
+
"X-Payment-Response",
|
|
3213
|
+
encodePaymentResponseHeader(
|
|
3214
|
+
settleResult
|
|
3215
|
+
)
|
|
3216
|
+
);
|
|
2837
3217
|
} else {
|
|
2838
3218
|
const verifyResult = await localFacilitator.verify(
|
|
2839
3219
|
payload,
|
|
@@ -2850,15 +3230,18 @@ function createX402Handler(opts) {
|
|
|
2850
3230
|
payload,
|
|
2851
3231
|
requirements
|
|
2852
3232
|
);
|
|
2853
|
-
|
|
2854
|
-
if (
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
encodePaymentResponseHeader(
|
|
2858
|
-
settleResult
|
|
2859
|
-
)
|
|
2860
|
-
);
|
|
3233
|
+
const outcome = readSettleResult(settleResult);
|
|
3234
|
+
if (!outcome.ok) {
|
|
3235
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
3236
|
+
return;
|
|
2861
3237
|
}
|
|
3238
|
+
txHash = outcome.txHash;
|
|
3239
|
+
res.setHeader(
|
|
3240
|
+
"X-Payment-Response",
|
|
3241
|
+
encodePaymentResponseHeader(
|
|
3242
|
+
settleResult
|
|
3243
|
+
)
|
|
3244
|
+
);
|
|
2862
3245
|
}
|
|
2863
3246
|
if (idempotencyKey) {
|
|
2864
3247
|
const headers = {};
|
|
@@ -2866,17 +3249,21 @@ function createX402Handler(opts) {
|
|
|
2866
3249
|
if (typeof paymentResponse === "string") {
|
|
2867
3250
|
headers["X-Payment-Response"] = paymentResponse;
|
|
2868
3251
|
}
|
|
2869
|
-
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
3252
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
2870
3253
|
}
|
|
2871
3254
|
if (txHash && opts.onSettled) {
|
|
2872
3255
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
2873
|
-
|
|
3256
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
2874
3257
|
opts.onCallbackError?.(err, "onSettled");
|
|
2875
3258
|
});
|
|
2876
3259
|
}
|
|
2877
3260
|
next();
|
|
2878
3261
|
} catch (err) {
|
|
2879
|
-
|
|
3262
|
+
if (err instanceof RouteDockManifestError) {
|
|
3263
|
+
next(err);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
2880
3267
|
res.status(500).json({ error: "Payment settlement failed" });
|
|
2881
3268
|
}
|
|
2882
3269
|
};
|
|
@@ -2889,20 +3276,37 @@ function createMppChargeHandler(opts) {
|
|
|
2889
3276
|
const networkId = MPP_NETWORK[opts.network];
|
|
2890
3277
|
const amountHumanReadable = opts.amount;
|
|
2891
3278
|
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
2892
|
-
const
|
|
2893
|
-
const
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3279
|
+
const logger = resolveLogger(opts.logger);
|
|
3280
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
3281
|
+
const mppxInstances = /* @__PURE__ */ new Map();
|
|
3282
|
+
function getMppx(contract) {
|
|
3283
|
+
let instance = mppxInstances.get(contract);
|
|
3284
|
+
if (!instance) {
|
|
3285
|
+
instance = Mppx$1.create({
|
|
3286
|
+
secretKey: opts.payeeSecretKey,
|
|
3287
|
+
methods: [
|
|
3288
|
+
stellar$3.charge({
|
|
3289
|
+
recipient,
|
|
3290
|
+
currency: contract,
|
|
3291
|
+
network: networkId,
|
|
3292
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
3293
|
+
})
|
|
3294
|
+
]
|
|
3295
|
+
});
|
|
3296
|
+
mppxInstances.set(contract, instance);
|
|
3297
|
+
}
|
|
3298
|
+
return instance;
|
|
3299
|
+
}
|
|
2904
3300
|
return async (req, res, next) => {
|
|
2905
3301
|
try {
|
|
3302
|
+
const endpoint = req.path || req.originalUrl;
|
|
3303
|
+
const assetContract = resolveAssetContract(
|
|
3304
|
+
opts.manifest,
|
|
3305
|
+
"mpp-charge",
|
|
3306
|
+
endpoint,
|
|
3307
|
+
opts.assetContract
|
|
3308
|
+
);
|
|
3309
|
+
const mppx = getMppx(assetContract);
|
|
2906
3310
|
let payerAddress = null;
|
|
2907
3311
|
try {
|
|
2908
3312
|
const authHeader = req.headers["authorization"];
|
|
@@ -2918,15 +3322,27 @@ function createMppChargeHandler(opts) {
|
|
|
2918
3322
|
}
|
|
2919
3323
|
} catch {
|
|
2920
3324
|
}
|
|
2921
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3325
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
3326
|
+
(name) => {
|
|
3327
|
+
const v = req.headers[name.toLowerCase()];
|
|
3328
|
+
return Array.isArray(v) ? v[0] : v;
|
|
3329
|
+
},
|
|
3330
|
+
{
|
|
3331
|
+
method: req.method,
|
|
3332
|
+
path: req.originalUrl.split("?")[0],
|
|
3333
|
+
amount: amountHumanReadable,
|
|
3334
|
+
payTo: recipient
|
|
3335
|
+
}
|
|
3336
|
+
);
|
|
2925
3337
|
if (idempotencyKey) {
|
|
2926
|
-
const
|
|
2927
|
-
if (
|
|
2928
|
-
|
|
2929
|
-
|
|
3338
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
3339
|
+
if (replayCheck.kind === "spent") {
|
|
3340
|
+
res.status(402).json({ error: "Payment already used" });
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
if (replayCheck.kind === "replay") {
|
|
3344
|
+
if (replayCheck.record.headers) {
|
|
3345
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
2930
3346
|
res.setHeader(k, val);
|
|
2931
3347
|
}
|
|
2932
3348
|
}
|
|
@@ -2938,7 +3354,7 @@ function createMppChargeHandler(opts) {
|
|
|
2938
3354
|
const handler = mppx["stellar/charge"];
|
|
2939
3355
|
const result = await handler({
|
|
2940
3356
|
amount: amountHumanReadable,
|
|
2941
|
-
currency:
|
|
3357
|
+
currency: assetContract,
|
|
2942
3358
|
recipient,
|
|
2943
3359
|
description: opts.manifest.name
|
|
2944
3360
|
})(fetchReq);
|
|
@@ -2969,12 +3385,13 @@ function createMppChargeHandler(opts) {
|
|
|
2969
3385
|
if (idempotencyKey) {
|
|
2970
3386
|
await seenTxStore.set(idempotencyKey, {
|
|
2971
3387
|
txHash: reference ?? null,
|
|
2972
|
-
headers: receiptHeaders
|
|
3388
|
+
headers: receiptHeaders,
|
|
3389
|
+
createdAt: Date.now()
|
|
2973
3390
|
});
|
|
2974
3391
|
}
|
|
2975
3392
|
if (reference && opts.onSettled) {
|
|
2976
3393
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
2977
|
-
|
|
3394
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
2978
3395
|
opts.onCallbackError?.(err, "onSettled");
|
|
2979
3396
|
});
|
|
2980
3397
|
}
|
|
@@ -3075,6 +3492,7 @@ function createMppSessionHandler(opts) {
|
|
|
3075
3492
|
const networkId = MPP_NETWORK2[opts.network];
|
|
3076
3493
|
const rateHuman = opts.rate;
|
|
3077
3494
|
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
3495
|
+
const logger = resolveLogger(opts.logger);
|
|
3078
3496
|
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
3079
3497
|
const innerStore = Store.memory();
|
|
3080
3498
|
let record = null;
|
|
@@ -3126,7 +3544,7 @@ function createMppSessionHandler(opts) {
|
|
|
3126
3544
|
reason
|
|
3127
3545
|
});
|
|
3128
3546
|
} catch (err) {
|
|
3129
|
-
|
|
3547
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
3130
3548
|
}
|
|
3131
3549
|
}
|
|
3132
3550
|
}
|
|
@@ -3173,7 +3591,7 @@ function createMppSessionHandler(opts) {
|
|
|
3173
3591
|
sessionOpened = true;
|
|
3174
3592
|
if (opts.onSessionOpen) {
|
|
3175
3593
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
3176
|
-
|
|
3594
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
3177
3595
|
opts.onCallbackError?.(err, "onSessionOpen");
|
|
3178
3596
|
});
|
|
3179
3597
|
}
|
|
@@ -3181,7 +3599,7 @@ function createMppSessionHandler(opts) {
|
|
|
3181
3599
|
if (opts.onVoucher) {
|
|
3182
3600
|
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
3183
3601
|
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
3184
|
-
|
|
3602
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
3185
3603
|
opts.onCallbackError?.(err, "onVoucher");
|
|
3186
3604
|
});
|
|
3187
3605
|
}
|
|
@@ -3191,7 +3609,7 @@ function createMppSessionHandler(opts) {
|
|
|
3191
3609
|
methods: [
|
|
3192
3610
|
withTypedChannelErrors(
|
|
3193
3611
|
onVerifiedCredential(
|
|
3194
|
-
stellar$
|
|
3612
|
+
stellar$2.channel({
|
|
3195
3613
|
channel: opts.channelFactory,
|
|
3196
3614
|
commitmentKey: opts.commitmentPublicKey,
|
|
3197
3615
|
network: networkId,
|
|
@@ -3234,7 +3652,7 @@ function createMppSessionHandler(opts) {
|
|
|
3234
3652
|
if (opts.onSettled) {
|
|
3235
3653
|
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
3236
3654
|
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
3237
|
-
|
|
3655
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
3238
3656
|
opts.onCallbackError?.(err, "onSettled");
|
|
3239
3657
|
});
|
|
3240
3658
|
}
|
|
@@ -3245,7 +3663,7 @@ function createMppSessionHandler(opts) {
|
|
|
3245
3663
|
payeeKeypair.publicKey()
|
|
3246
3664
|
);
|
|
3247
3665
|
if (!settlementAddresses) {
|
|
3248
|
-
|
|
3666
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
3249
3667
|
} else {
|
|
3250
3668
|
try {
|
|
3251
3669
|
const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
|
|
@@ -3268,7 +3686,7 @@ function createMppSessionHandler(opts) {
|
|
|
3268
3686
|
preparedTx.sign(payeeKeypair);
|
|
3269
3687
|
await server.sendTransaction(preparedTx);
|
|
3270
3688
|
} catch (recordErr) {
|
|
3271
|
-
|
|
3689
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
3272
3690
|
}
|
|
3273
3691
|
}
|
|
3274
3692
|
}
|
|
@@ -3325,12 +3743,13 @@ function routedock(opts) {
|
|
|
3325
3743
|
payeeSecretKey: opts.payeeSecretKey,
|
|
3326
3744
|
network: opts.network,
|
|
3327
3745
|
amount: x402Price,
|
|
3328
|
-
assetContract: opts.assetContract,
|
|
3746
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3329
3747
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
3330
3748
|
manifest: signedManifest,
|
|
3331
3749
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
3332
3750
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3333
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
3751
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
3752
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3334
3753
|
})
|
|
3335
3754
|
);
|
|
3336
3755
|
}
|
|
@@ -3344,11 +3763,12 @@ function routedock(opts) {
|
|
|
3344
3763
|
payeeSecretKey: opts.payeeSecretKey,
|
|
3345
3764
|
network: opts.network,
|
|
3346
3765
|
amount: chargePrice,
|
|
3347
|
-
assetContract: opts.assetContract,
|
|
3766
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3348
3767
|
manifest: signedManifest,
|
|
3349
3768
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
3350
3769
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3351
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
3770
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
3771
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3352
3772
|
})
|
|
3353
3773
|
);
|
|
3354
3774
|
}
|
|
@@ -3366,7 +3786,7 @@ function routedock(opts) {
|
|
|
3366
3786
|
network: opts.network,
|
|
3367
3787
|
channelFactory: sessionPricing.channelFactory,
|
|
3368
3788
|
rate: sessionPricing.rate,
|
|
3369
|
-
assetContract: opts.assetContract,
|
|
3789
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3370
3790
|
manifest: signedManifest,
|
|
3371
3791
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
3372
3792
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
@@ -3374,7 +3794,8 @@ function routedock(opts) {
|
|
|
3374
3794
|
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
3375
3795
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3376
3796
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
3377
|
-
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
|
|
3797
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
3798
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3378
3799
|
})
|
|
3379
3800
|
);
|
|
3380
3801
|
}
|
|
@@ -3538,25 +3959,26 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3538
3959
|
}
|
|
3539
3960
|
}
|
|
3540
3961
|
async function runStartupReconciliation(opts, logger) {
|
|
3541
|
-
const log = logger
|
|
3542
|
-
log("[SessionReconciler] Running startup reconciliation...");
|
|
3962
|
+
const log = resolveLogger(logger);
|
|
3963
|
+
log("info", "[SessionReconciler] Running startup reconciliation...");
|
|
3543
3964
|
try {
|
|
3544
3965
|
const stats = await reconcileAbandonedSessions(opts);
|
|
3545
3966
|
if (stats.orphanedCount === 0) {
|
|
3546
|
-
log("[SessionReconciler] No orphaned sessions found");
|
|
3967
|
+
log("info", "[SessionReconciler] No orphaned sessions found");
|
|
3547
3968
|
return;
|
|
3548
3969
|
}
|
|
3549
3970
|
log(
|
|
3971
|
+
"info",
|
|
3550
3972
|
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
3551
3973
|
);
|
|
3552
3974
|
if (stats.errors.length > 0) {
|
|
3553
|
-
log("[SessionReconciler] Reconciliation errors:");
|
|
3975
|
+
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
3554
3976
|
for (const err of stats.errors) {
|
|
3555
|
-
log(` - ${err.channelId}: ${err.reason}`);
|
|
3977
|
+
log("warn", ` - ${err.channelId}: ${err.reason}`);
|
|
3556
3978
|
}
|
|
3557
3979
|
}
|
|
3558
3980
|
} catch (err) {
|
|
3559
|
-
log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
3981
|
+
log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
3560
3982
|
}
|
|
3561
3983
|
}
|
|
3562
3984
|
function tryDecodeBase64(value) {
|
|
@@ -3701,4 +4123,4 @@ var ProviderRegistry = class {
|
|
|
3701
4123
|
}
|
|
3702
4124
|
};
|
|
3703
4125
|
|
|
3704
|
-
export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, NulthPolicyError, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSeenTxStore, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, createNulthSigner, createPolicyState, decimalToStroops, decodeAuthSignature, fetchLedgerSequence, fetchManifest, httpStatusToError, invalidateManifest, manifestDigest, parseRetryAfterMs, paymentContextFromManifest, prepareNulthSigner, rankProvidersByLatency, reconcileAbandonedSessions, registerProvider, routedock, runStartupReconciliation, selectMode, setNulthPaymentContext, signManifest, usdcToMicros, usdcToUnits, verifyManifestSignature, withRetry, wrapFetchError };
|
|
4126
|
+
export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, NulthPolicyError, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSeenTxStore, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, consoleLogger, createConsoleLogger, createNulthSigner, createPolicyState, decimalToStroops, decodeAuthSignature, fetchLedgerSequence, fetchManifest, getEligibleAssets, httpStatusToError, invalidateManifest, isAssetEligible, manifestDigest, noopLogger, normalizeManifestAssets, parseRetryAfterMs, paymentContextFromManifest, prepareNulthSigner, rankProvidersByLatency, reconcileAbandonedSessions, registerProvider, resolveAssetContract, resolveLogger, routedock, runStartupReconciliation, selectAsset, selectMode, setNulthPaymentContext, signManifest, usdcToMicros, usdcToUnits, verifyManifestSignature, withRetry, wrapFetchError, wrapMppError };
|