@routedock/routedock 0.1.1 → 0.2.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 +281 -19
- package/dist/MppSessionHandler-B4TyD7Xg.d.cts +121 -0
- package/dist/MppSessionHandler-CLB5SzgN.d.ts +121 -0
- package/dist/ProviderRegistry-BJ9oJLVz.d.cts +142 -0
- package/dist/ProviderRegistry-C50nyhaF.d.ts +142 -0
- package/dist/RouteDockClient-B1roCa8h.d.cts +455 -0
- package/dist/RouteDockClient-BbkqjhMG.d.ts +455 -0
- package/dist/SessionReconciler-Dscw43F3.d.ts +125 -0
- package/dist/SessionReconciler-bi0z2qtN.d.cts +125 -0
- package/dist/SessionStore-BpK8vv0L.d.cts +111 -0
- package/dist/SessionStore-DvkZv8-E.d.ts +111 -0
- package/dist/client.cjs +2419 -202
- package/dist/client.d.cts +50 -81
- package/dist/client.d.ts +50 -81
- package/dist/client.js +2373 -203
- package/dist/index.cjs +3080 -298
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +3026 -298
- package/dist/provider/fastify.cjs +882 -0
- package/dist/provider/fastify.d.cts +35 -0
- package/dist/provider/fastify.d.ts +35 -0
- package/dist/provider/fastify.js +880 -0
- package/dist/provider/hono.cjs +948 -0
- package/dist/provider/hono.d.cts +99 -0
- package/dist/provider/hono.d.ts +99 -0
- package/dist/provider/hono.js +938 -0
- package/dist/provider.cjs +827 -97
- package/dist/provider.d.cts +32 -25
- package/dist/provider.d.ts +32 -25
- package/dist/provider.js +810 -99
- package/dist/react.cjs +2641 -0
- package/dist/react.d.cts +107 -0
- package/dist/react.d.ts +107 -0
- package/dist/react.js +2634 -0
- package/dist/routedockMiddleware-BlDsHuPl.d.ts +59 -0
- package/dist/routedockMiddleware-D97A5ptQ.d.cts +59 -0
- package/dist/schema.cjs +364 -0
- package/dist/schema.d.cts +3 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +359 -0
- package/dist/sign-CUf3rhTx.d.cts +53 -0
- package/dist/sign-Dpj2Mp7k.d.ts +53 -0
- package/dist/testing.cjs +57 -0
- package/dist/testing.d.cts +95 -0
- package/dist/testing.d.ts +95 -0
- package/dist/testing.js +54 -0
- package/dist/types-DmhmfLzP.d.cts +410 -0
- package/dist/types-DmhmfLzP.d.ts +410 -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
package/dist/provider.cjs
CHANGED
|
@@ -6,43 +6,175 @@ var server$1 = require('@x402/stellar/exact/server');
|
|
|
6
6
|
var server = require('@x402/core/server');
|
|
7
7
|
var stellar = require('@x402/stellar');
|
|
8
8
|
var http = require('@x402/core/http');
|
|
9
|
+
var nulthSdk = require('@routedock/nulth-sdk');
|
|
9
10
|
var server$3 = require('@stellar/mpp/charge/server');
|
|
10
11
|
var server$2 = require('mppx/server');
|
|
11
12
|
var server$4 = require('@stellar/mpp/channel/server');
|
|
13
|
+
var mppx = require('mppx');
|
|
14
|
+
var crypto$1 = require('crypto');
|
|
12
15
|
|
|
13
|
-
// src/
|
|
16
|
+
// src/errors.ts
|
|
14
17
|
var RouteDockError = class extends Error {
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
code;
|
|
19
|
+
retryable;
|
|
20
|
+
retryAfterMs;
|
|
21
|
+
constructor(message, code, retryable, options = {}) {
|
|
22
|
+
super(message, { cause: options.cause });
|
|
17
23
|
this.name = "RouteDockError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.retryable = retryable;
|
|
26
|
+
if (options.retryAfterMs !== void 0) {
|
|
27
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
28
|
+
}
|
|
18
29
|
}
|
|
19
30
|
};
|
|
20
31
|
var RouteDockManifestError = class extends RouteDockError {
|
|
21
|
-
constructor(message) {
|
|
22
|
-
super(message);
|
|
32
|
+
constructor(message, options = {}) {
|
|
33
|
+
super(message, "MANIFEST", false, options);
|
|
23
34
|
this.name = "RouteDockManifestError";
|
|
24
35
|
}
|
|
25
36
|
};
|
|
26
37
|
var RouteDockNoSupportedModeError = class extends RouteDockError {
|
|
27
|
-
constructor(message) {
|
|
28
|
-
super(message);
|
|
38
|
+
constructor(message, options = {}) {
|
|
39
|
+
super(message, "NO_SUPPORTED_MODE", false, options);
|
|
29
40
|
this.name = "RouteDockNoSupportedModeError";
|
|
30
41
|
}
|
|
31
42
|
};
|
|
32
|
-
var
|
|
33
|
-
constructor(message) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.name = "
|
|
43
|
+
var RouteDockClientVersionError = class extends RouteDockError {
|
|
44
|
+
constructor(message, options = {}) {
|
|
45
|
+
super(message, "CLIENT_VERSION_TOO_OLD", false, options);
|
|
46
|
+
this.name = "RouteDockClientVersionError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var RouteDockTrustlineError = class extends RouteDockError {
|
|
50
|
+
asset;
|
|
51
|
+
issuer;
|
|
52
|
+
remediation;
|
|
53
|
+
constructor(asset, issuer, remediation, options = {}) {
|
|
54
|
+
super(
|
|
55
|
+
`Missing trustline for ${asset} (issuer: ${issuer}). ${remediation}`,
|
|
56
|
+
"TRUSTLINE",
|
|
57
|
+
false,
|
|
58
|
+
options
|
|
59
|
+
);
|
|
60
|
+
this.name = "RouteDockTrustlineError";
|
|
61
|
+
this.asset = asset;
|
|
62
|
+
this.issuer = issuer;
|
|
63
|
+
this.remediation = remediation;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var RouteDockFacilitatorError = class extends RouteDockError {
|
|
67
|
+
status;
|
|
68
|
+
constructor(message, status, options = {}) {
|
|
69
|
+
super(message, "FACILITATOR", true, options);
|
|
70
|
+
this.name = "RouteDockFacilitatorError";
|
|
71
|
+
this.status = status;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var RouteDockNetworkError = class extends RouteDockError {
|
|
75
|
+
constructor(message, options = {}) {
|
|
76
|
+
super(message, "NETWORK", true, options);
|
|
77
|
+
this.name = "RouteDockNetworkError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var RouteDockSignatureError = class extends RouteDockError {
|
|
81
|
+
constructor(message, options = {}) {
|
|
82
|
+
super(message, "SIGNATURE", false, options);
|
|
83
|
+
this.name = "RouteDockSignatureError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var RouteDockVoucherMonotonicityError = class extends RouteDockError {
|
|
87
|
+
constructor(message, options = {}) {
|
|
88
|
+
super(message, "VOUCHER_MONOTONICITY", false, options);
|
|
89
|
+
this.name = "RouteDockVoucherMonotonicityError";
|
|
36
90
|
}
|
|
37
91
|
};
|
|
38
|
-
var
|
|
92
|
+
var RouteDockPolicyRejectError = class extends RouteDockError {
|
|
39
93
|
reason;
|
|
40
|
-
constructor(reason) {
|
|
41
|
-
super(`Payment rejected by policy: ${reason}
|
|
42
|
-
this.name = "
|
|
94
|
+
constructor(reason, options = {}) {
|
|
95
|
+
super(`Payment rejected by policy: ${reason}`, "POLICY_REJECT", false, options);
|
|
96
|
+
this.name = "RouteDockPolicyRejectError";
|
|
43
97
|
this.reason = reason;
|
|
44
98
|
}
|
|
45
99
|
};
|
|
100
|
+
var RouteDockChannelStateError = class extends RouteDockError {
|
|
101
|
+
constructor(message, options = {}) {
|
|
102
|
+
super(message, "CHANNEL_STATE", false, options);
|
|
103
|
+
this.name = "RouteDockChannelStateError";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var RouteDockDisputeError = class extends RouteDockError {
|
|
107
|
+
constructor(message, options = {}) {
|
|
108
|
+
super(message, "DISPUTE", false, options);
|
|
109
|
+
this.name = "RouteDockDisputeError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var RouteDockRefundWindowError = class extends RouteDockError {
|
|
113
|
+
constructor(message, options = {}) {
|
|
114
|
+
super(message, "REFUND_WINDOW", false, options);
|
|
115
|
+
this.name = "RouteDockRefundWindowError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var RouteDockPolicyRejectedError = RouteDockPolicyRejectError;
|
|
119
|
+
var RouteDockSessionError = RouteDockVoucherMonotonicityError;
|
|
120
|
+
|
|
121
|
+
// src/types.ts
|
|
122
|
+
var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
123
|
+
|
|
124
|
+
// src/provider/payee.ts
|
|
125
|
+
function resolvePayee(manifest, mode) {
|
|
126
|
+
const override = manifest.pricing[mode]?.payee;
|
|
127
|
+
return override ?? manifest.payee;
|
|
128
|
+
}
|
|
129
|
+
var usdcToUnits = nulthSdk.usdcToStroops;
|
|
130
|
+
function extractPayerAddress(key) {
|
|
131
|
+
if (typeof key !== "string") return null;
|
|
132
|
+
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/provider/SeenTxStore.ts
|
|
136
|
+
var InMemorySeenTxStore = class {
|
|
137
|
+
map = /* @__PURE__ */ new Map();
|
|
138
|
+
order = [];
|
|
139
|
+
constructor(maxEntriesOrOptions = 1e4) {
|
|
140
|
+
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
141
|
+
this.maxEntries = options.maxEntries ?? 1e4;
|
|
142
|
+
if (options.warn !== false) {
|
|
143
|
+
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
144
|
+
if (isServerless) {
|
|
145
|
+
console.warn(
|
|
146
|
+
"[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."
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
maxEntries;
|
|
152
|
+
get(key) {
|
|
153
|
+
return this.map.get(key);
|
|
154
|
+
}
|
|
155
|
+
set(key, record) {
|
|
156
|
+
if (!this.map.has(key)) {
|
|
157
|
+
this.order.push(key);
|
|
158
|
+
if (this.order.length > this.maxEntries) {
|
|
159
|
+
const evicted = this.order.shift();
|
|
160
|
+
if (evicted !== void 0) this.map.delete(evicted);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.map.set(key, record);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
async function sha256Hex(input) {
|
|
167
|
+
const buf = new TextEncoder().encode(input);
|
|
168
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
169
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
170
|
+
}
|
|
171
|
+
async function paymentIdempotencyKey(getHeader) {
|
|
172
|
+
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
173
|
+
if (!material) return null;
|
|
174
|
+
return sha256Hex(material);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/provider/x402Handler.ts
|
|
46
178
|
var CAIP2 = {
|
|
47
179
|
testnet: "stellar:testnet",
|
|
48
180
|
mainnet: "stellar:pubnet"
|
|
@@ -52,6 +184,7 @@ function createX402Handler(opts) {
|
|
|
52
184
|
const caip2 = CAIP2[opts.network];
|
|
53
185
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
54
186
|
const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
187
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
|
|
55
188
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
56
189
|
const localFacilitator = new facilitator.ExactStellarScheme([signer], {
|
|
57
190
|
areFeesSponsored: true
|
|
@@ -70,13 +203,14 @@ function createX402Handler(opts) {
|
|
|
70
203
|
ozServer = new server.x402ResourceServer(facilitator);
|
|
71
204
|
ozServer.register(caip2, new server$1.ExactStellarScheme());
|
|
72
205
|
}
|
|
73
|
-
const amountInBaseUnits = String(
|
|
206
|
+
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
207
|
+
const payTo = resolvePayee(opts.manifest, "x402");
|
|
74
208
|
const requirements = {
|
|
75
209
|
scheme: "exact",
|
|
76
210
|
network: caip2,
|
|
77
211
|
asset: opts.assetContract,
|
|
78
212
|
amount: amountInBaseUnits,
|
|
79
|
-
payTo
|
|
213
|
+
payTo,
|
|
80
214
|
maxTimeoutSeconds: 60,
|
|
81
215
|
extra: {
|
|
82
216
|
areFeesSponsored: true,
|
|
@@ -110,8 +244,31 @@ function createX402Handler(opts) {
|
|
|
110
244
|
}
|
|
111
245
|
return;
|
|
112
246
|
}
|
|
247
|
+
const idempotencyKey = await paymentIdempotencyKey((name) => {
|
|
248
|
+
const v = req.headers[name.toLowerCase()];
|
|
249
|
+
return Array.isArray(v) ? v[0] : v;
|
|
250
|
+
});
|
|
251
|
+
if (idempotencyKey) {
|
|
252
|
+
const cached = await seenTxStore.get(idempotencyKey);
|
|
253
|
+
if (cached) {
|
|
254
|
+
if (cached.headers) {
|
|
255
|
+
for (const [k, val] of Object.entries(cached.headers)) {
|
|
256
|
+
res.setHeader(k, val);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
next();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
113
263
|
const payload = http.decodePaymentSignatureHeader(paymentHeader);
|
|
114
264
|
let txHash = null;
|
|
265
|
+
let payerAddress = null;
|
|
266
|
+
try {
|
|
267
|
+
const creds = payload.authorization?.credentials;
|
|
268
|
+
const key = Array.isArray(creds) ? creds[0]?.publicKey : void 0;
|
|
269
|
+
payerAddress = extractPayerAddress(key);
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
115
272
|
if (ozServer) {
|
|
116
273
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
117
274
|
txHash = settleResult.transaction ?? null;
|
|
@@ -149,8 +306,19 @@ function createX402Handler(opts) {
|
|
|
149
306
|
);
|
|
150
307
|
}
|
|
151
308
|
}
|
|
309
|
+
if (idempotencyKey) {
|
|
310
|
+
const headers = {};
|
|
311
|
+
const paymentResponse = res.getHeader("X-Payment-Response");
|
|
312
|
+
if (typeof paymentResponse === "string") {
|
|
313
|
+
headers["X-Payment-Response"] = paymentResponse;
|
|
314
|
+
}
|
|
315
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
316
|
+
}
|
|
152
317
|
if (txHash && opts.onSettled) {
|
|
153
|
-
|
|
318
|
+
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
319
|
+
console.error("[x402] onSettled callback error:", err);
|
|
320
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
321
|
+
});
|
|
154
322
|
}
|
|
155
323
|
next();
|
|
156
324
|
} catch (err) {
|
|
@@ -166,11 +334,13 @@ var MPP_NETWORK = {
|
|
|
166
334
|
function createMppChargeHandler(opts) {
|
|
167
335
|
const networkId = MPP_NETWORK[opts.network];
|
|
168
336
|
const amountHumanReadable = opts.amount;
|
|
337
|
+
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
338
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
|
|
169
339
|
const mppx = server$2.Mppx.create({
|
|
170
340
|
secretKey: opts.payeeSecretKey,
|
|
171
341
|
methods: [
|
|
172
342
|
server$3.stellar.charge({
|
|
173
|
-
recipient
|
|
343
|
+
recipient,
|
|
174
344
|
currency: opts.assetContract,
|
|
175
345
|
network: networkId,
|
|
176
346
|
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
@@ -179,12 +349,43 @@ function createMppChargeHandler(opts) {
|
|
|
179
349
|
});
|
|
180
350
|
return async (req, res, next) => {
|
|
181
351
|
try {
|
|
352
|
+
let payerAddress = null;
|
|
353
|
+
try {
|
|
354
|
+
const authHeader = req.headers["authorization"];
|
|
355
|
+
if (typeof authHeader === "string" && authHeader.startsWith("Payment ")) {
|
|
356
|
+
const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
357
|
+
if (credPart) {
|
|
358
|
+
const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
|
|
359
|
+
const credJson = Buffer.from(b64, "base64").toString("utf8");
|
|
360
|
+
const cred = JSON.parse(credJson);
|
|
361
|
+
const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
|
|
362
|
+
payerAddress = extractPayerAddress(key);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
const idempotencyKey = await paymentIdempotencyKey((name) => {
|
|
368
|
+
const v = req.headers[name.toLowerCase()];
|
|
369
|
+
return Array.isArray(v) ? v[0] : v;
|
|
370
|
+
});
|
|
371
|
+
if (idempotencyKey) {
|
|
372
|
+
const cached = await seenTxStore.get(idempotencyKey);
|
|
373
|
+
if (cached) {
|
|
374
|
+
if (cached.headers) {
|
|
375
|
+
for (const [k, val] of Object.entries(cached.headers)) {
|
|
376
|
+
res.setHeader(k, val);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
next();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
182
383
|
const fetchReq = server$2.Request.fromNodeListener(req, res);
|
|
183
384
|
const handler = mppx["stellar/charge"];
|
|
184
385
|
const result = await handler({
|
|
185
386
|
amount: amountHumanReadable,
|
|
186
387
|
currency: opts.assetContract,
|
|
187
|
-
recipient
|
|
388
|
+
recipient,
|
|
188
389
|
description: opts.manifest.name
|
|
189
390
|
})(fetchReq);
|
|
190
391
|
if (result.status === 402) {
|
|
@@ -195,25 +396,130 @@ function createMppChargeHandler(opts) {
|
|
|
195
396
|
return;
|
|
196
397
|
}
|
|
197
398
|
const receipt = result.withReceipt(new Response(""));
|
|
198
|
-
|
|
399
|
+
const receiptHeaders = {};
|
|
400
|
+
receipt.headers.forEach((v, k) => {
|
|
401
|
+
res.setHeader(k, v);
|
|
402
|
+
receiptHeaders[k] = v;
|
|
403
|
+
});
|
|
199
404
|
const receiptHeader = receipt.headers.get("payment-receipt");
|
|
200
|
-
|
|
405
|
+
let reference;
|
|
406
|
+
if (receiptHeader) {
|
|
201
407
|
try {
|
|
202
408
|
const parsed = JSON.parse(
|
|
203
409
|
Buffer.from(receiptHeader, "base64").toString("utf8")
|
|
204
410
|
);
|
|
205
|
-
|
|
206
|
-
await opts.onSettled(parsed.reference, opts.amount, "mpp-charge");
|
|
207
|
-
}
|
|
411
|
+
reference = parsed.reference;
|
|
208
412
|
} catch {
|
|
209
413
|
}
|
|
210
414
|
}
|
|
415
|
+
if (idempotencyKey) {
|
|
416
|
+
await seenTxStore.set(idempotencyKey, {
|
|
417
|
+
txHash: reference ?? null,
|
|
418
|
+
headers: receiptHeaders
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (reference && opts.onSettled) {
|
|
422
|
+
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
423
|
+
console.error("[mpp-charge] onSettled callback error:", err);
|
|
424
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
425
|
+
});
|
|
426
|
+
}
|
|
211
427
|
next();
|
|
212
428
|
} catch (err) {
|
|
213
429
|
next(err);
|
|
214
430
|
}
|
|
215
431
|
};
|
|
216
432
|
}
|
|
433
|
+
function resolveVaultSettlementAddresses(payer, payee) {
|
|
434
|
+
if (!payer) return null;
|
|
435
|
+
return { payer, payee };
|
|
436
|
+
}
|
|
437
|
+
function buildSessionSettlementTransaction(source, vaultContractId, channelId, addresses, amount, voucherCount, networkPassphrase) {
|
|
438
|
+
const vault = new stellarSdk.Contract(vaultContractId);
|
|
439
|
+
const op = vault.call(
|
|
440
|
+
"record_session_settlement",
|
|
441
|
+
stellarSdk.nativeToScVal(channelId, { type: "address" }),
|
|
442
|
+
stellarSdk.nativeToScVal(addresses.payer, { type: "address" }),
|
|
443
|
+
stellarSdk.nativeToScVal(addresses.payee, { type: "address" }),
|
|
444
|
+
stellarSdk.nativeToScVal(amount, { type: "i128" }),
|
|
445
|
+
stellarSdk.nativeToScVal(voucherCount, { type: "u32" })
|
|
446
|
+
);
|
|
447
|
+
return new stellarSdk.TransactionBuilder(source, {
|
|
448
|
+
fee: stellarSdk.BASE_FEE,
|
|
449
|
+
networkPassphrase
|
|
450
|
+
}).addOperation(op).setTimeout(30).build();
|
|
451
|
+
}
|
|
452
|
+
function channelAuthorizer(envelopeSigner) {
|
|
453
|
+
return { envelopeSigner };
|
|
454
|
+
}
|
|
455
|
+
function formatMppError(error) {
|
|
456
|
+
if (error instanceof Error) return error.message;
|
|
457
|
+
if (typeof error === "string") return error;
|
|
458
|
+
try {
|
|
459
|
+
const serialized = JSON.stringify(error);
|
|
460
|
+
return serialized === void 0 ? String(error) : serialized;
|
|
461
|
+
} catch {
|
|
462
|
+
return String(error);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function withTypedChannelErrors(method) {
|
|
466
|
+
return {
|
|
467
|
+
...method,
|
|
468
|
+
async verify(parameters) {
|
|
469
|
+
try {
|
|
470
|
+
return await method.verify(parameters);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
throw mapChannelError(error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function onVerifiedCredential(method, commit) {
|
|
478
|
+
return {
|
|
479
|
+
...method,
|
|
480
|
+
async verify(parameters) {
|
|
481
|
+
const receipt = await method.verify(parameters);
|
|
482
|
+
await commit(parameters.credential);
|
|
483
|
+
return receipt;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function mapChannelError(error) {
|
|
488
|
+
const message = formatMppError(error);
|
|
489
|
+
const details = error && typeof error === "object" && "details" in error ? error.details : void 0;
|
|
490
|
+
const reason = details && Object.keys(details).length > 0 ? `${message} (${JSON.stringify(details)})` : message;
|
|
491
|
+
if (/signer mismatch|authori[sz]er|unauthori[sz]ed/i.test(message)) {
|
|
492
|
+
return new mppx.Errors.SignerMismatchError({ reason });
|
|
493
|
+
}
|
|
494
|
+
if (/already used|replay|challenge/i.test(message)) {
|
|
495
|
+
return new mppx.Errors.InvalidChallengeError({ reason });
|
|
496
|
+
}
|
|
497
|
+
if (/channel has been closed|channel is closed|finalized/i.test(message)) {
|
|
498
|
+
return new mppx.Errors.ChannelClosedError({ reason });
|
|
499
|
+
}
|
|
500
|
+
if (/exceeds channel balance|exceeds.*deposit/i.test(message)) {
|
|
501
|
+
return new mppx.Errors.AmountExceedsDepositError({ reason });
|
|
502
|
+
}
|
|
503
|
+
if (/channel not found|no channel/i.test(message)) {
|
|
504
|
+
return new mppx.Errors.ChannelNotFoundError({ reason });
|
|
505
|
+
}
|
|
506
|
+
if (/must be greater|does not cover|below.*minimum/i.test(message)) {
|
|
507
|
+
return new mppx.Errors.DeltaTooSmallError({ reason });
|
|
508
|
+
}
|
|
509
|
+
if (/signature/i.test(message)) {
|
|
510
|
+
return new mppx.Errors.InvalidSignatureError({ reason });
|
|
511
|
+
}
|
|
512
|
+
return new mppx.Errors.VerificationFailedError({ reason });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/provider/MppSessionHandler.ts
|
|
516
|
+
function isVoucherStoreValue(value) {
|
|
517
|
+
if (typeof value !== "object" || value === null) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const candidate = value;
|
|
521
|
+
return typeof candidate.amount === "string" && /^\d+$/.test(candidate.amount);
|
|
522
|
+
}
|
|
217
523
|
var MPP_NETWORK2 = {
|
|
218
524
|
testnet: "stellar:testnet",
|
|
219
525
|
mainnet: "stellar:pubnet"
|
|
@@ -222,67 +528,202 @@ function createMppSessionHandler(opts) {
|
|
|
222
528
|
const networkId = MPP_NETWORK2[opts.network];
|
|
223
529
|
const rateHuman = opts.rate;
|
|
224
530
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
225
|
-
const
|
|
531
|
+
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
226
532
|
const innerStore = server$4.Store.memory();
|
|
227
|
-
let
|
|
533
|
+
let record = null;
|
|
228
534
|
let voucherCount = 0;
|
|
229
535
|
let sessionOpened = false;
|
|
536
|
+
let idleTimer = null;
|
|
537
|
+
let settledCleanly = false;
|
|
538
|
+
let closeListenerArmed = false;
|
|
539
|
+
function clearIdleTimer() {
|
|
540
|
+
if (idleTimer) {
|
|
541
|
+
clearTimeout(idleTimer);
|
|
542
|
+
idleTimer = null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function armIdleTimer() {
|
|
546
|
+
if (!opts.idleTimeoutMs) return;
|
|
547
|
+
clearIdleTimer();
|
|
548
|
+
idleTimer = setTimeout(() => {
|
|
549
|
+
void flagOrphan("idle-timeout");
|
|
550
|
+
}, opts.idleTimeoutMs);
|
|
551
|
+
if (typeof idleTimer.unref === "function") idleTimer.unref();
|
|
552
|
+
}
|
|
553
|
+
async function loadPersistedRecord() {
|
|
554
|
+
if (record) return record;
|
|
555
|
+
try {
|
|
556
|
+
const stored = await innerStore.get(voucherRecordKey);
|
|
557
|
+
if (!stored || typeof stored.amount !== "string" || typeof stored.signature !== "string") {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
561
|
+
} catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
return record;
|
|
565
|
+
}
|
|
566
|
+
async function flagOrphan(reason) {
|
|
567
|
+
await loadPersistedRecord();
|
|
568
|
+
if (!sessionOpened || settledCleanly) return;
|
|
569
|
+
sessionOpened = false;
|
|
570
|
+
closeListenerArmed = false;
|
|
571
|
+
clearIdleTimer();
|
|
572
|
+
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
573
|
+
if (opts.onOrphaned) {
|
|
574
|
+
try {
|
|
575
|
+
await opts.onOrphaned(opts.channelFactory, {
|
|
576
|
+
cumulativeAmount,
|
|
577
|
+
lastSignature: record?.signature ?? "",
|
|
578
|
+
voucherCount,
|
|
579
|
+
reason
|
|
580
|
+
});
|
|
581
|
+
} catch (err) {
|
|
582
|
+
console.error("[mpp-session] onOrphaned handler failed:", err);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
230
586
|
const wrappedStore = {
|
|
231
587
|
async get(key) {
|
|
232
588
|
return innerStore.get(key);
|
|
233
589
|
},
|
|
234
590
|
async put(key, value) {
|
|
235
|
-
|
|
236
|
-
if (key === cumulativeKey && value && typeof value === "object" && "amount" in value) {
|
|
237
|
-
lastCumulativeAmount = BigInt(value.amount);
|
|
238
|
-
voucherCount++;
|
|
239
|
-
if (!sessionOpened) {
|
|
240
|
-
sessionOpened = true;
|
|
241
|
-
if (opts.onSessionOpen) {
|
|
242
|
-
await opts.onSessionOpen(opts.channelContract);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (opts.onVoucher) {
|
|
246
|
-
const humanAmount = (Number(lastCumulativeAmount) / 1e7).toFixed(7);
|
|
247
|
-
await opts.onVoucher(voucherCount, humanAmount);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
591
|
+
return innerStore.put(key, value);
|
|
250
592
|
},
|
|
251
593
|
async delete(key) {
|
|
252
594
|
return innerStore.delete(key);
|
|
595
|
+
},
|
|
596
|
+
async update(key, fn) {
|
|
597
|
+
const storeWithUpdate = innerStore;
|
|
598
|
+
if (typeof storeWithUpdate.update === "function") {
|
|
599
|
+
return storeWithUpdate.update(key, fn);
|
|
600
|
+
}
|
|
601
|
+
throw new Error("Store does not support atomic update operations");
|
|
253
602
|
}
|
|
254
603
|
};
|
|
604
|
+
async function commit(credential) {
|
|
605
|
+
await loadPersistedRecord();
|
|
606
|
+
const payload = credential.payload;
|
|
607
|
+
if (typeof payload?.amount !== "string" || typeof payload.signature !== "string") return;
|
|
608
|
+
let amount;
|
|
609
|
+
try {
|
|
610
|
+
amount = BigInt(payload.amount);
|
|
611
|
+
} catch {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (record && amount < record.amount) return;
|
|
615
|
+
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
616
|
+
record = { amount, signature: payload.signature, payer };
|
|
617
|
+
await innerStore.put(voucherRecordKey, {
|
|
618
|
+
amount: record.amount.toString(),
|
|
619
|
+
signature: record.signature,
|
|
620
|
+
payer: record.payer
|
|
621
|
+
});
|
|
622
|
+
voucherCount++;
|
|
623
|
+
settledCleanly = false;
|
|
624
|
+
armIdleTimer();
|
|
625
|
+
if (!sessionOpened) {
|
|
626
|
+
sessionOpened = true;
|
|
627
|
+
if (opts.onSessionOpen) {
|
|
628
|
+
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
629
|
+
console.error("[mpp-session] onSessionOpen callback error:", err);
|
|
630
|
+
opts.onCallbackError?.(err, "onSessionOpen");
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (opts.onVoucher) {
|
|
635
|
+
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
636
|
+
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
637
|
+
console.error("[mpp-session] onVoucher callback error:", err);
|
|
638
|
+
opts.onCallbackError?.(err, "onVoucher");
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
255
642
|
const mppx = server$2.Mppx.create({
|
|
256
643
|
secretKey: opts.payeeSecretKey,
|
|
257
644
|
methods: [
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
645
|
+
withTypedChannelErrors(
|
|
646
|
+
onVerifiedCredential(
|
|
647
|
+
server$4.stellar.channel({
|
|
648
|
+
channel: opts.channelFactory,
|
|
649
|
+
commitmentKey: opts.commitmentPublicKey,
|
|
650
|
+
network: networkId,
|
|
651
|
+
store: wrappedStore,
|
|
652
|
+
sourceAccount: payeeKeypair.publicKey(),
|
|
653
|
+
feePayer: channelAuthorizer(payeeKeypair)
|
|
654
|
+
}),
|
|
655
|
+
commit
|
|
656
|
+
)
|
|
657
|
+
)
|
|
266
658
|
]
|
|
267
659
|
});
|
|
268
|
-
let lastSignatureHex = "";
|
|
269
660
|
return async (req, res, next) => {
|
|
270
661
|
try {
|
|
271
662
|
if (req.method === "DELETE") {
|
|
663
|
+
await loadPersistedRecord();
|
|
272
664
|
const body = req.body;
|
|
273
|
-
const
|
|
274
|
-
const
|
|
665
|
+
const bodyAmount = body?.amount ? BigInt(body.amount) : 0n;
|
|
666
|
+
const recordAmount = record?.amount ?? 0n;
|
|
667
|
+
let closeAmount;
|
|
668
|
+
let closeSig;
|
|
669
|
+
if (bodyAmount > recordAmount) {
|
|
670
|
+
closeAmount = bodyAmount;
|
|
671
|
+
closeSig = body?.signature ?? record?.signature ?? "";
|
|
672
|
+
} else {
|
|
673
|
+
closeAmount = recordAmount;
|
|
674
|
+
closeSig = record?.signature ?? "";
|
|
675
|
+
}
|
|
275
676
|
if (closeAmount > 0n && closeSig) {
|
|
677
|
+
const closePayer = record?.payer ?? null;
|
|
276
678
|
const closeTxHash = await server$4.close({
|
|
277
|
-
channel: opts.
|
|
679
|
+
channel: opts.channelFactory,
|
|
278
680
|
amount: closeAmount,
|
|
279
681
|
signature: Buffer.from(closeSig, "hex"),
|
|
280
|
-
feePayer:
|
|
682
|
+
feePayer: channelAuthorizer(payeeKeypair),
|
|
281
683
|
network: networkId
|
|
282
684
|
});
|
|
685
|
+
settledCleanly = true;
|
|
686
|
+
clearIdleTimer();
|
|
283
687
|
if (opts.onSettled) {
|
|
284
|
-
const totalPaid = (Number(
|
|
285
|
-
|
|
688
|
+
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
689
|
+
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
690
|
+
console.error("[mpp-session] onSettled callback error:", err);
|
|
691
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const vaultContract = process.env.AGENT_VAULT_CONTRACT;
|
|
695
|
+
if (vaultContract) {
|
|
696
|
+
const settlementAddresses = resolveVaultSettlementAddresses(
|
|
697
|
+
closePayer,
|
|
698
|
+
payeeKeypair.publicKey()
|
|
699
|
+
);
|
|
700
|
+
if (!settlementAddresses) {
|
|
701
|
+
console.error("[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
702
|
+
} else {
|
|
703
|
+
try {
|
|
704
|
+
const { Networks } = await import('@stellar/stellar-sdk');
|
|
705
|
+
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
706
|
+
const networkPassphrase = opts.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
707
|
+
const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
|
|
708
|
+
const server = new Server(rpcUrl);
|
|
709
|
+
const payeePublicKey = payeeKeypair.publicKey();
|
|
710
|
+
const sourceAccount = await server.getAccount(payeePublicKey);
|
|
711
|
+
const tx = buildSessionSettlementTransaction(
|
|
712
|
+
sourceAccount,
|
|
713
|
+
vaultContract,
|
|
714
|
+
opts.channelFactory,
|
|
715
|
+
settlementAddresses,
|
|
716
|
+
closeAmount,
|
|
717
|
+
voucherCount,
|
|
718
|
+
networkPassphrase
|
|
719
|
+
);
|
|
720
|
+
const preparedTx = await server.prepareTransaction(tx);
|
|
721
|
+
preparedTx.sign(payeeKeypair);
|
|
722
|
+
await server.sendTransaction(preparedTx);
|
|
723
|
+
} catch (recordErr) {
|
|
724
|
+
console.error("[mpp-session] failed to record session_settled on vault:", recordErr);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
286
727
|
}
|
|
287
728
|
res.json({ closeTxHash });
|
|
288
729
|
} else {
|
|
@@ -290,23 +731,12 @@ function createMppSessionHandler(opts) {
|
|
|
290
731
|
}
|
|
291
732
|
sessionOpened = false;
|
|
292
733
|
voucherCount = 0;
|
|
293
|
-
|
|
734
|
+
record = null;
|
|
735
|
+
await innerStore.delete(voucherRecordKey);
|
|
736
|
+
closeListenerArmed = false;
|
|
737
|
+
clearIdleTimer();
|
|
294
738
|
return;
|
|
295
739
|
}
|
|
296
|
-
const authHeader = req.headers["authorization"];
|
|
297
|
-
if (typeof authHeader === "string" && authHeader.startsWith("Payment ")) {
|
|
298
|
-
try {
|
|
299
|
-
const credB64 = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
300
|
-
if (credB64) {
|
|
301
|
-
const credJson = Buffer.from(credB64.split("=").slice(1).join("=").replace(/^"|"$/g, ""), "base64").toString("utf8");
|
|
302
|
-
const cred = JSON.parse(credJson);
|
|
303
|
-
if (cred.payload?.signature) {
|
|
304
|
-
lastSignatureHex = cred.payload.signature;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
} catch {
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
740
|
const fetchReq = server$2.Request.fromNodeListener(req, res);
|
|
311
741
|
const result = await mppx.channel({
|
|
312
742
|
amount: rateHuman,
|
|
@@ -320,28 +750,100 @@ function createMppSessionHandler(opts) {
|
|
|
320
750
|
res.send(body);
|
|
321
751
|
return;
|
|
322
752
|
}
|
|
753
|
+
if (!closeListenerArmed) {
|
|
754
|
+
closeListenerArmed = true;
|
|
755
|
+
req.on("close", () => {
|
|
756
|
+
void flagOrphan("connection-closed");
|
|
757
|
+
});
|
|
758
|
+
}
|
|
323
759
|
next();
|
|
324
760
|
} catch (err) {
|
|
325
761
|
next(err);
|
|
326
762
|
}
|
|
327
763
|
};
|
|
328
764
|
}
|
|
765
|
+
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
766
|
+
function canonicalize(value) {
|
|
767
|
+
if (Array.isArray(value)) {
|
|
768
|
+
return value.map(canonicalize);
|
|
769
|
+
}
|
|
770
|
+
if (value !== null && typeof value === "object") {
|
|
771
|
+
return Object.fromEntries(
|
|
772
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
return value;
|
|
776
|
+
}
|
|
777
|
+
function manifestDigest(manifest) {
|
|
778
|
+
const { signature: _omit, ...rest } = manifest;
|
|
779
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
780
|
+
return crypto$1.createHash("sha256").update(canonical, "utf8").digest();
|
|
781
|
+
}
|
|
782
|
+
function signManifest(manifest, payeeSecretKey) {
|
|
783
|
+
const keypair = stellarSdk.Keypair.fromSecret(payeeSecretKey);
|
|
784
|
+
const versionedManifest = {
|
|
785
|
+
...manifest,
|
|
786
|
+
signature_version: MANIFEST_SIGNATURE_VERSION
|
|
787
|
+
};
|
|
788
|
+
const digest = manifestDigest(versionedManifest);
|
|
789
|
+
const sig = keypair.sign(digest);
|
|
790
|
+
return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
|
|
791
|
+
}
|
|
792
|
+
function verifyManifestSignature(manifest, expectedPayee) {
|
|
793
|
+
if (!manifest.signature) {
|
|
794
|
+
throw new RouteDockSignatureError(
|
|
795
|
+
"Manifest is missing a signature field \u2014 cannot verify payee authenticity"
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
|
|
799
|
+
throw new RouteDockSignatureError(
|
|
800
|
+
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
|
|
804
|
+
const digest = manifestDigest(manifest);
|
|
805
|
+
let sigBytes;
|
|
806
|
+
try {
|
|
807
|
+
sigBytes = Buffer.from(manifest.signature, "base64");
|
|
808
|
+
} catch {
|
|
809
|
+
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
810
|
+
}
|
|
811
|
+
let valid = false;
|
|
812
|
+
try {
|
|
813
|
+
valid = keypair.verify(digest, sigBytes);
|
|
814
|
+
} catch {
|
|
815
|
+
throw new RouteDockSignatureError(
|
|
816
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
if (!valid) {
|
|
820
|
+
throw new RouteDockSignatureError(
|
|
821
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
329
825
|
|
|
330
826
|
// src/provider/routedockMiddleware.ts
|
|
827
|
+
var MPP_MODES = ["mpp-session", "mpp-charge"];
|
|
331
828
|
function routedock(opts) {
|
|
332
|
-
const
|
|
829
|
+
const startTime = Date.now();
|
|
830
|
+
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
831
|
+
const handlerMap = /* @__PURE__ */ new Map();
|
|
333
832
|
if (opts.modes.includes("x402")) {
|
|
334
833
|
const x402Price = opts.pricing.x402;
|
|
335
834
|
if (x402Price) {
|
|
336
|
-
|
|
835
|
+
handlerMap.set(
|
|
836
|
+
"x402",
|
|
337
837
|
createX402Handler({
|
|
338
838
|
payeeSecretKey: opts.payeeSecretKey,
|
|
339
839
|
network: opts.network,
|
|
340
840
|
amount: x402Price,
|
|
341
841
|
assetContract: opts.assetContract,
|
|
342
842
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
343
|
-
manifest:
|
|
344
|
-
...opts.onSettled ? { onSettled: opts.onSettled } : {}
|
|
843
|
+
manifest: signedManifest,
|
|
844
|
+
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
845
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
846
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
345
847
|
})
|
|
346
848
|
);
|
|
347
849
|
}
|
|
@@ -349,14 +851,17 @@ function routedock(opts) {
|
|
|
349
851
|
if (opts.modes.includes("mpp-charge")) {
|
|
350
852
|
const chargePrice = opts.pricing["mpp-charge"];
|
|
351
853
|
if (chargePrice) {
|
|
352
|
-
|
|
854
|
+
handlerMap.set(
|
|
855
|
+
"mpp-charge",
|
|
353
856
|
createMppChargeHandler({
|
|
354
857
|
payeeSecretKey: opts.payeeSecretKey,
|
|
355
858
|
network: opts.network,
|
|
356
859
|
amount: chargePrice,
|
|
357
860
|
assetContract: opts.assetContract,
|
|
358
|
-
manifest:
|
|
359
|
-
...opts.onSettled ? { onSettled: opts.onSettled } : {}
|
|
861
|
+
manifest: signedManifest,
|
|
862
|
+
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
863
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
864
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
|
|
360
865
|
})
|
|
361
866
|
);
|
|
362
867
|
}
|
|
@@ -367,39 +872,52 @@ function routedock(opts) {
|
|
|
367
872
|
if (!opts.commitmentPublicKey) {
|
|
368
873
|
throw new Error("routedock: mpp-session mode requires commitmentPublicKey");
|
|
369
874
|
}
|
|
370
|
-
|
|
875
|
+
handlerMap.set(
|
|
876
|
+
"mpp-session",
|
|
371
877
|
createMppSessionHandler({
|
|
372
878
|
payeeSecretKey: opts.payeeSecretKey,
|
|
373
879
|
network: opts.network,
|
|
374
|
-
|
|
880
|
+
channelFactory: sessionPricing.channelFactory,
|
|
375
881
|
rate: sessionPricing.rate,
|
|
376
882
|
assetContract: opts.assetContract,
|
|
377
|
-
manifest:
|
|
883
|
+
manifest: signedManifest,
|
|
378
884
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
379
885
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
380
886
|
...opts.onSessionOpen ? { onSessionOpen: opts.onSessionOpen } : {},
|
|
381
|
-
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {}
|
|
887
|
+
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
888
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
889
|
+
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
890
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
|
|
382
891
|
})
|
|
383
892
|
);
|
|
384
893
|
}
|
|
385
894
|
}
|
|
895
|
+
const defaultMode = MPP_MODES.find((mode) => handlerMap.has(mode)) ?? (handlerMap.has("x402") ? "x402" : void 0);
|
|
386
896
|
return (req, res, next) => {
|
|
387
897
|
if (req.path === "/.well-known/routedock.json") {
|
|
388
|
-
res.json(
|
|
898
|
+
res.json(signedManifest);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (req.path === "/health") {
|
|
902
|
+
res.json({
|
|
903
|
+
status: "ok",
|
|
904
|
+
routedock: opts.manifest.routedock,
|
|
905
|
+
network: opts.network,
|
|
906
|
+
payee: opts.payee,
|
|
907
|
+
modesActive: opts.modes,
|
|
908
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3)
|
|
909
|
+
});
|
|
389
910
|
return;
|
|
390
911
|
}
|
|
391
|
-
if (
|
|
912
|
+
if (handlerMap.size === 0) {
|
|
392
913
|
next();
|
|
393
914
|
return;
|
|
394
915
|
}
|
|
395
916
|
const hasX402Header = !!(req.headers["payment-signature"] || req.headers["x-payment"]);
|
|
396
917
|
const prefersX402 = req.headers["x-preferred-mode"] === "x402";
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
} else {
|
|
401
|
-
handler = handlers[handlers.length - 1];
|
|
402
|
-
}
|
|
918
|
+
const x402Handler = handlerMap.get("x402");
|
|
919
|
+
const defaultHandler = defaultMode ? handlerMap.get(defaultMode) : void 0;
|
|
920
|
+
const handler = hasX402Header || prefersX402 ? x402Handler ?? defaultHandler : defaultHandler;
|
|
403
921
|
if (!handler) {
|
|
404
922
|
next();
|
|
405
923
|
return;
|
|
@@ -412,6 +930,148 @@ function routedock(opts) {
|
|
|
412
930
|
})).then(() => next()).catch((err) => next(err));
|
|
413
931
|
};
|
|
414
932
|
}
|
|
933
|
+
var MPP_NETWORK3 = {
|
|
934
|
+
testnet: "stellar:testnet",
|
|
935
|
+
mainnet: "stellar:pubnet"
|
|
936
|
+
};
|
|
937
|
+
function decimalToStroops(amountStr) {
|
|
938
|
+
return nulthSdk.usdcToStroops(amountStr);
|
|
939
|
+
}
|
|
940
|
+
async function reconcileAbandonedSessions(opts) {
|
|
941
|
+
const stats = {
|
|
942
|
+
orphanedCount: 0,
|
|
943
|
+
recoveredCount: 0,
|
|
944
|
+
skippedCount: 0,
|
|
945
|
+
failedCount: 0,
|
|
946
|
+
errors: []
|
|
947
|
+
};
|
|
948
|
+
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
949
|
+
const networkId = MPP_NETWORK3[opts.network];
|
|
950
|
+
try {
|
|
951
|
+
const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
|
|
952
|
+
if (error) {
|
|
953
|
+
throw new Error(`Failed to query sessions: ${error.message}`);
|
|
954
|
+
}
|
|
955
|
+
stats.orphanedCount = closingSessions?.length ?? 0;
|
|
956
|
+
if (!closingSessions || closingSessions.length === 0) {
|
|
957
|
+
return stats;
|
|
958
|
+
}
|
|
959
|
+
for (const session of closingSessions) {
|
|
960
|
+
try {
|
|
961
|
+
const {
|
|
962
|
+
channel_id: sessionId,
|
|
963
|
+
channel_contract: channelContract,
|
|
964
|
+
cumulative_amount,
|
|
965
|
+
last_signature
|
|
966
|
+
} = session;
|
|
967
|
+
const channelId = channelContract || sessionId;
|
|
968
|
+
if (!channelId || !cumulative_amount || !last_signature) {
|
|
969
|
+
stats.skippedCount++;
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const closeAmount = decimalToStroops(cumulative_amount);
|
|
973
|
+
const closeSig = Buffer.from(last_signature, "hex");
|
|
974
|
+
const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
|
|
975
|
+
const closeTxHash = await closeFn({
|
|
976
|
+
channel: channelId,
|
|
977
|
+
amount: closeAmount,
|
|
978
|
+
signature: closeSig,
|
|
979
|
+
feePayer: channelAuthorizer(payeeKeypair),
|
|
980
|
+
network: networkId
|
|
981
|
+
});
|
|
982
|
+
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
983
|
+
const { error: updateError } = await opts.supabase.from("sessions").update({
|
|
984
|
+
status: "closed",
|
|
985
|
+
settlement_tx_hash: closeTxHash,
|
|
986
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
987
|
+
}).eq("channel_id", sessionId);
|
|
988
|
+
if (updateError) {
|
|
989
|
+
throw new Error(`Failed to update session: ${updateError.message}`);
|
|
990
|
+
}
|
|
991
|
+
stats.recoveredCount++;
|
|
992
|
+
if (opts.onRecovered) {
|
|
993
|
+
await opts.onRecovered(channelId, closeTxHash, totalPaid);
|
|
994
|
+
}
|
|
995
|
+
} catch (err) {
|
|
996
|
+
stats.failedCount++;
|
|
997
|
+
stats.errors.push({
|
|
998
|
+
channelId: session.channel_id,
|
|
999
|
+
reason: formatMppError(err)
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return stats;
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
throw new Error(
|
|
1006
|
+
`Session reconciliation failed: ${formatMppError(err)}`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async function runStartupReconciliation(opts, logger) {
|
|
1011
|
+
const log = logger || console.log;
|
|
1012
|
+
log("[SessionReconciler] Running startup reconciliation...");
|
|
1013
|
+
try {
|
|
1014
|
+
const stats = await reconcileAbandonedSessions(opts);
|
|
1015
|
+
if (stats.orphanedCount === 0) {
|
|
1016
|
+
log("[SessionReconciler] No orphaned sessions found");
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
log(
|
|
1020
|
+
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
1021
|
+
);
|
|
1022
|
+
if (stats.errors.length > 0) {
|
|
1023
|
+
log("[SessionReconciler] Reconciliation errors:");
|
|
1024
|
+
for (const err of stats.errors) {
|
|
1025
|
+
log(` - ${err.channelId}: ${err.reason}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/provider/registerProvider.ts
|
|
1034
|
+
async function registerProvider(optionsOrSupabase, manifestArg, baseUrlArg, verifiedArg) {
|
|
1035
|
+
let supabase;
|
|
1036
|
+
let manifest;
|
|
1037
|
+
let baseUrl;
|
|
1038
|
+
let verified;
|
|
1039
|
+
if ("supabase" in optionsOrSupabase) {
|
|
1040
|
+
supabase = optionsOrSupabase.supabase;
|
|
1041
|
+
manifest = optionsOrSupabase.manifest;
|
|
1042
|
+
baseUrl = optionsOrSupabase.baseUrl;
|
|
1043
|
+
verified = optionsOrSupabase.verified ?? true;
|
|
1044
|
+
} else {
|
|
1045
|
+
supabase = optionsOrSupabase;
|
|
1046
|
+
if (!manifestArg || !baseUrlArg) {
|
|
1047
|
+
throw new RouteDockManifestError("manifest and baseUrl are required for registerProvider");
|
|
1048
|
+
}
|
|
1049
|
+
manifest = manifestArg;
|
|
1050
|
+
baseUrl = baseUrlArg;
|
|
1051
|
+
verified = verifiedArg ?? true;
|
|
1052
|
+
}
|
|
1053
|
+
verifyManifestSignature(manifest);
|
|
1054
|
+
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
1055
|
+
const { error } = await supabase.from("providers").upsert(
|
|
1056
|
+
{
|
|
1057
|
+
name: manifest.name,
|
|
1058
|
+
description: manifest.description ?? null,
|
|
1059
|
+
base_url: normalizedBaseUrl,
|
|
1060
|
+
modes: manifest.modes,
|
|
1061
|
+
tags: manifest.tags ?? [],
|
|
1062
|
+
categories: manifest.categories ?? [],
|
|
1063
|
+
network: manifest.network,
|
|
1064
|
+
payee: manifest.payee,
|
|
1065
|
+
manifest,
|
|
1066
|
+
verified,
|
|
1067
|
+
registered_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1068
|
+
},
|
|
1069
|
+
{ onConflict: "base_url" }
|
|
1070
|
+
);
|
|
1071
|
+
if (error) {
|
|
1072
|
+
throw new Error(`Failed to register provider in Supabase: ${error.message}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
415
1075
|
|
|
416
1076
|
// src/store/SessionStore.ts
|
|
417
1077
|
var SupabaseSessionStore = class {
|
|
@@ -421,12 +1081,16 @@ var SupabaseSessionStore = class {
|
|
|
421
1081
|
supabase;
|
|
422
1082
|
async get(channelId) {
|
|
423
1083
|
const { data, error } = await this.supabase.from("sessions").select("*").eq("channel_id", channelId).maybeSingle();
|
|
424
|
-
if (error)
|
|
1084
|
+
if (error) {
|
|
1085
|
+
throw new RouteDockNetworkError(`SessionStore.get failed: ${error.message}`);
|
|
1086
|
+
}
|
|
425
1087
|
if (!data) return null;
|
|
426
1088
|
return {
|
|
427
1089
|
channel_id: data.channel_id,
|
|
428
1090
|
payee: data.payee,
|
|
429
1091
|
payer: data.payer,
|
|
1092
|
+
channel_contract: data.channel_contract,
|
|
1093
|
+
network: data.network,
|
|
430
1094
|
cumulative_amount: String(data.cumulative_amount),
|
|
431
1095
|
last_signature: data.last_signature,
|
|
432
1096
|
status: data.status,
|
|
@@ -441,7 +1105,7 @@ var SupabaseSessionStore = class {
|
|
|
441
1105
|
const prev = parseFloat(existing.cumulative_amount);
|
|
442
1106
|
const next = parseFloat(state.cumulative_amount);
|
|
443
1107
|
if (next <= prev) {
|
|
444
|
-
throw new
|
|
1108
|
+
throw new RouteDockVoucherMonotonicityError(
|
|
445
1109
|
`cumulative_amount must be strictly increasing: ${next} <= ${prev}`
|
|
446
1110
|
);
|
|
447
1111
|
}
|
|
@@ -451,6 +1115,8 @@ var SupabaseSessionStore = class {
|
|
|
451
1115
|
channel_id: state.channel_id,
|
|
452
1116
|
payee: state.payee,
|
|
453
1117
|
payer: state.payer,
|
|
1118
|
+
channel_contract: state.channel_contract,
|
|
1119
|
+
network: state.network,
|
|
454
1120
|
cumulative_amount: state.cumulative_amount,
|
|
455
1121
|
last_signature: state.last_signature,
|
|
456
1122
|
status: state.status,
|
|
@@ -462,26 +1128,90 @@ var SupabaseSessionStore = class {
|
|
|
462
1128
|
);
|
|
463
1129
|
if (error) {
|
|
464
1130
|
if (error.message.includes("strictly increasing")) {
|
|
465
|
-
throw new
|
|
1131
|
+
throw new RouteDockVoucherMonotonicityError(
|
|
466
1132
|
`cumulative_amount must be strictly increasing (DB): ${error.message}`
|
|
467
1133
|
);
|
|
468
1134
|
}
|
|
469
|
-
throw new
|
|
1135
|
+
throw new RouteDockNetworkError(`SessionStore.upsert failed: ${error.message}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
async setStatus(channelId, status, settlementTxHash) {
|
|
1139
|
+
const payload = {
|
|
1140
|
+
status,
|
|
1141
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1142
|
+
};
|
|
1143
|
+
if (settlementTxHash !== void 0) {
|
|
1144
|
+
payload.settlement_tx_hash = settlementTxHash;
|
|
1145
|
+
}
|
|
1146
|
+
const { error } = await this.supabase.from("sessions").update(payload).eq("channel_id", channelId);
|
|
1147
|
+
if (error) {
|
|
1148
|
+
throw new RouteDockNetworkError(`SessionStore.setStatus failed: ${error.message}`);
|
|
470
1149
|
}
|
|
471
1150
|
}
|
|
472
1151
|
async close(channelId) {
|
|
473
|
-
|
|
474
|
-
|
|
1152
|
+
await this.setStatus(channelId, "closed");
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// src/store/SupabaseSeenTxStore.ts
|
|
1157
|
+
var SupabaseSeenTxStore = class {
|
|
1158
|
+
constructor(supabase) {
|
|
1159
|
+
this.supabase = supabase;
|
|
1160
|
+
}
|
|
1161
|
+
supabase;
|
|
1162
|
+
async get(key) {
|
|
1163
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
|
|
1164
|
+
if (error) {
|
|
1165
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
1166
|
+
}
|
|
1167
|
+
if (!data) return void 0;
|
|
1168
|
+
const headers = data.headers;
|
|
1169
|
+
return {
|
|
1170
|
+
txHash: data.tx_hash ?? null,
|
|
1171
|
+
...headers ? { headers } : {}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
async set(key, record) {
|
|
1175
|
+
const { error } = await this.supabase.from("settlements").upsert(
|
|
1176
|
+
{
|
|
1177
|
+
key,
|
|
1178
|
+
tx_hash: record.txHash,
|
|
1179
|
+
headers: record.headers ?? null
|
|
1180
|
+
},
|
|
1181
|
+
{ onConflict: "key" }
|
|
1182
|
+
);
|
|
1183
|
+
if (error) {
|
|
1184
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
1185
|
+
}
|
|
475
1186
|
}
|
|
476
1187
|
};
|
|
477
1188
|
|
|
1189
|
+
exports.DEFAULT_MAX_SESSION_DURATION_MS = DEFAULT_MAX_SESSION_DURATION_MS;
|
|
1190
|
+
exports.InMemorySeenTxStore = InMemorySeenTxStore;
|
|
1191
|
+
exports.RouteDockChannelStateError = RouteDockChannelStateError;
|
|
1192
|
+
exports.RouteDockClientVersionError = RouteDockClientVersionError;
|
|
1193
|
+
exports.RouteDockDisputeError = RouteDockDisputeError;
|
|
478
1194
|
exports.RouteDockError = RouteDockError;
|
|
1195
|
+
exports.RouteDockFacilitatorError = RouteDockFacilitatorError;
|
|
479
1196
|
exports.RouteDockManifestError = RouteDockManifestError;
|
|
1197
|
+
exports.RouteDockNetworkError = RouteDockNetworkError;
|
|
480
1198
|
exports.RouteDockNoSupportedModeError = RouteDockNoSupportedModeError;
|
|
1199
|
+
exports.RouteDockPolicyRejectError = RouteDockPolicyRejectError;
|
|
481
1200
|
exports.RouteDockPolicyRejectedError = RouteDockPolicyRejectedError;
|
|
1201
|
+
exports.RouteDockRefundWindowError = RouteDockRefundWindowError;
|
|
482
1202
|
exports.RouteDockSessionError = RouteDockSessionError;
|
|
1203
|
+
exports.RouteDockSignatureError = RouteDockSignatureError;
|
|
1204
|
+
exports.RouteDockTrustlineError = RouteDockTrustlineError;
|
|
1205
|
+
exports.RouteDockVoucherMonotonicityError = RouteDockVoucherMonotonicityError;
|
|
1206
|
+
exports.SupabaseSeenTxStore = SupabaseSeenTxStore;
|
|
483
1207
|
exports.SupabaseSessionStore = SupabaseSessionStore;
|
|
484
1208
|
exports.createMppChargeHandler = createMppChargeHandler;
|
|
485
1209
|
exports.createMppSessionHandler = createMppSessionHandler;
|
|
486
1210
|
exports.createX402Handler = createX402Handler;
|
|
1211
|
+
exports.decimalToStroops = decimalToStroops;
|
|
1212
|
+
exports.isVoucherStoreValue = isVoucherStoreValue;
|
|
1213
|
+
exports.paymentIdempotencyKey = paymentIdempotencyKey;
|
|
1214
|
+
exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
|
|
1215
|
+
exports.registerProvider = registerProvider;
|
|
487
1216
|
exports.routedock = routedock;
|
|
1217
|
+
exports.runStartupReconciliation = runStartupReconciliation;
|