@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/provider.cjs
CHANGED
|
@@ -7,9 +7,9 @@ var server = require('@x402/core/server');
|
|
|
7
7
|
var stellar = require('@x402/stellar');
|
|
8
8
|
var http = require('@x402/core/http');
|
|
9
9
|
var nulthSdk = require('@routedock/nulth-sdk');
|
|
10
|
-
var server$
|
|
10
|
+
var server$4 = require('@stellar/mpp/charge/server');
|
|
11
11
|
var server$2 = require('mppx/server');
|
|
12
|
-
var server$
|
|
12
|
+
var server$3 = require('@stellar/mpp/channel/server');
|
|
13
13
|
var mppx = require('mppx');
|
|
14
14
|
var crypto$1 = require('crypto');
|
|
15
15
|
|
|
@@ -121,7 +121,85 @@ var RouteDockSessionError = RouteDockVoucherMonotonicityError;
|
|
|
121
121
|
// src/types.ts
|
|
122
122
|
var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
123
123
|
|
|
124
|
-
// src/
|
|
124
|
+
// src/internal/assetUtils.ts
|
|
125
|
+
function normalizeManifestAssets(manifest) {
|
|
126
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
127
|
+
return manifest.assets;
|
|
128
|
+
}
|
|
129
|
+
if (manifest.asset && manifest.asset_contract) {
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
asset: manifest.asset,
|
|
133
|
+
asset_contract: manifest.asset_contract
|
|
134
|
+
}
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
throw new RouteDockManifestError(
|
|
138
|
+
"Manifest must define either assets[] or root asset/asset_contract fields"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
function getEligibleAssets(manifest, mode, endpoint) {
|
|
142
|
+
const assets = normalizeManifestAssets(manifest);
|
|
143
|
+
return assets.filter((asset) => {
|
|
144
|
+
if (asset.modes && !asset.modes.includes(mode)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
|
|
148
|
+
const trimmedEndpoint = endpoint.replace(/^\//, "");
|
|
149
|
+
if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (manifest.endpoints) {
|
|
153
|
+
let pathname = endpoint;
|
|
154
|
+
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
|
|
155
|
+
try {
|
|
156
|
+
pathname = new URL(endpoint).pathname;
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const trimmedPathname = pathname.replace(/^\//, "");
|
|
161
|
+
const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
|
|
162
|
+
const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
|
|
163
|
+
const keyMatches = key === endpoint || key === trimmedEndpoint;
|
|
164
|
+
if (pathMatches || keyMatches) {
|
|
165
|
+
return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
});
|
|
169
|
+
if (!matchesEndpoint) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
|
|
180
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
181
|
+
return selectAsset(manifest, mode, endpoint).asset_contract;
|
|
182
|
+
}
|
|
183
|
+
return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
|
|
184
|
+
}
|
|
185
|
+
function selectAsset(manifest, mode, endpoint) {
|
|
186
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
187
|
+
if (eligible.length === 0) {
|
|
188
|
+
const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
|
|
189
|
+
throw new RouteDockManifestError(
|
|
190
|
+
`No eligible assets found for mode '${mode}'${endpointMsg}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return eligible[0];
|
|
194
|
+
}
|
|
195
|
+
function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
196
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
197
|
+
return eligible.some(
|
|
198
|
+
(asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/internal/payee.ts
|
|
125
203
|
function resolvePayee(manifest, mode) {
|
|
126
204
|
const override = manifest.pricing[mode]?.payee;
|
|
127
205
|
return override ?? manifest.payee;
|
|
@@ -132,9 +210,41 @@ function extractPayerAddress(key) {
|
|
|
132
210
|
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
133
211
|
}
|
|
134
212
|
|
|
213
|
+
// src/internal/logger.ts
|
|
214
|
+
function createConsoleLogger() {
|
|
215
|
+
return (level, message, fields) => {
|
|
216
|
+
const values = fields ? Object.values(fields) : [];
|
|
217
|
+
if (values.length > 0) {
|
|
218
|
+
console[level](message, ...values);
|
|
219
|
+
} else {
|
|
220
|
+
console[level](message);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
var consoleLogger = createConsoleLogger();
|
|
225
|
+
function resolveLogger(logger, fallback = consoleLogger) {
|
|
226
|
+
return logger ?? fallback;
|
|
227
|
+
}
|
|
228
|
+
|
|
135
229
|
// src/provider/SeenTxStore.ts
|
|
230
|
+
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
231
|
+
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
232
|
+
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
233
|
+
const record = await store.get(key);
|
|
234
|
+
if (!record) return { kind: "miss" };
|
|
235
|
+
const { createdAt } = record;
|
|
236
|
+
if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
|
|
237
|
+
return { kind: "spent" };
|
|
238
|
+
}
|
|
239
|
+
if (store.claimReplay) {
|
|
240
|
+
const replayed = await store.claimReplay(key);
|
|
241
|
+
if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
|
|
242
|
+
}
|
|
243
|
+
return { kind: "replay", record };
|
|
244
|
+
}
|
|
136
245
|
var InMemorySeenTxStore = class {
|
|
137
246
|
map = /* @__PURE__ */ new Map();
|
|
247
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
138
248
|
order = [];
|
|
139
249
|
constructor(maxEntriesOrOptions = 1e4) {
|
|
140
250
|
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
@@ -142,7 +252,8 @@ var InMemorySeenTxStore = class {
|
|
|
142
252
|
if (options.warn !== false) {
|
|
143
253
|
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
144
254
|
if (isServerless) {
|
|
145
|
-
|
|
255
|
+
resolveLogger(options.logger)(
|
|
256
|
+
"warn",
|
|
146
257
|
"[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."
|
|
147
258
|
);
|
|
148
259
|
}
|
|
@@ -157,21 +268,43 @@ var InMemorySeenTxStore = class {
|
|
|
157
268
|
this.order.push(key);
|
|
158
269
|
if (this.order.length > this.maxEntries) {
|
|
159
270
|
const evicted = this.order.shift();
|
|
160
|
-
if (evicted !== void 0)
|
|
271
|
+
if (evicted !== void 0) {
|
|
272
|
+
this.map.delete(evicted);
|
|
273
|
+
this.replayCounts.delete(evicted);
|
|
274
|
+
}
|
|
161
275
|
}
|
|
162
276
|
}
|
|
163
277
|
this.map.set(key, record);
|
|
164
278
|
}
|
|
279
|
+
claimReplay(key) {
|
|
280
|
+
if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
|
|
281
|
+
const next = (this.replayCounts.get(key) ?? 0) + 1;
|
|
282
|
+
this.replayCounts.set(key, next);
|
|
283
|
+
return next;
|
|
284
|
+
}
|
|
165
285
|
};
|
|
166
286
|
async function sha256Hex(input) {
|
|
167
287
|
const buf = new TextEncoder().encode(input);
|
|
168
288
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
169
289
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
170
290
|
}
|
|
171
|
-
async function paymentIdempotencyKey(getHeader) {
|
|
291
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
172
292
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
173
293
|
if (!material) return null;
|
|
174
|
-
return sha256Hex(
|
|
294
|
+
return sha256Hex(
|
|
295
|
+
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/provider/settleResult.ts
|
|
300
|
+
function readSettleResult(result) {
|
|
301
|
+
const settle = result;
|
|
302
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
303
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
304
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
305
|
+
return failed(settle.errorReason);
|
|
306
|
+
}
|
|
307
|
+
return { ok: true, txHash: settle.transaction };
|
|
175
308
|
}
|
|
176
309
|
|
|
177
310
|
// src/provider/x402Handler.ts
|
|
@@ -184,7 +317,8 @@ function createX402Handler(opts) {
|
|
|
184
317
|
const caip2 = CAIP2[opts.network];
|
|
185
318
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
186
319
|
const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
187
|
-
const
|
|
320
|
+
const logger = resolveLogger(opts.logger);
|
|
321
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
188
322
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
189
323
|
const localFacilitator = new facilitator.ExactStellarScheme([signer], {
|
|
190
324
|
areFeesSponsored: true
|
|
@@ -205,22 +339,29 @@ function createX402Handler(opts) {
|
|
|
205
339
|
}
|
|
206
340
|
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
207
341
|
const payTo = resolvePayee(opts.manifest, "x402");
|
|
208
|
-
const requirements = {
|
|
209
|
-
scheme: "exact",
|
|
210
|
-
network: caip2,
|
|
211
|
-
asset: opts.assetContract,
|
|
212
|
-
amount: amountInBaseUnits,
|
|
213
|
-
payTo,
|
|
214
|
-
maxTimeoutSeconds: 60,
|
|
215
|
-
extra: {
|
|
216
|
-
areFeesSponsored: true,
|
|
217
|
-
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
342
|
return async (req, res, next) => {
|
|
221
343
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
344
|
+
const endpoint = req.path || req.originalUrl;
|
|
345
|
+
const assetContract = resolveAssetContract(
|
|
346
|
+
opts.manifest,
|
|
347
|
+
"x402",
|
|
348
|
+
endpoint,
|
|
349
|
+
opts.assetContract
|
|
350
|
+
);
|
|
351
|
+
const requirements = {
|
|
352
|
+
scheme: "exact",
|
|
353
|
+
network: caip2,
|
|
354
|
+
asset: assetContract,
|
|
355
|
+
amount: amountInBaseUnits,
|
|
356
|
+
payTo,
|
|
357
|
+
maxTimeoutSeconds: 60,
|
|
358
|
+
extra: {
|
|
359
|
+
areFeesSponsored: true,
|
|
360
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const respondPaymentRequired = async (error, reason) => {
|
|
364
|
+
const body = { error, ...reason ? { reason } : {} };
|
|
224
365
|
if (ozServer) {
|
|
225
366
|
const resourceInfo = {
|
|
226
367
|
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
@@ -230,7 +371,7 @@ function createX402Handler(opts) {
|
|
|
230
371
|
[requirements],
|
|
231
372
|
resourceInfo
|
|
232
373
|
);
|
|
233
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(
|
|
374
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
234
375
|
} else {
|
|
235
376
|
const x402Response = {
|
|
236
377
|
x402Version: 2,
|
|
@@ -240,19 +381,35 @@ function createX402Handler(opts) {
|
|
|
240
381
|
},
|
|
241
382
|
accepts: [requirements]
|
|
242
383
|
};
|
|
243
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(
|
|
384
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
|
|
244
385
|
}
|
|
386
|
+
};
|
|
387
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
388
|
+
if (!paymentHeader) {
|
|
389
|
+
await respondPaymentRequired("Payment Required");
|
|
245
390
|
return;
|
|
246
391
|
}
|
|
247
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
392
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
393
|
+
(name) => {
|
|
394
|
+
const v = req.headers[name.toLowerCase()];
|
|
395
|
+
return Array.isArray(v) ? v[0] : v;
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
method: req.method,
|
|
399
|
+
path: req.originalUrl.split("?")[0],
|
|
400
|
+
amount: requirements.amount,
|
|
401
|
+
payTo: requirements.payTo
|
|
402
|
+
}
|
|
403
|
+
);
|
|
251
404
|
if (idempotencyKey) {
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
405
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
406
|
+
if (replayCheck.kind === "spent") {
|
|
407
|
+
res.status(402).json({ error: "Payment already used" });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (replayCheck.kind === "replay") {
|
|
411
|
+
if (replayCheck.record.headers) {
|
|
412
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
256
413
|
res.setHeader(k, val);
|
|
257
414
|
}
|
|
258
415
|
}
|
|
@@ -271,15 +428,18 @@ function createX402Handler(opts) {
|
|
|
271
428
|
}
|
|
272
429
|
if (ozServer) {
|
|
273
430
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
http.encodePaymentResponseHeader(
|
|
279
|
-
settleResult
|
|
280
|
-
)
|
|
281
|
-
);
|
|
431
|
+
const outcome = readSettleResult(settleResult);
|
|
432
|
+
if (!outcome.ok) {
|
|
433
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
434
|
+
return;
|
|
282
435
|
}
|
|
436
|
+
txHash = outcome.txHash;
|
|
437
|
+
res.setHeader(
|
|
438
|
+
"X-Payment-Response",
|
|
439
|
+
http.encodePaymentResponseHeader(
|
|
440
|
+
settleResult
|
|
441
|
+
)
|
|
442
|
+
);
|
|
283
443
|
} else {
|
|
284
444
|
const verifyResult = await localFacilitator.verify(
|
|
285
445
|
payload,
|
|
@@ -296,15 +456,18 @@ function createX402Handler(opts) {
|
|
|
296
456
|
payload,
|
|
297
457
|
requirements
|
|
298
458
|
);
|
|
299
|
-
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
http.encodePaymentResponseHeader(
|
|
304
|
-
settleResult
|
|
305
|
-
)
|
|
306
|
-
);
|
|
459
|
+
const outcome = readSettleResult(settleResult);
|
|
460
|
+
if (!outcome.ok) {
|
|
461
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
462
|
+
return;
|
|
307
463
|
}
|
|
464
|
+
txHash = outcome.txHash;
|
|
465
|
+
res.setHeader(
|
|
466
|
+
"X-Payment-Response",
|
|
467
|
+
http.encodePaymentResponseHeader(
|
|
468
|
+
settleResult
|
|
469
|
+
)
|
|
470
|
+
);
|
|
308
471
|
}
|
|
309
472
|
if (idempotencyKey) {
|
|
310
473
|
const headers = {};
|
|
@@ -312,17 +475,21 @@ function createX402Handler(opts) {
|
|
|
312
475
|
if (typeof paymentResponse === "string") {
|
|
313
476
|
headers["X-Payment-Response"] = paymentResponse;
|
|
314
477
|
}
|
|
315
|
-
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
478
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
316
479
|
}
|
|
317
480
|
if (txHash && opts.onSettled) {
|
|
318
481
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
319
|
-
|
|
482
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
320
483
|
opts.onCallbackError?.(err, "onSettled");
|
|
321
484
|
});
|
|
322
485
|
}
|
|
323
486
|
next();
|
|
324
487
|
} catch (err) {
|
|
325
|
-
|
|
488
|
+
if (err instanceof RouteDockManifestError) {
|
|
489
|
+
next(err);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
326
493
|
res.status(500).json({ error: "Payment settlement failed" });
|
|
327
494
|
}
|
|
328
495
|
};
|
|
@@ -335,20 +502,37 @@ function createMppChargeHandler(opts) {
|
|
|
335
502
|
const networkId = MPP_NETWORK[opts.network];
|
|
336
503
|
const amountHumanReadable = opts.amount;
|
|
337
504
|
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
505
|
+
const logger = resolveLogger(opts.logger);
|
|
506
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
507
|
+
const mppxInstances = /* @__PURE__ */ new Map();
|
|
508
|
+
function getMppx(contract) {
|
|
509
|
+
let instance = mppxInstances.get(contract);
|
|
510
|
+
if (!instance) {
|
|
511
|
+
instance = server$2.Mppx.create({
|
|
512
|
+
secretKey: opts.payeeSecretKey,
|
|
513
|
+
methods: [
|
|
514
|
+
server$4.stellar.charge({
|
|
515
|
+
recipient,
|
|
516
|
+
currency: contract,
|
|
517
|
+
network: networkId,
|
|
518
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
519
|
+
})
|
|
520
|
+
]
|
|
521
|
+
});
|
|
522
|
+
mppxInstances.set(contract, instance);
|
|
523
|
+
}
|
|
524
|
+
return instance;
|
|
525
|
+
}
|
|
350
526
|
return async (req, res, next) => {
|
|
351
527
|
try {
|
|
528
|
+
const endpoint = req.path || req.originalUrl;
|
|
529
|
+
const assetContract = resolveAssetContract(
|
|
530
|
+
opts.manifest,
|
|
531
|
+
"mpp-charge",
|
|
532
|
+
endpoint,
|
|
533
|
+
opts.assetContract
|
|
534
|
+
);
|
|
535
|
+
const mppx = getMppx(assetContract);
|
|
352
536
|
let payerAddress = null;
|
|
353
537
|
try {
|
|
354
538
|
const authHeader = req.headers["authorization"];
|
|
@@ -364,15 +548,27 @@ function createMppChargeHandler(opts) {
|
|
|
364
548
|
}
|
|
365
549
|
} catch {
|
|
366
550
|
}
|
|
367
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
551
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
552
|
+
(name) => {
|
|
553
|
+
const v = req.headers[name.toLowerCase()];
|
|
554
|
+
return Array.isArray(v) ? v[0] : v;
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
method: req.method,
|
|
558
|
+
path: req.originalUrl.split("?")[0],
|
|
559
|
+
amount: amountHumanReadable,
|
|
560
|
+
payTo: recipient
|
|
561
|
+
}
|
|
562
|
+
);
|
|
371
563
|
if (idempotencyKey) {
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
564
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
565
|
+
if (replayCheck.kind === "spent") {
|
|
566
|
+
res.status(402).json({ error: "Payment already used" });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (replayCheck.kind === "replay") {
|
|
570
|
+
if (replayCheck.record.headers) {
|
|
571
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
376
572
|
res.setHeader(k, val);
|
|
377
573
|
}
|
|
378
574
|
}
|
|
@@ -384,7 +580,7 @@ function createMppChargeHandler(opts) {
|
|
|
384
580
|
const handler = mppx["stellar/charge"];
|
|
385
581
|
const result = await handler({
|
|
386
582
|
amount: amountHumanReadable,
|
|
387
|
-
currency:
|
|
583
|
+
currency: assetContract,
|
|
388
584
|
recipient,
|
|
389
585
|
description: opts.manifest.name
|
|
390
586
|
})(fetchReq);
|
|
@@ -415,12 +611,13 @@ function createMppChargeHandler(opts) {
|
|
|
415
611
|
if (idempotencyKey) {
|
|
416
612
|
await seenTxStore.set(idempotencyKey, {
|
|
417
613
|
txHash: reference ?? null,
|
|
418
|
-
headers: receiptHeaders
|
|
614
|
+
headers: receiptHeaders,
|
|
615
|
+
createdAt: Date.now()
|
|
419
616
|
});
|
|
420
617
|
}
|
|
421
618
|
if (reference && opts.onSettled) {
|
|
422
619
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
423
|
-
|
|
620
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
424
621
|
opts.onCallbackError?.(err, "onSettled");
|
|
425
622
|
});
|
|
426
623
|
}
|
|
@@ -528,8 +725,9 @@ function createMppSessionHandler(opts) {
|
|
|
528
725
|
const networkId = MPP_NETWORK2[opts.network];
|
|
529
726
|
const rateHuman = opts.rate;
|
|
530
727
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
728
|
+
const logger = resolveLogger(opts.logger);
|
|
531
729
|
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
532
|
-
const innerStore = server$
|
|
730
|
+
const innerStore = server$3.Store.memory();
|
|
533
731
|
let record = null;
|
|
534
732
|
let voucherCount = 0;
|
|
535
733
|
let sessionOpened = false;
|
|
@@ -579,7 +777,7 @@ function createMppSessionHandler(opts) {
|
|
|
579
777
|
reason
|
|
580
778
|
});
|
|
581
779
|
} catch (err) {
|
|
582
|
-
|
|
780
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
583
781
|
}
|
|
584
782
|
}
|
|
585
783
|
}
|
|
@@ -626,7 +824,7 @@ function createMppSessionHandler(opts) {
|
|
|
626
824
|
sessionOpened = true;
|
|
627
825
|
if (opts.onSessionOpen) {
|
|
628
826
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
629
|
-
|
|
827
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
630
828
|
opts.onCallbackError?.(err, "onSessionOpen");
|
|
631
829
|
});
|
|
632
830
|
}
|
|
@@ -634,7 +832,7 @@ function createMppSessionHandler(opts) {
|
|
|
634
832
|
if (opts.onVoucher) {
|
|
635
833
|
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
636
834
|
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
637
|
-
|
|
835
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
638
836
|
opts.onCallbackError?.(err, "onVoucher");
|
|
639
837
|
});
|
|
640
838
|
}
|
|
@@ -644,7 +842,7 @@ function createMppSessionHandler(opts) {
|
|
|
644
842
|
methods: [
|
|
645
843
|
withTypedChannelErrors(
|
|
646
844
|
onVerifiedCredential(
|
|
647
|
-
server$
|
|
845
|
+
server$3.stellar.channel({
|
|
648
846
|
channel: opts.channelFactory,
|
|
649
847
|
commitmentKey: opts.commitmentPublicKey,
|
|
650
848
|
network: networkId,
|
|
@@ -675,7 +873,7 @@ function createMppSessionHandler(opts) {
|
|
|
675
873
|
}
|
|
676
874
|
if (closeAmount > 0n && closeSig) {
|
|
677
875
|
const closePayer = record?.payer ?? null;
|
|
678
|
-
const closeTxHash = await server$
|
|
876
|
+
const closeTxHash = await server$3.close({
|
|
679
877
|
channel: opts.channelFactory,
|
|
680
878
|
amount: closeAmount,
|
|
681
879
|
signature: Buffer.from(closeSig, "hex"),
|
|
@@ -687,7 +885,7 @@ function createMppSessionHandler(opts) {
|
|
|
687
885
|
if (opts.onSettled) {
|
|
688
886
|
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
689
887
|
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
690
|
-
|
|
888
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
691
889
|
opts.onCallbackError?.(err, "onSettled");
|
|
692
890
|
});
|
|
693
891
|
}
|
|
@@ -698,7 +896,7 @@ function createMppSessionHandler(opts) {
|
|
|
698
896
|
payeeKeypair.publicKey()
|
|
699
897
|
);
|
|
700
898
|
if (!settlementAddresses) {
|
|
701
|
-
|
|
899
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
702
900
|
} else {
|
|
703
901
|
try {
|
|
704
902
|
const { Networks } = await import('@stellar/stellar-sdk');
|
|
@@ -721,7 +919,7 @@ function createMppSessionHandler(opts) {
|
|
|
721
919
|
preparedTx.sign(payeeKeypair);
|
|
722
920
|
await server.sendTransaction(preparedTx);
|
|
723
921
|
} catch (recordErr) {
|
|
724
|
-
|
|
922
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
725
923
|
}
|
|
726
924
|
}
|
|
727
925
|
}
|
|
@@ -838,12 +1036,13 @@ function routedock(opts) {
|
|
|
838
1036
|
payeeSecretKey: opts.payeeSecretKey,
|
|
839
1037
|
network: opts.network,
|
|
840
1038
|
amount: x402Price,
|
|
841
|
-
assetContract: opts.assetContract,
|
|
1039
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
842
1040
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
843
1041
|
manifest: signedManifest,
|
|
844
1042
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
845
1043
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
846
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
1044
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1045
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
847
1046
|
})
|
|
848
1047
|
);
|
|
849
1048
|
}
|
|
@@ -857,11 +1056,12 @@ function routedock(opts) {
|
|
|
857
1056
|
payeeSecretKey: opts.payeeSecretKey,
|
|
858
1057
|
network: opts.network,
|
|
859
1058
|
amount: chargePrice,
|
|
860
|
-
assetContract: opts.assetContract,
|
|
1059
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
861
1060
|
manifest: signedManifest,
|
|
862
1061
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
863
1062
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
864
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
1063
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1064
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
865
1065
|
})
|
|
866
1066
|
);
|
|
867
1067
|
}
|
|
@@ -879,7 +1079,7 @@ function routedock(opts) {
|
|
|
879
1079
|
network: opts.network,
|
|
880
1080
|
channelFactory: sessionPricing.channelFactory,
|
|
881
1081
|
rate: sessionPricing.rate,
|
|
882
|
-
assetContract: opts.assetContract,
|
|
1082
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
883
1083
|
manifest: signedManifest,
|
|
884
1084
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
885
1085
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
@@ -887,7 +1087,8 @@ function routedock(opts) {
|
|
|
887
1087
|
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
888
1088
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
889
1089
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
890
|
-
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
|
|
1090
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
1091
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
891
1092
|
})
|
|
892
1093
|
);
|
|
893
1094
|
}
|
|
@@ -1008,25 +1209,26 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
1008
1209
|
}
|
|
1009
1210
|
}
|
|
1010
1211
|
async function runStartupReconciliation(opts, logger) {
|
|
1011
|
-
const log = logger
|
|
1012
|
-
log("[SessionReconciler] Running startup reconciliation...");
|
|
1212
|
+
const log = resolveLogger(logger);
|
|
1213
|
+
log("info", "[SessionReconciler] Running startup reconciliation...");
|
|
1013
1214
|
try {
|
|
1014
1215
|
const stats = await reconcileAbandonedSessions(opts);
|
|
1015
1216
|
if (stats.orphanedCount === 0) {
|
|
1016
|
-
log("[SessionReconciler] No orphaned sessions found");
|
|
1217
|
+
log("info", "[SessionReconciler] No orphaned sessions found");
|
|
1017
1218
|
return;
|
|
1018
1219
|
}
|
|
1019
1220
|
log(
|
|
1221
|
+
"info",
|
|
1020
1222
|
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
1021
1223
|
);
|
|
1022
1224
|
if (stats.errors.length > 0) {
|
|
1023
|
-
log("[SessionReconciler] Reconciliation errors:");
|
|
1225
|
+
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
1024
1226
|
for (const err of stats.errors) {
|
|
1025
|
-
log(` - ${err.channelId}: ${err.reason}`);
|
|
1227
|
+
log("warn", ` - ${err.channelId}: ${err.reason}`);
|
|
1026
1228
|
}
|
|
1027
1229
|
}
|
|
1028
1230
|
} catch (err) {
|
|
1029
|
-
log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
1231
|
+
log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
1030
1232
|
}
|
|
1031
1233
|
}
|
|
1032
1234
|
|
|
@@ -1160,7 +1362,7 @@ var SupabaseSeenTxStore = class {
|
|
|
1160
1362
|
}
|
|
1161
1363
|
supabase;
|
|
1162
1364
|
async get(key) {
|
|
1163
|
-
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
|
|
1365
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
|
|
1164
1366
|
if (error) {
|
|
1165
1367
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
1166
1368
|
}
|
|
@@ -1168,7 +1370,8 @@ var SupabaseSeenTxStore = class {
|
|
|
1168
1370
|
const headers = data.headers;
|
|
1169
1371
|
return {
|
|
1170
1372
|
txHash: data.tx_hash ?? null,
|
|
1171
|
-
...headers ? { headers } : {}
|
|
1373
|
+
...headers ? { headers } : {},
|
|
1374
|
+
createdAt: Date.parse(data.created_at)
|
|
1172
1375
|
};
|
|
1173
1376
|
}
|
|
1174
1377
|
async set(key, record) {
|
|
@@ -1176,7 +1379,8 @@ var SupabaseSeenTxStore = class {
|
|
|
1176
1379
|
{
|
|
1177
1380
|
key,
|
|
1178
1381
|
tx_hash: record.txHash,
|
|
1179
|
-
headers: record.headers ?? null
|
|
1382
|
+
headers: record.headers ?? null,
|
|
1383
|
+
created_at: new Date(record.createdAt ?? Date.now()).toISOString()
|
|
1180
1384
|
},
|
|
1181
1385
|
{ onConflict: "key" }
|
|
1182
1386
|
);
|
|
@@ -1184,10 +1388,27 @@ var SupabaseSeenTxStore = class {
|
|
|
1184
1388
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
1185
1389
|
}
|
|
1186
1390
|
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Atomically increment the settlement's replay counter through the
|
|
1393
|
+
* `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
|
|
1394
|
+
* row lock gives concurrent claims distinct counts. Returns `Infinity` when
|
|
1395
|
+
* the key no longer exists, so the caller treats it as spent.
|
|
1396
|
+
*/
|
|
1397
|
+
async claimReplay(key) {
|
|
1398
|
+
const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
|
|
1399
|
+
p_key: key
|
|
1400
|
+
});
|
|
1401
|
+
if (error) {
|
|
1402
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
|
|
1403
|
+
}
|
|
1404
|
+
if (data == null) return Number.POSITIVE_INFINITY;
|
|
1405
|
+
return data;
|
|
1406
|
+
}
|
|
1187
1407
|
};
|
|
1188
1408
|
|
|
1189
1409
|
exports.DEFAULT_MAX_SESSION_DURATION_MS = DEFAULT_MAX_SESSION_DURATION_MS;
|
|
1190
1410
|
exports.InMemorySeenTxStore = InMemorySeenTxStore;
|
|
1411
|
+
exports.MAX_SETTLEMENT_REPLAYS = MAX_SETTLEMENT_REPLAYS;
|
|
1191
1412
|
exports.RouteDockChannelStateError = RouteDockChannelStateError;
|
|
1192
1413
|
exports.RouteDockClientVersionError = RouteDockClientVersionError;
|
|
1193
1414
|
exports.RouteDockDisputeError = RouteDockDisputeError;
|
|
@@ -1203,15 +1424,22 @@ exports.RouteDockSessionError = RouteDockSessionError;
|
|
|
1203
1424
|
exports.RouteDockSignatureError = RouteDockSignatureError;
|
|
1204
1425
|
exports.RouteDockTrustlineError = RouteDockTrustlineError;
|
|
1205
1426
|
exports.RouteDockVoucherMonotonicityError = RouteDockVoucherMonotonicityError;
|
|
1427
|
+
exports.SETTLEMENT_REPLAY_WINDOW_MS = SETTLEMENT_REPLAY_WINDOW_MS;
|
|
1206
1428
|
exports.SupabaseSeenTxStore = SupabaseSeenTxStore;
|
|
1207
1429
|
exports.SupabaseSessionStore = SupabaseSessionStore;
|
|
1430
|
+
exports.checkSettlementReplay = checkSettlementReplay;
|
|
1208
1431
|
exports.createMppChargeHandler = createMppChargeHandler;
|
|
1209
1432
|
exports.createMppSessionHandler = createMppSessionHandler;
|
|
1210
1433
|
exports.createX402Handler = createX402Handler;
|
|
1211
1434
|
exports.decimalToStroops = decimalToStroops;
|
|
1435
|
+
exports.getEligibleAssets = getEligibleAssets;
|
|
1436
|
+
exports.isAssetEligible = isAssetEligible;
|
|
1212
1437
|
exports.isVoucherStoreValue = isVoucherStoreValue;
|
|
1438
|
+
exports.normalizeManifestAssets = normalizeManifestAssets;
|
|
1213
1439
|
exports.paymentIdempotencyKey = paymentIdempotencyKey;
|
|
1214
1440
|
exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
|
|
1215
1441
|
exports.registerProvider = registerProvider;
|
|
1442
|
+
exports.resolveAssetContract = resolveAssetContract;
|
|
1216
1443
|
exports.routedock = routedock;
|
|
1217
1444
|
exports.runStartupReconciliation = runStartupReconciliation;
|
|
1445
|
+
exports.selectAsset = selectAsset;
|