@routedock/routedock 0.1.2 → 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/LICENSE +21 -0
- package/README.md +532 -4
- package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
- package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
- package/dist/ProviderRegistry-CA_TdREG.d.ts +142 -0
- package/dist/ProviderRegistry-D-0rqK0N.d.cts +142 -0
- package/dist/RouteDockClient-Bh86bVly.d.ts +469 -0
- package/dist/RouteDockClient-go9aopfF.d.cts +469 -0
- package/dist/SessionReconciler-Bw-PoBVl.d.ts +134 -0
- package/dist/SessionReconciler-Cz2nuOlJ.d.cts +134 -0
- package/dist/SessionStore-B4-Hi4H8.d.cts +143 -0
- package/dist/SessionStore-BaYiegP0.d.ts +143 -0
- package/dist/client.cjs +2746 -217
- package/dist/client.d.cts +51 -81
- package/dist/client.d.ts +51 -81
- package/dist/client.js +2690 -218
- package/dist/index.cjs +3634 -420
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +3569 -419
- package/dist/logger-D8_Q2ytp.d.cts +467 -0
- package/dist/logger-D8_Q2ytp.d.ts +467 -0
- package/dist/provider/fastify.cjs +1099 -0
- package/dist/provider/fastify.d.cts +40 -0
- package/dist/provider/fastify.d.ts +40 -0
- package/dist/provider/fastify.js +1097 -0
- package/dist/provider/hono.cjs +1152 -0
- package/dist/provider/hono.d.cts +101 -0
- package/dist/provider/hono.d.ts +101 -0
- package/dist/provider/hono.js +1142 -0
- package/dist/provider.cjs +1105 -147
- package/dist/provider.d.cts +38 -27
- package/dist/provider.d.ts +38 -27
- package/dist/provider.js +1079 -148
- package/dist/react.cjs +2931 -0
- package/dist/react.d.cts +107 -0
- package/dist/react.d.ts +107 -0
- package/dist/react.js +2924 -0
- package/dist/routedockMiddleware-57gUaEMf.d.ts +64 -0
- package/dist/routedockMiddleware-DlyLrxp7.d.cts +64 -0
- package/dist/schema.cjs +404 -0
- package/dist/schema.d.cts +3 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +399 -0
- package/dist/sign-AKH67sft.d.cts +53 -0
- package/dist/sign-BEq-OsSq.d.ts +53 -0
- package/dist/testing.cjs +59 -0
- package/dist/testing.d.cts +114 -0
- package/dist/testing.d.ts +114 -0
- package/dist/testing.js +56 -0
- package/package.json +143 -23
- package/PUBLISH_CHECKLIST.md +0 -70
- package/dist/SessionStore-CCVO0PHM.d.cts +0 -152
- package/dist/SessionStore-CCVO0PHM.d.ts +0 -152
- package/dist/routedockMiddleware-B7WTB3LT.d.cts +0 -53
- package/dist/routedockMiddleware-DRgdrycG.d.ts +0 -53
- package/src/client/ModeRouter.ts +0 -104
- package/src/client/MppChargeClient.ts +0 -44
- package/src/client/MppSessionClient.ts +0 -144
- package/src/client/RouteDockClient.ts +0 -124
- package/src/client/index.ts +0 -8
- package/src/client/x402Client.ts +0 -84
- package/src/index.ts +0 -9
- package/src/provider/MppChargeHandler.ts +0 -90
- package/src/provider/MppSessionHandler.ts +0 -157
- package/src/provider/index.ts +0 -7
- package/src/provider/routedockMiddleware.ts +0 -154
- package/src/provider/x402Handler.ts +0 -164
- package/src/schemas/routedock.schema.json +0 -159
- package/src/store/SessionStore.ts +0 -90
- package/src/tests/smoke.test.ts +0 -228
- package/src/types.ts +0 -165
- package/tsconfig.json +0 -14
- package/tsup.config.ts +0 -15
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
import { Keypair, StrKey, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
|
|
2
|
+
import { ExactStellarScheme } from '@x402/stellar/exact/facilitator';
|
|
3
|
+
import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/server';
|
|
4
|
+
import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
|
|
5
|
+
import { createEd25519Signer } from '@x402/stellar';
|
|
6
|
+
import { encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymentResponseHeader } from '@x402/core/http';
|
|
7
|
+
import { usdcToStroops } from '@routedock/nulth-sdk';
|
|
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
|
+
import { Errors } from 'mppx';
|
|
12
|
+
import { createHash } from 'crypto';
|
|
13
|
+
|
|
14
|
+
// src/provider/x402Handler.ts
|
|
15
|
+
|
|
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
|
|
39
|
+
function resolvePayee(manifest, mode) {
|
|
40
|
+
const override = manifest.pricing[mode]?.payee;
|
|
41
|
+
return override ?? manifest.payee;
|
|
42
|
+
}
|
|
43
|
+
var usdcToUnits = usdcToStroops;
|
|
44
|
+
function extractPayerAddress(key) {
|
|
45
|
+
if (typeof key !== "string") return null;
|
|
46
|
+
return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
47
|
+
}
|
|
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
|
+
|
|
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
|
+
}
|
|
153
|
+
var InMemorySeenTxStore = class {
|
|
154
|
+
map = /* @__PURE__ */ new Map();
|
|
155
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
156
|
+
order = [];
|
|
157
|
+
constructor(maxEntriesOrOptions = 1e4) {
|
|
158
|
+
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
159
|
+
this.maxEntries = options.maxEntries ?? 1e4;
|
|
160
|
+
if (options.warn !== false) {
|
|
161
|
+
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
162
|
+
if (isServerless) {
|
|
163
|
+
resolveLogger(options.logger)(
|
|
164
|
+
"warn",
|
|
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."
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
maxEntries;
|
|
171
|
+
get(key) {
|
|
172
|
+
return this.map.get(key);
|
|
173
|
+
}
|
|
174
|
+
set(key, record) {
|
|
175
|
+
if (!this.map.has(key)) {
|
|
176
|
+
this.order.push(key);
|
|
177
|
+
if (this.order.length > this.maxEntries) {
|
|
178
|
+
const evicted = this.order.shift();
|
|
179
|
+
if (evicted !== void 0) {
|
|
180
|
+
this.map.delete(evicted);
|
|
181
|
+
this.replayCounts.delete(evicted);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
this.map.set(key, record);
|
|
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
|
+
}
|
|
193
|
+
};
|
|
194
|
+
async function sha256Hex(input) {
|
|
195
|
+
const buf = new TextEncoder().encode(input);
|
|
196
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
197
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
198
|
+
}
|
|
199
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
200
|
+
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
201
|
+
if (!material) return null;
|
|
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 };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/provider/x402Handler.ts
|
|
219
|
+
var CAIP2 = {
|
|
220
|
+
testnet: "stellar:testnet",
|
|
221
|
+
mainnet: "stellar:pubnet"
|
|
222
|
+
};
|
|
223
|
+
var OZ_FACILITATOR_URL = "https://channels.openzeppelin.com/x402";
|
|
224
|
+
function createX402Handler(opts) {
|
|
225
|
+
const caip2 = CAIP2[opts.network];
|
|
226
|
+
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
227
|
+
const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
228
|
+
const logger = resolveLogger(opts.logger);
|
|
229
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
230
|
+
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
231
|
+
const localFacilitator = new ExactStellarScheme([signer], {
|
|
232
|
+
areFeesSponsored: true
|
|
233
|
+
});
|
|
234
|
+
let ozServer = null;
|
|
235
|
+
if (useOzFacilitator) {
|
|
236
|
+
const apiKey = opts.facilitatorApiKey;
|
|
237
|
+
const facilitator = new HTTPFacilitatorClient({
|
|
238
|
+
url: OZ_FACILITATOR_URL,
|
|
239
|
+
createAuthHeaders: async () => ({
|
|
240
|
+
verify: { Authorization: `Bearer ${apiKey}` },
|
|
241
|
+
settle: { Authorization: `Bearer ${apiKey}` },
|
|
242
|
+
supported: { Authorization: `Bearer ${apiKey}` }
|
|
243
|
+
})
|
|
244
|
+
});
|
|
245
|
+
ozServer = new x402ResourceServer(facilitator);
|
|
246
|
+
ozServer.register(caip2, new ExactStellarScheme$1());
|
|
247
|
+
}
|
|
248
|
+
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
249
|
+
const payTo = resolvePayee(opts.manifest, "x402");
|
|
250
|
+
return async (req, res, next) => {
|
|
251
|
+
try {
|
|
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 } : {} };
|
|
273
|
+
if (ozServer) {
|
|
274
|
+
const resourceInfo = {
|
|
275
|
+
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
276
|
+
description: opts.manifest.name
|
|
277
|
+
};
|
|
278
|
+
const paymentRequired = await ozServer.createPaymentRequiredResponse(
|
|
279
|
+
[requirements],
|
|
280
|
+
resourceInfo
|
|
281
|
+
);
|
|
282
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
283
|
+
} else {
|
|
284
|
+
const x402Response = {
|
|
285
|
+
x402Version: 2,
|
|
286
|
+
resource: {
|
|
287
|
+
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
288
|
+
description: opts.manifest.name
|
|
289
|
+
},
|
|
290
|
+
accepts: [requirements]
|
|
291
|
+
};
|
|
292
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(body);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
296
|
+
if (!paymentHeader) {
|
|
297
|
+
await respondPaymentRequired("Payment Required");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
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
|
+
);
|
|
312
|
+
if (idempotencyKey) {
|
|
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)) {
|
|
321
|
+
res.setHeader(k, val);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
next();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const payload = decodePaymentSignatureHeader(paymentHeader);
|
|
329
|
+
let txHash = null;
|
|
330
|
+
let payerAddress = null;
|
|
331
|
+
try {
|
|
332
|
+
const creds = payload.authorization?.credentials;
|
|
333
|
+
const key = Array.isArray(creds) ? creds[0]?.publicKey : void 0;
|
|
334
|
+
payerAddress = extractPayerAddress(key);
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
if (ozServer) {
|
|
338
|
+
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
339
|
+
const outcome = readSettleResult(settleResult);
|
|
340
|
+
if (!outcome.ok) {
|
|
341
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
txHash = outcome.txHash;
|
|
345
|
+
res.setHeader(
|
|
346
|
+
"X-Payment-Response",
|
|
347
|
+
encodePaymentResponseHeader(
|
|
348
|
+
settleResult
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
const verifyResult = await localFacilitator.verify(
|
|
353
|
+
payload,
|
|
354
|
+
requirements
|
|
355
|
+
);
|
|
356
|
+
if (!verifyResult.isValid) {
|
|
357
|
+
res.status(401).json({
|
|
358
|
+
error: "Payment verification failed",
|
|
359
|
+
reason: verifyResult.invalidReason
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const settleResult = await localFacilitator.settle(
|
|
364
|
+
payload,
|
|
365
|
+
requirements
|
|
366
|
+
);
|
|
367
|
+
const outcome = readSettleResult(settleResult);
|
|
368
|
+
if (!outcome.ok) {
|
|
369
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
txHash = outcome.txHash;
|
|
373
|
+
res.setHeader(
|
|
374
|
+
"X-Payment-Response",
|
|
375
|
+
encodePaymentResponseHeader(
|
|
376
|
+
settleResult
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (idempotencyKey) {
|
|
381
|
+
const headers = {};
|
|
382
|
+
const paymentResponse = res.getHeader("X-Payment-Response");
|
|
383
|
+
if (typeof paymentResponse === "string") {
|
|
384
|
+
headers["X-Payment-Response"] = paymentResponse;
|
|
385
|
+
}
|
|
386
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
387
|
+
}
|
|
388
|
+
if (txHash && opts.onSettled) {
|
|
389
|
+
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
390
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
391
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
next();
|
|
395
|
+
} catch (err) {
|
|
396
|
+
if (err instanceof RouteDockManifestError) {
|
|
397
|
+
next(err);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
401
|
+
res.status(500).json({ error: "Payment settlement failed" });
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
var MPP_NETWORK = {
|
|
406
|
+
testnet: "stellar:testnet",
|
|
407
|
+
mainnet: "stellar:pubnet"
|
|
408
|
+
};
|
|
409
|
+
function createMppChargeHandler(opts) {
|
|
410
|
+
const networkId = MPP_NETWORK[opts.network];
|
|
411
|
+
const amountHumanReadable = opts.amount;
|
|
412
|
+
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
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
|
+
}
|
|
434
|
+
return async (req, res, next) => {
|
|
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);
|
|
444
|
+
let payerAddress = null;
|
|
445
|
+
try {
|
|
446
|
+
const authHeader = req.headers["authorization"];
|
|
447
|
+
if (typeof authHeader === "string" && authHeader.startsWith("Payment ")) {
|
|
448
|
+
const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
449
|
+
if (credPart) {
|
|
450
|
+
const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
|
|
451
|
+
const credJson = Buffer.from(b64, "base64").toString("utf8");
|
|
452
|
+
const cred = JSON.parse(credJson);
|
|
453
|
+
const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
|
|
454
|
+
payerAddress = extractPayerAddress(key);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
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
|
+
);
|
|
471
|
+
if (idempotencyKey) {
|
|
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)) {
|
|
480
|
+
res.setHeader(k, val);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
next();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const fetchReq = Request.fromNodeListener(req, res);
|
|
488
|
+
const handler = mppx["stellar/charge"];
|
|
489
|
+
const result = await handler({
|
|
490
|
+
amount: amountHumanReadable,
|
|
491
|
+
currency: assetContract,
|
|
492
|
+
recipient,
|
|
493
|
+
description: opts.manifest.name
|
|
494
|
+
})(fetchReq);
|
|
495
|
+
if (result.status === 402) {
|
|
496
|
+
const challenge = result.challenge;
|
|
497
|
+
res.status(402);
|
|
498
|
+
challenge.headers.forEach((v, k) => res.setHeader(k, v));
|
|
499
|
+
res.send(await challenge.text());
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const receipt = result.withReceipt(new Response(""));
|
|
503
|
+
const receiptHeaders = {};
|
|
504
|
+
receipt.headers.forEach((v, k) => {
|
|
505
|
+
res.setHeader(k, v);
|
|
506
|
+
receiptHeaders[k] = v;
|
|
507
|
+
});
|
|
508
|
+
const receiptHeader = receipt.headers.get("payment-receipt");
|
|
509
|
+
let reference;
|
|
510
|
+
if (receiptHeader) {
|
|
511
|
+
try {
|
|
512
|
+
const parsed = JSON.parse(
|
|
513
|
+
Buffer.from(receiptHeader, "base64").toString("utf8")
|
|
514
|
+
);
|
|
515
|
+
reference = parsed.reference;
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (idempotencyKey) {
|
|
520
|
+
await seenTxStore.set(idempotencyKey, {
|
|
521
|
+
txHash: reference ?? null,
|
|
522
|
+
headers: receiptHeaders,
|
|
523
|
+
createdAt: Date.now()
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (reference && opts.onSettled) {
|
|
527
|
+
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
528
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
529
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
next();
|
|
533
|
+
} catch (err) {
|
|
534
|
+
next(err);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function resolveVaultSettlementAddresses(payer, payee) {
|
|
539
|
+
if (!payer) return null;
|
|
540
|
+
return { payer, payee };
|
|
541
|
+
}
|
|
542
|
+
function buildSessionSettlementTransaction(source, vaultContractId, channelId, addresses, amount, voucherCount, networkPassphrase) {
|
|
543
|
+
const vault = new Contract(vaultContractId);
|
|
544
|
+
const op = vault.call(
|
|
545
|
+
"record_session_settlement",
|
|
546
|
+
nativeToScVal(channelId, { type: "address" }),
|
|
547
|
+
nativeToScVal(addresses.payer, { type: "address" }),
|
|
548
|
+
nativeToScVal(addresses.payee, { type: "address" }),
|
|
549
|
+
nativeToScVal(amount, { type: "i128" }),
|
|
550
|
+
nativeToScVal(voucherCount, { type: "u32" })
|
|
551
|
+
);
|
|
552
|
+
return new TransactionBuilder(source, {
|
|
553
|
+
fee: BASE_FEE,
|
|
554
|
+
networkPassphrase
|
|
555
|
+
}).addOperation(op).setTimeout(30).build();
|
|
556
|
+
}
|
|
557
|
+
function channelAuthorizer(envelopeSigner) {
|
|
558
|
+
return { envelopeSigner };
|
|
559
|
+
}
|
|
560
|
+
function formatMppError(error) {
|
|
561
|
+
if (error instanceof Error) return error.message;
|
|
562
|
+
if (typeof error === "string") return error;
|
|
563
|
+
try {
|
|
564
|
+
const serialized = JSON.stringify(error);
|
|
565
|
+
return serialized === void 0 ? String(error) : serialized;
|
|
566
|
+
} catch {
|
|
567
|
+
return String(error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function withTypedChannelErrors(method) {
|
|
571
|
+
return {
|
|
572
|
+
...method,
|
|
573
|
+
async verify(parameters) {
|
|
574
|
+
try {
|
|
575
|
+
return await method.verify(parameters);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
throw mapChannelError(error);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function onVerifiedCredential(method, commit) {
|
|
583
|
+
return {
|
|
584
|
+
...method,
|
|
585
|
+
async verify(parameters) {
|
|
586
|
+
const receipt = await method.verify(parameters);
|
|
587
|
+
await commit(parameters.credential);
|
|
588
|
+
return receipt;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function mapChannelError(error) {
|
|
593
|
+
const message = formatMppError(error);
|
|
594
|
+
const details = error && typeof error === "object" && "details" in error ? error.details : void 0;
|
|
595
|
+
const reason = details && Object.keys(details).length > 0 ? `${message} (${JSON.stringify(details)})` : message;
|
|
596
|
+
if (/signer mismatch|authori[sz]er|unauthori[sz]ed/i.test(message)) {
|
|
597
|
+
return new Errors.SignerMismatchError({ reason });
|
|
598
|
+
}
|
|
599
|
+
if (/already used|replay|challenge/i.test(message)) {
|
|
600
|
+
return new Errors.InvalidChallengeError({ reason });
|
|
601
|
+
}
|
|
602
|
+
if (/channel has been closed|channel is closed|finalized/i.test(message)) {
|
|
603
|
+
return new Errors.ChannelClosedError({ reason });
|
|
604
|
+
}
|
|
605
|
+
if (/exceeds channel balance|exceeds.*deposit/i.test(message)) {
|
|
606
|
+
return new Errors.AmountExceedsDepositError({ reason });
|
|
607
|
+
}
|
|
608
|
+
if (/channel not found|no channel/i.test(message)) {
|
|
609
|
+
return new Errors.ChannelNotFoundError({ reason });
|
|
610
|
+
}
|
|
611
|
+
if (/must be greater|does not cover|below.*minimum/i.test(message)) {
|
|
612
|
+
return new Errors.DeltaTooSmallError({ reason });
|
|
613
|
+
}
|
|
614
|
+
if (/signature/i.test(message)) {
|
|
615
|
+
return new Errors.InvalidSignatureError({ reason });
|
|
616
|
+
}
|
|
617
|
+
return new Errors.VerificationFailedError({ reason });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/provider/MppSessionHandler.ts
|
|
621
|
+
var MPP_NETWORK2 = {
|
|
622
|
+
testnet: "stellar:testnet",
|
|
623
|
+
mainnet: "stellar:pubnet"
|
|
624
|
+
};
|
|
625
|
+
function createMppSessionHandler(opts) {
|
|
626
|
+
const networkId = MPP_NETWORK2[opts.network];
|
|
627
|
+
const rateHuman = opts.rate;
|
|
628
|
+
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
629
|
+
const logger = resolveLogger(opts.logger);
|
|
630
|
+
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
631
|
+
const innerStore = Store.memory();
|
|
632
|
+
let record = null;
|
|
633
|
+
let voucherCount = 0;
|
|
634
|
+
let sessionOpened = false;
|
|
635
|
+
let idleTimer = null;
|
|
636
|
+
let settledCleanly = false;
|
|
637
|
+
let closeListenerArmed = false;
|
|
638
|
+
function clearIdleTimer() {
|
|
639
|
+
if (idleTimer) {
|
|
640
|
+
clearTimeout(idleTimer);
|
|
641
|
+
idleTimer = null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function armIdleTimer() {
|
|
645
|
+
if (!opts.idleTimeoutMs) return;
|
|
646
|
+
clearIdleTimer();
|
|
647
|
+
idleTimer = setTimeout(() => {
|
|
648
|
+
void flagOrphan("idle-timeout");
|
|
649
|
+
}, opts.idleTimeoutMs);
|
|
650
|
+
if (typeof idleTimer.unref === "function") idleTimer.unref();
|
|
651
|
+
}
|
|
652
|
+
async function loadPersistedRecord() {
|
|
653
|
+
if (record) return record;
|
|
654
|
+
try {
|
|
655
|
+
const stored = await innerStore.get(voucherRecordKey);
|
|
656
|
+
if (!stored || typeof stored.amount !== "string" || typeof stored.signature !== "string") {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
660
|
+
} catch {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
return record;
|
|
664
|
+
}
|
|
665
|
+
async function flagOrphan(reason) {
|
|
666
|
+
await loadPersistedRecord();
|
|
667
|
+
if (!sessionOpened || settledCleanly) return;
|
|
668
|
+
sessionOpened = false;
|
|
669
|
+
closeListenerArmed = false;
|
|
670
|
+
clearIdleTimer();
|
|
671
|
+
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
672
|
+
if (opts.onOrphaned) {
|
|
673
|
+
try {
|
|
674
|
+
await opts.onOrphaned(opts.channelFactory, {
|
|
675
|
+
cumulativeAmount,
|
|
676
|
+
lastSignature: record?.signature ?? "",
|
|
677
|
+
voucherCount,
|
|
678
|
+
reason
|
|
679
|
+
});
|
|
680
|
+
} catch (err) {
|
|
681
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const wrappedStore = {
|
|
686
|
+
async get(key) {
|
|
687
|
+
return innerStore.get(key);
|
|
688
|
+
},
|
|
689
|
+
async put(key, value) {
|
|
690
|
+
return innerStore.put(key, value);
|
|
691
|
+
},
|
|
692
|
+
async delete(key) {
|
|
693
|
+
return innerStore.delete(key);
|
|
694
|
+
},
|
|
695
|
+
async update(key, fn) {
|
|
696
|
+
const storeWithUpdate = innerStore;
|
|
697
|
+
if (typeof storeWithUpdate.update === "function") {
|
|
698
|
+
return storeWithUpdate.update(key, fn);
|
|
699
|
+
}
|
|
700
|
+
throw new Error("Store does not support atomic update operations");
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
async function commit(credential) {
|
|
704
|
+
await loadPersistedRecord();
|
|
705
|
+
const payload = credential.payload;
|
|
706
|
+
if (typeof payload?.amount !== "string" || typeof payload.signature !== "string") return;
|
|
707
|
+
let amount;
|
|
708
|
+
try {
|
|
709
|
+
amount = BigInt(payload.amount);
|
|
710
|
+
} catch {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (record && amount < record.amount) return;
|
|
714
|
+
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
715
|
+
record = { amount, signature: payload.signature, payer };
|
|
716
|
+
await innerStore.put(voucherRecordKey, {
|
|
717
|
+
amount: record.amount.toString(),
|
|
718
|
+
signature: record.signature,
|
|
719
|
+
payer: record.payer
|
|
720
|
+
});
|
|
721
|
+
voucherCount++;
|
|
722
|
+
settledCleanly = false;
|
|
723
|
+
armIdleTimer();
|
|
724
|
+
if (!sessionOpened) {
|
|
725
|
+
sessionOpened = true;
|
|
726
|
+
if (opts.onSessionOpen) {
|
|
727
|
+
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
728
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
729
|
+
opts.onCallbackError?.(err, "onSessionOpen");
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (opts.onVoucher) {
|
|
734
|
+
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
735
|
+
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
736
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
737
|
+
opts.onCallbackError?.(err, "onVoucher");
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const mppx = Mppx.create({
|
|
742
|
+
secretKey: opts.payeeSecretKey,
|
|
743
|
+
methods: [
|
|
744
|
+
withTypedChannelErrors(
|
|
745
|
+
onVerifiedCredential(
|
|
746
|
+
stellar.channel({
|
|
747
|
+
channel: opts.channelFactory,
|
|
748
|
+
commitmentKey: opts.commitmentPublicKey,
|
|
749
|
+
network: networkId,
|
|
750
|
+
store: wrappedStore,
|
|
751
|
+
sourceAccount: payeeKeypair.publicKey(),
|
|
752
|
+
feePayer: channelAuthorizer(payeeKeypair)
|
|
753
|
+
}),
|
|
754
|
+
commit
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
]
|
|
758
|
+
});
|
|
759
|
+
return async (req, res, next) => {
|
|
760
|
+
try {
|
|
761
|
+
if (req.method === "DELETE") {
|
|
762
|
+
await loadPersistedRecord();
|
|
763
|
+
const body = req.body;
|
|
764
|
+
const bodyAmount = body?.amount ? BigInt(body.amount) : 0n;
|
|
765
|
+
const recordAmount = record?.amount ?? 0n;
|
|
766
|
+
let closeAmount;
|
|
767
|
+
let closeSig;
|
|
768
|
+
if (bodyAmount > recordAmount) {
|
|
769
|
+
closeAmount = bodyAmount;
|
|
770
|
+
closeSig = body?.signature ?? record?.signature ?? "";
|
|
771
|
+
} else {
|
|
772
|
+
closeAmount = recordAmount;
|
|
773
|
+
closeSig = record?.signature ?? "";
|
|
774
|
+
}
|
|
775
|
+
if (closeAmount > 0n && closeSig) {
|
|
776
|
+
const closePayer = record?.payer ?? null;
|
|
777
|
+
const closeTxHash = await close({
|
|
778
|
+
channel: opts.channelFactory,
|
|
779
|
+
amount: closeAmount,
|
|
780
|
+
signature: Buffer.from(closeSig, "hex"),
|
|
781
|
+
feePayer: channelAuthorizer(payeeKeypair),
|
|
782
|
+
network: networkId
|
|
783
|
+
});
|
|
784
|
+
settledCleanly = true;
|
|
785
|
+
clearIdleTimer();
|
|
786
|
+
if (opts.onSettled) {
|
|
787
|
+
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
788
|
+
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
789
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
790
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
const vaultContract = process.env.AGENT_VAULT_CONTRACT;
|
|
794
|
+
if (vaultContract) {
|
|
795
|
+
const settlementAddresses = resolveVaultSettlementAddresses(
|
|
796
|
+
closePayer,
|
|
797
|
+
payeeKeypair.publicKey()
|
|
798
|
+
);
|
|
799
|
+
if (!settlementAddresses) {
|
|
800
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
801
|
+
} else {
|
|
802
|
+
try {
|
|
803
|
+
const { Networks } = await import('@stellar/stellar-sdk');
|
|
804
|
+
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
805
|
+
const networkPassphrase = opts.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
806
|
+
const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
|
|
807
|
+
const server = new Server(rpcUrl);
|
|
808
|
+
const payeePublicKey = payeeKeypair.publicKey();
|
|
809
|
+
const sourceAccount = await server.getAccount(payeePublicKey);
|
|
810
|
+
const tx = buildSessionSettlementTransaction(
|
|
811
|
+
sourceAccount,
|
|
812
|
+
vaultContract,
|
|
813
|
+
opts.channelFactory,
|
|
814
|
+
settlementAddresses,
|
|
815
|
+
closeAmount,
|
|
816
|
+
voucherCount,
|
|
817
|
+
networkPassphrase
|
|
818
|
+
);
|
|
819
|
+
const preparedTx = await server.prepareTransaction(tx);
|
|
820
|
+
preparedTx.sign(payeeKeypair);
|
|
821
|
+
await server.sendTransaction(preparedTx);
|
|
822
|
+
} catch (recordErr) {
|
|
823
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
res.json({ closeTxHash });
|
|
828
|
+
} else {
|
|
829
|
+
res.json({ closeTxHash: null, message: "no vouchers received" });
|
|
830
|
+
}
|
|
831
|
+
sessionOpened = false;
|
|
832
|
+
voucherCount = 0;
|
|
833
|
+
record = null;
|
|
834
|
+
await innerStore.delete(voucherRecordKey);
|
|
835
|
+
closeListenerArmed = false;
|
|
836
|
+
clearIdleTimer();
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const fetchReq = Request.fromNodeListener(req, res);
|
|
840
|
+
const result = await mppx.channel({
|
|
841
|
+
amount: rateHuman,
|
|
842
|
+
description: opts.manifest.name
|
|
843
|
+
})(fetchReq);
|
|
844
|
+
if (result.status === 402) {
|
|
845
|
+
const challenge = result.challenge;
|
|
846
|
+
res.status(402);
|
|
847
|
+
challenge.headers.forEach((v, k) => res.setHeader(k, v));
|
|
848
|
+
const body = await challenge.text();
|
|
849
|
+
res.send(body);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (!closeListenerArmed) {
|
|
853
|
+
closeListenerArmed = true;
|
|
854
|
+
req.on("close", () => {
|
|
855
|
+
void flagOrphan("connection-closed");
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
next();
|
|
859
|
+
} catch (err) {
|
|
860
|
+
next(err);
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
865
|
+
function canonicalize(value) {
|
|
866
|
+
if (Array.isArray(value)) {
|
|
867
|
+
return value.map(canonicalize);
|
|
868
|
+
}
|
|
869
|
+
if (value !== null && typeof value === "object") {
|
|
870
|
+
return Object.fromEntries(
|
|
871
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
function manifestDigest(manifest) {
|
|
877
|
+
const { signature: _omit, ...rest } = manifest;
|
|
878
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
879
|
+
return createHash("sha256").update(canonical, "utf8").digest();
|
|
880
|
+
}
|
|
881
|
+
function signManifest(manifest, payeeSecretKey) {
|
|
882
|
+
const keypair = Keypair.fromSecret(payeeSecretKey);
|
|
883
|
+
const versionedManifest = {
|
|
884
|
+
...manifest,
|
|
885
|
+
signature_version: MANIFEST_SIGNATURE_VERSION
|
|
886
|
+
};
|
|
887
|
+
const digest = manifestDigest(versionedManifest);
|
|
888
|
+
const sig = keypair.sign(digest);
|
|
889
|
+
return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/provider/fastify.ts
|
|
893
|
+
function buildExpressShims(rawReq, rawRes, fastifyRequest, fastifyReply) {
|
|
894
|
+
const req = rawReq;
|
|
895
|
+
const urlStr = rawReq.url ?? "/";
|
|
896
|
+
const parsedUrl = new URL(urlStr, "http://placeholder");
|
|
897
|
+
const reqGet = (name) => {
|
|
898
|
+
const val = rawReq.headers[name.toLowerCase()];
|
|
899
|
+
return Array.isArray(val) ? val[0] : val;
|
|
900
|
+
};
|
|
901
|
+
if (!("get" in req && typeof req.get === "function")) {
|
|
902
|
+
Object.defineProperty(req, "get", { value: reqGet, configurable: true });
|
|
903
|
+
Object.defineProperty(req, "header", { value: reqGet, configurable: true });
|
|
904
|
+
}
|
|
905
|
+
if (!("protocol" in req)) {
|
|
906
|
+
Object.defineProperty(req, "protocol", {
|
|
907
|
+
get: () => fastifyRequest.protocol ?? "http",
|
|
908
|
+
configurable: true
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
if (!("originalUrl" in req)) {
|
|
912
|
+
Object.defineProperty(req, "originalUrl", { value: urlStr, configurable: true });
|
|
913
|
+
}
|
|
914
|
+
if (!("path" in req)) {
|
|
915
|
+
Object.defineProperty(req, "path", { value: parsedUrl.pathname, configurable: true });
|
|
916
|
+
}
|
|
917
|
+
if (!("query" in req)) {
|
|
918
|
+
const q = {};
|
|
919
|
+
parsedUrl.searchParams.forEach((v, k) => {
|
|
920
|
+
q[k] = v;
|
|
921
|
+
});
|
|
922
|
+
Object.defineProperty(req, "query", { value: q, configurable: true });
|
|
923
|
+
}
|
|
924
|
+
const res = rawRes;
|
|
925
|
+
const nativeSetHeader = rawRes.setHeader.bind(rawRes);
|
|
926
|
+
const nativeGetHeader = rawRes.getHeader.bind(rawRes);
|
|
927
|
+
const nativeEnd = rawRes.end.bind(rawRes);
|
|
928
|
+
const extraHeaders = {};
|
|
929
|
+
const resSend = (body) => {
|
|
930
|
+
if (!rawRes.headersSent) {
|
|
931
|
+
for (const [k, v] of Object.entries(extraHeaders)) {
|
|
932
|
+
nativeSetHeader(k, v);
|
|
933
|
+
}
|
|
934
|
+
if (typeof body === "string") {
|
|
935
|
+
nativeEnd(body);
|
|
936
|
+
} else if (body !== void 0 && body !== null) {
|
|
937
|
+
if (!nativeGetHeader("content-type")) {
|
|
938
|
+
nativeSetHeader("Content-Type", "application/json");
|
|
939
|
+
}
|
|
940
|
+
nativeEnd(JSON.stringify(body));
|
|
941
|
+
} else {
|
|
942
|
+
nativeEnd();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return res;
|
|
946
|
+
};
|
|
947
|
+
const resJson = (data) => {
|
|
948
|
+
if (!nativeGetHeader("content-type")) {
|
|
949
|
+
nativeSetHeader("Content-Type", "application/json");
|
|
950
|
+
}
|
|
951
|
+
return resSend(data);
|
|
952
|
+
};
|
|
953
|
+
const resStatus = (code) => {
|
|
954
|
+
rawRes.statusCode = code;
|
|
955
|
+
return res;
|
|
956
|
+
};
|
|
957
|
+
const resSet = (field, val) => {
|
|
958
|
+
if (typeof field === "object") {
|
|
959
|
+
for (const [k, v] of Object.entries(field)) nativeSetHeader(k, v);
|
|
960
|
+
} else if (val !== void 0) {
|
|
961
|
+
extraHeaders[field] = val;
|
|
962
|
+
nativeSetHeader(field, val);
|
|
963
|
+
}
|
|
964
|
+
return res;
|
|
965
|
+
};
|
|
966
|
+
const resSetHeader = (name, value) => {
|
|
967
|
+
nativeSetHeader(name, value);
|
|
968
|
+
return res;
|
|
969
|
+
};
|
|
970
|
+
if (!("status" in res && typeof res.status === "function")) {
|
|
971
|
+
Object.defineProperty(res, "status", { value: resStatus, configurable: true });
|
|
972
|
+
Object.defineProperty(res, "json", { value: resJson, configurable: true });
|
|
973
|
+
Object.defineProperty(res, "send", { value: resSend, configurable: true });
|
|
974
|
+
Object.defineProperty(res, "set", { value: resSet, configurable: true });
|
|
975
|
+
Object.defineProperty(res, "setHeader", { value: resSetHeader, configurable: true });
|
|
976
|
+
Object.defineProperty(res, "end", {
|
|
977
|
+
value: (data) => {
|
|
978
|
+
nativeEnd(data);
|
|
979
|
+
},
|
|
980
|
+
configurable: true
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
return { req, res };
|
|
984
|
+
}
|
|
985
|
+
function runExpressHandler(handler, fastifyRequest, fastifyReply) {
|
|
986
|
+
const rawRes = fastifyReply.raw;
|
|
987
|
+
const { req, res } = buildExpressShims(
|
|
988
|
+
fastifyRequest.raw,
|
|
989
|
+
rawRes,
|
|
990
|
+
fastifyRequest);
|
|
991
|
+
return new Promise((resolve, reject) => {
|
|
992
|
+
let settled = false;
|
|
993
|
+
const finish = (err) => {
|
|
994
|
+
if (settled) return;
|
|
995
|
+
settled = true;
|
|
996
|
+
if (err != null) return reject(err);
|
|
997
|
+
if (rawRes.writableEnded || rawRes.headersSent) fastifyReply.hijack();
|
|
998
|
+
resolve();
|
|
999
|
+
};
|
|
1000
|
+
Promise.resolve(handler(req, res, (err) => finish(err))).then(() => finish(), finish);
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
var MPP_MODES = ["mpp-session", "mpp-charge"];
|
|
1004
|
+
function routedockFastify(opts) {
|
|
1005
|
+
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
1006
|
+
const handlerMap = /* @__PURE__ */ new Map();
|
|
1007
|
+
if (opts.modes.includes("x402")) {
|
|
1008
|
+
const x402Price = opts.pricing.x402;
|
|
1009
|
+
if (x402Price) {
|
|
1010
|
+
handlerMap.set(
|
|
1011
|
+
"x402",
|
|
1012
|
+
createX402Handler({
|
|
1013
|
+
payeeSecretKey: opts.payeeSecretKey,
|
|
1014
|
+
network: opts.network,
|
|
1015
|
+
amount: x402Price,
|
|
1016
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
1017
|
+
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
1018
|
+
manifest: signedManifest,
|
|
1019
|
+
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
1020
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
1021
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1022
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
1023
|
+
})
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (opts.modes.includes("mpp-charge")) {
|
|
1028
|
+
const chargePrice = opts.pricing["mpp-charge"];
|
|
1029
|
+
if (chargePrice) {
|
|
1030
|
+
handlerMap.set(
|
|
1031
|
+
"mpp-charge",
|
|
1032
|
+
createMppChargeHandler({
|
|
1033
|
+
payeeSecretKey: opts.payeeSecretKey,
|
|
1034
|
+
network: opts.network,
|
|
1035
|
+
amount: chargePrice,
|
|
1036
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
1037
|
+
manifest: signedManifest,
|
|
1038
|
+
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
1039
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
1040
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1041
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
1042
|
+
})
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (opts.modes.includes("mpp-session")) {
|
|
1047
|
+
const sessionPricing = opts.pricing["mpp-session"];
|
|
1048
|
+
if (sessionPricing) {
|
|
1049
|
+
if (!opts.commitmentPublicKey) {
|
|
1050
|
+
throw new Error("routedockFastify: mpp-session mode requires commitmentPublicKey");
|
|
1051
|
+
}
|
|
1052
|
+
handlerMap.set(
|
|
1053
|
+
"mpp-session",
|
|
1054
|
+
createMppSessionHandler({
|
|
1055
|
+
payeeSecretKey: opts.payeeSecretKey,
|
|
1056
|
+
network: opts.network,
|
|
1057
|
+
channelFactory: sessionPricing.channelFactory,
|
|
1058
|
+
rate: sessionPricing.rate,
|
|
1059
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
1060
|
+
manifest: signedManifest,
|
|
1061
|
+
commitmentPublicKey: opts.commitmentPublicKey,
|
|
1062
|
+
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
1063
|
+
...opts.onSessionOpen ? { onSessionOpen: opts.onSessionOpen } : {},
|
|
1064
|
+
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
1065
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
1066
|
+
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
1067
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
1068
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
1069
|
+
})
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const defaultMode = MPP_MODES.find((mode) => handlerMap.has(mode)) ?? (handlerMap.has("x402") ? "x402" : void 0);
|
|
1074
|
+
const plugin = async (fastify) => {
|
|
1075
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
1076
|
+
if (request.url === "/.well-known/routedock.json") {
|
|
1077
|
+
reply.hijack();
|
|
1078
|
+
const body = JSON.stringify(signedManifest);
|
|
1079
|
+
reply.raw.setHeader("Content-Type", "application/json");
|
|
1080
|
+
reply.raw.setHeader("Content-Length", Buffer.byteLength(body));
|
|
1081
|
+
reply.raw.end(body);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (handlerMap.size === 0) return;
|
|
1085
|
+
const prefersX402 = !!(request.headers["payment-signature"] || request.headers["x-payment"]) || request.headers["x-preferred-mode"] === "x402";
|
|
1086
|
+
const selectedMode = prefersX402 ? handlerMap.has("x402") ? "x402" : defaultMode : defaultMode;
|
|
1087
|
+
if (!selectedMode) return;
|
|
1088
|
+
const handler = handlerMap.get(selectedMode);
|
|
1089
|
+
if (!handler) return;
|
|
1090
|
+
await runExpressHandler(handler, request, reply);
|
|
1091
|
+
});
|
|
1092
|
+
};
|
|
1093
|
+
Object.defineProperty(plugin, /* @__PURE__ */ Symbol.for("skip-override"), { value: true });
|
|
1094
|
+
return plugin;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
export { routedockFastify };
|