@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
|
@@ -7,15 +7,37 @@ 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
|
|
|
16
16
|
// src/provider/x402Handler.ts
|
|
17
17
|
|
|
18
|
-
// src/
|
|
18
|
+
// src/errors.ts
|
|
19
|
+
var RouteDockError = class extends Error {
|
|
20
|
+
code;
|
|
21
|
+
retryable;
|
|
22
|
+
retryAfterMs;
|
|
23
|
+
constructor(message, code, retryable, options = {}) {
|
|
24
|
+
super(message, { cause: options.cause });
|
|
25
|
+
this.name = "RouteDockError";
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.retryable = retryable;
|
|
28
|
+
if (options.retryAfterMs !== void 0) {
|
|
29
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var RouteDockManifestError = class extends RouteDockError {
|
|
34
|
+
constructor(message, options = {}) {
|
|
35
|
+
super(message, "MANIFEST", false, options);
|
|
36
|
+
this.name = "RouteDockManifestError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/internal/payee.ts
|
|
19
41
|
function resolvePayee(manifest, mode) {
|
|
20
42
|
const override = manifest.pricing[mode]?.payee;
|
|
21
43
|
return override ?? manifest.payee;
|
|
@@ -26,9 +48,113 @@ function extractPayerAddress(key) {
|
|
|
26
48
|
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
// src/internal/assetUtils.ts
|
|
52
|
+
function normalizeManifestAssets(manifest) {
|
|
53
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
54
|
+
return manifest.assets;
|
|
55
|
+
}
|
|
56
|
+
if (manifest.asset && manifest.asset_contract) {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
asset: manifest.asset,
|
|
60
|
+
asset_contract: manifest.asset_contract
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
throw new RouteDockManifestError(
|
|
65
|
+
"Manifest must define either assets[] or root asset/asset_contract fields"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function getEligibleAssets(manifest, mode, endpoint) {
|
|
69
|
+
const assets = normalizeManifestAssets(manifest);
|
|
70
|
+
return assets.filter((asset) => {
|
|
71
|
+
if (asset.modes && !asset.modes.includes(mode)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
|
|
75
|
+
const trimmedEndpoint = endpoint.replace(/^\//, "");
|
|
76
|
+
if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (manifest.endpoints) {
|
|
80
|
+
let pathname = endpoint;
|
|
81
|
+
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
|
|
82
|
+
try {
|
|
83
|
+
pathname = new URL(endpoint).pathname;
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const trimmedPathname = pathname.replace(/^\//, "");
|
|
88
|
+
const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
|
|
89
|
+
const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
|
|
90
|
+
const keyMatches = key === endpoint || key === trimmedEndpoint;
|
|
91
|
+
if (pathMatches || keyMatches) {
|
|
92
|
+
return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
if (!matchesEndpoint) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
|
|
107
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
108
|
+
return selectAsset(manifest, mode, endpoint).asset_contract;
|
|
109
|
+
}
|
|
110
|
+
return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
|
|
111
|
+
}
|
|
112
|
+
function selectAsset(manifest, mode, endpoint) {
|
|
113
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
114
|
+
if (eligible.length === 0) {
|
|
115
|
+
const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
|
|
116
|
+
throw new RouteDockManifestError(
|
|
117
|
+
`No eligible assets found for mode '${mode}'${endpointMsg}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return eligible[0];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/internal/logger.ts
|
|
124
|
+
function createConsoleLogger() {
|
|
125
|
+
return (level, message, fields) => {
|
|
126
|
+
const values = fields ? Object.values(fields) : [];
|
|
127
|
+
if (values.length > 0) {
|
|
128
|
+
console[level](message, ...values);
|
|
129
|
+
} else {
|
|
130
|
+
console[level](message);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
var consoleLogger = createConsoleLogger();
|
|
135
|
+
function resolveLogger(logger, fallback = consoleLogger) {
|
|
136
|
+
return logger ?? fallback;
|
|
137
|
+
}
|
|
138
|
+
|
|
29
139
|
// src/provider/SeenTxStore.ts
|
|
140
|
+
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
141
|
+
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
142
|
+
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
143
|
+
const record = await store.get(key);
|
|
144
|
+
if (!record) return { kind: "miss" };
|
|
145
|
+
const { createdAt } = record;
|
|
146
|
+
if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
|
|
147
|
+
return { kind: "spent" };
|
|
148
|
+
}
|
|
149
|
+
if (store.claimReplay) {
|
|
150
|
+
const replayed = await store.claimReplay(key);
|
|
151
|
+
if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
|
|
152
|
+
}
|
|
153
|
+
return { kind: "replay", record };
|
|
154
|
+
}
|
|
30
155
|
var InMemorySeenTxStore = class {
|
|
31
156
|
map = /* @__PURE__ */ new Map();
|
|
157
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
32
158
|
order = [];
|
|
33
159
|
constructor(maxEntriesOrOptions = 1e4) {
|
|
34
160
|
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
@@ -36,7 +162,8 @@ var InMemorySeenTxStore = class {
|
|
|
36
162
|
if (options.warn !== false) {
|
|
37
163
|
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
38
164
|
if (isServerless) {
|
|
39
|
-
|
|
165
|
+
resolveLogger(options.logger)(
|
|
166
|
+
"warn",
|
|
40
167
|
"[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."
|
|
41
168
|
);
|
|
42
169
|
}
|
|
@@ -51,21 +178,43 @@ var InMemorySeenTxStore = class {
|
|
|
51
178
|
this.order.push(key);
|
|
52
179
|
if (this.order.length > this.maxEntries) {
|
|
53
180
|
const evicted = this.order.shift();
|
|
54
|
-
if (evicted !== void 0)
|
|
181
|
+
if (evicted !== void 0) {
|
|
182
|
+
this.map.delete(evicted);
|
|
183
|
+
this.replayCounts.delete(evicted);
|
|
184
|
+
}
|
|
55
185
|
}
|
|
56
186
|
}
|
|
57
187
|
this.map.set(key, record);
|
|
58
188
|
}
|
|
189
|
+
claimReplay(key) {
|
|
190
|
+
if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
|
|
191
|
+
const next = (this.replayCounts.get(key) ?? 0) + 1;
|
|
192
|
+
this.replayCounts.set(key, next);
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
59
195
|
};
|
|
60
196
|
async function sha256Hex(input) {
|
|
61
197
|
const buf = new TextEncoder().encode(input);
|
|
62
198
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
63
199
|
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
64
200
|
}
|
|
65
|
-
async function paymentIdempotencyKey(getHeader) {
|
|
201
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
66
202
|
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
67
203
|
if (!material) return null;
|
|
68
|
-
return sha256Hex(
|
|
204
|
+
return sha256Hex(
|
|
205
|
+
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/provider/settleResult.ts
|
|
210
|
+
function readSettleResult(result) {
|
|
211
|
+
const settle = result;
|
|
212
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
213
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
214
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
215
|
+
return failed(settle.errorReason);
|
|
216
|
+
}
|
|
217
|
+
return { ok: true, txHash: settle.transaction };
|
|
69
218
|
}
|
|
70
219
|
|
|
71
220
|
// src/provider/x402Handler.ts
|
|
@@ -78,7 +227,8 @@ function createX402Handler(opts) {
|
|
|
78
227
|
const caip2 = CAIP2[opts.network];
|
|
79
228
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
80
229
|
const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
81
|
-
const
|
|
230
|
+
const logger = resolveLogger(opts.logger);
|
|
231
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
82
232
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
83
233
|
const localFacilitator = new facilitator.ExactStellarScheme([signer], {
|
|
84
234
|
areFeesSponsored: true
|
|
@@ -99,22 +249,29 @@ function createX402Handler(opts) {
|
|
|
99
249
|
}
|
|
100
250
|
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
101
251
|
const payTo = resolvePayee(opts.manifest, "x402");
|
|
102
|
-
const requirements = {
|
|
103
|
-
scheme: "exact",
|
|
104
|
-
network: caip2,
|
|
105
|
-
asset: opts.assetContract,
|
|
106
|
-
amount: amountInBaseUnits,
|
|
107
|
-
payTo,
|
|
108
|
-
maxTimeoutSeconds: 60,
|
|
109
|
-
extra: {
|
|
110
|
-
areFeesSponsored: true,
|
|
111
|
-
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
252
|
return async (req, res, next) => {
|
|
115
253
|
try {
|
|
116
|
-
const
|
|
117
|
-
|
|
254
|
+
const endpoint = req.path || req.originalUrl;
|
|
255
|
+
const assetContract = resolveAssetContract(
|
|
256
|
+
opts.manifest,
|
|
257
|
+
"x402",
|
|
258
|
+
endpoint,
|
|
259
|
+
opts.assetContract
|
|
260
|
+
);
|
|
261
|
+
const requirements = {
|
|
262
|
+
scheme: "exact",
|
|
263
|
+
network: caip2,
|
|
264
|
+
asset: assetContract,
|
|
265
|
+
amount: amountInBaseUnits,
|
|
266
|
+
payTo,
|
|
267
|
+
maxTimeoutSeconds: 60,
|
|
268
|
+
extra: {
|
|
269
|
+
areFeesSponsored: true,
|
|
270
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const respondPaymentRequired = async (error, reason) => {
|
|
274
|
+
const body = { error, ...reason ? { reason } : {} };
|
|
118
275
|
if (ozServer) {
|
|
119
276
|
const resourceInfo = {
|
|
120
277
|
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
@@ -124,7 +281,7 @@ function createX402Handler(opts) {
|
|
|
124
281
|
[requirements],
|
|
125
282
|
resourceInfo
|
|
126
283
|
);
|
|
127
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(
|
|
284
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
128
285
|
} else {
|
|
129
286
|
const x402Response = {
|
|
130
287
|
x402Version: 2,
|
|
@@ -134,19 +291,35 @@ function createX402Handler(opts) {
|
|
|
134
291
|
},
|
|
135
292
|
accepts: [requirements]
|
|
136
293
|
};
|
|
137
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(
|
|
294
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
|
|
138
295
|
}
|
|
296
|
+
};
|
|
297
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
298
|
+
if (!paymentHeader) {
|
|
299
|
+
await respondPaymentRequired("Payment Required");
|
|
139
300
|
return;
|
|
140
301
|
}
|
|
141
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
302
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
303
|
+
(name) => {
|
|
304
|
+
const v = req.headers[name.toLowerCase()];
|
|
305
|
+
return Array.isArray(v) ? v[0] : v;
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
method: req.method,
|
|
309
|
+
path: req.originalUrl.split("?")[0],
|
|
310
|
+
amount: requirements.amount,
|
|
311
|
+
payTo: requirements.payTo
|
|
312
|
+
}
|
|
313
|
+
);
|
|
145
314
|
if (idempotencyKey) {
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
315
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
316
|
+
if (replayCheck.kind === "spent") {
|
|
317
|
+
res.status(402).json({ error: "Payment already used" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (replayCheck.kind === "replay") {
|
|
321
|
+
if (replayCheck.record.headers) {
|
|
322
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
150
323
|
res.setHeader(k, val);
|
|
151
324
|
}
|
|
152
325
|
}
|
|
@@ -165,15 +338,18 @@ function createX402Handler(opts) {
|
|
|
165
338
|
}
|
|
166
339
|
if (ozServer) {
|
|
167
340
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
http.encodePaymentResponseHeader(
|
|
173
|
-
settleResult
|
|
174
|
-
)
|
|
175
|
-
);
|
|
341
|
+
const outcome = readSettleResult(settleResult);
|
|
342
|
+
if (!outcome.ok) {
|
|
343
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
344
|
+
return;
|
|
176
345
|
}
|
|
346
|
+
txHash = outcome.txHash;
|
|
347
|
+
res.setHeader(
|
|
348
|
+
"X-Payment-Response",
|
|
349
|
+
http.encodePaymentResponseHeader(
|
|
350
|
+
settleResult
|
|
351
|
+
)
|
|
352
|
+
);
|
|
177
353
|
} else {
|
|
178
354
|
const verifyResult = await localFacilitator.verify(
|
|
179
355
|
payload,
|
|
@@ -190,15 +366,18 @@ function createX402Handler(opts) {
|
|
|
190
366
|
payload,
|
|
191
367
|
requirements
|
|
192
368
|
);
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
http.encodePaymentResponseHeader(
|
|
198
|
-
settleResult
|
|
199
|
-
)
|
|
200
|
-
);
|
|
369
|
+
const outcome = readSettleResult(settleResult);
|
|
370
|
+
if (!outcome.ok) {
|
|
371
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
372
|
+
return;
|
|
201
373
|
}
|
|
374
|
+
txHash = outcome.txHash;
|
|
375
|
+
res.setHeader(
|
|
376
|
+
"X-Payment-Response",
|
|
377
|
+
http.encodePaymentResponseHeader(
|
|
378
|
+
settleResult
|
|
379
|
+
)
|
|
380
|
+
);
|
|
202
381
|
}
|
|
203
382
|
if (idempotencyKey) {
|
|
204
383
|
const headers = {};
|
|
@@ -206,17 +385,21 @@ function createX402Handler(opts) {
|
|
|
206
385
|
if (typeof paymentResponse === "string") {
|
|
207
386
|
headers["X-Payment-Response"] = paymentResponse;
|
|
208
387
|
}
|
|
209
|
-
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
388
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
210
389
|
}
|
|
211
390
|
if (txHash && opts.onSettled) {
|
|
212
391
|
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
213
|
-
|
|
392
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
214
393
|
opts.onCallbackError?.(err, "onSettled");
|
|
215
394
|
});
|
|
216
395
|
}
|
|
217
396
|
next();
|
|
218
397
|
} catch (err) {
|
|
219
|
-
|
|
398
|
+
if (err instanceof RouteDockManifestError) {
|
|
399
|
+
next(err);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
220
403
|
res.status(500).json({ error: "Payment settlement failed" });
|
|
221
404
|
}
|
|
222
405
|
};
|
|
@@ -229,20 +412,37 @@ function createMppChargeHandler(opts) {
|
|
|
229
412
|
const networkId = MPP_NETWORK[opts.network];
|
|
230
413
|
const amountHumanReadable = opts.amount;
|
|
231
414
|
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
415
|
+
const logger = resolveLogger(opts.logger);
|
|
416
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
417
|
+
const mppxInstances = /* @__PURE__ */ new Map();
|
|
418
|
+
function getMppx(contract) {
|
|
419
|
+
let instance = mppxInstances.get(contract);
|
|
420
|
+
if (!instance) {
|
|
421
|
+
instance = server$2.Mppx.create({
|
|
422
|
+
secretKey: opts.payeeSecretKey,
|
|
423
|
+
methods: [
|
|
424
|
+
server$4.stellar.charge({
|
|
425
|
+
recipient,
|
|
426
|
+
currency: contract,
|
|
427
|
+
network: networkId,
|
|
428
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
429
|
+
})
|
|
430
|
+
]
|
|
431
|
+
});
|
|
432
|
+
mppxInstances.set(contract, instance);
|
|
433
|
+
}
|
|
434
|
+
return instance;
|
|
435
|
+
}
|
|
244
436
|
return async (req, res, next) => {
|
|
245
437
|
try {
|
|
438
|
+
const endpoint = req.path || req.originalUrl;
|
|
439
|
+
const assetContract = resolveAssetContract(
|
|
440
|
+
opts.manifest,
|
|
441
|
+
"mpp-charge",
|
|
442
|
+
endpoint,
|
|
443
|
+
opts.assetContract
|
|
444
|
+
);
|
|
445
|
+
const mppx = getMppx(assetContract);
|
|
246
446
|
let payerAddress = null;
|
|
247
447
|
try {
|
|
248
448
|
const authHeader = req.headers["authorization"];
|
|
@@ -258,15 +458,27 @@ function createMppChargeHandler(opts) {
|
|
|
258
458
|
}
|
|
259
459
|
} catch {
|
|
260
460
|
}
|
|
261
|
-
const idempotencyKey = await paymentIdempotencyKey(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
461
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
462
|
+
(name) => {
|
|
463
|
+
const v = req.headers[name.toLowerCase()];
|
|
464
|
+
return Array.isArray(v) ? v[0] : v;
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
method: req.method,
|
|
468
|
+
path: req.originalUrl.split("?")[0],
|
|
469
|
+
amount: amountHumanReadable,
|
|
470
|
+
payTo: recipient
|
|
471
|
+
}
|
|
472
|
+
);
|
|
265
473
|
if (idempotencyKey) {
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
474
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
475
|
+
if (replayCheck.kind === "spent") {
|
|
476
|
+
res.status(402).json({ error: "Payment already used" });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (replayCheck.kind === "replay") {
|
|
480
|
+
if (replayCheck.record.headers) {
|
|
481
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
270
482
|
res.setHeader(k, val);
|
|
271
483
|
}
|
|
272
484
|
}
|
|
@@ -278,7 +490,7 @@ function createMppChargeHandler(opts) {
|
|
|
278
490
|
const handler = mppx["stellar/charge"];
|
|
279
491
|
const result = await handler({
|
|
280
492
|
amount: amountHumanReadable,
|
|
281
|
-
currency:
|
|
493
|
+
currency: assetContract,
|
|
282
494
|
recipient,
|
|
283
495
|
description: opts.manifest.name
|
|
284
496
|
})(fetchReq);
|
|
@@ -309,12 +521,13 @@ function createMppChargeHandler(opts) {
|
|
|
309
521
|
if (idempotencyKey) {
|
|
310
522
|
await seenTxStore.set(idempotencyKey, {
|
|
311
523
|
txHash: reference ?? null,
|
|
312
|
-
headers: receiptHeaders
|
|
524
|
+
headers: receiptHeaders,
|
|
525
|
+
createdAt: Date.now()
|
|
313
526
|
});
|
|
314
527
|
}
|
|
315
528
|
if (reference && opts.onSettled) {
|
|
316
529
|
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
317
|
-
|
|
530
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
318
531
|
opts.onCallbackError?.(err, "onSettled");
|
|
319
532
|
});
|
|
320
533
|
}
|
|
@@ -415,8 +628,9 @@ function createMppSessionHandler(opts) {
|
|
|
415
628
|
const networkId = MPP_NETWORK2[opts.network];
|
|
416
629
|
const rateHuman = opts.rate;
|
|
417
630
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
631
|
+
const logger = resolveLogger(opts.logger);
|
|
418
632
|
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
419
|
-
const innerStore = server$
|
|
633
|
+
const innerStore = server$3.Store.memory();
|
|
420
634
|
let record = null;
|
|
421
635
|
let voucherCount = 0;
|
|
422
636
|
let sessionOpened = false;
|
|
@@ -466,7 +680,7 @@ function createMppSessionHandler(opts) {
|
|
|
466
680
|
reason
|
|
467
681
|
});
|
|
468
682
|
} catch (err) {
|
|
469
|
-
|
|
683
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
470
684
|
}
|
|
471
685
|
}
|
|
472
686
|
}
|
|
@@ -513,7 +727,7 @@ function createMppSessionHandler(opts) {
|
|
|
513
727
|
sessionOpened = true;
|
|
514
728
|
if (opts.onSessionOpen) {
|
|
515
729
|
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
516
|
-
|
|
730
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
517
731
|
opts.onCallbackError?.(err, "onSessionOpen");
|
|
518
732
|
});
|
|
519
733
|
}
|
|
@@ -521,7 +735,7 @@ function createMppSessionHandler(opts) {
|
|
|
521
735
|
if (opts.onVoucher) {
|
|
522
736
|
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
523
737
|
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
524
|
-
|
|
738
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
525
739
|
opts.onCallbackError?.(err, "onVoucher");
|
|
526
740
|
});
|
|
527
741
|
}
|
|
@@ -531,7 +745,7 @@ function createMppSessionHandler(opts) {
|
|
|
531
745
|
methods: [
|
|
532
746
|
withTypedChannelErrors(
|
|
533
747
|
onVerifiedCredential(
|
|
534
|
-
server$
|
|
748
|
+
server$3.stellar.channel({
|
|
535
749
|
channel: opts.channelFactory,
|
|
536
750
|
commitmentKey: opts.commitmentPublicKey,
|
|
537
751
|
network: networkId,
|
|
@@ -562,7 +776,7 @@ function createMppSessionHandler(opts) {
|
|
|
562
776
|
}
|
|
563
777
|
if (closeAmount > 0n && closeSig) {
|
|
564
778
|
const closePayer = record?.payer ?? null;
|
|
565
|
-
const closeTxHash = await server$
|
|
779
|
+
const closeTxHash = await server$3.close({
|
|
566
780
|
channel: opts.channelFactory,
|
|
567
781
|
amount: closeAmount,
|
|
568
782
|
signature: Buffer.from(closeSig, "hex"),
|
|
@@ -574,7 +788,7 @@ function createMppSessionHandler(opts) {
|
|
|
574
788
|
if (opts.onSettled) {
|
|
575
789
|
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
576
790
|
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
577
|
-
|
|
791
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
578
792
|
opts.onCallbackError?.(err, "onSettled");
|
|
579
793
|
});
|
|
580
794
|
}
|
|
@@ -585,7 +799,7 @@ function createMppSessionHandler(opts) {
|
|
|
585
799
|
payeeKeypair.publicKey()
|
|
586
800
|
);
|
|
587
801
|
if (!settlementAddresses) {
|
|
588
|
-
|
|
802
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
589
803
|
} else {
|
|
590
804
|
try {
|
|
591
805
|
const { Networks } = await import('@stellar/stellar-sdk');
|
|
@@ -608,7 +822,7 @@ function createMppSessionHandler(opts) {
|
|
|
608
822
|
preparedTx.sign(payeeKeypair);
|
|
609
823
|
await server.sendTransaction(preparedTx);
|
|
610
824
|
} catch (recordErr) {
|
|
611
|
-
|
|
825
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
612
826
|
}
|
|
613
827
|
}
|
|
614
828
|
}
|
|
@@ -801,12 +1015,13 @@ function routedockFastify(opts) {
|
|
|
801
1015
|
payeeSecretKey: opts.payeeSecretKey,
|
|
802
1016
|
network: opts.network,
|
|
803
1017
|
amount: x402Price,
|
|
804
|
-
assetContract: opts.assetContract,
|
|
1018
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
805
1019
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
806
1020
|
manifest: signedManifest,
|
|
807
1021
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
808
1022
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
809
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
1023
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1024
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
810
1025
|
})
|
|
811
1026
|
);
|
|
812
1027
|
}
|
|
@@ -820,11 +1035,12 @@ function routedockFastify(opts) {
|
|
|
820
1035
|
payeeSecretKey: opts.payeeSecretKey,
|
|
821
1036
|
network: opts.network,
|
|
822
1037
|
amount: chargePrice,
|
|
823
|
-
assetContract: opts.assetContract,
|
|
1038
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
824
1039
|
manifest: signedManifest,
|
|
825
1040
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
826
1041
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
827
|
-
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
1042
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1043
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
828
1044
|
})
|
|
829
1045
|
);
|
|
830
1046
|
}
|
|
@@ -842,7 +1058,7 @@ function routedockFastify(opts) {
|
|
|
842
1058
|
network: opts.network,
|
|
843
1059
|
channelFactory: sessionPricing.channelFactory,
|
|
844
1060
|
rate: sessionPricing.rate,
|
|
845
|
-
assetContract: opts.assetContract,
|
|
1061
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
846
1062
|
manifest: signedManifest,
|
|
847
1063
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
848
1064
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
@@ -850,7 +1066,8 @@ function routedockFastify(opts) {
|
|
|
850
1066
|
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
851
1067
|
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
852
1068
|
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
853
|
-
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
|
|
1069
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
1070
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
854
1071
|
})
|
|
855
1072
|
);
|
|
856
1073
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FastifyPluginAsync } from 'fastify';
|
|
2
|
-
import { P as PaymentMode,
|
|
3
|
-
import { O as OrphanedSessionInfo, S as SeenTxStore } from '../MppSessionHandler-
|
|
2
|
+
import { P as PaymentMode, h as RouteDockManifest, g as RouteDockLogger } from '../logger-D8_Q2ytp.cjs';
|
|
3
|
+
import { O as OrphanedSessionInfo, S as SeenTxStore } from '../MppSessionHandler-1EL8QBAn.cjs';
|
|
4
4
|
import 'mppx';
|
|
5
5
|
import 'express';
|
|
6
6
|
|
|
@@ -14,8 +14,8 @@ interface RouteDockFastifyOptions {
|
|
|
14
14
|
channelFactory: string;
|
|
15
15
|
};
|
|
16
16
|
};
|
|
17
|
-
asset
|
|
18
|
-
assetContract
|
|
17
|
+
asset?: string;
|
|
18
|
+
assetContract?: string;
|
|
19
19
|
payee: string;
|
|
20
20
|
network: 'testnet' | 'mainnet';
|
|
21
21
|
payeeSecretKey: string;
|
|
@@ -29,6 +29,11 @@ interface RouteDockFastifyOptions {
|
|
|
29
29
|
onOrphaned?: (channelId: string, info: OrphanedSessionInfo) => Promise<void>;
|
|
30
30
|
idleTimeoutMs?: number;
|
|
31
31
|
seenTxStore?: SeenTxStore;
|
|
32
|
+
/**
|
|
33
|
+
* Structured log sink for every adapter diagnostic (settlement errors,
|
|
34
|
+
* callback failures, orphaned sessions). Defaults to a console-backed logger.
|
|
35
|
+
*/
|
|
36
|
+
logger?: RouteDockLogger;
|
|
32
37
|
}
|
|
33
38
|
declare function routedockFastify(opts: RouteDockFastifyOptions): FastifyPluginAsync;
|
|
34
39
|
|