@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
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { Keypair, StrKey } from '@stellar/stellar-sdk';
|
|
2
|
+
import { ExactStellarScheme } from '@x402/stellar/exact/facilitator';
|
|
3
|
+
import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/server';
|
|
4
|
+
import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
|
|
5
|
+
import { createEd25519Signer } from '@x402/stellar';
|
|
6
|
+
import { encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymentResponseHeader } from '@x402/core/http';
|
|
7
|
+
import { stellar } from '@stellar/mpp/charge/server';
|
|
8
|
+
import { Store, stellar as stellar$1, close } from '@stellar/mpp/channel/server';
|
|
9
|
+
import { Mppx } from 'mppx/server';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { usdcToStroops } from '@routedock/nulth-sdk';
|
|
12
|
+
import { Errors } from 'mppx';
|
|
13
|
+
|
|
14
|
+
// src/provider/hono.ts
|
|
15
|
+
|
|
16
|
+
// src/errors.ts
|
|
17
|
+
var RouteDockError = class extends Error {
|
|
18
|
+
code;
|
|
19
|
+
retryable;
|
|
20
|
+
retryAfterMs;
|
|
21
|
+
constructor(message, code, retryable, options = {}) {
|
|
22
|
+
super(message, { cause: options.cause });
|
|
23
|
+
this.name = "RouteDockError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.retryable = retryable;
|
|
26
|
+
if (options.retryAfterMs !== void 0) {
|
|
27
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var RouteDockManifestError = class extends RouteDockError {
|
|
32
|
+
constructor(message, options = {}) {
|
|
33
|
+
super(message, "MANIFEST", false, options);
|
|
34
|
+
this.name = "RouteDockManifestError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var RouteDockNetworkError = class extends RouteDockError {
|
|
38
|
+
constructor(message, options = {}) {
|
|
39
|
+
super(message, "NETWORK", true, options);
|
|
40
|
+
this.name = "RouteDockNetworkError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var RouteDockSignatureError = class extends RouteDockError {
|
|
44
|
+
constructor(message, options = {}) {
|
|
45
|
+
super(message, "SIGNATURE", false, options);
|
|
46
|
+
this.name = "RouteDockSignatureError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/manifest/sign.ts
|
|
51
|
+
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
52
|
+
function canonicalize(value) {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
return value.map(canonicalize);
|
|
55
|
+
}
|
|
56
|
+
if (value !== null && typeof value === "object") {
|
|
57
|
+
return Object.fromEntries(
|
|
58
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function manifestDigest(manifest) {
|
|
64
|
+
const { signature: _omit, ...rest } = manifest;
|
|
65
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
66
|
+
return createHash("sha256").update(canonical, "utf8").digest();
|
|
67
|
+
}
|
|
68
|
+
function signManifest(manifest, payeeSecretKey) {
|
|
69
|
+
const keypair = Keypair.fromSecret(payeeSecretKey);
|
|
70
|
+
const versionedManifest = {
|
|
71
|
+
...manifest,
|
|
72
|
+
signature_version: MANIFEST_SIGNATURE_VERSION
|
|
73
|
+
};
|
|
74
|
+
const digest = manifestDigest(versionedManifest);
|
|
75
|
+
const sig = keypair.sign(digest);
|
|
76
|
+
return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
|
|
77
|
+
}
|
|
78
|
+
function verifyManifestSignature(manifest, expectedPayee) {
|
|
79
|
+
if (!manifest.signature) {
|
|
80
|
+
throw new RouteDockSignatureError(
|
|
81
|
+
"Manifest is missing a signature field \u2014 cannot verify payee authenticity"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
|
|
85
|
+
throw new RouteDockSignatureError(
|
|
86
|
+
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const keypair = Keypair.fromPublicKey(manifest.payee);
|
|
90
|
+
const digest = manifestDigest(manifest);
|
|
91
|
+
let sigBytes;
|
|
92
|
+
try {
|
|
93
|
+
sigBytes = Buffer.from(manifest.signature, "base64");
|
|
94
|
+
} catch {
|
|
95
|
+
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
96
|
+
}
|
|
97
|
+
let valid = false;
|
|
98
|
+
try {
|
|
99
|
+
valid = keypair.verify(digest, sigBytes);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new RouteDockSignatureError(
|
|
102
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!valid) {
|
|
106
|
+
throw new RouteDockSignatureError(
|
|
107
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/provider/payee.ts
|
|
113
|
+
function resolvePayee(manifest, mode) {
|
|
114
|
+
const override = manifest.pricing[mode]?.payee;
|
|
115
|
+
return override ?? manifest.payee;
|
|
116
|
+
}
|
|
117
|
+
var usdcToUnits = usdcToStroops;
|
|
118
|
+
function extractPayerAddress(key) {
|
|
119
|
+
if (typeof key !== "string") return null;
|
|
120
|
+
return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
121
|
+
}
|
|
122
|
+
function channelAuthorizer(envelopeSigner) {
|
|
123
|
+
return { envelopeSigner };
|
|
124
|
+
}
|
|
125
|
+
function formatMppError(error) {
|
|
126
|
+
if (error instanceof Error) return error.message;
|
|
127
|
+
if (typeof error === "string") return error;
|
|
128
|
+
try {
|
|
129
|
+
const serialized = JSON.stringify(error);
|
|
130
|
+
return serialized === void 0 ? String(error) : serialized;
|
|
131
|
+
} catch {
|
|
132
|
+
return String(error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function withTypedChannelErrors(method) {
|
|
136
|
+
return {
|
|
137
|
+
...method,
|
|
138
|
+
async verify(parameters) {
|
|
139
|
+
try {
|
|
140
|
+
return await method.verify(parameters);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw mapChannelError(error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function onVerifiedCredential(method, commit) {
|
|
148
|
+
return {
|
|
149
|
+
...method,
|
|
150
|
+
async verify(parameters) {
|
|
151
|
+
const receipt = await method.verify(parameters);
|
|
152
|
+
await commit(parameters.credential);
|
|
153
|
+
return receipt;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function mapChannelError(error) {
|
|
158
|
+
const message = formatMppError(error);
|
|
159
|
+
const details = error && typeof error === "object" && "details" in error ? error.details : void 0;
|
|
160
|
+
const reason = details && Object.keys(details).length > 0 ? `${message} (${JSON.stringify(details)})` : message;
|
|
161
|
+
if (/signer mismatch|authori[sz]er|unauthori[sz]ed/i.test(message)) {
|
|
162
|
+
return new Errors.SignerMismatchError({ reason });
|
|
163
|
+
}
|
|
164
|
+
if (/already used|replay|challenge/i.test(message)) {
|
|
165
|
+
return new Errors.InvalidChallengeError({ reason });
|
|
166
|
+
}
|
|
167
|
+
if (/channel has been closed|channel is closed|finalized/i.test(message)) {
|
|
168
|
+
return new Errors.ChannelClosedError({ reason });
|
|
169
|
+
}
|
|
170
|
+
if (/exceeds channel balance|exceeds.*deposit/i.test(message)) {
|
|
171
|
+
return new Errors.AmountExceedsDepositError({ reason });
|
|
172
|
+
}
|
|
173
|
+
if (/channel not found|no channel/i.test(message)) {
|
|
174
|
+
return new Errors.ChannelNotFoundError({ reason });
|
|
175
|
+
}
|
|
176
|
+
if (/must be greater|does not cover|below.*minimum/i.test(message)) {
|
|
177
|
+
return new Errors.DeltaTooSmallError({ reason });
|
|
178
|
+
}
|
|
179
|
+
if (/signature/i.test(message)) {
|
|
180
|
+
return new Errors.InvalidSignatureError({ reason });
|
|
181
|
+
}
|
|
182
|
+
return new Errors.VerificationFailedError({ reason });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/provider/encoding.ts
|
|
186
|
+
function base64ToUtf8(b64) {
|
|
187
|
+
const binary = atob(b64);
|
|
188
|
+
const bytes = new Uint8Array(binary.length);
|
|
189
|
+
for (let i = 0; i < binary.length; i++) {
|
|
190
|
+
bytes[i] = binary.charCodeAt(i);
|
|
191
|
+
}
|
|
192
|
+
return new TextDecoder().decode(bytes);
|
|
193
|
+
}
|
|
194
|
+
function hexToBytes(hex) {
|
|
195
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
196
|
+
if (clean.length % 2 !== 0) {
|
|
197
|
+
throw new Error("hexToBytes: hex string must have an even length");
|
|
198
|
+
}
|
|
199
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
200
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
201
|
+
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
202
|
+
}
|
|
203
|
+
return bytes;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/provider/SeenTxStore.ts
|
|
207
|
+
var InMemorySeenTxStore = class {
|
|
208
|
+
map = /* @__PURE__ */ new Map();
|
|
209
|
+
order = [];
|
|
210
|
+
constructor(maxEntriesOrOptions = 1e4) {
|
|
211
|
+
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
212
|
+
this.maxEntries = options.maxEntries ?? 1e4;
|
|
213
|
+
if (options.warn !== false) {
|
|
214
|
+
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
215
|
+
if (isServerless) {
|
|
216
|
+
console.warn(
|
|
217
|
+
"[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."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
maxEntries;
|
|
223
|
+
get(key) {
|
|
224
|
+
return this.map.get(key);
|
|
225
|
+
}
|
|
226
|
+
set(key, record) {
|
|
227
|
+
if (!this.map.has(key)) {
|
|
228
|
+
this.order.push(key);
|
|
229
|
+
if (this.order.length > this.maxEntries) {
|
|
230
|
+
const evicted = this.order.shift();
|
|
231
|
+
if (evicted !== void 0) this.map.delete(evicted);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
this.map.set(key, record);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
async function sha256Hex(input) {
|
|
238
|
+
const buf = new TextEncoder().encode(input);
|
|
239
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
240
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
241
|
+
}
|
|
242
|
+
async function paymentIdempotencyKey(getHeader) {
|
|
243
|
+
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
244
|
+
if (!material) return null;
|
|
245
|
+
return sha256Hex(material);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/provider/registerProvider.ts
|
|
249
|
+
async function registerProvider(optionsOrSupabase, manifestArg, baseUrlArg, verifiedArg) {
|
|
250
|
+
let supabase;
|
|
251
|
+
let manifest;
|
|
252
|
+
let baseUrl;
|
|
253
|
+
let verified;
|
|
254
|
+
if ("supabase" in optionsOrSupabase) {
|
|
255
|
+
supabase = optionsOrSupabase.supabase;
|
|
256
|
+
manifest = optionsOrSupabase.manifest;
|
|
257
|
+
baseUrl = optionsOrSupabase.baseUrl;
|
|
258
|
+
verified = optionsOrSupabase.verified ?? true;
|
|
259
|
+
} else {
|
|
260
|
+
supabase = optionsOrSupabase;
|
|
261
|
+
if (!manifestArg || !baseUrlArg) {
|
|
262
|
+
throw new RouteDockManifestError("manifest and baseUrl are required for registerProvider");
|
|
263
|
+
}
|
|
264
|
+
manifest = manifestArg;
|
|
265
|
+
baseUrl = baseUrlArg;
|
|
266
|
+
verified = verifiedArg ?? true;
|
|
267
|
+
}
|
|
268
|
+
verifyManifestSignature(manifest);
|
|
269
|
+
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
270
|
+
const { error } = await supabase.from("providers").upsert(
|
|
271
|
+
{
|
|
272
|
+
name: manifest.name,
|
|
273
|
+
description: manifest.description ?? null,
|
|
274
|
+
base_url: normalizedBaseUrl,
|
|
275
|
+
modes: manifest.modes,
|
|
276
|
+
tags: manifest.tags ?? [],
|
|
277
|
+
categories: manifest.categories ?? [],
|
|
278
|
+
network: manifest.network,
|
|
279
|
+
payee: manifest.payee,
|
|
280
|
+
manifest,
|
|
281
|
+
verified,
|
|
282
|
+
registered_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
283
|
+
},
|
|
284
|
+
{ onConflict: "base_url" }
|
|
285
|
+
);
|
|
286
|
+
if (error) {
|
|
287
|
+
throw new Error(`Failed to register provider in Supabase: ${error.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/store/SupabaseSeenTxStore.ts
|
|
292
|
+
var SupabaseSeenTxStore = class {
|
|
293
|
+
constructor(supabase) {
|
|
294
|
+
this.supabase = supabase;
|
|
295
|
+
}
|
|
296
|
+
supabase;
|
|
297
|
+
async get(key) {
|
|
298
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
|
|
299
|
+
if (error) {
|
|
300
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
301
|
+
}
|
|
302
|
+
if (!data) return void 0;
|
|
303
|
+
const headers = data.headers;
|
|
304
|
+
return {
|
|
305
|
+
txHash: data.tx_hash ?? null,
|
|
306
|
+
...headers ? { headers } : {}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async set(key, record) {
|
|
310
|
+
const { error } = await this.supabase.from("settlements").upsert(
|
|
311
|
+
{
|
|
312
|
+
key,
|
|
313
|
+
tx_hash: record.txHash,
|
|
314
|
+
headers: record.headers ?? null
|
|
315
|
+
},
|
|
316
|
+
{ onConflict: "key" }
|
|
317
|
+
);
|
|
318
|
+
if (error) {
|
|
319
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
var MPP_NETWORK = {
|
|
324
|
+
testnet: "stellar:testnet",
|
|
325
|
+
mainnet: "stellar:pubnet"
|
|
326
|
+
};
|
|
327
|
+
function decimalToStroops(amountStr) {
|
|
328
|
+
return usdcToStroops(amountStr);
|
|
329
|
+
}
|
|
330
|
+
async function reconcileAbandonedSessions(opts) {
|
|
331
|
+
const stats = {
|
|
332
|
+
orphanedCount: 0,
|
|
333
|
+
recoveredCount: 0,
|
|
334
|
+
skippedCount: 0,
|
|
335
|
+
failedCount: 0,
|
|
336
|
+
errors: []
|
|
337
|
+
};
|
|
338
|
+
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
339
|
+
const networkId = MPP_NETWORK[opts.network];
|
|
340
|
+
try {
|
|
341
|
+
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);
|
|
342
|
+
if (error) {
|
|
343
|
+
throw new Error(`Failed to query sessions: ${error.message}`);
|
|
344
|
+
}
|
|
345
|
+
stats.orphanedCount = closingSessions?.length ?? 0;
|
|
346
|
+
if (!closingSessions || closingSessions.length === 0) {
|
|
347
|
+
return stats;
|
|
348
|
+
}
|
|
349
|
+
for (const session of closingSessions) {
|
|
350
|
+
try {
|
|
351
|
+
const {
|
|
352
|
+
channel_id: sessionId,
|
|
353
|
+
channel_contract: channelContract,
|
|
354
|
+
cumulative_amount,
|
|
355
|
+
last_signature
|
|
356
|
+
} = session;
|
|
357
|
+
const channelId = channelContract || sessionId;
|
|
358
|
+
if (!channelId || !cumulative_amount || !last_signature) {
|
|
359
|
+
stats.skippedCount++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const closeAmount = decimalToStroops(cumulative_amount);
|
|
363
|
+
const closeSig = Buffer.from(last_signature, "hex");
|
|
364
|
+
const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
|
|
365
|
+
const closeTxHash = await closeFn({
|
|
366
|
+
channel: channelId,
|
|
367
|
+
amount: closeAmount,
|
|
368
|
+
signature: closeSig,
|
|
369
|
+
feePayer: channelAuthorizer(payeeKeypair),
|
|
370
|
+
network: networkId
|
|
371
|
+
});
|
|
372
|
+
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
373
|
+
const { error: updateError } = await opts.supabase.from("sessions").update({
|
|
374
|
+
status: "closed",
|
|
375
|
+
settlement_tx_hash: closeTxHash,
|
|
376
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
377
|
+
}).eq("channel_id", sessionId);
|
|
378
|
+
if (updateError) {
|
|
379
|
+
throw new Error(`Failed to update session: ${updateError.message}`);
|
|
380
|
+
}
|
|
381
|
+
stats.recoveredCount++;
|
|
382
|
+
if (opts.onRecovered) {
|
|
383
|
+
await opts.onRecovered(channelId, closeTxHash, totalPaid);
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
stats.failedCount++;
|
|
387
|
+
stats.errors.push({
|
|
388
|
+
channelId: session.channel_id,
|
|
389
|
+
reason: formatMppError(err)
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return stats;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Session reconciliation failed: ${formatMppError(err)}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function runStartupReconciliation(opts, logger) {
|
|
401
|
+
const log = logger || console.log;
|
|
402
|
+
log("[SessionReconciler] Running startup reconciliation...");
|
|
403
|
+
try {
|
|
404
|
+
const stats = await reconcileAbandonedSessions(opts);
|
|
405
|
+
if (stats.orphanedCount === 0) {
|
|
406
|
+
log("[SessionReconciler] No orphaned sessions found");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
log(
|
|
410
|
+
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
411
|
+
);
|
|
412
|
+
if (stats.errors.length > 0) {
|
|
413
|
+
log("[SessionReconciler] Reconciliation errors:");
|
|
414
|
+
for (const err of stats.errors) {
|
|
415
|
+
log(` - ${err.channelId}: ${err.reason}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch (err) {
|
|
419
|
+
log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/provider/hono.ts
|
|
424
|
+
var CAIP2 = {
|
|
425
|
+
testnet: "stellar:testnet",
|
|
426
|
+
mainnet: "stellar:pubnet"
|
|
427
|
+
};
|
|
428
|
+
var OZ_FACILITATOR_URL = "https://channels.openzeppelin.com/x402";
|
|
429
|
+
function createX402HonoHandler(opts) {
|
|
430
|
+
const caip2 = CAIP2[opts.network];
|
|
431
|
+
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
432
|
+
const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
433
|
+
const x402Price = opts.pricing.x402;
|
|
434
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
|
|
435
|
+
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
436
|
+
const localFacilitator = new ExactStellarScheme([signer], {
|
|
437
|
+
areFeesSponsored: true
|
|
438
|
+
});
|
|
439
|
+
let ozServer = null;
|
|
440
|
+
if (useOzFacilitator) {
|
|
441
|
+
const apiKey = opts.facilitatorApiKey;
|
|
442
|
+
const facilitator = new HTTPFacilitatorClient({
|
|
443
|
+
url: OZ_FACILITATOR_URL,
|
|
444
|
+
createAuthHeaders: async () => ({
|
|
445
|
+
verify: { Authorization: `Bearer ${apiKey}` },
|
|
446
|
+
settle: { Authorization: `Bearer ${apiKey}` },
|
|
447
|
+
supported: { Authorization: `Bearer ${apiKey}` }
|
|
448
|
+
})
|
|
449
|
+
});
|
|
450
|
+
ozServer = new x402ResourceServer(facilitator);
|
|
451
|
+
ozServer.register(caip2, new ExactStellarScheme$1());
|
|
452
|
+
}
|
|
453
|
+
const amountInBaseUnits = String(usdcToUnits(x402Price));
|
|
454
|
+
const requirements = {
|
|
455
|
+
scheme: "exact",
|
|
456
|
+
network: caip2,
|
|
457
|
+
asset: opts.assetContract,
|
|
458
|
+
amount: amountInBaseUnits,
|
|
459
|
+
payTo: resolvePayee(opts.manifest, "x402"),
|
|
460
|
+
maxTimeoutSeconds: 60,
|
|
461
|
+
extra: {
|
|
462
|
+
areFeesSponsored: true,
|
|
463
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
return async (c, next) => {
|
|
467
|
+
try {
|
|
468
|
+
const paymentHeader = c.req.header("payment-signature") ?? c.req.header("x-payment");
|
|
469
|
+
if (!paymentHeader) {
|
|
470
|
+
if (ozServer) {
|
|
471
|
+
const resourceInfo = {
|
|
472
|
+
url: c.req.url,
|
|
473
|
+
description: opts.manifest.name
|
|
474
|
+
};
|
|
475
|
+
const paymentRequired = await ozServer.createPaymentRequiredResponse(
|
|
476
|
+
[requirements],
|
|
477
|
+
resourceInfo
|
|
478
|
+
);
|
|
479
|
+
c.header("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired));
|
|
480
|
+
return c.json({ error: "Payment Required" }, 402);
|
|
481
|
+
} else {
|
|
482
|
+
const x402Response = {
|
|
483
|
+
x402Version: 2,
|
|
484
|
+
resource: { url: c.req.url, description: opts.manifest.name },
|
|
485
|
+
accepts: [requirements]
|
|
486
|
+
};
|
|
487
|
+
c.header("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response));
|
|
488
|
+
return c.json({ error: "Payment Required" }, 402);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const idempotencyKey = await paymentIdempotencyKey((name) => c.req.header(name));
|
|
492
|
+
if (idempotencyKey) {
|
|
493
|
+
const cached = await seenTxStore.get(idempotencyKey);
|
|
494
|
+
if (cached) {
|
|
495
|
+
if (cached.headers) {
|
|
496
|
+
for (const [k, val] of Object.entries(cached.headers)) {
|
|
497
|
+
c.header(k, val);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
await next();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const payload = decodePaymentSignatureHeader(paymentHeader);
|
|
505
|
+
let txHash = null;
|
|
506
|
+
let paymentResponseHeader;
|
|
507
|
+
let payerAddress = null;
|
|
508
|
+
try {
|
|
509
|
+
const creds = payload.authorization?.credentials;
|
|
510
|
+
const key = Array.isArray(creds) ? creds[0]?.publicKey : void 0;
|
|
511
|
+
payerAddress = extractPayerAddress(key);
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
if (ozServer) {
|
|
515
|
+
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
516
|
+
txHash = settleResult.transaction ?? null;
|
|
517
|
+
if (settleResult) {
|
|
518
|
+
paymentResponseHeader = encodePaymentResponseHeader(
|
|
519
|
+
settleResult
|
|
520
|
+
);
|
|
521
|
+
c.header("X-Payment-Response", paymentResponseHeader);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
const verifyResult = await localFacilitator.verify(
|
|
525
|
+
payload,
|
|
526
|
+
requirements
|
|
527
|
+
);
|
|
528
|
+
if (!verifyResult.isValid) {
|
|
529
|
+
return c.json(
|
|
530
|
+
{
|
|
531
|
+
error: "Payment verification failed",
|
|
532
|
+
reason: verifyResult.invalidReason
|
|
533
|
+
},
|
|
534
|
+
401
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const settleResult = await localFacilitator.settle(
|
|
538
|
+
payload,
|
|
539
|
+
requirements
|
|
540
|
+
);
|
|
541
|
+
txHash = settleResult.transaction ?? null;
|
|
542
|
+
if (settleResult) {
|
|
543
|
+
paymentResponseHeader = encodePaymentResponseHeader(
|
|
544
|
+
settleResult
|
|
545
|
+
);
|
|
546
|
+
c.header("X-Payment-Response", paymentResponseHeader);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (idempotencyKey) {
|
|
550
|
+
const headers = {};
|
|
551
|
+
if (paymentResponseHeader) headers["X-Payment-Response"] = paymentResponseHeader;
|
|
552
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers });
|
|
553
|
+
}
|
|
554
|
+
if (txHash && opts.onSettled) {
|
|
555
|
+
Promise.resolve().then(() => opts.onSettled(txHash, x402Price, "x402", payerAddress)).catch((err) => {
|
|
556
|
+
console.error("[x402] onSettled callback error:", err);
|
|
557
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
await next();
|
|
561
|
+
} catch (err) {
|
|
562
|
+
console.error("[x402] Settlement error:", err);
|
|
563
|
+
return c.json({ error: "Payment settlement failed" }, 500);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function createMppChargeHonoHandler(opts) {
|
|
568
|
+
const networkId = CAIP2[opts.network];
|
|
569
|
+
const chargePrice = opts.pricing["mpp-charge"];
|
|
570
|
+
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
571
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
|
|
572
|
+
const mppx = Mppx.create({
|
|
573
|
+
secretKey: opts.payeeSecretKey,
|
|
574
|
+
methods: [
|
|
575
|
+
stellar({
|
|
576
|
+
recipient,
|
|
577
|
+
currency: opts.assetContract,
|
|
578
|
+
network: networkId,
|
|
579
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
580
|
+
})
|
|
581
|
+
]
|
|
582
|
+
});
|
|
583
|
+
return async (c, next) => {
|
|
584
|
+
try {
|
|
585
|
+
let payerAddress = null;
|
|
586
|
+
try {
|
|
587
|
+
const authHeader = c.req.header("authorization");
|
|
588
|
+
if (authHeader?.startsWith("Payment ")) {
|
|
589
|
+
const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
590
|
+
if (credPart) {
|
|
591
|
+
const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
|
|
592
|
+
const credJson = base64ToUtf8(b64);
|
|
593
|
+
const cred = JSON.parse(credJson);
|
|
594
|
+
const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
|
|
595
|
+
payerAddress = extractPayerAddress(key);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} catch {
|
|
599
|
+
}
|
|
600
|
+
const idempotencyKey = await paymentIdempotencyKey((name) => c.req.header(name));
|
|
601
|
+
if (idempotencyKey) {
|
|
602
|
+
const cached = await seenTxStore.get(idempotencyKey);
|
|
603
|
+
if (cached) {
|
|
604
|
+
if (cached.headers) {
|
|
605
|
+
for (const [k, val] of Object.entries(cached.headers)) {
|
|
606
|
+
c.header(k, val);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
await next();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const handler = mppx["stellar/charge"];
|
|
614
|
+
const result = await handler({
|
|
615
|
+
amount: chargePrice,
|
|
616
|
+
currency: opts.assetContract,
|
|
617
|
+
recipient,
|
|
618
|
+
description: opts.manifest.name
|
|
619
|
+
})(c.req.raw);
|
|
620
|
+
if (result.status === 402) {
|
|
621
|
+
const challenge = result.challenge;
|
|
622
|
+
const headers = {};
|
|
623
|
+
challenge.headers.forEach((v, k) => {
|
|
624
|
+
headers[k] = v;
|
|
625
|
+
});
|
|
626
|
+
return new Response(await challenge.text(), { status: 402, headers });
|
|
627
|
+
}
|
|
628
|
+
const receipt = result.withReceipt(new Response(""));
|
|
629
|
+
const receiptHeaders = {};
|
|
630
|
+
receipt.headers.forEach((v, k) => {
|
|
631
|
+
c.header(k, v);
|
|
632
|
+
receiptHeaders[k] = v;
|
|
633
|
+
});
|
|
634
|
+
const receiptHeader = receipt.headers.get("payment-receipt");
|
|
635
|
+
let reference;
|
|
636
|
+
if (receiptHeader) {
|
|
637
|
+
try {
|
|
638
|
+
const parsed = JSON.parse(base64ToUtf8(receiptHeader));
|
|
639
|
+
reference = parsed.reference;
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (idempotencyKey) {
|
|
644
|
+
await seenTxStore.set(idempotencyKey, {
|
|
645
|
+
txHash: reference ?? null,
|
|
646
|
+
headers: receiptHeaders
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (reference && opts.onSettled) {
|
|
650
|
+
Promise.resolve().then(() => opts.onSettled(reference, chargePrice, "mpp-charge", payerAddress)).catch((err) => {
|
|
651
|
+
console.error("[mpp-charge] onSettled callback error:", err);
|
|
652
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
await next();
|
|
656
|
+
} catch (err) {
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
var MPP_SESSION_WS_VERIFIED = "routedock.mppSessionWs.verified";
|
|
662
|
+
function mppSessionWsVerified(c) {
|
|
663
|
+
return c.get(MPP_SESSION_WS_VERIFIED) === true;
|
|
664
|
+
}
|
|
665
|
+
function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
|
|
666
|
+
const networkId = CAIP2[opts.network];
|
|
667
|
+
const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
|
|
668
|
+
const voucherRecordKey = `routedock:session:voucher:${sessionPricing.channelFactory}`;
|
|
669
|
+
const innerStore = opts.sessionStore ?? Store.memory();
|
|
670
|
+
let record = null;
|
|
671
|
+
let voucherCount = 0;
|
|
672
|
+
let sessionOpened = false;
|
|
673
|
+
let idleTimer = null;
|
|
674
|
+
let settledCleanly = false;
|
|
675
|
+
let abortListenerArmed = false;
|
|
676
|
+
function clearIdleTimer() {
|
|
677
|
+
if (idleTimer) {
|
|
678
|
+
clearTimeout(idleTimer);
|
|
679
|
+
idleTimer = null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function armIdleTimer() {
|
|
683
|
+
if (!opts.idleTimeoutMs) return;
|
|
684
|
+
clearIdleTimer();
|
|
685
|
+
idleTimer = setTimeout(() => {
|
|
686
|
+
void flagOrphan("idle-timeout");
|
|
687
|
+
}, opts.idleTimeoutMs);
|
|
688
|
+
const maybeUnref = idleTimer;
|
|
689
|
+
if (typeof maybeUnref.unref === "function") maybeUnref.unref();
|
|
690
|
+
}
|
|
691
|
+
async function loadPersistedRecord() {
|
|
692
|
+
if (record) return record;
|
|
693
|
+
try {
|
|
694
|
+
const stored = await innerStore.get(voucherRecordKey);
|
|
695
|
+
if (!stored || typeof stored.amount !== "string" || typeof stored.signature !== "string") {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
699
|
+
} catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
return record;
|
|
703
|
+
}
|
|
704
|
+
async function flagOrphan(reason) {
|
|
705
|
+
await loadPersistedRecord();
|
|
706
|
+
if (!sessionOpened || settledCleanly) return;
|
|
707
|
+
sessionOpened = false;
|
|
708
|
+
abortListenerArmed = false;
|
|
709
|
+
clearIdleTimer();
|
|
710
|
+
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
711
|
+
if (opts.onOrphaned) {
|
|
712
|
+
try {
|
|
713
|
+
await opts.onOrphaned(sessionPricing.channelFactory, {
|
|
714
|
+
cumulativeAmount,
|
|
715
|
+
lastSignature: record?.signature ?? "",
|
|
716
|
+
voucherCount,
|
|
717
|
+
reason
|
|
718
|
+
});
|
|
719
|
+
} catch (err) {
|
|
720
|
+
console.error("[mpp-session] onOrphaned handler failed:", err);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const wrappedStore = {
|
|
725
|
+
async get(key) {
|
|
726
|
+
return innerStore.get(key);
|
|
727
|
+
},
|
|
728
|
+
async put(key, value) {
|
|
729
|
+
return innerStore.put(key, value);
|
|
730
|
+
},
|
|
731
|
+
async delete(key) {
|
|
732
|
+
return innerStore.delete(key);
|
|
733
|
+
},
|
|
734
|
+
async update(key, fn) {
|
|
735
|
+
const storeWithUpdate = innerStore;
|
|
736
|
+
if (typeof storeWithUpdate.update === "function") {
|
|
737
|
+
return storeWithUpdate.update(key, fn);
|
|
738
|
+
}
|
|
739
|
+
throw new Error("Store does not support atomic update operations");
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
async function commit(credential) {
|
|
743
|
+
await loadPersistedRecord();
|
|
744
|
+
const payload = credential.payload;
|
|
745
|
+
if (typeof payload?.amount !== "string" || typeof payload.signature !== "string") return;
|
|
746
|
+
let amount;
|
|
747
|
+
try {
|
|
748
|
+
amount = BigInt(payload.amount);
|
|
749
|
+
} catch {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (record && amount < record.amount) return;
|
|
753
|
+
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
754
|
+
record = { amount, signature: payload.signature, payer };
|
|
755
|
+
await innerStore.put(voucherRecordKey, {
|
|
756
|
+
amount: record.amount.toString(),
|
|
757
|
+
signature: record.signature,
|
|
758
|
+
payer: record.payer
|
|
759
|
+
});
|
|
760
|
+
voucherCount++;
|
|
761
|
+
settledCleanly = false;
|
|
762
|
+
armIdleTimer();
|
|
763
|
+
if (!sessionOpened) {
|
|
764
|
+
sessionOpened = true;
|
|
765
|
+
if (opts.onSessionOpen) {
|
|
766
|
+
Promise.resolve().then(() => opts.onSessionOpen(sessionPricing.channelFactory, record.payer)).catch((err) => {
|
|
767
|
+
console.error("[mpp-session] onSessionOpen callback error:", err);
|
|
768
|
+
opts.onCallbackError?.(err, "onSessionOpen");
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (opts.onVoucher) {
|
|
773
|
+
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
774
|
+
Promise.resolve().then(() => opts.onVoucher(sessionPricing.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
775
|
+
console.error("[mpp-session] onVoucher callback error:", err);
|
|
776
|
+
opts.onCallbackError?.(err, "onVoucher");
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const mppx = Mppx.create({
|
|
781
|
+
secretKey: opts.payeeSecretKey,
|
|
782
|
+
methods: [
|
|
783
|
+
withTypedChannelErrors(
|
|
784
|
+
onVerifiedCredential(
|
|
785
|
+
stellar$1({
|
|
786
|
+
channel: sessionPricing.channelFactory,
|
|
787
|
+
commitmentKey: opts.commitmentPublicKey,
|
|
788
|
+
network: networkId,
|
|
789
|
+
store: wrappedStore,
|
|
790
|
+
sourceAccount: payeeKeypair.publicKey(),
|
|
791
|
+
feePayer: channelAuthorizer(payeeKeypair)
|
|
792
|
+
}),
|
|
793
|
+
commit
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
]
|
|
797
|
+
});
|
|
798
|
+
return {
|
|
799
|
+
mppx,
|
|
800
|
+
async handleDelete(c) {
|
|
801
|
+
await loadPersistedRecord();
|
|
802
|
+
let body;
|
|
803
|
+
try {
|
|
804
|
+
body = await c.req.json();
|
|
805
|
+
} catch {
|
|
806
|
+
}
|
|
807
|
+
const bodyAmount = body?.amount ? BigInt(body.amount) : 0n;
|
|
808
|
+
const recordAmount = record?.amount ?? 0n;
|
|
809
|
+
let closeAmount;
|
|
810
|
+
let closeSig;
|
|
811
|
+
if (bodyAmount > recordAmount) {
|
|
812
|
+
closeAmount = bodyAmount;
|
|
813
|
+
closeSig = body?.signature ?? record?.signature ?? "";
|
|
814
|
+
} else {
|
|
815
|
+
closeAmount = recordAmount;
|
|
816
|
+
closeSig = record?.signature || (bodyAmount === recordAmount ? body?.signature ?? "" : "");
|
|
817
|
+
}
|
|
818
|
+
if (closeAmount > 0n && closeSig) {
|
|
819
|
+
const closeTxHash = await close({
|
|
820
|
+
channel: sessionPricing.channelFactory,
|
|
821
|
+
amount: closeAmount,
|
|
822
|
+
signature: hexToBytes(closeSig),
|
|
823
|
+
feePayer: channelAuthorizer(payeeKeypair),
|
|
824
|
+
network: networkId
|
|
825
|
+
});
|
|
826
|
+
settledCleanly = true;
|
|
827
|
+
clearIdleTimer();
|
|
828
|
+
const closePayer = record?.payer ?? null;
|
|
829
|
+
if (opts.onSettled) {
|
|
830
|
+
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
831
|
+
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, reportMode, closePayer)).catch((err) => {
|
|
832
|
+
console.error(`[mpp-session] onSettled callback error:`, err);
|
|
833
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
sessionOpened = false;
|
|
837
|
+
voucherCount = 0;
|
|
838
|
+
record = null;
|
|
839
|
+
await innerStore.delete(voucherRecordKey);
|
|
840
|
+
abortListenerArmed = false;
|
|
841
|
+
return c.json({ closeTxHash });
|
|
842
|
+
}
|
|
843
|
+
return c.json({ closeTxHash: null, message: "no vouchers received" });
|
|
844
|
+
},
|
|
845
|
+
armAbortGuard(c) {
|
|
846
|
+
const signal = c.req.raw.signal;
|
|
847
|
+
if (signal && !abortListenerArmed) {
|
|
848
|
+
abortListenerArmed = true;
|
|
849
|
+
signal.addEventListener(
|
|
850
|
+
"abort",
|
|
851
|
+
() => {
|
|
852
|
+
void flagOrphan("connection-closed");
|
|
853
|
+
},
|
|
854
|
+
{ once: true }
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function createMppSessionHonoHandler(opts, config, state) {
|
|
861
|
+
const sessionPricing = opts.pricing[config.mode];
|
|
862
|
+
return async (c, next) => {
|
|
863
|
+
try {
|
|
864
|
+
if (c.req.method === "DELETE") {
|
|
865
|
+
return state.handleDelete(c);
|
|
866
|
+
}
|
|
867
|
+
const result = await state.mppx.channel({
|
|
868
|
+
amount: sessionPricing.rate,
|
|
869
|
+
description: opts.manifest.name
|
|
870
|
+
})(c.req.raw);
|
|
871
|
+
if (result.status === 402) {
|
|
872
|
+
const challenge = result.challenge;
|
|
873
|
+
const headers = {};
|
|
874
|
+
challenge.headers.forEach((v, k) => {
|
|
875
|
+
headers[k] = v;
|
|
876
|
+
});
|
|
877
|
+
return new Response(await challenge.text(), { status: 402, headers });
|
|
878
|
+
}
|
|
879
|
+
state.armAbortGuard(c);
|
|
880
|
+
if (config.mode === "mpp-session-ws" && isWebSocketUpgradeRequest(c)) {
|
|
881
|
+
c.set(MPP_SESSION_WS_VERIFIED, true);
|
|
882
|
+
await next();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
await next();
|
|
886
|
+
} catch (err) {
|
|
887
|
+
throw err;
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
function isWebSocketUpgradeRequest(c) {
|
|
892
|
+
return (c.req.header("upgrade") ?? "").toLowerCase() === "websocket";
|
|
893
|
+
}
|
|
894
|
+
function routedockHono(opts) {
|
|
895
|
+
const handlers = [];
|
|
896
|
+
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
897
|
+
if (opts.modes.includes("x402") && opts.pricing.x402) {
|
|
898
|
+
handlers.push(createX402HonoHandler({ ...opts, manifest: signedManifest }));
|
|
899
|
+
}
|
|
900
|
+
if (opts.modes.includes("mpp-charge") && opts.pricing["mpp-charge"]) {
|
|
901
|
+
handlers.push(createMppChargeHonoHandler({ ...opts, manifest: signedManifest }));
|
|
902
|
+
}
|
|
903
|
+
const sessionModes = ["mpp-session", "mpp-session-ws"].filter(
|
|
904
|
+
(mode) => opts.modes.includes(mode) && !!opts.pricing[mode]
|
|
905
|
+
);
|
|
906
|
+
const primarySessionMode = sessionModes[0];
|
|
907
|
+
if (primarySessionMode) {
|
|
908
|
+
if (!opts.commitmentPublicKey) {
|
|
909
|
+
throw new Error("routedockHono: mpp-session/mpp-session-ws mode requires commitmentPublicKey");
|
|
910
|
+
}
|
|
911
|
+
const sessionPricing = opts.pricing[primarySessionMode];
|
|
912
|
+
const sharedState = createMppSessionHandlerState(opts, sessionPricing, primarySessionMode);
|
|
913
|
+
for (const mode of sessionModes) {
|
|
914
|
+
handlers.push(
|
|
915
|
+
createMppSessionHonoHandler({ ...opts, manifest: signedManifest }, { mode }, sharedState)
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return async (c, next) => {
|
|
920
|
+
if (c.req.path === "/.well-known/routedock.json") {
|
|
921
|
+
return c.json(signedManifest);
|
|
922
|
+
}
|
|
923
|
+
if (handlers.length === 0) {
|
|
924
|
+
await next();
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const hasX402Header = !!(c.req.header("payment-signature") || c.req.header("x-payment"));
|
|
928
|
+
const prefersX402 = c.req.header("x-preferred-mode") === "x402";
|
|
929
|
+
const handler = hasX402Header || prefersX402 ? handlers[0] : handlers[handlers.length - 1];
|
|
930
|
+
if (!handler) {
|
|
931
|
+
await next();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
return handler(c, next);
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export { InMemorySeenTxStore, MPP_SESSION_WS_VERIFIED, SupabaseSeenTxStore, mppSessionWsVerified, reconcileAbandonedSessions, registerProvider, routedockHono, runStartupReconciliation, signManifest };
|