@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.
- package/PUBLISH_CHECKLIST.md +70 -0
- package/README.md +88 -0
- package/dist/SessionStore-CCVO0PHM.d.cts +152 -0
- package/dist/SessionStore-CCVO0PHM.d.ts +152 -0
- package/dist/client.cjs +618 -0
- package/dist/client.d.cts +98 -0
- package/dist/client.d.ts +98 -0
- package/dist/client.js +601 -0
- package/dist/index.cjs +932 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +914 -0
- package/dist/provider.cjs +425 -0
- package/dist/provider.d.cts +44 -0
- package/dist/provider.d.ts +44 -0
- package/dist/provider.js +414 -0
- package/dist/routedockMiddleware-Beqan6QS.d.cts +49 -0
- package/dist/routedockMiddleware-CqnyHjhO.d.ts +49 -0
- package/package.json +61 -0
- package/src/client/ModeRouter.ts +104 -0
- package/src/client/MppChargeClient.ts +44 -0
- package/src/client/MppSessionClient.ts +144 -0
- package/src/client/RouteDockClient.ts +124 -0
- package/src/client/index.ts +8 -0
- package/src/client/x402Client.ts +84 -0
- package/src/index.ts +9 -0
- package/src/provider/MppChargeHandler.ts +90 -0
- package/src/provider/MppSessionHandler.ts +136 -0
- package/src/provider/index.ts +7 -0
- package/src/provider/routedockMiddleware.ts +148 -0
- package/src/provider/x402Handler.ts +112 -0
- package/src/schemas/routedock.schema.json +159 -0
- package/src/store/SessionStore.ts +90 -0
- package/src/tests/smoke.test.ts +228 -0
- package/src/types.ts +165 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +15 -0
package/dist/provider.js
ADDED
|
@@ -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
|
+
}
|