@routedock/routedock 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -0
- package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
- package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
- package/dist/{ProviderRegistry-BJ9oJLVz.d.cts → ProviderRegistry-CyZ5eryO.d.cts} +2 -2
- package/dist/{ProviderRegistry-C50nyhaF.d.ts → ProviderRegistry-w4_hkI_1.d.ts} +2 -2
- package/dist/{RouteDockClient-BbkqjhMG.d.ts → RouteDockClient-CXvWXix5.d.cts} +25 -9
- package/dist/{RouteDockClient-B1roCa8h.d.cts → RouteDockClient-CjM2VyOd.d.ts} +25 -9
- package/dist/{SessionReconciler-Dscw43F3.d.ts → SessionReconciler-Bw-PoBVl.d.ts} +14 -5
- package/dist/{SessionReconciler-bi0z2qtN.d.cts → SessionReconciler-Cz2nuOlJ.d.cts} +14 -5
- package/dist/{SessionStore-DvkZv8-E.d.ts → SessionStore-B4-Hi4H8.d.cts} +34 -2
- package/dist/{SessionStore-BpK8vv0L.d.cts → SessionStore-BaYiegP0.d.ts} +34 -2
- package/dist/client.cjs +432 -125
- package/dist/client.d.cts +9 -8
- package/dist/client.d.ts +9 -8
- package/dist/client.js +424 -127
- package/dist/index.cjs +655 -228
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +646 -229
- package/dist/{types-DmhmfLzP.d.cts → logger-D8_Q2ytp.d.cts} +58 -1
- package/dist/{types-DmhmfLzP.d.ts → logger-D8_Q2ytp.d.ts} +58 -1
- package/dist/provider/fastify.cjs +306 -89
- package/dist/provider/fastify.d.cts +9 -4
- package/dist/provider/fastify.d.ts +9 -4
- package/dist/provider/fastify.js +305 -88
- package/dist/provider/hono.cjs +286 -82
- package/dist/provider/hono.d.cts +9 -7
- package/dist/provider/hono.d.ts +9 -7
- package/dist/provider/hono.js +282 -78
- package/dist/provider.cjs +326 -98
- package/dist/provider.d.cts +14 -10
- package/dist/provider.d.ts +14 -10
- package/dist/provider.js +318 -98
- package/dist/react.cjs +410 -125
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +411 -126
- package/dist/{routedockMiddleware-BlDsHuPl.d.ts → routedockMiddleware-57gUaEMf.d.ts} +9 -4
- package/dist/{routedockMiddleware-D97A5ptQ.d.cts → routedockMiddleware-DlyLrxp7.d.cts} +9 -4
- package/dist/schema.cjs +40 -0
- package/dist/schema.js +40 -0
- package/dist/{sign-Dpj2Mp7k.d.ts → sign-AKH67sft.d.cts} +1 -1
- package/dist/{sign-CUf3rhTx.d.cts → sign-BEq-OsSq.d.ts} +1 -1
- package/dist/testing.cjs +6 -4
- package/dist/testing.d.cts +32 -13
- package/dist/testing.d.ts +32 -13
- package/dist/testing.js +6 -4
- package/package.json +2 -2
- package/dist/MppSessionHandler-B4TyD7Xg.d.cts +0 -121
- package/dist/MppSessionHandler-CLB5SzgN.d.ts +0 -121
package/dist/index.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.1"};
|
|
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 = [];
|
|
@@ -1916,11 +2193,6 @@ function encodeAuthSignature(proof) {
|
|
|
1916
2193
|
const payload = { nulth: "zk-v1", proof };
|
|
1917
2194
|
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
1918
2195
|
}
|
|
1919
|
-
function usdcToStroops2(amount) {
|
|
1920
|
-
const [whole = "0", frac = ""] = amount.split(".");
|
|
1921
|
-
const padded = (frac + "0000000").slice(0, 7);
|
|
1922
|
-
return BigInt(whole) * 10000000n + BigInt(padded);
|
|
1923
|
-
}
|
|
1924
2196
|
var NulthClient = class {
|
|
1925
2197
|
constructor(config) {
|
|
1926
2198
|
this.config = config;
|
|
@@ -1936,7 +2208,10 @@ var NulthClient = class {
|
|
|
1936
2208
|
`NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
|
|
1937
2209
|
);
|
|
1938
2210
|
}
|
|
1939
|
-
|
|
2211
|
+
resolveLogger(config.logger)(
|
|
2212
|
+
"warn",
|
|
2213
|
+
"NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
|
|
2214
|
+
);
|
|
1940
2215
|
}
|
|
1941
2216
|
config;
|
|
1942
2217
|
policy;
|
|
@@ -1988,6 +2263,12 @@ var NulthClient = class {
|
|
|
1988
2263
|
return proof;
|
|
1989
2264
|
}
|
|
1990
2265
|
enforcePolicy(context) {
|
|
2266
|
+
if (context.amountStroops < 0n) {
|
|
2267
|
+
throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
|
|
2268
|
+
}
|
|
2269
|
+
if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
|
|
2270
|
+
throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
|
|
2271
|
+
}
|
|
1991
2272
|
if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
|
|
1992
2273
|
throw new NulthPolicyError("session_expired", "Nulth session expired");
|
|
1993
2274
|
}
|
|
@@ -2022,14 +2303,14 @@ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
|
|
|
2022
2303
|
const pricing = manifest.pricing[mode];
|
|
2023
2304
|
if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
|
|
2024
2305
|
return {
|
|
2025
|
-
payee: manifest.payee,
|
|
2026
|
-
amountStroops:
|
|
2306
|
+
payee: pricing.payee ?? manifest.payee,
|
|
2307
|
+
amountStroops: usdcToStroops(pricing.amount),
|
|
2027
2308
|
assetContract: manifest.asset_contract,
|
|
2028
2309
|
ledgerSequence
|
|
2029
2310
|
};
|
|
2030
2311
|
}
|
|
2031
2312
|
function createPolicyState(input) {
|
|
2032
|
-
const dailyCapStroops =
|
|
2313
|
+
const dailyCapStroops = usdcToStroops(input.dailyCapUsdc);
|
|
2033
2314
|
return {
|
|
2034
2315
|
dailyCapStroops,
|
|
2035
2316
|
allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
|
|
@@ -2131,7 +2412,8 @@ var InMemorySpendStore = class {
|
|
|
2131
2412
|
state = null;
|
|
2132
2413
|
constructor(options = {}) {
|
|
2133
2414
|
if (options.warn !== false) {
|
|
2134
|
-
|
|
2415
|
+
resolveLogger(options.logger)(
|
|
2416
|
+
"warn",
|
|
2135
2417
|
"[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
2418
|
);
|
|
2137
2419
|
}
|
|
@@ -2221,7 +2503,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2221
2503
|
this.network = config.network;
|
|
2222
2504
|
this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
|
|
2223
2505
|
this.retryPolicy = config.retryPolicy;
|
|
2224
|
-
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2506
|
+
this.spendStore = config.spendStore ?? new InMemorySpendStore({
|
|
2507
|
+
warn: !!config.spendCap,
|
|
2508
|
+
...config.logger && { logger: config.logger }
|
|
2509
|
+
});
|
|
2225
2510
|
this.logger = config.logger;
|
|
2226
2511
|
this.manifestTimeoutMs = config.manifestTimeoutMs;
|
|
2227
2512
|
this.expectedPayee = config.expectedPayee;
|
|
@@ -2233,9 +2518,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2233
2518
|
_secrets.set(this, config.commitmentSecret);
|
|
2234
2519
|
}
|
|
2235
2520
|
const secretKey = this.keypair.secret();
|
|
2236
|
-
this.
|
|
2237
|
-
this.
|
|
2238
|
-
this.
|
|
2521
|
+
const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
|
|
2522
|
+
this.x402 = new X402Client(secretKey, this.network, retryPolicy);
|
|
2523
|
+
this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
|
|
2524
|
+
this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
|
|
2239
2525
|
}
|
|
2240
2526
|
/** Fetch manifest and select mode — shared by pay() and estimateCost(). */
|
|
2241
2527
|
async _resolveManifest(url, options) {
|
|
@@ -2264,12 +2550,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2264
2550
|
* call before committing to a payment — for approval gates and manual
|
|
2265
2551
|
* trustline remediation.
|
|
2266
2552
|
*/
|
|
2267
|
-
async preflight(manifest) {
|
|
2553
|
+
async preflight(manifest, mode, endpoint) {
|
|
2268
2554
|
assertManifestValid(manifest);
|
|
2269
|
-
|
|
2555
|
+
const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
|
|
2556
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2270
2557
|
return {
|
|
2271
2558
|
hasTrustline: true,
|
|
2272
|
-
asset:
|
|
2559
|
+
asset: selectedAsset,
|
|
2273
2560
|
modes: manifest.modes,
|
|
2274
2561
|
...manifest.regions && { regions: manifest.regions },
|
|
2275
2562
|
...manifest.latency_hints && { latency_hints: manifest.latency_hints },
|
|
@@ -2283,8 +2570,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2283
2570
|
* trustline is missing. Results are cached per (account, asset) so the
|
|
2284
2571
|
* per-payment overhead is one RPC call amortized to zero after the first.
|
|
2285
2572
|
*/
|
|
2286
|
-
async _checkTrustline(manifest) {
|
|
2287
|
-
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${
|
|
2573
|
+
async _checkTrustline(manifest, targetAsset = manifest.asset) {
|
|
2574
|
+
const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
|
|
2288
2575
|
const cached = _RouteDockClient._trustlineCache.get(cacheKey);
|
|
2289
2576
|
if (cached && Date.now() < cached.expiresAt) return;
|
|
2290
2577
|
const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
|
|
@@ -2292,13 +2579,13 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2292
2579
|
try {
|
|
2293
2580
|
const account = await server.loadAccount(this.keypair.publicKey());
|
|
2294
2581
|
const balances = account.balances;
|
|
2295
|
-
const expectedIssuer = getAssetIssuer(
|
|
2582
|
+
const expectedIssuer = getAssetIssuer(targetAsset, this.network);
|
|
2296
2583
|
const hasTrustline = balances.some(
|
|
2297
|
-
(b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code ===
|
|
2584
|
+
(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
2585
|
);
|
|
2299
2586
|
if (!hasTrustline) {
|
|
2300
|
-
const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${
|
|
2301
|
-
throw new RouteDockTrustlineError(
|
|
2587
|
+
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}`;
|
|
2588
|
+
throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
|
|
2302
2589
|
}
|
|
2303
2590
|
_RouteDockClient._trustlineCache.set(cacheKey, {
|
|
2304
2591
|
exists: true,
|
|
@@ -2307,7 +2594,8 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2307
2594
|
} catch (err) {
|
|
2308
2595
|
if (err instanceof RouteDockTrustlineError) throw err;
|
|
2309
2596
|
this.logger?.(
|
|
2310
|
-
|
|
2597
|
+
"warn",
|
|
2598
|
+
`[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
|
|
2311
2599
|
);
|
|
2312
2600
|
}
|
|
2313
2601
|
}
|
|
@@ -2315,18 +2603,18 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2315
2603
|
* Pay for one request at `url`. Fetches manifest, selects payment mode,
|
|
2316
2604
|
* runs trustline preflight, reserves local spend cap BEFORE executing the
|
|
2317
2605
|
* payment, then commits the spend record on success. Rolls back the
|
|
2318
|
-
* reservation if the on-chain payment fails
|
|
2319
|
-
*
|
|
2606
|
+
* reservation if the on-chain payment fails, and also when the call
|
|
2607
|
+
* completes without paying anything (an x402 endpoint answering a plain
|
|
2608
|
+
* 200 with no challenge). Concurrent pay() calls are serialized through a
|
|
2609
|
+
* per-instance mutex to prevent spend-cap overrun.
|
|
2320
2610
|
*/
|
|
2321
2611
|
async pay(url, options) {
|
|
2322
2612
|
const baseUrl = new URL(url).origin;
|
|
2323
2613
|
const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
|
|
2324
2614
|
assertEndpointActive(manifest, url, this.logger);
|
|
2325
2615
|
const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
return this._payWithNulthVault(url, manifest, mode);
|
|
2329
|
-
}
|
|
2616
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2617
|
+
await this._checkTrustline(manifest, selectedAsset);
|
|
2330
2618
|
let amount;
|
|
2331
2619
|
switch (mode) {
|
|
2332
2620
|
case "x402":
|
|
@@ -2346,22 +2634,30 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2346
2634
|
const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
|
|
2347
2635
|
let result;
|
|
2348
2636
|
try {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2637
|
+
if (this.vault?.mode === "nulth") {
|
|
2638
|
+
result = await this._payWithNulthVault(url, manifest, mode);
|
|
2639
|
+
} else {
|
|
2640
|
+
switch (mode) {
|
|
2641
|
+
case "x402":
|
|
2642
|
+
result = await this.x402.pay(url, manifest);
|
|
2643
|
+
break;
|
|
2644
|
+
case "mpp-charge":
|
|
2645
|
+
result = await this.charge.pay(url, manifest);
|
|
2646
|
+
break;
|
|
2647
|
+
default:
|
|
2648
|
+
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2649
|
+
}
|
|
2358
2650
|
}
|
|
2359
2651
|
} catch (err) {
|
|
2360
2652
|
await this._rollbackSpend(reserveId).catch(() => {
|
|
2361
2653
|
});
|
|
2362
2654
|
throw err;
|
|
2363
2655
|
}
|
|
2364
|
-
|
|
2656
|
+
if (result.txHash === null && result.amount === "0") {
|
|
2657
|
+
await this._rollbackSpend(reserveId);
|
|
2658
|
+
} else {
|
|
2659
|
+
await this._commitSpend(reserveId);
|
|
2660
|
+
}
|
|
2365
2661
|
return result;
|
|
2366
2662
|
}
|
|
2367
2663
|
/** Nulth ZK vault path — proof built off-chain, attached as auth signature */
|
|
@@ -2411,9 +2707,10 @@ var RouteDockClient = class _RouteDockClient {
|
|
|
2411
2707
|
default:
|
|
2412
2708
|
throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
|
|
2413
2709
|
}
|
|
2710
|
+
const selectedAsset = selectAsset(manifest, mode, url).asset;
|
|
2414
2711
|
return {
|
|
2415
2712
|
amount,
|
|
2416
|
-
asset:
|
|
2713
|
+
asset: selectedAsset,
|
|
2417
2714
|
mode,
|
|
2418
2715
|
manifest,
|
|
2419
2716
|
...manifest.regions && { regions: manifest.regions },
|
|
@@ -2650,7 +2947,7 @@ var SupabaseSeenTxStore = class {
|
|
|
2650
2947
|
}
|
|
2651
2948
|
supabase;
|
|
2652
2949
|
async get(key) {
|
|
2653
|
-
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
|
|
2950
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
|
|
2654
2951
|
if (error) {
|
|
2655
2952
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
2656
2953
|
}
|
|
@@ -2658,7 +2955,8 @@ var SupabaseSeenTxStore = class {
|
|
|
2658
2955
|
const headers = data.headers;
|
|
2659
2956
|
return {
|
|
2660
2957
|
txHash: data.tx_hash ?? null,
|
|
2661
|
-
...headers ? { headers } : {}
|
|
2958
|
+
...headers ? { headers } : {},
|
|
2959
|
+
createdAt: Date.parse(data.created_at)
|
|
2662
2960
|
};
|
|
2663
2961
|
}
|
|
2664
2962
|
async set(key, record) {
|
|
@@ -2666,7 +2964,8 @@ var SupabaseSeenTxStore = class {
|
|
|
2666
2964
|
{
|
|
2667
2965
|
key,
|
|
2668
2966
|
tx_hash: record.txHash,
|
|
2669
|
-
headers: record.headers ?? null
|
|
2967
|
+
headers: record.headers ?? null,
|
|
2968
|
+
created_at: new Date(record.createdAt ?? Date.now()).toISOString()
|
|
2670
2969
|
},
|
|
2671
2970
|
{ onConflict: "key" }
|
|
2672
2971
|
);
|
|
@@ -2674,21 +2973,47 @@ var SupabaseSeenTxStore = class {
|
|
|
2674
2973
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
2675
2974
|
}
|
|
2676
2975
|
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Atomically increment the settlement's replay counter through the
|
|
2978
|
+
* `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
|
|
2979
|
+
* row lock gives concurrent claims distinct counts. Returns `Infinity` when
|
|
2980
|
+
* the key no longer exists, so the caller treats it as spent.
|
|
2981
|
+
*/
|
|
2982
|
+
async claimReplay(key) {
|
|
2983
|
+
const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
|
|
2984
|
+
p_key: key
|
|
2985
|
+
});
|
|
2986
|
+
if (error) {
|
|
2987
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
|
|
2988
|
+
}
|
|
2989
|
+
if (data == null) return Number.POSITIVE_INFINITY;
|
|
2990
|
+
return data;
|
|
2991
|
+
}
|
|
2677
2992
|
};
|
|
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
2993
|
function extractPayerAddress(key) {
|
|
2685
2994
|
if (typeof key !== "string") return null;
|
|
2686
2995
|
return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
2687
2996
|
}
|
|
2688
2997
|
|
|
2689
2998
|
// src/provider/SeenTxStore.ts
|
|
2999
|
+
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
3000
|
+
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
3001
|
+
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
3002
|
+
const record = await store.get(key);
|
|
3003
|
+
if (!record) return { kind: "miss" };
|
|
3004
|
+
const { createdAt } = record;
|
|
3005
|
+
if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
|
|
3006
|
+
return { kind: "spent" };
|
|
3007
|
+
}
|
|
3008
|
+
if (store.claimReplay) {
|
|
3009
|
+
const replayed = await store.claimReplay(key);
|
|
3010
|
+
if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
|
|
3011
|
+
}
|
|
3012
|
+
return { kind: "replay", record };
|
|
3013
|
+
}
|
|
2690
3014
|
var InMemorySeenTxStore = class {
|
|
2691
3015
|
map = /* @__PURE__ */ new Map();
|
|
3016
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
2692
3017
|
order = [];
|
|
2693
3018
|
constructor(maxEntriesOrOptions = 1e4) {
|
|
2694
3019
|
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
@@ -2696,7 +3021,8 @@ var InMemorySeenTxStore = class {
|
|
|
2696
3021
|
if (options.warn !== false) {
|
|
2697
3022
|
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
2698
3023
|
if (isServerless) {
|
|
2699
|
-
|
|
3024
|
+
resolveLogger(options.logger)(
|
|
3025
|
+
"warn",
|
|
2700
3026
|
"[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
3027
|
);
|
|
2702
3028
|
}
|
|
@@ -2711,21 +3037,43 @@ var InMemorySeenTxStore = class {
|
|
|
2711
3037
|
this.order.push(key);
|
|
2712
3038
|
if (this.order.length > this.maxEntries) {
|
|
2713
3039
|
const evicted = this.order.shift();
|
|
2714
|
-
if (evicted !== void 0)
|
|
3040
|
+
if (evicted !== void 0) {
|
|
3041
|
+
this.map.delete(evicted);
|
|
3042
|
+
this.replayCounts.delete(evicted);
|
|
3043
|
+
}
|
|
2715
3044
|
}
|
|
2716
3045
|
}
|
|
2717
3046
|
this.map.set(key, record);
|
|
2718
3047
|
}
|
|
3048
|
+
claimReplay(key) {
|
|
3049
|
+
if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
|
|
3050
|
+
const next = (this.replayCounts.get(key) ?? 0) + 1;
|
|
3051
|
+
this.replayCounts.set(key, next);
|
|
3052
|
+
return next;
|
|
3053
|
+
}
|
|
2719
3054
|
};
|
|
2720
3055
|
async function sha256Hex2(input) {
|
|
2721
3056
|
const buf = new TextEncoder().encode(input);
|
|
2722
3057
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
2723
3058
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2724
3059
|
}
|
|
2725
|
-
async function paymentIdempotencyKey(getHeader) {
|
|
3060
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
2726
3061
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
2727
3062
|
if (!material) return null;
|
|
2728
|
-
return sha256Hex2(
|
|
3063
|
+
return sha256Hex2(
|
|
3064
|
+
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// src/provider/settleResult.ts
|
|
3069
|
+
function readSettleResult(result) {
|
|
3070
|
+
const settle = result;
|
|
3071
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
3072
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
3073
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
3074
|
+
return failed(settle.errorReason);
|
|
3075
|
+
}
|
|
3076
|
+
return { ok: true, txHash: settle.transaction };
|
|
2729
3077
|
}
|
|
2730
3078
|
|
|
2731
3079
|
// src/provider/x402Handler.ts
|
|
@@ -2738,7 +3086,8 @@ function createX402Handler(opts) {
|
|
|
2738
3086
|
const caip2 = CAIP22[opts.network];
|
|
2739
3087
|
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
2740
3088
|
const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
2741
|
-
const
|
|
3089
|
+
const logger = resolveLogger(opts.logger);
|
|
3090
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
2742
3091
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
2743
3092
|
const localFacilitator = new ExactStellarScheme$1([signer], {
|
|
2744
3093
|
areFeesSponsored: true
|
|
@@ -2759,22 +3108,29 @@ function createX402Handler(opts) {
|
|
|
2759
3108
|
}
|
|
2760
3109
|
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
2761
3110
|
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
3111
|
return async (req, res, next) => {
|
|
2775
3112
|
try {
|
|
2776
|
-
const
|
|
2777
|
-
|
|
3113
|
+
const endpoint = req.path || req.originalUrl;
|
|
3114
|
+
const assetContract = resolveAssetContract(
|
|
3115
|
+
opts.manifest,
|
|
3116
|
+
"x402",
|
|
3117
|
+
endpoint,
|
|
3118
|
+
opts.assetContract
|
|
3119
|
+
);
|
|
3120
|
+
const requirements = {
|
|
3121
|
+
scheme: "exact",
|
|
3122
|
+
network: caip2,
|
|
3123
|
+
asset: assetContract,
|
|
3124
|
+
amount: amountInBaseUnits,
|
|
3125
|
+
payTo,
|
|
3126
|
+
maxTimeoutSeconds: 60,
|
|
3127
|
+
extra: {
|
|
3128
|
+
areFeesSponsored: true,
|
|
3129
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3132
|
+
const respondPaymentRequired = async (error, reason) => {
|
|
3133
|
+
const body = { error, ...reason ? { reason } : {} };
|
|
2778
3134
|
if (ozServer) {
|
|
2779
3135
|
const resourceInfo = {
|
|
2780
3136
|
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
@@ -2784,7 +3140,7 @@ function createX402Handler(opts) {
|
|
|
2784
3140
|
[requirements],
|
|
2785
3141
|
resourceInfo
|
|
2786
3142
|
);
|
|
2787
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(
|
|
3143
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
2788
3144
|
} else {
|
|
2789
3145
|
const x402Response = {
|
|
2790
3146
|
x402Version: 2,
|
|
@@ -2794,19 +3150,35 @@ function createX402Handler(opts) {
|
|
|
2794
3150
|
},
|
|
2795
3151
|
accepts: [requirements]
|
|
2796
3152
|
};
|
|
2797
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(
|
|
3153
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(body);
|
|
2798
3154
|
}
|
|
3155
|
+
};
|
|
3156
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
3157
|
+
if (!paymentHeader) {
|
|
3158
|
+
await respondPaymentRequired("Payment Required");
|
|
2799
3159
|
return;
|
|
2800
3160
|
}
|
|
2801
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3161
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
3162
|
+
(name) => {
|
|
3163
|
+
const v = req.headers[name.toLowerCase()];
|
|
3164
|
+
return Array.isArray(v) ? v[0] : v;
|
|
3165
|
+
},
|
|
3166
|
+
{
|
|
3167
|
+
method: req.method,
|
|
3168
|
+
path: req.originalUrl.split("?")[0],
|
|
3169
|
+
amount: requirements.amount,
|
|
3170
|
+
payTo: requirements.payTo
|
|
3171
|
+
}
|
|
3172
|
+
);
|
|
2805
3173
|
if (idempotencyKey) {
|
|
2806
|
-
const
|
|
2807
|
-
if (
|
|
2808
|
-
|
|
2809
|
-
|
|
3174
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
3175
|
+
if (replayCheck.kind === "spent") {
|
|
3176
|
+
res.status(402).json({ error: "Payment already used" });
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
if (replayCheck.kind === "replay") {
|
|
3180
|
+
if (replayCheck.record.headers) {
|
|
3181
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
2810
3182
|
res.setHeader(k, val);
|
|
2811
3183
|
}
|
|
2812
3184
|
}
|
|
@@ -2825,15 +3197,18 @@ function createX402Handler(opts) {
|
|
|
2825
3197
|
}
|
|
2826
3198
|
if (ozServer) {
|
|
2827
3199
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
2828
|
-
|
|
2829
|
-
if (
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
encodePaymentResponseHeader(
|
|
2833
|
-
settleResult
|
|
2834
|
-
)
|
|
2835
|
-
);
|
|
3200
|
+
const outcome = readSettleResult(settleResult);
|
|
3201
|
+
if (!outcome.ok) {
|
|
3202
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
3203
|
+
return;
|
|
2836
3204
|
}
|
|
3205
|
+
txHash = outcome.txHash;
|
|
3206
|
+
res.setHeader(
|
|
3207
|
+
"X-Payment-Response",
|
|
3208
|
+
encodePaymentResponseHeader(
|
|
3209
|
+
settleResult
|
|
3210
|
+
)
|
|
3211
|
+
);
|
|
2837
3212
|
} else {
|
|
2838
3213
|
const verifyResult = await localFacilitator.verify(
|
|
2839
3214
|
payload,
|
|
@@ -2850,15 +3225,18 @@ function createX402Handler(opts) {
|
|
|
2850
3225
|
payload,
|
|
2851
3226
|
requirements
|
|
2852
3227
|
);
|
|
2853
|
-
|
|
2854
|
-
if (
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
encodePaymentResponseHeader(
|
|
2858
|
-
settleResult
|
|
2859
|
-
)
|
|
2860
|
-
);
|
|
3228
|
+
const outcome = readSettleResult(settleResult);
|
|
3229
|
+
if (!outcome.ok) {
|
|
3230
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
3231
|
+
return;
|
|
2861
3232
|
}
|
|
3233
|
+
txHash = outcome.txHash;
|
|
3234
|
+
res.setHeader(
|
|
3235
|
+
"X-Payment-Response",
|
|
3236
|
+
encodePaymentResponseHeader(
|
|
3237
|
+
settleResult
|
|
3238
|
+
)
|
|
3239
|
+
);
|
|
2862
3240
|
}
|
|
2863
3241
|
if (idempotencyKey) {
|
|
2864
3242
|
const headers = {};
|
|
@@ -2866,17 +3244,21 @@ function createX402Handler(opts) {
|
|
|
2866
3244
|
if (typeof paymentResponse === "string") {
|
|
2867
3245
|
headers["X-Payment-Response"] = paymentResponse;
|
|
2868
3246
|
}
|
|
2869
|
-
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
3247
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
2870
3248
|
}
|
|
2871
3249
|
if (txHash && opts.onSettled) {
|
|
2872
3250
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
2873
|
-
|
|
3251
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
2874
3252
|
opts.onCallbackError?.(err, "onSettled");
|
|
2875
3253
|
});
|
|
2876
3254
|
}
|
|
2877
3255
|
next();
|
|
2878
3256
|
} catch (err) {
|
|
2879
|
-
|
|
3257
|
+
if (err instanceof RouteDockManifestError) {
|
|
3258
|
+
next(err);
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
2880
3262
|
res.status(500).json({ error: "Payment settlement failed" });
|
|
2881
3263
|
}
|
|
2882
3264
|
};
|
|
@@ -2889,20 +3271,37 @@ function createMppChargeHandler(opts) {
|
|
|
2889
3271
|
const networkId = MPP_NETWORK[opts.network];
|
|
2890
3272
|
const amountHumanReadable = opts.amount;
|
|
2891
3273
|
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
2892
|
-
const
|
|
2893
|
-
const
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3274
|
+
const logger = resolveLogger(opts.logger);
|
|
3275
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
3276
|
+
const mppxInstances = /* @__PURE__ */ new Map();
|
|
3277
|
+
function getMppx(contract) {
|
|
3278
|
+
let instance = mppxInstances.get(contract);
|
|
3279
|
+
if (!instance) {
|
|
3280
|
+
instance = Mppx$1.create({
|
|
3281
|
+
secretKey: opts.payeeSecretKey,
|
|
3282
|
+
methods: [
|
|
3283
|
+
stellar$3.charge({
|
|
3284
|
+
recipient,
|
|
3285
|
+
currency: contract,
|
|
3286
|
+
network: networkId,
|
|
3287
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
3288
|
+
})
|
|
3289
|
+
]
|
|
3290
|
+
});
|
|
3291
|
+
mppxInstances.set(contract, instance);
|
|
3292
|
+
}
|
|
3293
|
+
return instance;
|
|
3294
|
+
}
|
|
2904
3295
|
return async (req, res, next) => {
|
|
2905
3296
|
try {
|
|
3297
|
+
const endpoint = req.path || req.originalUrl;
|
|
3298
|
+
const assetContract = resolveAssetContract(
|
|
3299
|
+
opts.manifest,
|
|
3300
|
+
"mpp-charge",
|
|
3301
|
+
endpoint,
|
|
3302
|
+
opts.assetContract
|
|
3303
|
+
);
|
|
3304
|
+
const mppx = getMppx(assetContract);
|
|
2906
3305
|
let payerAddress = null;
|
|
2907
3306
|
try {
|
|
2908
3307
|
const authHeader = req.headers["authorization"];
|
|
@@ -2918,15 +3317,27 @@ function createMppChargeHandler(opts) {
|
|
|
2918
3317
|
}
|
|
2919
3318
|
} catch {
|
|
2920
3319
|
}
|
|
2921
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3320
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
3321
|
+
(name) => {
|
|
3322
|
+
const v = req.headers[name.toLowerCase()];
|
|
3323
|
+
return Array.isArray(v) ? v[0] : v;
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
method: req.method,
|
|
3327
|
+
path: req.originalUrl.split("?")[0],
|
|
3328
|
+
amount: amountHumanReadable,
|
|
3329
|
+
payTo: recipient
|
|
3330
|
+
}
|
|
3331
|
+
);
|
|
2925
3332
|
if (idempotencyKey) {
|
|
2926
|
-
const
|
|
2927
|
-
if (
|
|
2928
|
-
|
|
2929
|
-
|
|
3333
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
3334
|
+
if (replayCheck.kind === "spent") {
|
|
3335
|
+
res.status(402).json({ error: "Payment already used" });
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
if (replayCheck.kind === "replay") {
|
|
3339
|
+
if (replayCheck.record.headers) {
|
|
3340
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
2930
3341
|
res.setHeader(k, val);
|
|
2931
3342
|
}
|
|
2932
3343
|
}
|
|
@@ -2938,7 +3349,7 @@ function createMppChargeHandler(opts) {
|
|
|
2938
3349
|
const handler = mppx["stellar/charge"];
|
|
2939
3350
|
const result = await handler({
|
|
2940
3351
|
amount: amountHumanReadable,
|
|
2941
|
-
currency:
|
|
3352
|
+
currency: assetContract,
|
|
2942
3353
|
recipient,
|
|
2943
3354
|
description: opts.manifest.name
|
|
2944
3355
|
})(fetchReq);
|
|
@@ -2969,12 +3380,13 @@ function createMppChargeHandler(opts) {
|
|
|
2969
3380
|
if (idempotencyKey) {
|
|
2970
3381
|
await seenTxStore.set(idempotencyKey, {
|
|
2971
3382
|
txHash: reference ?? null,
|
|
2972
|
-
headers: receiptHeaders
|
|
3383
|
+
headers: receiptHeaders,
|
|
3384
|
+
createdAt: Date.now()
|
|
2973
3385
|
});
|
|
2974
3386
|
}
|
|
2975
3387
|
if (reference && opts.onSettled) {
|
|
2976
3388
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
2977
|
-
|
|
3389
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
2978
3390
|
opts.onCallbackError?.(err, "onSettled");
|
|
2979
3391
|
});
|
|
2980
3392
|
}
|
|
@@ -3075,6 +3487,7 @@ function createMppSessionHandler(opts) {
|
|
|
3075
3487
|
const networkId = MPP_NETWORK2[opts.network];
|
|
3076
3488
|
const rateHuman = opts.rate;
|
|
3077
3489
|
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
3490
|
+
const logger = resolveLogger(opts.logger);
|
|
3078
3491
|
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
3079
3492
|
const innerStore = Store.memory();
|
|
3080
3493
|
let record = null;
|
|
@@ -3126,7 +3539,7 @@ function createMppSessionHandler(opts) {
|
|
|
3126
3539
|
reason
|
|
3127
3540
|
});
|
|
3128
3541
|
} catch (err) {
|
|
3129
|
-
|
|
3542
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
3130
3543
|
}
|
|
3131
3544
|
}
|
|
3132
3545
|
}
|
|
@@ -3173,7 +3586,7 @@ function createMppSessionHandler(opts) {
|
|
|
3173
3586
|
sessionOpened = true;
|
|
3174
3587
|
if (opts.onSessionOpen) {
|
|
3175
3588
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
3176
|
-
|
|
3589
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
3177
3590
|
opts.onCallbackError?.(err, "onSessionOpen");
|
|
3178
3591
|
});
|
|
3179
3592
|
}
|
|
@@ -3181,7 +3594,7 @@ function createMppSessionHandler(opts) {
|
|
|
3181
3594
|
if (opts.onVoucher) {
|
|
3182
3595
|
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
3183
3596
|
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
3184
|
-
|
|
3597
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
3185
3598
|
opts.onCallbackError?.(err, "onVoucher");
|
|
3186
3599
|
});
|
|
3187
3600
|
}
|
|
@@ -3191,7 +3604,7 @@ function createMppSessionHandler(opts) {
|
|
|
3191
3604
|
methods: [
|
|
3192
3605
|
withTypedChannelErrors(
|
|
3193
3606
|
onVerifiedCredential(
|
|
3194
|
-
stellar$
|
|
3607
|
+
stellar$2.channel({
|
|
3195
3608
|
channel: opts.channelFactory,
|
|
3196
3609
|
commitmentKey: opts.commitmentPublicKey,
|
|
3197
3610
|
network: networkId,
|
|
@@ -3234,7 +3647,7 @@ function createMppSessionHandler(opts) {
|
|
|
3234
3647
|
if (opts.onSettled) {
|
|
3235
3648
|
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
3236
3649
|
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
3237
|
-
|
|
3650
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
3238
3651
|
opts.onCallbackError?.(err, "onSettled");
|
|
3239
3652
|
});
|
|
3240
3653
|
}
|
|
@@ -3245,7 +3658,7 @@ function createMppSessionHandler(opts) {
|
|
|
3245
3658
|
payeeKeypair.publicKey()
|
|
3246
3659
|
);
|
|
3247
3660
|
if (!settlementAddresses) {
|
|
3248
|
-
|
|
3661
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
3249
3662
|
} else {
|
|
3250
3663
|
try {
|
|
3251
3664
|
const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
|
|
@@ -3268,7 +3681,7 @@ function createMppSessionHandler(opts) {
|
|
|
3268
3681
|
preparedTx.sign(payeeKeypair);
|
|
3269
3682
|
await server.sendTransaction(preparedTx);
|
|
3270
3683
|
} catch (recordErr) {
|
|
3271
|
-
|
|
3684
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
3272
3685
|
}
|
|
3273
3686
|
}
|
|
3274
3687
|
}
|
|
@@ -3325,12 +3738,13 @@ function routedock(opts) {
|
|
|
3325
3738
|
payeeSecretKey: opts.payeeSecretKey,
|
|
3326
3739
|
network: opts.network,
|
|
3327
3740
|
amount: x402Price,
|
|
3328
|
-
assetContract: opts.assetContract,
|
|
3741
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3329
3742
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
3330
3743
|
manifest: signedManifest,
|
|
3331
3744
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
3332
3745
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3333
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
3746
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
3747
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3334
3748
|
})
|
|
3335
3749
|
);
|
|
3336
3750
|
}
|
|
@@ -3344,11 +3758,12 @@ function routedock(opts) {
|
|
|
3344
3758
|
payeeSecretKey: opts.payeeSecretKey,
|
|
3345
3759
|
network: opts.network,
|
|
3346
3760
|
amount: chargePrice,
|
|
3347
|
-
assetContract: opts.assetContract,
|
|
3761
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3348
3762
|
manifest: signedManifest,
|
|
3349
3763
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
3350
3764
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3351
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
3765
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
3766
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3352
3767
|
})
|
|
3353
3768
|
);
|
|
3354
3769
|
}
|
|
@@ -3366,7 +3781,7 @@ function routedock(opts) {
|
|
|
3366
3781
|
network: opts.network,
|
|
3367
3782
|
channelFactory: sessionPricing.channelFactory,
|
|
3368
3783
|
rate: sessionPricing.rate,
|
|
3369
|
-
assetContract: opts.assetContract,
|
|
3784
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
3370
3785
|
manifest: signedManifest,
|
|
3371
3786
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
3372
3787
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
@@ -3374,7 +3789,8 @@ function routedock(opts) {
|
|
|
3374
3789
|
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
3375
3790
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
3376
3791
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
3377
|
-
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
|
|
3792
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
3793
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
3378
3794
|
})
|
|
3379
3795
|
);
|
|
3380
3796
|
}
|
|
@@ -3538,25 +3954,26 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
3538
3954
|
}
|
|
3539
3955
|
}
|
|
3540
3956
|
async function runStartupReconciliation(opts, logger) {
|
|
3541
|
-
const log = logger
|
|
3542
|
-
log("[SessionReconciler] Running startup reconciliation...");
|
|
3957
|
+
const log = resolveLogger(logger);
|
|
3958
|
+
log("info", "[SessionReconciler] Running startup reconciliation...");
|
|
3543
3959
|
try {
|
|
3544
3960
|
const stats = await reconcileAbandonedSessions(opts);
|
|
3545
3961
|
if (stats.orphanedCount === 0) {
|
|
3546
|
-
log("[SessionReconciler] No orphaned sessions found");
|
|
3962
|
+
log("info", "[SessionReconciler] No orphaned sessions found");
|
|
3547
3963
|
return;
|
|
3548
3964
|
}
|
|
3549
3965
|
log(
|
|
3966
|
+
"info",
|
|
3550
3967
|
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
3551
3968
|
);
|
|
3552
3969
|
if (stats.errors.length > 0) {
|
|
3553
|
-
log("[SessionReconciler] Reconciliation errors:");
|
|
3970
|
+
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
3554
3971
|
for (const err of stats.errors) {
|
|
3555
|
-
log(` - ${err.channelId}: ${err.reason}`);
|
|
3972
|
+
log("warn", ` - ${err.channelId}: ${err.reason}`);
|
|
3556
3973
|
}
|
|
3557
3974
|
}
|
|
3558
3975
|
} catch (err) {
|
|
3559
|
-
log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
3976
|
+
log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
3560
3977
|
}
|
|
3561
3978
|
}
|
|
3562
3979
|
function tryDecodeBase64(value) {
|
|
@@ -3701,4 +4118,4 @@ var ProviderRegistry = class {
|
|
|
3701
4118
|
}
|
|
3702
4119
|
};
|
|
3703
4120
|
|
|
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 };
|
|
4121
|
+
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 };
|