@routedock/routedock 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -0
- package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
- package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
- package/dist/{ProviderRegistry-C50nyhaF.d.ts → ProviderRegistry-CA_TdREG.d.ts} +2 -2
- package/dist/{ProviderRegistry-BJ9oJLVz.d.cts → ProviderRegistry-D-0rqK0N.d.cts} +2 -2
- package/dist/{RouteDockClient-B1roCa8h.d.cts → RouteDockClient-Bh86bVly.d.ts} +23 -9
- package/dist/{RouteDockClient-BbkqjhMG.d.ts → RouteDockClient-go9aopfF.d.cts} +23 -9
- package/dist/{SessionReconciler-Dscw43F3.d.ts → SessionReconciler-Bw-PoBVl.d.ts} +14 -5
- package/dist/{SessionReconciler-bi0z2qtN.d.cts → SessionReconciler-Cz2nuOlJ.d.cts} +14 -5
- package/dist/{SessionStore-DvkZv8-E.d.ts → SessionStore-B4-Hi4H8.d.cts} +34 -2
- package/dist/{SessionStore-BpK8vv0L.d.cts → SessionStore-BaYiegP0.d.ts} +34 -2
- package/dist/client.cjs +429 -117
- package/dist/client.d.cts +9 -8
- package/dist/client.d.ts +9 -8
- package/dist/client.js +421 -119
- package/dist/index.cjs +652 -220
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +643 -221
- package/dist/{types-DmhmfLzP.d.cts → logger-D8_Q2ytp.d.cts} +58 -1
- package/dist/{types-DmhmfLzP.d.ts → logger-D8_Q2ytp.d.ts} +58 -1
- package/dist/provider/fastify.cjs +306 -89
- package/dist/provider/fastify.d.cts +9 -4
- package/dist/provider/fastify.d.ts +9 -4
- package/dist/provider/fastify.js +305 -88
- package/dist/provider/hono.cjs +286 -82
- package/dist/provider/hono.d.cts +9 -7
- package/dist/provider/hono.d.ts +9 -7
- package/dist/provider/hono.js +282 -78
- package/dist/provider.cjs +326 -98
- package/dist/provider.d.cts +14 -10
- package/dist/provider.d.ts +14 -10
- package/dist/provider.js +318 -98
- package/dist/react.cjs +407 -117
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +408 -118
- package/dist/{routedockMiddleware-BlDsHuPl.d.ts → routedockMiddleware-57gUaEMf.d.ts} +9 -4
- package/dist/{routedockMiddleware-D97A5ptQ.d.cts → routedockMiddleware-DlyLrxp7.d.cts} +9 -4
- package/dist/schema.cjs +40 -0
- package/dist/schema.js +40 -0
- package/dist/{sign-Dpj2Mp7k.d.ts → sign-AKH67sft.d.cts} +1 -1
- package/dist/{sign-CUf3rhTx.d.cts → sign-BEq-OsSq.d.ts} +1 -1
- package/dist/testing.cjs +6 -4
- package/dist/testing.d.cts +32 -13
- package/dist/testing.d.ts +32 -13
- package/dist/testing.js +6 -4
- package/package.json +2 -2
- package/dist/MppSessionHandler-B4TyD7Xg.d.cts +0 -121
- package/dist/MppSessionHandler-CLB5SzgN.d.ts +0 -121
package/dist/provider.js
CHANGED
|
@@ -5,9 +5,9 @@ import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
|
|
|
5
5
|
import { createEd25519Signer } from '@x402/stellar';
|
|
6
6
|
import { encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymentResponseHeader } from '@x402/core/http';
|
|
7
7
|
import { usdcToStroops } from '@routedock/nulth-sdk';
|
|
8
|
-
import { stellar } from '@stellar/mpp/charge/server';
|
|
9
|
-
import {
|
|
10
|
-
import { Store, stellar
|
|
8
|
+
import { stellar as stellar$1 } from '@stellar/mpp/charge/server';
|
|
9
|
+
import { Request, Mppx } from 'mppx/server';
|
|
10
|
+
import { Store, stellar, close } from '@stellar/mpp/channel/server';
|
|
11
11
|
import { Errors } from 'mppx';
|
|
12
12
|
import { createHash } from 'crypto';
|
|
13
13
|
|
|
@@ -119,7 +119,85 @@ var RouteDockSessionError = RouteDockVoucherMonotonicityError;
|
|
|
119
119
|
// src/types.ts
|
|
120
120
|
var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
121
121
|
|
|
122
|
-
// src/
|
|
122
|
+
// src/internal/assetUtils.ts
|
|
123
|
+
function normalizeManifestAssets(manifest) {
|
|
124
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
125
|
+
return manifest.assets;
|
|
126
|
+
}
|
|
127
|
+
if (manifest.asset && manifest.asset_contract) {
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
asset: manifest.asset,
|
|
131
|
+
asset_contract: manifest.asset_contract
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
throw new RouteDockManifestError(
|
|
136
|
+
"Manifest must define either assets[] or root asset/asset_contract fields"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function getEligibleAssets(manifest, mode, endpoint) {
|
|
140
|
+
const assets = normalizeManifestAssets(manifest);
|
|
141
|
+
return assets.filter((asset) => {
|
|
142
|
+
if (asset.modes && !asset.modes.includes(mode)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
|
|
146
|
+
const trimmedEndpoint = endpoint.replace(/^\//, "");
|
|
147
|
+
if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (manifest.endpoints) {
|
|
151
|
+
let pathname = endpoint;
|
|
152
|
+
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
|
|
153
|
+
try {
|
|
154
|
+
pathname = new URL(endpoint).pathname;
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const trimmedPathname = pathname.replace(/^\//, "");
|
|
159
|
+
const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
|
|
160
|
+
const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
|
|
161
|
+
const keyMatches = key === endpoint || key === trimmedEndpoint;
|
|
162
|
+
if (pathMatches || keyMatches) {
|
|
163
|
+
return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
});
|
|
167
|
+
if (!matchesEndpoint) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
|
|
178
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
179
|
+
return selectAsset(manifest, mode, endpoint).asset_contract;
|
|
180
|
+
}
|
|
181
|
+
return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
|
|
182
|
+
}
|
|
183
|
+
function selectAsset(manifest, mode, endpoint) {
|
|
184
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
185
|
+
if (eligible.length === 0) {
|
|
186
|
+
const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
|
|
187
|
+
throw new RouteDockManifestError(
|
|
188
|
+
`No eligible assets found for mode '${mode}'${endpointMsg}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return eligible[0];
|
|
192
|
+
}
|
|
193
|
+
function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
194
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
195
|
+
return eligible.some(
|
|
196
|
+
(asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/internal/payee.ts
|
|
123
201
|
function resolvePayee(manifest, mode) {
|
|
124
202
|
const override = manifest.pricing[mode]?.payee;
|
|
125
203
|
return override ?? manifest.payee;
|
|
@@ -130,9 +208,41 @@ function extractPayerAddress(key) {
|
|
|
130
208
|
return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
131
209
|
}
|
|
132
210
|
|
|
211
|
+
// src/internal/logger.ts
|
|
212
|
+
function createConsoleLogger() {
|
|
213
|
+
return (level, message, fields) => {
|
|
214
|
+
const values = fields ? Object.values(fields) : [];
|
|
215
|
+
if (values.length > 0) {
|
|
216
|
+
console[level](message, ...values);
|
|
217
|
+
} else {
|
|
218
|
+
console[level](message);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
var consoleLogger = createConsoleLogger();
|
|
223
|
+
function resolveLogger(logger, fallback = consoleLogger) {
|
|
224
|
+
return logger ?? fallback;
|
|
225
|
+
}
|
|
226
|
+
|
|
133
227
|
// src/provider/SeenTxStore.ts
|
|
228
|
+
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
229
|
+
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
230
|
+
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
231
|
+
const record = await store.get(key);
|
|
232
|
+
if (!record) return { kind: "miss" };
|
|
233
|
+
const { createdAt } = record;
|
|
234
|
+
if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
|
|
235
|
+
return { kind: "spent" };
|
|
236
|
+
}
|
|
237
|
+
if (store.claimReplay) {
|
|
238
|
+
const replayed = await store.claimReplay(key);
|
|
239
|
+
if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
|
|
240
|
+
}
|
|
241
|
+
return { kind: "replay", record };
|
|
242
|
+
}
|
|
134
243
|
var InMemorySeenTxStore = class {
|
|
135
244
|
map = /* @__PURE__ */ new Map();
|
|
245
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
136
246
|
order = [];
|
|
137
247
|
constructor(maxEntriesOrOptions = 1e4) {
|
|
138
248
|
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
@@ -140,7 +250,8 @@ var InMemorySeenTxStore = class {
|
|
|
140
250
|
if (options.warn !== false) {
|
|
141
251
|
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
142
252
|
if (isServerless) {
|
|
143
|
-
|
|
253
|
+
resolveLogger(options.logger)(
|
|
254
|
+
"warn",
|
|
144
255
|
"[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."
|
|
145
256
|
);
|
|
146
257
|
}
|
|
@@ -155,21 +266,43 @@ var InMemorySeenTxStore = class {
|
|
|
155
266
|
this.order.push(key);
|
|
156
267
|
if (this.order.length > this.maxEntries) {
|
|
157
268
|
const evicted = this.order.shift();
|
|
158
|
-
if (evicted !== void 0)
|
|
269
|
+
if (evicted !== void 0) {
|
|
270
|
+
this.map.delete(evicted);
|
|
271
|
+
this.replayCounts.delete(evicted);
|
|
272
|
+
}
|
|
159
273
|
}
|
|
160
274
|
}
|
|
161
275
|
this.map.set(key, record);
|
|
162
276
|
}
|
|
277
|
+
claimReplay(key) {
|
|
278
|
+
if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
|
|
279
|
+
const next = (this.replayCounts.get(key) ?? 0) + 1;
|
|
280
|
+
this.replayCounts.set(key, next);
|
|
281
|
+
return next;
|
|
282
|
+
}
|
|
163
283
|
};
|
|
164
284
|
async function sha256Hex(input) {
|
|
165
285
|
const buf = new TextEncoder().encode(input);
|
|
166
286
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
167
287
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
168
288
|
}
|
|
169
|
-
async function paymentIdempotencyKey(getHeader) {
|
|
289
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
170
290
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
171
291
|
if (!material) return null;
|
|
172
|
-
return sha256Hex(
|
|
292
|
+
return sha256Hex(
|
|
293
|
+
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/provider/settleResult.ts
|
|
298
|
+
function readSettleResult(result) {
|
|
299
|
+
const settle = result;
|
|
300
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
301
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
302
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
303
|
+
return failed(settle.errorReason);
|
|
304
|
+
}
|
|
305
|
+
return { ok: true, txHash: settle.transaction };
|
|
173
306
|
}
|
|
174
307
|
|
|
175
308
|
// src/provider/x402Handler.ts
|
|
@@ -182,7 +315,8 @@ function createX402Handler(opts) {
|
|
|
182
315
|
const caip2 = CAIP2[opts.network];
|
|
183
316
|
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
184
317
|
const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
185
|
-
const
|
|
318
|
+
const logger = resolveLogger(opts.logger);
|
|
319
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
186
320
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
187
321
|
const localFacilitator = new ExactStellarScheme([signer], {
|
|
188
322
|
areFeesSponsored: true
|
|
@@ -203,22 +337,29 @@ function createX402Handler(opts) {
|
|
|
203
337
|
}
|
|
204
338
|
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
205
339
|
const payTo = resolvePayee(opts.manifest, "x402");
|
|
206
|
-
const requirements = {
|
|
207
|
-
scheme: "exact",
|
|
208
|
-
network: caip2,
|
|
209
|
-
asset: opts.assetContract,
|
|
210
|
-
amount: amountInBaseUnits,
|
|
211
|
-
payTo,
|
|
212
|
-
maxTimeoutSeconds: 60,
|
|
213
|
-
extra: {
|
|
214
|
-
areFeesSponsored: true,
|
|
215
|
-
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
340
|
return async (req, res, next) => {
|
|
219
341
|
try {
|
|
220
|
-
const
|
|
221
|
-
|
|
342
|
+
const endpoint = req.path || req.originalUrl;
|
|
343
|
+
const assetContract = resolveAssetContract(
|
|
344
|
+
opts.manifest,
|
|
345
|
+
"x402",
|
|
346
|
+
endpoint,
|
|
347
|
+
opts.assetContract
|
|
348
|
+
);
|
|
349
|
+
const requirements = {
|
|
350
|
+
scheme: "exact",
|
|
351
|
+
network: caip2,
|
|
352
|
+
asset: assetContract,
|
|
353
|
+
amount: amountInBaseUnits,
|
|
354
|
+
payTo,
|
|
355
|
+
maxTimeoutSeconds: 60,
|
|
356
|
+
extra: {
|
|
357
|
+
areFeesSponsored: true,
|
|
358
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const respondPaymentRequired = async (error, reason) => {
|
|
362
|
+
const body = { error, ...reason ? { reason } : {} };
|
|
222
363
|
if (ozServer) {
|
|
223
364
|
const resourceInfo = {
|
|
224
365
|
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
@@ -228,7 +369,7 @@ function createX402Handler(opts) {
|
|
|
228
369
|
[requirements],
|
|
229
370
|
resourceInfo
|
|
230
371
|
);
|
|
231
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(
|
|
372
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
232
373
|
} else {
|
|
233
374
|
const x402Response = {
|
|
234
375
|
x402Version: 2,
|
|
@@ -238,19 +379,35 @@ function createX402Handler(opts) {
|
|
|
238
379
|
},
|
|
239
380
|
accepts: [requirements]
|
|
240
381
|
};
|
|
241
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(
|
|
382
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(body);
|
|
242
383
|
}
|
|
384
|
+
};
|
|
385
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
386
|
+
if (!paymentHeader) {
|
|
387
|
+
await respondPaymentRequired("Payment Required");
|
|
243
388
|
return;
|
|
244
389
|
}
|
|
245
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
390
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
391
|
+
(name) => {
|
|
392
|
+
const v = req.headers[name.toLowerCase()];
|
|
393
|
+
return Array.isArray(v) ? v[0] : v;
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
method: req.method,
|
|
397
|
+
path: req.originalUrl.split("?")[0],
|
|
398
|
+
amount: requirements.amount,
|
|
399
|
+
payTo: requirements.payTo
|
|
400
|
+
}
|
|
401
|
+
);
|
|
249
402
|
if (idempotencyKey) {
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
403
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
404
|
+
if (replayCheck.kind === "spent") {
|
|
405
|
+
res.status(402).json({ error: "Payment already used" });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (replayCheck.kind === "replay") {
|
|
409
|
+
if (replayCheck.record.headers) {
|
|
410
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
254
411
|
res.setHeader(k, val);
|
|
255
412
|
}
|
|
256
413
|
}
|
|
@@ -269,15 +426,18 @@ function createX402Handler(opts) {
|
|
|
269
426
|
}
|
|
270
427
|
if (ozServer) {
|
|
271
428
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
encodePaymentResponseHeader(
|
|
277
|
-
settleResult
|
|
278
|
-
)
|
|
279
|
-
);
|
|
429
|
+
const outcome = readSettleResult(settleResult);
|
|
430
|
+
if (!outcome.ok) {
|
|
431
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
432
|
+
return;
|
|
280
433
|
}
|
|
434
|
+
txHash = outcome.txHash;
|
|
435
|
+
res.setHeader(
|
|
436
|
+
"X-Payment-Response",
|
|
437
|
+
encodePaymentResponseHeader(
|
|
438
|
+
settleResult
|
|
439
|
+
)
|
|
440
|
+
);
|
|
281
441
|
} else {
|
|
282
442
|
const verifyResult = await localFacilitator.verify(
|
|
283
443
|
payload,
|
|
@@ -294,15 +454,18 @@ function createX402Handler(opts) {
|
|
|
294
454
|
payload,
|
|
295
455
|
requirements
|
|
296
456
|
);
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
encodePaymentResponseHeader(
|
|
302
|
-
settleResult
|
|
303
|
-
)
|
|
304
|
-
);
|
|
457
|
+
const outcome = readSettleResult(settleResult);
|
|
458
|
+
if (!outcome.ok) {
|
|
459
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
460
|
+
return;
|
|
305
461
|
}
|
|
462
|
+
txHash = outcome.txHash;
|
|
463
|
+
res.setHeader(
|
|
464
|
+
"X-Payment-Response",
|
|
465
|
+
encodePaymentResponseHeader(
|
|
466
|
+
settleResult
|
|
467
|
+
)
|
|
468
|
+
);
|
|
306
469
|
}
|
|
307
470
|
if (idempotencyKey) {
|
|
308
471
|
const headers = {};
|
|
@@ -310,17 +473,21 @@ function createX402Handler(opts) {
|
|
|
310
473
|
if (typeof paymentResponse === "string") {
|
|
311
474
|
headers["X-Payment-Response"] = paymentResponse;
|
|
312
475
|
}
|
|
313
|
-
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
476
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
314
477
|
}
|
|
315
478
|
if (txHash && opts.onSettled) {
|
|
316
479
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
317
|
-
|
|
480
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
318
481
|
opts.onCallbackError?.(err, "onSettled");
|
|
319
482
|
});
|
|
320
483
|
}
|
|
321
484
|
next();
|
|
322
485
|
} catch (err) {
|
|
323
|
-
|
|
486
|
+
if (err instanceof RouteDockManifestError) {
|
|
487
|
+
next(err);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
324
491
|
res.status(500).json({ error: "Payment settlement failed" });
|
|
325
492
|
}
|
|
326
493
|
};
|
|
@@ -333,20 +500,37 @@ function createMppChargeHandler(opts) {
|
|
|
333
500
|
const networkId = MPP_NETWORK[opts.network];
|
|
334
501
|
const amountHumanReadable = opts.amount;
|
|
335
502
|
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
503
|
+
const logger = resolveLogger(opts.logger);
|
|
504
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
505
|
+
const mppxInstances = /* @__PURE__ */ new Map();
|
|
506
|
+
function getMppx(contract) {
|
|
507
|
+
let instance = mppxInstances.get(contract);
|
|
508
|
+
if (!instance) {
|
|
509
|
+
instance = Mppx.create({
|
|
510
|
+
secretKey: opts.payeeSecretKey,
|
|
511
|
+
methods: [
|
|
512
|
+
stellar$1.charge({
|
|
513
|
+
recipient,
|
|
514
|
+
currency: contract,
|
|
515
|
+
network: networkId,
|
|
516
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
517
|
+
})
|
|
518
|
+
]
|
|
519
|
+
});
|
|
520
|
+
mppxInstances.set(contract, instance);
|
|
521
|
+
}
|
|
522
|
+
return instance;
|
|
523
|
+
}
|
|
348
524
|
return async (req, res, next) => {
|
|
349
525
|
try {
|
|
526
|
+
const endpoint = req.path || req.originalUrl;
|
|
527
|
+
const assetContract = resolveAssetContract(
|
|
528
|
+
opts.manifest,
|
|
529
|
+
"mpp-charge",
|
|
530
|
+
endpoint,
|
|
531
|
+
opts.assetContract
|
|
532
|
+
);
|
|
533
|
+
const mppx = getMppx(assetContract);
|
|
350
534
|
let payerAddress = null;
|
|
351
535
|
try {
|
|
352
536
|
const authHeader = req.headers["authorization"];
|
|
@@ -362,15 +546,27 @@ function createMppChargeHandler(opts) {
|
|
|
362
546
|
}
|
|
363
547
|
} catch {
|
|
364
548
|
}
|
|
365
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
549
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
550
|
+
(name) => {
|
|
551
|
+
const v = req.headers[name.toLowerCase()];
|
|
552
|
+
return Array.isArray(v) ? v[0] : v;
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
method: req.method,
|
|
556
|
+
path: req.originalUrl.split("?")[0],
|
|
557
|
+
amount: amountHumanReadable,
|
|
558
|
+
payTo: recipient
|
|
559
|
+
}
|
|
560
|
+
);
|
|
369
561
|
if (idempotencyKey) {
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
562
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
563
|
+
if (replayCheck.kind === "spent") {
|
|
564
|
+
res.status(402).json({ error: "Payment already used" });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (replayCheck.kind === "replay") {
|
|
568
|
+
if (replayCheck.record.headers) {
|
|
569
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
374
570
|
res.setHeader(k, val);
|
|
375
571
|
}
|
|
376
572
|
}
|
|
@@ -382,7 +578,7 @@ function createMppChargeHandler(opts) {
|
|
|
382
578
|
const handler = mppx["stellar/charge"];
|
|
383
579
|
const result = await handler({
|
|
384
580
|
amount: amountHumanReadable,
|
|
385
|
-
currency:
|
|
581
|
+
currency: assetContract,
|
|
386
582
|
recipient,
|
|
387
583
|
description: opts.manifest.name
|
|
388
584
|
})(fetchReq);
|
|
@@ -413,12 +609,13 @@ function createMppChargeHandler(opts) {
|
|
|
413
609
|
if (idempotencyKey) {
|
|
414
610
|
await seenTxStore.set(idempotencyKey, {
|
|
415
611
|
txHash: reference ?? null,
|
|
416
|
-
headers: receiptHeaders
|
|
612
|
+
headers: receiptHeaders,
|
|
613
|
+
createdAt: Date.now()
|
|
417
614
|
});
|
|
418
615
|
}
|
|
419
616
|
if (reference && opts.onSettled) {
|
|
420
617
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
421
|
-
|
|
618
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
422
619
|
opts.onCallbackError?.(err, "onSettled");
|
|
423
620
|
});
|
|
424
621
|
}
|
|
@@ -526,6 +723,7 @@ function createMppSessionHandler(opts) {
|
|
|
526
723
|
const networkId = MPP_NETWORK2[opts.network];
|
|
527
724
|
const rateHuman = opts.rate;
|
|
528
725
|
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
726
|
+
const logger = resolveLogger(opts.logger);
|
|
529
727
|
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
530
728
|
const innerStore = Store.memory();
|
|
531
729
|
let record = null;
|
|
@@ -577,7 +775,7 @@ function createMppSessionHandler(opts) {
|
|
|
577
775
|
reason
|
|
578
776
|
});
|
|
579
777
|
} catch (err) {
|
|
580
|
-
|
|
778
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
581
779
|
}
|
|
582
780
|
}
|
|
583
781
|
}
|
|
@@ -624,7 +822,7 @@ function createMppSessionHandler(opts) {
|
|
|
624
822
|
sessionOpened = true;
|
|
625
823
|
if (opts.onSessionOpen) {
|
|
626
824
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
627
|
-
|
|
825
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
628
826
|
opts.onCallbackError?.(err, "onSessionOpen");
|
|
629
827
|
});
|
|
630
828
|
}
|
|
@@ -632,7 +830,7 @@ function createMppSessionHandler(opts) {
|
|
|
632
830
|
if (opts.onVoucher) {
|
|
633
831
|
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
634
832
|
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
635
|
-
|
|
833
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
636
834
|
opts.onCallbackError?.(err, "onVoucher");
|
|
637
835
|
});
|
|
638
836
|
}
|
|
@@ -642,7 +840,7 @@ function createMppSessionHandler(opts) {
|
|
|
642
840
|
methods: [
|
|
643
841
|
withTypedChannelErrors(
|
|
644
842
|
onVerifiedCredential(
|
|
645
|
-
stellar
|
|
843
|
+
stellar.channel({
|
|
646
844
|
channel: opts.channelFactory,
|
|
647
845
|
commitmentKey: opts.commitmentPublicKey,
|
|
648
846
|
network: networkId,
|
|
@@ -685,7 +883,7 @@ function createMppSessionHandler(opts) {
|
|
|
685
883
|
if (opts.onSettled) {
|
|
686
884
|
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
687
885
|
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
688
|
-
|
|
886
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
689
887
|
opts.onCallbackError?.(err, "onSettled");
|
|
690
888
|
});
|
|
691
889
|
}
|
|
@@ -696,7 +894,7 @@ function createMppSessionHandler(opts) {
|
|
|
696
894
|
payeeKeypair.publicKey()
|
|
697
895
|
);
|
|
698
896
|
if (!settlementAddresses) {
|
|
699
|
-
|
|
897
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
700
898
|
} else {
|
|
701
899
|
try {
|
|
702
900
|
const { Networks } = await import('@stellar/stellar-sdk');
|
|
@@ -719,7 +917,7 @@ function createMppSessionHandler(opts) {
|
|
|
719
917
|
preparedTx.sign(payeeKeypair);
|
|
720
918
|
await server.sendTransaction(preparedTx);
|
|
721
919
|
} catch (recordErr) {
|
|
722
|
-
|
|
920
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
723
921
|
}
|
|
724
922
|
}
|
|
725
923
|
}
|
|
@@ -836,12 +1034,13 @@ function routedock(opts) {
|
|
|
836
1034
|
payeeSecretKey: opts.payeeSecretKey,
|
|
837
1035
|
network: opts.network,
|
|
838
1036
|
amount: x402Price,
|
|
839
|
-
assetContract: opts.assetContract,
|
|
1037
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
840
1038
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
841
1039
|
manifest: signedManifest,
|
|
842
1040
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
843
1041
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
844
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
1042
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1043
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
845
1044
|
})
|
|
846
1045
|
);
|
|
847
1046
|
}
|
|
@@ -855,11 +1054,12 @@ function routedock(opts) {
|
|
|
855
1054
|
payeeSecretKey: opts.payeeSecretKey,
|
|
856
1055
|
network: opts.network,
|
|
857
1056
|
amount: chargePrice,
|
|
858
|
-
assetContract: opts.assetContract,
|
|
1057
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
859
1058
|
manifest: signedManifest,
|
|
860
1059
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
861
1060
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
862
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
1061
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1062
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
863
1063
|
})
|
|
864
1064
|
);
|
|
865
1065
|
}
|
|
@@ -877,7 +1077,7 @@ function routedock(opts) {
|
|
|
877
1077
|
network: opts.network,
|
|
878
1078
|
channelFactory: sessionPricing.channelFactory,
|
|
879
1079
|
rate: sessionPricing.rate,
|
|
880
|
-
assetContract: opts.assetContract,
|
|
1080
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
881
1081
|
manifest: signedManifest,
|
|
882
1082
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
883
1083
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
@@ -885,7 +1085,8 @@ function routedock(opts) {
|
|
|
885
1085
|
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
886
1086
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
887
1087
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
888
|
-
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
|
|
1088
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
1089
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
889
1090
|
})
|
|
890
1091
|
);
|
|
891
1092
|
}
|
|
@@ -1006,25 +1207,26 @@ async function reconcileAbandonedSessions(opts) {
|
|
|
1006
1207
|
}
|
|
1007
1208
|
}
|
|
1008
1209
|
async function runStartupReconciliation(opts, logger) {
|
|
1009
|
-
const log = logger
|
|
1010
|
-
log("[SessionReconciler] Running startup reconciliation...");
|
|
1210
|
+
const log = resolveLogger(logger);
|
|
1211
|
+
log("info", "[SessionReconciler] Running startup reconciliation...");
|
|
1011
1212
|
try {
|
|
1012
1213
|
const stats = await reconcileAbandonedSessions(opts);
|
|
1013
1214
|
if (stats.orphanedCount === 0) {
|
|
1014
|
-
log("[SessionReconciler] No orphaned sessions found");
|
|
1215
|
+
log("info", "[SessionReconciler] No orphaned sessions found");
|
|
1015
1216
|
return;
|
|
1016
1217
|
}
|
|
1017
1218
|
log(
|
|
1219
|
+
"info",
|
|
1018
1220
|
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
1019
1221
|
);
|
|
1020
1222
|
if (stats.errors.length > 0) {
|
|
1021
|
-
log("[SessionReconciler] Reconciliation errors:");
|
|
1223
|
+
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
1022
1224
|
for (const err of stats.errors) {
|
|
1023
|
-
log(` - ${err.channelId}: ${err.reason}`);
|
|
1225
|
+
log("warn", ` - ${err.channelId}: ${err.reason}`);
|
|
1024
1226
|
}
|
|
1025
1227
|
}
|
|
1026
1228
|
} catch (err) {
|
|
1027
|
-
log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
1229
|
+
log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
1028
1230
|
}
|
|
1029
1231
|
}
|
|
1030
1232
|
|
|
@@ -1158,7 +1360,7 @@ var SupabaseSeenTxStore = class {
|
|
|
1158
1360
|
}
|
|
1159
1361
|
supabase;
|
|
1160
1362
|
async get(key) {
|
|
1161
|
-
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
|
|
1363
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
|
|
1162
1364
|
if (error) {
|
|
1163
1365
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
1164
1366
|
}
|
|
@@ -1166,7 +1368,8 @@ var SupabaseSeenTxStore = class {
|
|
|
1166
1368
|
const headers = data.headers;
|
|
1167
1369
|
return {
|
|
1168
1370
|
txHash: data.tx_hash ?? null,
|
|
1169
|
-
...headers ? { headers } : {}
|
|
1371
|
+
...headers ? { headers } : {},
|
|
1372
|
+
createdAt: Date.parse(data.created_at)
|
|
1170
1373
|
};
|
|
1171
1374
|
}
|
|
1172
1375
|
async set(key, record) {
|
|
@@ -1174,7 +1377,8 @@ var SupabaseSeenTxStore = class {
|
|
|
1174
1377
|
{
|
|
1175
1378
|
key,
|
|
1176
1379
|
tx_hash: record.txHash,
|
|
1177
|
-
headers: record.headers ?? null
|
|
1380
|
+
headers: record.headers ?? null,
|
|
1381
|
+
created_at: new Date(record.createdAt ?? Date.now()).toISOString()
|
|
1178
1382
|
},
|
|
1179
1383
|
{ onConflict: "key" }
|
|
1180
1384
|
);
|
|
@@ -1182,6 +1386,22 @@ var SupabaseSeenTxStore = class {
|
|
|
1182
1386
|
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
1183
1387
|
}
|
|
1184
1388
|
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Atomically increment the settlement's replay counter through the
|
|
1391
|
+
* `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
|
|
1392
|
+
* row lock gives concurrent claims distinct counts. Returns `Infinity` when
|
|
1393
|
+
* the key no longer exists, so the caller treats it as spent.
|
|
1394
|
+
*/
|
|
1395
|
+
async claimReplay(key) {
|
|
1396
|
+
const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
|
|
1397
|
+
p_key: key
|
|
1398
|
+
});
|
|
1399
|
+
if (error) {
|
|
1400
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
|
|
1401
|
+
}
|
|
1402
|
+
if (data == null) return Number.POSITIVE_INFINITY;
|
|
1403
|
+
return data;
|
|
1404
|
+
}
|
|
1185
1405
|
};
|
|
1186
1406
|
|
|
1187
|
-
export { DEFAULT_MAX_SESSION_DURATION_MS, InMemorySeenTxStore, RouteDockChannelStateError, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSeenTxStore, SupabaseSessionStore, createMppChargeHandler, createMppSessionHandler, createX402Handler, decimalToStroops, isVoucherStoreValue, paymentIdempotencyKey, reconcileAbandonedSessions, registerProvider, routedock, runStartupReconciliation };
|
|
1407
|
+
export { DEFAULT_MAX_SESSION_DURATION_MS, InMemorySeenTxStore, MAX_SETTLEMENT_REPLAYS, RouteDockChannelStateError, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SETTLEMENT_REPLAY_WINDOW_MS, SupabaseSeenTxStore, SupabaseSessionStore, checkSettlementReplay, createMppChargeHandler, createMppSessionHandler, createX402Handler, decimalToStroops, getEligibleAssets, isAssetEligible, isVoucherStoreValue, normalizeManifestAssets, paymentIdempotencyKey, reconcileAbandonedSessions, registerProvider, resolveAssetContract, routedock, runStartupReconciliation, selectAsset };
|