@routedock/routedock 0.1.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.
@@ -0,0 +1,414 @@
1
+ import { Keypair } from '@stellar/stellar-sdk';
2
+ import { ExactStellarScheme } from '@x402/stellar/exact/facilitator';
3
+ import { createEd25519Signer } from '@x402/stellar';
4
+ import { encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymentResponseHeader } from '@x402/core/http';
5
+ import { stellar } from '@stellar/mpp/charge/server';
6
+ import { Mppx, Request } from 'mppx/server';
7
+ import { Store, stellar as stellar$1, close } from '@stellar/mpp/channel/server';
8
+
9
+ // src/types.ts
10
+ var RouteDockError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "RouteDockError";
14
+ }
15
+ };
16
+ var RouteDockManifestError = class extends RouteDockError {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "RouteDockManifestError";
20
+ }
21
+ };
22
+ var RouteDockNoSupportedModeError = class extends RouteDockError {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "RouteDockNoSupportedModeError";
26
+ }
27
+ };
28
+ var RouteDockSessionError = class extends RouteDockError {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "RouteDockSessionError";
32
+ }
33
+ };
34
+ var RouteDockPolicyRejectedError = class extends RouteDockError {
35
+ reason;
36
+ constructor(reason) {
37
+ super(`Payment rejected by policy: ${reason}`);
38
+ this.name = "RouteDockPolicyRejectedError";
39
+ this.reason = reason;
40
+ }
41
+ };
42
+ var CAIP2 = {
43
+ testnet: "stellar:testnet",
44
+ mainnet: "stellar:pubnet"
45
+ };
46
+ function createX402Handler(opts) {
47
+ const caip2 = CAIP2[opts.network];
48
+ const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
49
+ const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
50
+ const facilitatorScheme = new ExactStellarScheme([signer], {
51
+ areFeesSponsored: true
52
+ });
53
+ const amountInBaseUnits = String(Math.round(parseFloat(opts.amount) * 1e7));
54
+ const requirements = {
55
+ scheme: "exact",
56
+ network: caip2,
57
+ asset: opts.assetContract,
58
+ amount: amountInBaseUnits,
59
+ payTo: opts.manifest.payee,
60
+ maxTimeoutSeconds: 60,
61
+ extra: {
62
+ areFeesSponsored: true,
63
+ facilitatorAddresses: [payeeKeypair.publicKey()]
64
+ }
65
+ };
66
+ return async (req, res, next) => {
67
+ try {
68
+ const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
69
+ if (!paymentHeader) {
70
+ const x402Response = {
71
+ x402Version: 2,
72
+ resource: {
73
+ url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
74
+ description: opts.manifest.name
75
+ },
76
+ accepts: [requirements]
77
+ };
78
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
79
+ return;
80
+ }
81
+ const payload = decodePaymentSignatureHeader(paymentHeader);
82
+ const verifyResult = await facilitatorScheme.verify(
83
+ payload,
84
+ requirements
85
+ );
86
+ if (!verifyResult.isValid) {
87
+ res.status(401).json({
88
+ error: "Payment verification failed",
89
+ reason: verifyResult.invalidReason
90
+ });
91
+ return;
92
+ }
93
+ const settleResult = await facilitatorScheme.settle(
94
+ payload,
95
+ requirements
96
+ );
97
+ const txHash = settleResult.transaction ?? null;
98
+ if (txHash && opts.onSettled) {
99
+ await opts.onSettled(txHash, opts.amount, "x402");
100
+ }
101
+ if (settleResult) {
102
+ res.setHeader(
103
+ "X-Payment-Response",
104
+ encodePaymentResponseHeader(
105
+ settleResult
106
+ )
107
+ );
108
+ }
109
+ next();
110
+ } catch (err) {
111
+ console.error("[x402] Settlement error:", err);
112
+ res.status(500).json({ error: "Payment settlement failed" });
113
+ }
114
+ };
115
+ }
116
+ var MPP_NETWORK = {
117
+ testnet: "stellar:testnet",
118
+ mainnet: "stellar:pubnet"
119
+ };
120
+ function createMppChargeHandler(opts) {
121
+ const networkId = MPP_NETWORK[opts.network];
122
+ const amountHumanReadable = opts.amount;
123
+ const mppx = Mppx.create({
124
+ secretKey: opts.payeeSecretKey,
125
+ methods: [
126
+ stellar.charge({
127
+ recipient: opts.manifest.payee,
128
+ currency: opts.assetContract,
129
+ network: networkId,
130
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
131
+ })
132
+ ]
133
+ });
134
+ return async (req, res, next) => {
135
+ try {
136
+ const fetchReq = Request.fromNodeListener(req, res);
137
+ const handler = mppx["stellar/charge"];
138
+ const result = await handler({
139
+ amount: amountHumanReadable,
140
+ currency: opts.assetContract,
141
+ recipient: opts.manifest.payee,
142
+ description: opts.manifest.name
143
+ })(fetchReq);
144
+ if (result.status === 402) {
145
+ const challenge = result.challenge;
146
+ res.status(402);
147
+ challenge.headers.forEach((v, k) => res.setHeader(k, v));
148
+ res.send(await challenge.text());
149
+ return;
150
+ }
151
+ const receipt = result.withReceipt(new Response(""));
152
+ receipt.headers.forEach((v, k) => res.setHeader(k, v));
153
+ const receiptHeader = receipt.headers.get("payment-receipt");
154
+ if (receiptHeader && opts.onSettled) {
155
+ try {
156
+ const parsed = JSON.parse(
157
+ Buffer.from(receiptHeader, "base64").toString("utf8")
158
+ );
159
+ if (parsed.reference) {
160
+ await opts.onSettled(parsed.reference, opts.amount, "mpp-charge");
161
+ }
162
+ } catch {
163
+ }
164
+ }
165
+ next();
166
+ } catch (err) {
167
+ next(err);
168
+ }
169
+ };
170
+ }
171
+ var MPP_NETWORK2 = {
172
+ testnet: "stellar:testnet",
173
+ mainnet: "stellar:pubnet"
174
+ };
175
+ function createMppSessionHandler(opts) {
176
+ const networkId = MPP_NETWORK2[opts.network];
177
+ const rateHuman = opts.rate;
178
+ const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
179
+ const cumulativeKey = `stellar:channel:cumulative:${opts.channelContract}`;
180
+ const innerStore = Store.memory();
181
+ let lastCumulativeAmount = 0n;
182
+ const wrappedStore = {
183
+ async get(key) {
184
+ return innerStore.get(key);
185
+ },
186
+ async put(key, value) {
187
+ await innerStore.put(key, value);
188
+ if (key === cumulativeKey && value && typeof value === "object" && "amount" in value) {
189
+ lastCumulativeAmount = BigInt(value.amount);
190
+ }
191
+ },
192
+ async delete(key) {
193
+ return innerStore.delete(key);
194
+ }
195
+ };
196
+ const mppx = Mppx.create({
197
+ secretKey: opts.payeeSecretKey,
198
+ methods: [
199
+ stellar$1.channel({
200
+ channel: opts.channelContract,
201
+ commitmentKey: opts.commitmentPublicKey,
202
+ network: networkId,
203
+ store: wrappedStore,
204
+ sourceAccount: payeeKeypair.publicKey(),
205
+ feePayer: { envelopeSigner: payeeKeypair }
206
+ })
207
+ ]
208
+ });
209
+ let lastSignatureHex = "";
210
+ return async (req, res, next) => {
211
+ try {
212
+ if (req.method === "DELETE") {
213
+ const body = req.body;
214
+ const closeAmount = body?.amount ? BigInt(body.amount) : lastCumulativeAmount;
215
+ const closeSig = body?.signature ?? lastSignatureHex;
216
+ if (closeAmount > 0n && closeSig) {
217
+ const closeTxHash = await close({
218
+ channel: opts.channelContract,
219
+ amount: closeAmount,
220
+ signature: Buffer.from(closeSig, "hex"),
221
+ feePayer: { envelopeSigner: payeeKeypair },
222
+ network: networkId
223
+ });
224
+ if (opts.onSettled) {
225
+ const totalPaid = (Number(lastCumulativeAmount) / 1e7).toFixed(7);
226
+ await opts.onSettled(closeTxHash, totalPaid, "mpp-session");
227
+ }
228
+ res.json({ closeTxHash });
229
+ } else {
230
+ res.json({ closeTxHash: null, message: "no vouchers received" });
231
+ }
232
+ return;
233
+ }
234
+ const authHeader = req.headers["authorization"];
235
+ if (typeof authHeader === "string" && authHeader.startsWith("Payment ")) {
236
+ try {
237
+ const credB64 = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
238
+ if (credB64) {
239
+ const credJson = Buffer.from(credB64.split("=").slice(1).join("=").replace(/^"|"$/g, ""), "base64").toString("utf8");
240
+ const cred = JSON.parse(credJson);
241
+ if (cred.payload?.signature) {
242
+ lastSignatureHex = cred.payload.signature;
243
+ }
244
+ }
245
+ } catch {
246
+ }
247
+ }
248
+ const fetchReq = Request.fromNodeListener(req, res);
249
+ const result = await mppx.channel({
250
+ amount: rateHuman,
251
+ description: opts.manifest.name
252
+ })(fetchReq);
253
+ if (result.status === 402) {
254
+ const challenge = result.challenge;
255
+ res.status(402);
256
+ challenge.headers.forEach((v, k) => res.setHeader(k, v));
257
+ const body = await challenge.text();
258
+ res.send(body);
259
+ return;
260
+ }
261
+ next();
262
+ } catch (err) {
263
+ next(err);
264
+ }
265
+ };
266
+ }
267
+
268
+ // src/provider/routedockMiddleware.ts
269
+ function routedock(opts) {
270
+ const handlers = [];
271
+ if (opts.modes.includes("x402")) {
272
+ const x402Price = opts.pricing.x402;
273
+ if (x402Price) {
274
+ handlers.push(
275
+ createX402Handler({
276
+ payeeSecretKey: opts.payeeSecretKey,
277
+ network: opts.network,
278
+ amount: x402Price,
279
+ assetContract: opts.assetContract,
280
+ ...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
281
+ manifest: opts.manifest,
282
+ ...opts.onSettled ? { onSettled: opts.onSettled } : {}
283
+ })
284
+ );
285
+ }
286
+ }
287
+ if (opts.modes.includes("mpp-charge")) {
288
+ const chargePrice = opts.pricing["mpp-charge"];
289
+ if (chargePrice) {
290
+ handlers.push(
291
+ createMppChargeHandler({
292
+ payeeSecretKey: opts.payeeSecretKey,
293
+ network: opts.network,
294
+ amount: chargePrice,
295
+ assetContract: opts.assetContract,
296
+ manifest: opts.manifest,
297
+ ...opts.onSettled ? { onSettled: opts.onSettled } : {}
298
+ })
299
+ );
300
+ }
301
+ }
302
+ if (opts.modes.includes("mpp-session")) {
303
+ const sessionPricing = opts.pricing["mpp-session"];
304
+ if (sessionPricing) {
305
+ if (!opts.commitmentPublicKey) {
306
+ throw new Error("routedock: mpp-session mode requires commitmentPublicKey");
307
+ }
308
+ handlers.push(
309
+ createMppSessionHandler({
310
+ payeeSecretKey: opts.payeeSecretKey,
311
+ network: opts.network,
312
+ channelContract: sessionPricing.channelContract,
313
+ rate: sessionPricing.rate,
314
+ assetContract: opts.assetContract,
315
+ manifest: opts.manifest,
316
+ commitmentPublicKey: opts.commitmentPublicKey,
317
+ ...opts.onSettled ? { onSettled: opts.onSettled } : {}
318
+ })
319
+ );
320
+ }
321
+ }
322
+ return (req, res, next) => {
323
+ if (req.path === "/.well-known/routedock.json") {
324
+ res.json(opts.manifest);
325
+ return;
326
+ }
327
+ if (handlers.length === 0) {
328
+ next();
329
+ return;
330
+ }
331
+ const hasX402Header = !!(req.headers["payment-signature"] || req.headers["x-payment"]);
332
+ const prefersX402 = req.headers["x-preferred-mode"] === "x402";
333
+ let handler;
334
+ if (hasX402Header || prefersX402) {
335
+ handler = handlers[0];
336
+ } else {
337
+ handler = handlers[handlers.length - 1];
338
+ }
339
+ if (!handler) {
340
+ next();
341
+ return;
342
+ }
343
+ void Promise.resolve().then(() => new Promise((resolve, reject) => {
344
+ handler(req, res, (handlerErr) => {
345
+ if (handlerErr != null) reject(handlerErr);
346
+ else resolve();
347
+ });
348
+ })).then(() => next()).catch((err) => next(err));
349
+ };
350
+ }
351
+
352
+ // src/store/SessionStore.ts
353
+ var SupabaseSessionStore = class {
354
+ constructor(supabase) {
355
+ this.supabase = supabase;
356
+ }
357
+ supabase;
358
+ async get(channelId) {
359
+ const { data, error } = await this.supabase.from("sessions").select("*").eq("channel_id", channelId).maybeSingle();
360
+ if (error) throw new RouteDockSessionError(`SessionStore.get failed: ${error.message}`);
361
+ if (!data) return null;
362
+ return {
363
+ channel_id: data.channel_id,
364
+ payee: data.payee,
365
+ payer: data.payer,
366
+ cumulative_amount: String(data.cumulative_amount),
367
+ last_signature: data.last_signature,
368
+ status: data.status,
369
+ opened_at: data.opened_at,
370
+ updated_at: data.updated_at,
371
+ settlement_tx_hash: data.settlement_tx_hash
372
+ };
373
+ }
374
+ async upsert(channelId, state) {
375
+ const existing = await this.get(channelId);
376
+ if (existing) {
377
+ const prev = parseFloat(existing.cumulative_amount);
378
+ const next = parseFloat(state.cumulative_amount);
379
+ if (next <= prev) {
380
+ throw new RouteDockSessionError(
381
+ `cumulative_amount must be strictly increasing: ${next} <= ${prev}`
382
+ );
383
+ }
384
+ }
385
+ const { error } = await this.supabase.from("sessions").upsert(
386
+ {
387
+ channel_id: state.channel_id,
388
+ payee: state.payee,
389
+ payer: state.payer,
390
+ cumulative_amount: state.cumulative_amount,
391
+ last_signature: state.last_signature,
392
+ status: state.status,
393
+ opened_at: state.opened_at,
394
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
395
+ settlement_tx_hash: state.settlement_tx_hash
396
+ },
397
+ { onConflict: "channel_id" }
398
+ );
399
+ if (error) {
400
+ if (error.message.includes("strictly increasing")) {
401
+ throw new RouteDockSessionError(
402
+ `cumulative_amount must be strictly increasing (DB): ${error.message}`
403
+ );
404
+ }
405
+ throw new RouteDockSessionError(`SessionStore.upsert failed: ${error.message}`);
406
+ }
407
+ }
408
+ async close(channelId) {
409
+ const { error } = await this.supabase.from("sessions").update({ status: "closed", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("channel_id", channelId);
410
+ if (error) throw new RouteDockSessionError(`SessionStore.close failed: ${error.message}`);
411
+ }
412
+ };
413
+
414
+ export { RouteDockError, RouteDockManifestError, RouteDockNoSupportedModeError, RouteDockPolicyRejectedError, RouteDockSessionError, SupabaseSessionStore, createMppChargeHandler, createMppSessionHandler, createX402Handler, routedock };
@@ -0,0 +1,49 @@
1
+ import { RequestHandler } from 'express';
2
+ import { P as PaymentMode, c as RouteDockManifest } from './SessionStore-CCVO0PHM.cjs';
3
+
4
+ interface RouteDockMiddlewareOptions {
5
+ modes: PaymentMode[];
6
+ pricing: {
7
+ x402?: string;
8
+ 'mpp-charge'?: string;
9
+ 'mpp-session'?: {
10
+ rate: string;
11
+ channelContract: string;
12
+ };
13
+ };
14
+ asset: string;
15
+ /** Stellar Asset Contract address for the payment asset */
16
+ assetContract: string;
17
+ payee: string;
18
+ network: 'testnet' | 'mainnet';
19
+ /** Private key (S...) of the server/payee account */
20
+ payeeSecretKey: string;
21
+ /** Required for x402 mode */
22
+ facilitatorApiKey?: string;
23
+ /** Required for mpp-session mode — ed25519 public key (G...) for verifying commitments */
24
+ commitmentPublicKey?: string;
25
+ /** Full RouteDock manifest — served at /.well-known/routedock.json */
26
+ manifest: RouteDockManifest;
27
+ /** Called after each successful on-chain settlement */
28
+ onSettled?: (txHash: string, amount: string, mode: string) => Promise<void>;
29
+ }
30
+ /**
31
+ * Express middleware factory for RouteDock provider endpoints.
32
+ *
33
+ * Automatically serves /.well-known/routedock.json and enforces payment
34
+ * verification for all three modes (x402, mpp-charge, mpp-session).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { routedock } from '@routedock/sdk/provider'
39
+ *
40
+ * app.use('/price', routedock({
41
+ * modes: ['x402', 'mpp-charge'],
42
+ * pricing: { x402: '0.001', 'mpp-charge': '0.0008' },
43
+ * ...
44
+ * }))
45
+ * ```
46
+ */
47
+ declare function routedock(opts: RouteDockMiddlewareOptions): RequestHandler;
48
+
49
+ export { type RouteDockMiddlewareOptions as R, routedock as r };
@@ -0,0 +1,49 @@
1
+ import { RequestHandler } from 'express';
2
+ import { P as PaymentMode, c as RouteDockManifest } from './SessionStore-CCVO0PHM.js';
3
+
4
+ interface RouteDockMiddlewareOptions {
5
+ modes: PaymentMode[];
6
+ pricing: {
7
+ x402?: string;
8
+ 'mpp-charge'?: string;
9
+ 'mpp-session'?: {
10
+ rate: string;
11
+ channelContract: string;
12
+ };
13
+ };
14
+ asset: string;
15
+ /** Stellar Asset Contract address for the payment asset */
16
+ assetContract: string;
17
+ payee: string;
18
+ network: 'testnet' | 'mainnet';
19
+ /** Private key (S...) of the server/payee account */
20
+ payeeSecretKey: string;
21
+ /** Required for x402 mode */
22
+ facilitatorApiKey?: string;
23
+ /** Required for mpp-session mode — ed25519 public key (G...) for verifying commitments */
24
+ commitmentPublicKey?: string;
25
+ /** Full RouteDock manifest — served at /.well-known/routedock.json */
26
+ manifest: RouteDockManifest;
27
+ /** Called after each successful on-chain settlement */
28
+ onSettled?: (txHash: string, amount: string, mode: string) => Promise<void>;
29
+ }
30
+ /**
31
+ * Express middleware factory for RouteDock provider endpoints.
32
+ *
33
+ * Automatically serves /.well-known/routedock.json and enforces payment
34
+ * verification for all three modes (x402, mpp-charge, mpp-session).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { routedock } from '@routedock/sdk/provider'
39
+ *
40
+ * app.use('/price', routedock({
41
+ * modes: ['x402', 'mpp-charge'],
42
+ * pricing: { x402: '0.001', 'mpp-charge': '0.0008' },
43
+ * ...
44
+ * }))
45
+ * ```
46
+ */
47
+ declare function routedock(opts: RouteDockMiddlewareOptions): RequestHandler;
48
+
49
+ export { type RouteDockMiddlewareOptions as R, routedock as r };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@routedock/routedock",
3
+ "version": "0.1.0",
4
+ "description": "Unified payment execution layer for autonomous agents on Stellar",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/winsznx/routedock.git",
9
+ "directory": "packages/sdk"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.cjs",
13
+ "module": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "typesVersions": {
16
+ "*": {
17
+ "client": ["dist/client.d.ts"],
18
+ "provider": ["dist/provider.d.ts"]
19
+ }
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs"
26
+ },
27
+ "./client": {
28
+ "types": "./dist/client.d.ts",
29
+ "import": "./dist/client.js",
30
+ "require": "./dist/client.cjs"
31
+ },
32
+ "./provider": {
33
+ "types": "./dist/provider.d.ts",
34
+ "import": "./dist/provider.js",
35
+ "require": "./dist/provider.cjs"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "dev": "tsup --watch",
41
+ "typecheck": "tsc --noEmit"
42
+ },
43
+ "dependencies": {
44
+ "@stellar/stellar-sdk": "^14.6.1",
45
+ "@supabase/supabase-js": "^2.47.0",
46
+ "@x402/core": "^2.9.0",
47
+ "@x402/stellar": "^2.9.0",
48
+ "@stellar/mpp": "^0.4.0",
49
+ "ajv": "^8.17.1",
50
+ "mppx": "^0.5.7"
51
+ },
52
+ "peerDependencies": {
53
+ "express": "^4.18.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/express": "^4.17.21",
57
+ "@types/node": "^20.0.0",
58
+ "tsup": "^8.0.0",
59
+ "typescript": "^5.4.5"
60
+ }
61
+ }
@@ -0,0 +1,104 @@
1
+ import Ajv from 'ajv'
2
+ import type { RouteDockManifest, PaymentMode } from '../types.js'
3
+ import { RouteDockManifestError, RouteDockNoSupportedModeError } from '../types.js'
4
+ import schema from '../schemas/routedock.schema.json' assert { type: 'json' }
5
+
6
+ const ajv = new Ajv()
7
+ const validateManifest = ajv.compile(schema)
8
+
9
+ interface CacheEntry {
10
+ manifest: RouteDockManifest
11
+ fetchedAt: number
12
+ }
13
+
14
+ const CACHE_TTL_MS = 60_000
15
+
16
+ /** In-memory manifest cache keyed by base URL. */
17
+ const manifestCache = new Map<string, CacheEntry>()
18
+
19
+ export interface ModeSelectOptions {
20
+ /** Force mpp-session if the provider supports it */
21
+ sustained?: boolean
22
+ session?: boolean
23
+ /**
24
+ * Override mode selection and use this specific mode.
25
+ * Throws RouteDockNoSupportedModeError if the provider does not support it.
26
+ */
27
+ forceMode?: PaymentMode
28
+ }
29
+
30
+ /** Fetch, validate, and cache a RouteDock manifest from `baseUrl`. */
31
+ export async function fetchManifest(baseUrl: string): Promise<RouteDockManifest> {
32
+ const cached = manifestCache.get(baseUrl)
33
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
34
+ return cached.manifest
35
+ }
36
+
37
+ const url = baseUrl.replace(/\/$/, '') + '/.well-known/routedock.json'
38
+ let raw: unknown
39
+ try {
40
+ const resp = await fetch(url)
41
+ if (!resp.ok) {
42
+ throw new RouteDockManifestError(`Manifest fetch failed: HTTP ${resp.status} from ${url}`)
43
+ }
44
+ raw = await resp.json()
45
+ } catch (err) {
46
+ if (err instanceof RouteDockManifestError) throw err
47
+ throw new RouteDockManifestError(`Manifest fetch error: ${String(err)}`)
48
+ }
49
+
50
+ if (!validateManifest(raw)) {
51
+ const msgs = ajv.errorsText(validateManifest.errors)
52
+ throw new RouteDockManifestError(`Invalid manifest at ${url}: ${msgs}`)
53
+ }
54
+
55
+ const manifest = raw as unknown as RouteDockManifest
56
+ manifestCache.set(baseUrl, { manifest, fetchedAt: Date.now() })
57
+ return manifest
58
+ }
59
+
60
+ /**
61
+ * Deterministic mode selection per Section 6.3 of ROUTEDOCK_MASTER.md:
62
+ * 1. If { sustained | session } AND manifest supports mpp-session → mpp-session
63
+ * 2. Else if manifest supports mpp-charge AND network is Stellar → mpp-charge
64
+ * 3. Else if manifest supports x402 → x402
65
+ * 4. Else throw RouteDockNoSupportedModeError
66
+ */
67
+ export function selectMode(
68
+ manifest: RouteDockManifest,
69
+ options: ModeSelectOptions = {},
70
+ ): PaymentMode {
71
+ const modes = manifest.modes
72
+
73
+ if (options.forceMode) {
74
+ if (!modes.includes(options.forceMode)) {
75
+ throw new RouteDockNoSupportedModeError(
76
+ `Provider does not support forced mode: ${options.forceMode} (available: ${modes.join(', ')})`,
77
+ )
78
+ }
79
+ console.log(`[RouteDock] ${manifest.name} → ${options.forceMode} (forced)`)
80
+ return options.forceMode
81
+ }
82
+
83
+ if ((options.sustained || options.session) && modes.includes('mpp-session')) {
84
+ const mode: PaymentMode = 'mpp-session'
85
+ console.log(`[RouteDock] ${manifest.name} → ${mode}`)
86
+ return mode
87
+ }
88
+
89
+ if (modes.includes('mpp-charge')) {
90
+ const mode: PaymentMode = 'mpp-charge'
91
+ console.log(`[RouteDock] ${manifest.name} → ${mode}`)
92
+ return mode
93
+ }
94
+
95
+ if (modes.includes('x402')) {
96
+ const mode: PaymentMode = 'x402'
97
+ console.log(`[RouteDock] ${manifest.name} → ${mode}`)
98
+ return mode
99
+ }
100
+
101
+ throw new RouteDockNoSupportedModeError(
102
+ `No supported payment mode found in manifest (modes: ${modes.join(', ')})`,
103
+ )
104
+ }