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