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