@routedock/routedock 0.1.2 → 0.3.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 +532 -4
- package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
- package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
- package/dist/ProviderRegistry-CA_TdREG.d.ts +142 -0
- package/dist/ProviderRegistry-D-0rqK0N.d.cts +142 -0
- package/dist/RouteDockClient-Bh86bVly.d.ts +469 -0
- package/dist/RouteDockClient-go9aopfF.d.cts +469 -0
- package/dist/SessionReconciler-Bw-PoBVl.d.ts +134 -0
- package/dist/SessionReconciler-Cz2nuOlJ.d.cts +134 -0
- package/dist/SessionStore-B4-Hi4H8.d.cts +143 -0
- package/dist/SessionStore-BaYiegP0.d.ts +143 -0
- package/dist/client.cjs +2746 -217
- package/dist/client.d.cts +51 -81
- package/dist/client.d.ts +51 -81
- package/dist/client.js +2690 -218
- package/dist/index.cjs +3634 -420
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +3569 -419
- package/dist/logger-D8_Q2ytp.d.cts +467 -0
- package/dist/logger-D8_Q2ytp.d.ts +467 -0
- package/dist/provider/fastify.cjs +1099 -0
- package/dist/provider/fastify.d.cts +40 -0
- package/dist/provider/fastify.d.ts +40 -0
- package/dist/provider/fastify.js +1097 -0
- package/dist/provider/hono.cjs +1152 -0
- package/dist/provider/hono.d.cts +101 -0
- package/dist/provider/hono.d.ts +101 -0
- package/dist/provider/hono.js +1142 -0
- package/dist/provider.cjs +1105 -147
- package/dist/provider.d.cts +38 -27
- package/dist/provider.d.ts +38 -27
- package/dist/provider.js +1079 -148
- package/dist/react.cjs +2931 -0
- package/dist/react.d.cts +107 -0
- package/dist/react.d.ts +107 -0
- package/dist/react.js +2924 -0
- package/dist/routedockMiddleware-57gUaEMf.d.ts +64 -0
- package/dist/routedockMiddleware-DlyLrxp7.d.cts +64 -0
- package/dist/schema.cjs +404 -0
- package/dist/schema.d.cts +3 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +399 -0
- package/dist/sign-AKH67sft.d.cts +53 -0
- package/dist/sign-BEq-OsSq.d.ts +53 -0
- package/dist/testing.cjs +59 -0
- package/dist/testing.d.cts +114 -0
- package/dist/testing.d.ts +114 -0
- package/dist/testing.js +56 -0
- package/package.json +143 -23
- package/PUBLISH_CHECKLIST.md +0 -70
- package/dist/SessionStore-CCVO0PHM.d.cts +0 -152
- package/dist/SessionStore-CCVO0PHM.d.ts +0 -152
- package/dist/routedockMiddleware-B7WTB3LT.d.cts +0 -53
- package/dist/routedockMiddleware-DRgdrycG.d.ts +0 -53
- package/src/client/ModeRouter.ts +0 -104
- package/src/client/MppChargeClient.ts +0 -44
- package/src/client/MppSessionClient.ts +0 -144
- package/src/client/RouteDockClient.ts +0 -124
- package/src/client/index.ts +0 -8
- package/src/client/x402Client.ts +0 -84
- package/src/index.ts +0 -9
- package/src/provider/MppChargeHandler.ts +0 -90
- package/src/provider/MppSessionHandler.ts +0 -157
- package/src/provider/index.ts +0 -7
- package/src/provider/routedockMiddleware.ts +0 -154
- package/src/provider/x402Handler.ts +0 -164
- package/src/schemas/routedock.schema.json +0 -159
- package/src/store/SessionStore.ts +0 -90
- package/src/tests/smoke.test.ts +0 -228
- package/src/types.ts +0 -165
- package/tsconfig.json +0 -14
- package/tsup.config.ts +0 -15
package/dist/provider.cjs
CHANGED
|
@@ -6,43 +6,308 @@ var server$1 = require('@x402/stellar/exact/server');
|
|
|
6
6
|
var server = require('@x402/core/server');
|
|
7
7
|
var stellar = require('@x402/stellar');
|
|
8
8
|
var http = require('@x402/core/http');
|
|
9
|
-
var
|
|
9
|
+
var nulthSdk = require('@routedock/nulth-sdk');
|
|
10
|
+
var server$4 = require('@stellar/mpp/charge/server');
|
|
10
11
|
var server$2 = require('mppx/server');
|
|
11
|
-
var server$
|
|
12
|
+
var server$3 = require('@stellar/mpp/channel/server');
|
|
13
|
+
var mppx = require('mppx');
|
|
14
|
+
var crypto$1 = require('crypto');
|
|
12
15
|
|
|
13
|
-
// src/
|
|
16
|
+
// src/errors.ts
|
|
14
17
|
var RouteDockError = class extends Error {
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
code;
|
|
19
|
+
retryable;
|
|
20
|
+
retryAfterMs;
|
|
21
|
+
constructor(message, code, retryable, options = {}) {
|
|
22
|
+
super(message, { cause: options.cause });
|
|
17
23
|
this.name = "RouteDockError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.retryable = retryable;
|
|
26
|
+
if (options.retryAfterMs !== void 0) {
|
|
27
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
28
|
+
}
|
|
18
29
|
}
|
|
19
30
|
};
|
|
20
31
|
var RouteDockManifestError = class extends RouteDockError {
|
|
21
|
-
constructor(message) {
|
|
22
|
-
super(message);
|
|
32
|
+
constructor(message, options = {}) {
|
|
33
|
+
super(message, "MANIFEST", false, options);
|
|
23
34
|
this.name = "RouteDockManifestError";
|
|
24
35
|
}
|
|
25
36
|
};
|
|
26
37
|
var RouteDockNoSupportedModeError = class extends RouteDockError {
|
|
27
|
-
constructor(message) {
|
|
28
|
-
super(message);
|
|
38
|
+
constructor(message, options = {}) {
|
|
39
|
+
super(message, "NO_SUPPORTED_MODE", false, options);
|
|
29
40
|
this.name = "RouteDockNoSupportedModeError";
|
|
30
41
|
}
|
|
31
42
|
};
|
|
32
|
-
var
|
|
33
|
-
constructor(message) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.name = "
|
|
43
|
+
var RouteDockClientVersionError = class extends RouteDockError {
|
|
44
|
+
constructor(message, options = {}) {
|
|
45
|
+
super(message, "CLIENT_VERSION_TOO_OLD", false, options);
|
|
46
|
+
this.name = "RouteDockClientVersionError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var RouteDockTrustlineError = class extends RouteDockError {
|
|
50
|
+
asset;
|
|
51
|
+
issuer;
|
|
52
|
+
remediation;
|
|
53
|
+
constructor(asset, issuer, remediation, options = {}) {
|
|
54
|
+
super(
|
|
55
|
+
`Missing trustline for ${asset} (issuer: ${issuer}). ${remediation}`,
|
|
56
|
+
"TRUSTLINE",
|
|
57
|
+
false,
|
|
58
|
+
options
|
|
59
|
+
);
|
|
60
|
+
this.name = "RouteDockTrustlineError";
|
|
61
|
+
this.asset = asset;
|
|
62
|
+
this.issuer = issuer;
|
|
63
|
+
this.remediation = remediation;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var RouteDockFacilitatorError = class extends RouteDockError {
|
|
67
|
+
status;
|
|
68
|
+
constructor(message, status, options = {}) {
|
|
69
|
+
super(message, "FACILITATOR", true, options);
|
|
70
|
+
this.name = "RouteDockFacilitatorError";
|
|
71
|
+
this.status = status;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var RouteDockNetworkError = class extends RouteDockError {
|
|
75
|
+
constructor(message, options = {}) {
|
|
76
|
+
super(message, "NETWORK", true, options);
|
|
77
|
+
this.name = "RouteDockNetworkError";
|
|
36
78
|
}
|
|
37
79
|
};
|
|
38
|
-
var
|
|
80
|
+
var RouteDockSignatureError = class extends RouteDockError {
|
|
81
|
+
constructor(message, options = {}) {
|
|
82
|
+
super(message, "SIGNATURE", false, options);
|
|
83
|
+
this.name = "RouteDockSignatureError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var RouteDockVoucherMonotonicityError = class extends RouteDockError {
|
|
87
|
+
constructor(message, options = {}) {
|
|
88
|
+
super(message, "VOUCHER_MONOTONICITY", false, options);
|
|
89
|
+
this.name = "RouteDockVoucherMonotonicityError";
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var RouteDockPolicyRejectError = class extends RouteDockError {
|
|
39
93
|
reason;
|
|
40
|
-
constructor(reason) {
|
|
41
|
-
super(`Payment rejected by policy: ${reason}
|
|
42
|
-
this.name = "
|
|
94
|
+
constructor(reason, options = {}) {
|
|
95
|
+
super(`Payment rejected by policy: ${reason}`, "POLICY_REJECT", false, options);
|
|
96
|
+
this.name = "RouteDockPolicyRejectError";
|
|
43
97
|
this.reason = reason;
|
|
44
98
|
}
|
|
45
99
|
};
|
|
100
|
+
var RouteDockChannelStateError = class extends RouteDockError {
|
|
101
|
+
constructor(message, options = {}) {
|
|
102
|
+
super(message, "CHANNEL_STATE", false, options);
|
|
103
|
+
this.name = "RouteDockChannelStateError";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var RouteDockDisputeError = class extends RouteDockError {
|
|
107
|
+
constructor(message, options = {}) {
|
|
108
|
+
super(message, "DISPUTE", false, options);
|
|
109
|
+
this.name = "RouteDockDisputeError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var RouteDockRefundWindowError = class extends RouteDockError {
|
|
113
|
+
constructor(message, options = {}) {
|
|
114
|
+
super(message, "REFUND_WINDOW", false, options);
|
|
115
|
+
this.name = "RouteDockRefundWindowError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var RouteDockPolicyRejectedError = RouteDockPolicyRejectError;
|
|
119
|
+
var RouteDockSessionError = RouteDockVoucherMonotonicityError;
|
|
120
|
+
|
|
121
|
+
// src/types.ts
|
|
122
|
+
var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
|
|
123
|
+
|
|
124
|
+
// src/internal/assetUtils.ts
|
|
125
|
+
function normalizeManifestAssets(manifest) {
|
|
126
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
127
|
+
return manifest.assets;
|
|
128
|
+
}
|
|
129
|
+
if (manifest.asset && manifest.asset_contract) {
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
asset: manifest.asset,
|
|
133
|
+
asset_contract: manifest.asset_contract
|
|
134
|
+
}
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
throw new RouteDockManifestError(
|
|
138
|
+
"Manifest must define either assets[] or root asset/asset_contract fields"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
function getEligibleAssets(manifest, mode, endpoint) {
|
|
142
|
+
const assets = normalizeManifestAssets(manifest);
|
|
143
|
+
return assets.filter((asset) => {
|
|
144
|
+
if (asset.modes && !asset.modes.includes(mode)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
|
|
148
|
+
const trimmedEndpoint = endpoint.replace(/^\//, "");
|
|
149
|
+
if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (manifest.endpoints) {
|
|
153
|
+
let pathname = endpoint;
|
|
154
|
+
if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
|
|
155
|
+
try {
|
|
156
|
+
pathname = new URL(endpoint).pathname;
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const trimmedPathname = pathname.replace(/^\//, "");
|
|
161
|
+
const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
|
|
162
|
+
const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
|
|
163
|
+
const keyMatches = key === endpoint || key === trimmedEndpoint;
|
|
164
|
+
if (pathMatches || keyMatches) {
|
|
165
|
+
return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
});
|
|
169
|
+
if (!matchesEndpoint) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
|
|
180
|
+
if (manifest.assets && manifest.assets.length > 0) {
|
|
181
|
+
return selectAsset(manifest, mode, endpoint).asset_contract;
|
|
182
|
+
}
|
|
183
|
+
return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
|
|
184
|
+
}
|
|
185
|
+
function selectAsset(manifest, mode, endpoint) {
|
|
186
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
187
|
+
if (eligible.length === 0) {
|
|
188
|
+
const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
|
|
189
|
+
throw new RouteDockManifestError(
|
|
190
|
+
`No eligible assets found for mode '${mode}'${endpointMsg}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return eligible[0];
|
|
194
|
+
}
|
|
195
|
+
function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
|
|
196
|
+
const eligible = getEligibleAssets(manifest, mode, endpoint);
|
|
197
|
+
return eligible.some(
|
|
198
|
+
(asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/internal/payee.ts
|
|
203
|
+
function resolvePayee(manifest, mode) {
|
|
204
|
+
const override = manifest.pricing[mode]?.payee;
|
|
205
|
+
return override ?? manifest.payee;
|
|
206
|
+
}
|
|
207
|
+
var usdcToUnits = nulthSdk.usdcToStroops;
|
|
208
|
+
function extractPayerAddress(key) {
|
|
209
|
+
if (typeof key !== "string") return null;
|
|
210
|
+
return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/internal/logger.ts
|
|
214
|
+
function createConsoleLogger() {
|
|
215
|
+
return (level, message, fields) => {
|
|
216
|
+
const values = fields ? Object.values(fields) : [];
|
|
217
|
+
if (values.length > 0) {
|
|
218
|
+
console[level](message, ...values);
|
|
219
|
+
} else {
|
|
220
|
+
console[level](message);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
var consoleLogger = createConsoleLogger();
|
|
225
|
+
function resolveLogger(logger, fallback = consoleLogger) {
|
|
226
|
+
return logger ?? fallback;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/provider/SeenTxStore.ts
|
|
230
|
+
var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
|
|
231
|
+
var MAX_SETTLEMENT_REPLAYS = 1;
|
|
232
|
+
async function checkSettlementReplay(store, key, now = Date.now()) {
|
|
233
|
+
const record = await store.get(key);
|
|
234
|
+
if (!record) return { kind: "miss" };
|
|
235
|
+
const { createdAt } = record;
|
|
236
|
+
if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
|
|
237
|
+
return { kind: "spent" };
|
|
238
|
+
}
|
|
239
|
+
if (store.claimReplay) {
|
|
240
|
+
const replayed = await store.claimReplay(key);
|
|
241
|
+
if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
|
|
242
|
+
}
|
|
243
|
+
return { kind: "replay", record };
|
|
244
|
+
}
|
|
245
|
+
var InMemorySeenTxStore = class {
|
|
246
|
+
map = /* @__PURE__ */ new Map();
|
|
247
|
+
replayCounts = /* @__PURE__ */ new Map();
|
|
248
|
+
order = [];
|
|
249
|
+
constructor(maxEntriesOrOptions = 1e4) {
|
|
250
|
+
const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
|
|
251
|
+
this.maxEntries = options.maxEntries ?? 1e4;
|
|
252
|
+
if (options.warn !== false) {
|
|
253
|
+
const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
254
|
+
if (isServerless) {
|
|
255
|
+
resolveLogger(options.logger)(
|
|
256
|
+
"warn",
|
|
257
|
+
"[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."
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
maxEntries;
|
|
263
|
+
get(key) {
|
|
264
|
+
return this.map.get(key);
|
|
265
|
+
}
|
|
266
|
+
set(key, record) {
|
|
267
|
+
if (!this.map.has(key)) {
|
|
268
|
+
this.order.push(key);
|
|
269
|
+
if (this.order.length > this.maxEntries) {
|
|
270
|
+
const evicted = this.order.shift();
|
|
271
|
+
if (evicted !== void 0) {
|
|
272
|
+
this.map.delete(evicted);
|
|
273
|
+
this.replayCounts.delete(evicted);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
this.map.set(key, record);
|
|
278
|
+
}
|
|
279
|
+
claimReplay(key) {
|
|
280
|
+
if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
|
|
281
|
+
const next = (this.replayCounts.get(key) ?? 0) + 1;
|
|
282
|
+
this.replayCounts.set(key, next);
|
|
283
|
+
return next;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
async function sha256Hex(input) {
|
|
287
|
+
const buf = new TextEncoder().encode(input);
|
|
288
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
289
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
290
|
+
}
|
|
291
|
+
async function paymentIdempotencyKey(getHeader, scope) {
|
|
292
|
+
const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
|
|
293
|
+
if (!material) return null;
|
|
294
|
+
return sha256Hex(
|
|
295
|
+
[scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/provider/settleResult.ts
|
|
300
|
+
function readSettleResult(result) {
|
|
301
|
+
const settle = result;
|
|
302
|
+
const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
|
|
303
|
+
if (!settle || settle.success !== true) return failed(settle?.errorReason);
|
|
304
|
+
if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
|
|
305
|
+
return failed(settle.errorReason);
|
|
306
|
+
}
|
|
307
|
+
return { ok: true, txHash: settle.transaction };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/provider/x402Handler.ts
|
|
46
311
|
var CAIP2 = {
|
|
47
312
|
testnet: "stellar:testnet",
|
|
48
313
|
mainnet: "stellar:pubnet"
|
|
@@ -52,6 +317,8 @@ function createX402Handler(opts) {
|
|
|
52
317
|
const caip2 = CAIP2[opts.network];
|
|
53
318
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
54
319
|
const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
|
|
320
|
+
const logger = resolveLogger(opts.logger);
|
|
321
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
55
322
|
const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
|
|
56
323
|
const localFacilitator = new facilitator.ExactStellarScheme([signer], {
|
|
57
324
|
areFeesSponsored: true
|
|
@@ -70,23 +337,31 @@ function createX402Handler(opts) {
|
|
|
70
337
|
ozServer = new server.x402ResourceServer(facilitator);
|
|
71
338
|
ozServer.register(caip2, new server$1.ExactStellarScheme());
|
|
72
339
|
}
|
|
73
|
-
const amountInBaseUnits = String(
|
|
74
|
-
const
|
|
75
|
-
scheme: "exact",
|
|
76
|
-
network: caip2,
|
|
77
|
-
asset: opts.assetContract,
|
|
78
|
-
amount: amountInBaseUnits,
|
|
79
|
-
payTo: opts.manifest.payee,
|
|
80
|
-
maxTimeoutSeconds: 60,
|
|
81
|
-
extra: {
|
|
82
|
-
areFeesSponsored: true,
|
|
83
|
-
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
84
|
-
}
|
|
85
|
-
};
|
|
340
|
+
const amountInBaseUnits = String(usdcToUnits(opts.amount));
|
|
341
|
+
const payTo = resolvePayee(opts.manifest, "x402");
|
|
86
342
|
return async (req, res, next) => {
|
|
87
343
|
try {
|
|
88
|
-
const
|
|
89
|
-
|
|
344
|
+
const endpoint = req.path || req.originalUrl;
|
|
345
|
+
const assetContract = resolveAssetContract(
|
|
346
|
+
opts.manifest,
|
|
347
|
+
"x402",
|
|
348
|
+
endpoint,
|
|
349
|
+
opts.assetContract
|
|
350
|
+
);
|
|
351
|
+
const requirements = {
|
|
352
|
+
scheme: "exact",
|
|
353
|
+
network: caip2,
|
|
354
|
+
asset: assetContract,
|
|
355
|
+
amount: amountInBaseUnits,
|
|
356
|
+
payTo,
|
|
357
|
+
maxTimeoutSeconds: 60,
|
|
358
|
+
extra: {
|
|
359
|
+
areFeesSponsored: true,
|
|
360
|
+
...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const respondPaymentRequired = async (error, reason) => {
|
|
364
|
+
const body = { error, ...reason ? { reason } : {} };
|
|
90
365
|
if (ozServer) {
|
|
91
366
|
const resourceInfo = {
|
|
92
367
|
url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
|
|
@@ -96,7 +371,7 @@ function createX402Handler(opts) {
|
|
|
96
371
|
[requirements],
|
|
97
372
|
resourceInfo
|
|
98
373
|
);
|
|
99
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(
|
|
374
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
|
|
100
375
|
} else {
|
|
101
376
|
const x402Response = {
|
|
102
377
|
x402Version: 2,
|
|
@@ -106,23 +381,65 @@ function createX402Handler(opts) {
|
|
|
106
381
|
},
|
|
107
382
|
accepts: [requirements]
|
|
108
383
|
};
|
|
109
|
-
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(
|
|
384
|
+
res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
|
|
110
385
|
}
|
|
386
|
+
};
|
|
387
|
+
const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
|
|
388
|
+
if (!paymentHeader) {
|
|
389
|
+
await respondPaymentRequired("Payment Required");
|
|
111
390
|
return;
|
|
112
391
|
}
|
|
392
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
393
|
+
(name) => {
|
|
394
|
+
const v = req.headers[name.toLowerCase()];
|
|
395
|
+
return Array.isArray(v) ? v[0] : v;
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
method: req.method,
|
|
399
|
+
path: req.originalUrl.split("?")[0],
|
|
400
|
+
amount: requirements.amount,
|
|
401
|
+
payTo: requirements.payTo
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
if (idempotencyKey) {
|
|
405
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
406
|
+
if (replayCheck.kind === "spent") {
|
|
407
|
+
res.status(402).json({ error: "Payment already used" });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (replayCheck.kind === "replay") {
|
|
411
|
+
if (replayCheck.record.headers) {
|
|
412
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
413
|
+
res.setHeader(k, val);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
next();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
113
420
|
const payload = http.decodePaymentSignatureHeader(paymentHeader);
|
|
114
421
|
let txHash = null;
|
|
422
|
+
let payerAddress = null;
|
|
423
|
+
try {
|
|
424
|
+
const creds = payload.authorization?.credentials;
|
|
425
|
+
const key = Array.isArray(creds) ? creds[0]?.publicKey : void 0;
|
|
426
|
+
payerAddress = extractPayerAddress(key);
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
115
429
|
if (ozServer) {
|
|
116
430
|
const settleResult = await ozServer.settlePayment(payload, requirements);
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
http.encodePaymentResponseHeader(
|
|
122
|
-
settleResult
|
|
123
|
-
)
|
|
124
|
-
);
|
|
431
|
+
const outcome = readSettleResult(settleResult);
|
|
432
|
+
if (!outcome.ok) {
|
|
433
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
434
|
+
return;
|
|
125
435
|
}
|
|
436
|
+
txHash = outcome.txHash;
|
|
437
|
+
res.setHeader(
|
|
438
|
+
"X-Payment-Response",
|
|
439
|
+
http.encodePaymentResponseHeader(
|
|
440
|
+
settleResult
|
|
441
|
+
)
|
|
442
|
+
);
|
|
126
443
|
} else {
|
|
127
444
|
const verifyResult = await localFacilitator.verify(
|
|
128
445
|
payload,
|
|
@@ -139,22 +456,40 @@ function createX402Handler(opts) {
|
|
|
139
456
|
payload,
|
|
140
457
|
requirements
|
|
141
458
|
);
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
459
|
+
const outcome = readSettleResult(settleResult);
|
|
460
|
+
if (!outcome.ok) {
|
|
461
|
+
await respondPaymentRequired("Payment settlement failed", outcome.reason);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
txHash = outcome.txHash;
|
|
465
|
+
res.setHeader(
|
|
466
|
+
"X-Payment-Response",
|
|
467
|
+
http.encodePaymentResponseHeader(
|
|
468
|
+
settleResult
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (idempotencyKey) {
|
|
473
|
+
const headers = {};
|
|
474
|
+
const paymentResponse = res.getHeader("X-Payment-Response");
|
|
475
|
+
if (typeof paymentResponse === "string") {
|
|
476
|
+
headers["X-Payment-Response"] = paymentResponse;
|
|
150
477
|
}
|
|
478
|
+
await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
|
|
151
479
|
}
|
|
152
480
|
if (txHash && opts.onSettled) {
|
|
153
|
-
|
|
481
|
+
Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
|
|
482
|
+
logger("error", "[x402] onSettled callback error", { error: err });
|
|
483
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
484
|
+
});
|
|
154
485
|
}
|
|
155
486
|
next();
|
|
156
487
|
} catch (err) {
|
|
157
|
-
|
|
488
|
+
if (err instanceof RouteDockManifestError) {
|
|
489
|
+
next(err);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
logger("error", "[x402] Settlement error", { error: err });
|
|
158
493
|
res.status(500).json({ error: "Payment settlement failed" });
|
|
159
494
|
}
|
|
160
495
|
};
|
|
@@ -166,25 +501,87 @@ var MPP_NETWORK = {
|
|
|
166
501
|
function createMppChargeHandler(opts) {
|
|
167
502
|
const networkId = MPP_NETWORK[opts.network];
|
|
168
503
|
const amountHumanReadable = opts.amount;
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
504
|
+
const recipient = resolvePayee(opts.manifest, "mpp-charge");
|
|
505
|
+
const logger = resolveLogger(opts.logger);
|
|
506
|
+
const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
|
|
507
|
+
const mppxInstances = /* @__PURE__ */ new Map();
|
|
508
|
+
function getMppx(contract) {
|
|
509
|
+
let instance = mppxInstances.get(contract);
|
|
510
|
+
if (!instance) {
|
|
511
|
+
instance = server$2.Mppx.create({
|
|
512
|
+
secretKey: opts.payeeSecretKey,
|
|
513
|
+
methods: [
|
|
514
|
+
server$4.stellar.charge({
|
|
515
|
+
recipient,
|
|
516
|
+
currency: contract,
|
|
517
|
+
network: networkId,
|
|
518
|
+
feePayer: { envelopeSigner: opts.payeeSecretKey }
|
|
519
|
+
})
|
|
520
|
+
]
|
|
521
|
+
});
|
|
522
|
+
mppxInstances.set(contract, instance);
|
|
523
|
+
}
|
|
524
|
+
return instance;
|
|
525
|
+
}
|
|
180
526
|
return async (req, res, next) => {
|
|
181
527
|
try {
|
|
528
|
+
const endpoint = req.path || req.originalUrl;
|
|
529
|
+
const assetContract = resolveAssetContract(
|
|
530
|
+
opts.manifest,
|
|
531
|
+
"mpp-charge",
|
|
532
|
+
endpoint,
|
|
533
|
+
opts.assetContract
|
|
534
|
+
);
|
|
535
|
+
const mppx = getMppx(assetContract);
|
|
536
|
+
let payerAddress = null;
|
|
537
|
+
try {
|
|
538
|
+
const authHeader = req.headers["authorization"];
|
|
539
|
+
if (typeof authHeader === "string" && authHeader.startsWith("Payment ")) {
|
|
540
|
+
const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
541
|
+
if (credPart) {
|
|
542
|
+
const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
|
|
543
|
+
const credJson = Buffer.from(b64, "base64").toString("utf8");
|
|
544
|
+
const cred = JSON.parse(credJson);
|
|
545
|
+
const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
|
|
546
|
+
payerAddress = extractPayerAddress(key);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
const idempotencyKey = await paymentIdempotencyKey(
|
|
552
|
+
(name) => {
|
|
553
|
+
const v = req.headers[name.toLowerCase()];
|
|
554
|
+
return Array.isArray(v) ? v[0] : v;
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
method: req.method,
|
|
558
|
+
path: req.originalUrl.split("?")[0],
|
|
559
|
+
amount: amountHumanReadable,
|
|
560
|
+
payTo: recipient
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
if (idempotencyKey) {
|
|
564
|
+
const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
|
|
565
|
+
if (replayCheck.kind === "spent") {
|
|
566
|
+
res.status(402).json({ error: "Payment already used" });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (replayCheck.kind === "replay") {
|
|
570
|
+
if (replayCheck.record.headers) {
|
|
571
|
+
for (const [k, val] of Object.entries(replayCheck.record.headers)) {
|
|
572
|
+
res.setHeader(k, val);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
next();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
182
579
|
const fetchReq = server$2.Request.fromNodeListener(req, res);
|
|
183
580
|
const handler = mppx["stellar/charge"];
|
|
184
581
|
const result = await handler({
|
|
185
582
|
amount: amountHumanReadable,
|
|
186
|
-
currency:
|
|
187
|
-
recipient
|
|
583
|
+
currency: assetContract,
|
|
584
|
+
recipient,
|
|
188
585
|
description: opts.manifest.name
|
|
189
586
|
})(fetchReq);
|
|
190
587
|
if (result.status === 402) {
|
|
@@ -195,25 +592,131 @@ function createMppChargeHandler(opts) {
|
|
|
195
592
|
return;
|
|
196
593
|
}
|
|
197
594
|
const receipt = result.withReceipt(new Response(""));
|
|
198
|
-
|
|
595
|
+
const receiptHeaders = {};
|
|
596
|
+
receipt.headers.forEach((v, k) => {
|
|
597
|
+
res.setHeader(k, v);
|
|
598
|
+
receiptHeaders[k] = v;
|
|
599
|
+
});
|
|
199
600
|
const receiptHeader = receipt.headers.get("payment-receipt");
|
|
200
|
-
|
|
601
|
+
let reference;
|
|
602
|
+
if (receiptHeader) {
|
|
201
603
|
try {
|
|
202
604
|
const parsed = JSON.parse(
|
|
203
605
|
Buffer.from(receiptHeader, "base64").toString("utf8")
|
|
204
606
|
);
|
|
205
|
-
|
|
206
|
-
await opts.onSettled(parsed.reference, opts.amount, "mpp-charge");
|
|
207
|
-
}
|
|
607
|
+
reference = parsed.reference;
|
|
208
608
|
} catch {
|
|
209
609
|
}
|
|
210
610
|
}
|
|
611
|
+
if (idempotencyKey) {
|
|
612
|
+
await seenTxStore.set(idempotencyKey, {
|
|
613
|
+
txHash: reference ?? null,
|
|
614
|
+
headers: receiptHeaders,
|
|
615
|
+
createdAt: Date.now()
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (reference && opts.onSettled) {
|
|
619
|
+
Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
|
|
620
|
+
logger("error", "[mpp-charge] onSettled callback error", { error: err });
|
|
621
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
622
|
+
});
|
|
623
|
+
}
|
|
211
624
|
next();
|
|
212
625
|
} catch (err) {
|
|
213
626
|
next(err);
|
|
214
627
|
}
|
|
215
628
|
};
|
|
216
629
|
}
|
|
630
|
+
function resolveVaultSettlementAddresses(payer, payee) {
|
|
631
|
+
if (!payer) return null;
|
|
632
|
+
return { payer, payee };
|
|
633
|
+
}
|
|
634
|
+
function buildSessionSettlementTransaction(source, vaultContractId, channelId, addresses, amount, voucherCount, networkPassphrase) {
|
|
635
|
+
const vault = new stellarSdk.Contract(vaultContractId);
|
|
636
|
+
const op = vault.call(
|
|
637
|
+
"record_session_settlement",
|
|
638
|
+
stellarSdk.nativeToScVal(channelId, { type: "address" }),
|
|
639
|
+
stellarSdk.nativeToScVal(addresses.payer, { type: "address" }),
|
|
640
|
+
stellarSdk.nativeToScVal(addresses.payee, { type: "address" }),
|
|
641
|
+
stellarSdk.nativeToScVal(amount, { type: "i128" }),
|
|
642
|
+
stellarSdk.nativeToScVal(voucherCount, { type: "u32" })
|
|
643
|
+
);
|
|
644
|
+
return new stellarSdk.TransactionBuilder(source, {
|
|
645
|
+
fee: stellarSdk.BASE_FEE,
|
|
646
|
+
networkPassphrase
|
|
647
|
+
}).addOperation(op).setTimeout(30).build();
|
|
648
|
+
}
|
|
649
|
+
function channelAuthorizer(envelopeSigner) {
|
|
650
|
+
return { envelopeSigner };
|
|
651
|
+
}
|
|
652
|
+
function formatMppError(error) {
|
|
653
|
+
if (error instanceof Error) return error.message;
|
|
654
|
+
if (typeof error === "string") return error;
|
|
655
|
+
try {
|
|
656
|
+
const serialized = JSON.stringify(error);
|
|
657
|
+
return serialized === void 0 ? String(error) : serialized;
|
|
658
|
+
} catch {
|
|
659
|
+
return String(error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function withTypedChannelErrors(method) {
|
|
663
|
+
return {
|
|
664
|
+
...method,
|
|
665
|
+
async verify(parameters) {
|
|
666
|
+
try {
|
|
667
|
+
return await method.verify(parameters);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
throw mapChannelError(error);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function onVerifiedCredential(method, commit) {
|
|
675
|
+
return {
|
|
676
|
+
...method,
|
|
677
|
+
async verify(parameters) {
|
|
678
|
+
const receipt = await method.verify(parameters);
|
|
679
|
+
await commit(parameters.credential);
|
|
680
|
+
return receipt;
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function mapChannelError(error) {
|
|
685
|
+
const message = formatMppError(error);
|
|
686
|
+
const details = error && typeof error === "object" && "details" in error ? error.details : void 0;
|
|
687
|
+
const reason = details && Object.keys(details).length > 0 ? `${message} (${JSON.stringify(details)})` : message;
|
|
688
|
+
if (/signer mismatch|authori[sz]er|unauthori[sz]ed/i.test(message)) {
|
|
689
|
+
return new mppx.Errors.SignerMismatchError({ reason });
|
|
690
|
+
}
|
|
691
|
+
if (/already used|replay|challenge/i.test(message)) {
|
|
692
|
+
return new mppx.Errors.InvalidChallengeError({ reason });
|
|
693
|
+
}
|
|
694
|
+
if (/channel has been closed|channel is closed|finalized/i.test(message)) {
|
|
695
|
+
return new mppx.Errors.ChannelClosedError({ reason });
|
|
696
|
+
}
|
|
697
|
+
if (/exceeds channel balance|exceeds.*deposit/i.test(message)) {
|
|
698
|
+
return new mppx.Errors.AmountExceedsDepositError({ reason });
|
|
699
|
+
}
|
|
700
|
+
if (/channel not found|no channel/i.test(message)) {
|
|
701
|
+
return new mppx.Errors.ChannelNotFoundError({ reason });
|
|
702
|
+
}
|
|
703
|
+
if (/must be greater|does not cover|below.*minimum/i.test(message)) {
|
|
704
|
+
return new mppx.Errors.DeltaTooSmallError({ reason });
|
|
705
|
+
}
|
|
706
|
+
if (/signature/i.test(message)) {
|
|
707
|
+
return new mppx.Errors.InvalidSignatureError({ reason });
|
|
708
|
+
}
|
|
709
|
+
return new mppx.Errors.VerificationFailedError({ reason });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/provider/MppSessionHandler.ts
|
|
713
|
+
function isVoucherStoreValue(value) {
|
|
714
|
+
if (typeof value !== "object" || value === null) {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
const candidate = value;
|
|
718
|
+
return typeof candidate.amount === "string" && /^\d+$/.test(candidate.amount);
|
|
719
|
+
}
|
|
217
720
|
var MPP_NETWORK2 = {
|
|
218
721
|
testnet: "stellar:testnet",
|
|
219
722
|
mainnet: "stellar:pubnet"
|
|
@@ -222,67 +725,203 @@ function createMppSessionHandler(opts) {
|
|
|
222
725
|
const networkId = MPP_NETWORK2[opts.network];
|
|
223
726
|
const rateHuman = opts.rate;
|
|
224
727
|
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
728
|
+
const logger = resolveLogger(opts.logger);
|
|
729
|
+
const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
|
|
730
|
+
const innerStore = server$3.Store.memory();
|
|
731
|
+
let record = null;
|
|
228
732
|
let voucherCount = 0;
|
|
229
733
|
let sessionOpened = false;
|
|
734
|
+
let idleTimer = null;
|
|
735
|
+
let settledCleanly = false;
|
|
736
|
+
let closeListenerArmed = false;
|
|
737
|
+
function clearIdleTimer() {
|
|
738
|
+
if (idleTimer) {
|
|
739
|
+
clearTimeout(idleTimer);
|
|
740
|
+
idleTimer = null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function armIdleTimer() {
|
|
744
|
+
if (!opts.idleTimeoutMs) return;
|
|
745
|
+
clearIdleTimer();
|
|
746
|
+
idleTimer = setTimeout(() => {
|
|
747
|
+
void flagOrphan("idle-timeout");
|
|
748
|
+
}, opts.idleTimeoutMs);
|
|
749
|
+
if (typeof idleTimer.unref === "function") idleTimer.unref();
|
|
750
|
+
}
|
|
751
|
+
async function loadPersistedRecord() {
|
|
752
|
+
if (record) return record;
|
|
753
|
+
try {
|
|
754
|
+
const stored = await innerStore.get(voucherRecordKey);
|
|
755
|
+
if (!stored || typeof stored.amount !== "string" || typeof stored.signature !== "string") {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
|
|
759
|
+
} catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
return record;
|
|
763
|
+
}
|
|
764
|
+
async function flagOrphan(reason) {
|
|
765
|
+
await loadPersistedRecord();
|
|
766
|
+
if (!sessionOpened || settledCleanly) return;
|
|
767
|
+
sessionOpened = false;
|
|
768
|
+
closeListenerArmed = false;
|
|
769
|
+
clearIdleTimer();
|
|
770
|
+
const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
|
|
771
|
+
if (opts.onOrphaned) {
|
|
772
|
+
try {
|
|
773
|
+
await opts.onOrphaned(opts.channelFactory, {
|
|
774
|
+
cumulativeAmount,
|
|
775
|
+
lastSignature: record?.signature ?? "",
|
|
776
|
+
voucherCount,
|
|
777
|
+
reason
|
|
778
|
+
});
|
|
779
|
+
} catch (err) {
|
|
780
|
+
logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
230
784
|
const wrappedStore = {
|
|
231
785
|
async get(key) {
|
|
232
786
|
return innerStore.get(key);
|
|
233
787
|
},
|
|
234
788
|
async put(key, value) {
|
|
235
|
-
|
|
236
|
-
if (key === cumulativeKey && value && typeof value === "object" && "amount" in value) {
|
|
237
|
-
lastCumulativeAmount = BigInt(value.amount);
|
|
238
|
-
voucherCount++;
|
|
239
|
-
if (!sessionOpened) {
|
|
240
|
-
sessionOpened = true;
|
|
241
|
-
if (opts.onSessionOpen) {
|
|
242
|
-
await opts.onSessionOpen(opts.channelContract);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (opts.onVoucher) {
|
|
246
|
-
const humanAmount = (Number(lastCumulativeAmount) / 1e7).toFixed(7);
|
|
247
|
-
await opts.onVoucher(voucherCount, humanAmount);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
789
|
+
return innerStore.put(key, value);
|
|
250
790
|
},
|
|
251
791
|
async delete(key) {
|
|
252
792
|
return innerStore.delete(key);
|
|
793
|
+
},
|
|
794
|
+
async update(key, fn) {
|
|
795
|
+
const storeWithUpdate = innerStore;
|
|
796
|
+
if (typeof storeWithUpdate.update === "function") {
|
|
797
|
+
return storeWithUpdate.update(key, fn);
|
|
798
|
+
}
|
|
799
|
+
throw new Error("Store does not support atomic update operations");
|
|
253
800
|
}
|
|
254
801
|
};
|
|
802
|
+
async function commit(credential) {
|
|
803
|
+
await loadPersistedRecord();
|
|
804
|
+
const payload = credential.payload;
|
|
805
|
+
if (typeof payload?.amount !== "string" || typeof payload.signature !== "string") return;
|
|
806
|
+
let amount;
|
|
807
|
+
try {
|
|
808
|
+
amount = BigInt(payload.amount);
|
|
809
|
+
} catch {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (record && amount < record.amount) return;
|
|
813
|
+
const payer = record?.payer ?? extractPayerAddress(credential.source);
|
|
814
|
+
record = { amount, signature: payload.signature, payer };
|
|
815
|
+
await innerStore.put(voucherRecordKey, {
|
|
816
|
+
amount: record.amount.toString(),
|
|
817
|
+
signature: record.signature,
|
|
818
|
+
payer: record.payer
|
|
819
|
+
});
|
|
820
|
+
voucherCount++;
|
|
821
|
+
settledCleanly = false;
|
|
822
|
+
armIdleTimer();
|
|
823
|
+
if (!sessionOpened) {
|
|
824
|
+
sessionOpened = true;
|
|
825
|
+
if (opts.onSessionOpen) {
|
|
826
|
+
Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
|
|
827
|
+
logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
|
|
828
|
+
opts.onCallbackError?.(err, "onSessionOpen");
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (opts.onVoucher) {
|
|
833
|
+
const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
|
|
834
|
+
Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
|
|
835
|
+
logger("error", "[mpp-session] onVoucher callback error", { error: err });
|
|
836
|
+
opts.onCallbackError?.(err, "onVoucher");
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
255
840
|
const mppx = server$2.Mppx.create({
|
|
256
841
|
secretKey: opts.payeeSecretKey,
|
|
257
842
|
methods: [
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
843
|
+
withTypedChannelErrors(
|
|
844
|
+
onVerifiedCredential(
|
|
845
|
+
server$3.stellar.channel({
|
|
846
|
+
channel: opts.channelFactory,
|
|
847
|
+
commitmentKey: opts.commitmentPublicKey,
|
|
848
|
+
network: networkId,
|
|
849
|
+
store: wrappedStore,
|
|
850
|
+
sourceAccount: payeeKeypair.publicKey(),
|
|
851
|
+
feePayer: channelAuthorizer(payeeKeypair)
|
|
852
|
+
}),
|
|
853
|
+
commit
|
|
854
|
+
)
|
|
855
|
+
)
|
|
266
856
|
]
|
|
267
857
|
});
|
|
268
|
-
let lastSignatureHex = "";
|
|
269
858
|
return async (req, res, next) => {
|
|
270
859
|
try {
|
|
271
860
|
if (req.method === "DELETE") {
|
|
861
|
+
await loadPersistedRecord();
|
|
272
862
|
const body = req.body;
|
|
273
|
-
const
|
|
274
|
-
const
|
|
863
|
+
const bodyAmount = body?.amount ? BigInt(body.amount) : 0n;
|
|
864
|
+
const recordAmount = record?.amount ?? 0n;
|
|
865
|
+
let closeAmount;
|
|
866
|
+
let closeSig;
|
|
867
|
+
if (bodyAmount > recordAmount) {
|
|
868
|
+
closeAmount = bodyAmount;
|
|
869
|
+
closeSig = body?.signature ?? record?.signature ?? "";
|
|
870
|
+
} else {
|
|
871
|
+
closeAmount = recordAmount;
|
|
872
|
+
closeSig = record?.signature ?? "";
|
|
873
|
+
}
|
|
275
874
|
if (closeAmount > 0n && closeSig) {
|
|
276
|
-
const
|
|
277
|
-
|
|
875
|
+
const closePayer = record?.payer ?? null;
|
|
876
|
+
const closeTxHash = await server$3.close({
|
|
877
|
+
channel: opts.channelFactory,
|
|
278
878
|
amount: closeAmount,
|
|
279
879
|
signature: Buffer.from(closeSig, "hex"),
|
|
280
|
-
feePayer:
|
|
880
|
+
feePayer: channelAuthorizer(payeeKeypair),
|
|
281
881
|
network: networkId
|
|
282
882
|
});
|
|
883
|
+
settledCleanly = true;
|
|
884
|
+
clearIdleTimer();
|
|
283
885
|
if (opts.onSettled) {
|
|
284
|
-
const totalPaid = (Number(
|
|
285
|
-
|
|
886
|
+
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
887
|
+
Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
|
|
888
|
+
logger("error", "[mpp-session] onSettled callback error", { error: err });
|
|
889
|
+
opts.onCallbackError?.(err, "onSettled");
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const vaultContract = process.env.AGENT_VAULT_CONTRACT;
|
|
893
|
+
if (vaultContract) {
|
|
894
|
+
const settlementAddresses = resolveVaultSettlementAddresses(
|
|
895
|
+
closePayer,
|
|
896
|
+
payeeKeypair.publicKey()
|
|
897
|
+
);
|
|
898
|
+
if (!settlementAddresses) {
|
|
899
|
+
logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
|
|
900
|
+
} else {
|
|
901
|
+
try {
|
|
902
|
+
const { Networks } = await import('@stellar/stellar-sdk');
|
|
903
|
+
const { Server } = await import('@stellar/stellar-sdk/rpc');
|
|
904
|
+
const networkPassphrase = opts.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
|
|
905
|
+
const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
|
|
906
|
+
const server = new Server(rpcUrl);
|
|
907
|
+
const payeePublicKey = payeeKeypair.publicKey();
|
|
908
|
+
const sourceAccount = await server.getAccount(payeePublicKey);
|
|
909
|
+
const tx = buildSessionSettlementTransaction(
|
|
910
|
+
sourceAccount,
|
|
911
|
+
vaultContract,
|
|
912
|
+
opts.channelFactory,
|
|
913
|
+
settlementAddresses,
|
|
914
|
+
closeAmount,
|
|
915
|
+
voucherCount,
|
|
916
|
+
networkPassphrase
|
|
917
|
+
);
|
|
918
|
+
const preparedTx = await server.prepareTransaction(tx);
|
|
919
|
+
preparedTx.sign(payeeKeypair);
|
|
920
|
+
await server.sendTransaction(preparedTx);
|
|
921
|
+
} catch (recordErr) {
|
|
922
|
+
logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
286
925
|
}
|
|
287
926
|
res.json({ closeTxHash });
|
|
288
927
|
} else {
|
|
@@ -290,23 +929,12 @@ function createMppSessionHandler(opts) {
|
|
|
290
929
|
}
|
|
291
930
|
sessionOpened = false;
|
|
292
931
|
voucherCount = 0;
|
|
293
|
-
|
|
932
|
+
record = null;
|
|
933
|
+
await innerStore.delete(voucherRecordKey);
|
|
934
|
+
closeListenerArmed = false;
|
|
935
|
+
clearIdleTimer();
|
|
294
936
|
return;
|
|
295
937
|
}
|
|
296
|
-
const authHeader = req.headers["authorization"];
|
|
297
|
-
if (typeof authHeader === "string" && authHeader.startsWith("Payment ")) {
|
|
298
|
-
try {
|
|
299
|
-
const credB64 = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
|
|
300
|
-
if (credB64) {
|
|
301
|
-
const credJson = Buffer.from(credB64.split("=").slice(1).join("=").replace(/^"|"$/g, ""), "base64").toString("utf8");
|
|
302
|
-
const cred = JSON.parse(credJson);
|
|
303
|
-
if (cred.payload?.signature) {
|
|
304
|
-
lastSignatureHex = cred.payload.signature;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
} catch {
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
938
|
const fetchReq = server$2.Request.fromNodeListener(req, res);
|
|
311
939
|
const result = await mppx.channel({
|
|
312
940
|
amount: rateHuman,
|
|
@@ -320,28 +948,101 @@ function createMppSessionHandler(opts) {
|
|
|
320
948
|
res.send(body);
|
|
321
949
|
return;
|
|
322
950
|
}
|
|
951
|
+
if (!closeListenerArmed) {
|
|
952
|
+
closeListenerArmed = true;
|
|
953
|
+
req.on("close", () => {
|
|
954
|
+
void flagOrphan("connection-closed");
|
|
955
|
+
});
|
|
956
|
+
}
|
|
323
957
|
next();
|
|
324
958
|
} catch (err) {
|
|
325
959
|
next(err);
|
|
326
960
|
}
|
|
327
961
|
};
|
|
328
962
|
}
|
|
963
|
+
var MANIFEST_SIGNATURE_VERSION = "2";
|
|
964
|
+
function canonicalize(value) {
|
|
965
|
+
if (Array.isArray(value)) {
|
|
966
|
+
return value.map(canonicalize);
|
|
967
|
+
}
|
|
968
|
+
if (value !== null && typeof value === "object") {
|
|
969
|
+
return Object.fromEntries(
|
|
970
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return value;
|
|
974
|
+
}
|
|
975
|
+
function manifestDigest(manifest) {
|
|
976
|
+
const { signature: _omit, ...rest } = manifest;
|
|
977
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
978
|
+
return crypto$1.createHash("sha256").update(canonical, "utf8").digest();
|
|
979
|
+
}
|
|
980
|
+
function signManifest(manifest, payeeSecretKey) {
|
|
981
|
+
const keypair = stellarSdk.Keypair.fromSecret(payeeSecretKey);
|
|
982
|
+
const versionedManifest = {
|
|
983
|
+
...manifest,
|
|
984
|
+
signature_version: MANIFEST_SIGNATURE_VERSION
|
|
985
|
+
};
|
|
986
|
+
const digest = manifestDigest(versionedManifest);
|
|
987
|
+
const sig = keypair.sign(digest);
|
|
988
|
+
return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
|
|
989
|
+
}
|
|
990
|
+
function verifyManifestSignature(manifest, expectedPayee) {
|
|
991
|
+
if (!manifest.signature) {
|
|
992
|
+
throw new RouteDockSignatureError(
|
|
993
|
+
"Manifest is missing a signature field \u2014 cannot verify payee authenticity"
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
|
|
997
|
+
throw new RouteDockSignatureError(
|
|
998
|
+
`Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
const keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
|
|
1002
|
+
const digest = manifestDigest(manifest);
|
|
1003
|
+
let sigBytes;
|
|
1004
|
+
try {
|
|
1005
|
+
sigBytes = Buffer.from(manifest.signature, "base64");
|
|
1006
|
+
} catch {
|
|
1007
|
+
throw new RouteDockSignatureError("Manifest signature is not valid base64");
|
|
1008
|
+
}
|
|
1009
|
+
let valid = false;
|
|
1010
|
+
try {
|
|
1011
|
+
valid = keypair.verify(digest, sigBytes);
|
|
1012
|
+
} catch {
|
|
1013
|
+
throw new RouteDockSignatureError(
|
|
1014
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
if (!valid) {
|
|
1018
|
+
throw new RouteDockSignatureError(
|
|
1019
|
+
`Manifest signature verification failed for payee ${manifest.payee}`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
329
1023
|
|
|
330
1024
|
// src/provider/routedockMiddleware.ts
|
|
1025
|
+
var MPP_MODES = ["mpp-session", "mpp-charge"];
|
|
331
1026
|
function routedock(opts) {
|
|
332
|
-
const
|
|
1027
|
+
const startTime = Date.now();
|
|
1028
|
+
const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
|
|
1029
|
+
const handlerMap = /* @__PURE__ */ new Map();
|
|
333
1030
|
if (opts.modes.includes("x402")) {
|
|
334
1031
|
const x402Price = opts.pricing.x402;
|
|
335
1032
|
if (x402Price) {
|
|
336
|
-
|
|
1033
|
+
handlerMap.set(
|
|
1034
|
+
"x402",
|
|
337
1035
|
createX402Handler({
|
|
338
1036
|
payeeSecretKey: opts.payeeSecretKey,
|
|
339
1037
|
network: opts.network,
|
|
340
1038
|
amount: x402Price,
|
|
341
|
-
assetContract: opts.assetContract,
|
|
1039
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
342
1040
|
...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
|
|
343
|
-
manifest:
|
|
344
|
-
...opts.onSettled ? { onSettled: opts.onSettled } : {}
|
|
1041
|
+
manifest: signedManifest,
|
|
1042
|
+
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
1043
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
1044
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1045
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
345
1046
|
})
|
|
346
1047
|
);
|
|
347
1048
|
}
|
|
@@ -349,14 +1050,18 @@ function routedock(opts) {
|
|
|
349
1050
|
if (opts.modes.includes("mpp-charge")) {
|
|
350
1051
|
const chargePrice = opts.pricing["mpp-charge"];
|
|
351
1052
|
if (chargePrice) {
|
|
352
|
-
|
|
1053
|
+
handlerMap.set(
|
|
1054
|
+
"mpp-charge",
|
|
353
1055
|
createMppChargeHandler({
|
|
354
1056
|
payeeSecretKey: opts.payeeSecretKey,
|
|
355
1057
|
network: opts.network,
|
|
356
1058
|
amount: chargePrice,
|
|
357
|
-
assetContract: opts.assetContract,
|
|
358
|
-
manifest:
|
|
359
|
-
...opts.onSettled ? { onSettled: opts.onSettled } : {}
|
|
1059
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
1060
|
+
manifest: signedManifest,
|
|
1061
|
+
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
1062
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
1063
|
+
...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
|
|
1064
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
360
1065
|
})
|
|
361
1066
|
);
|
|
362
1067
|
}
|
|
@@ -367,39 +1072,53 @@ function routedock(opts) {
|
|
|
367
1072
|
if (!opts.commitmentPublicKey) {
|
|
368
1073
|
throw new Error("routedock: mpp-session mode requires commitmentPublicKey");
|
|
369
1074
|
}
|
|
370
|
-
|
|
1075
|
+
handlerMap.set(
|
|
1076
|
+
"mpp-session",
|
|
371
1077
|
createMppSessionHandler({
|
|
372
1078
|
payeeSecretKey: opts.payeeSecretKey,
|
|
373
1079
|
network: opts.network,
|
|
374
|
-
|
|
1080
|
+
channelFactory: sessionPricing.channelFactory,
|
|
375
1081
|
rate: sessionPricing.rate,
|
|
376
|
-
assetContract: opts.assetContract,
|
|
377
|
-
manifest:
|
|
1082
|
+
...opts.assetContract ? { assetContract: opts.assetContract } : {},
|
|
1083
|
+
manifest: signedManifest,
|
|
378
1084
|
commitmentPublicKey: opts.commitmentPublicKey,
|
|
379
1085
|
...opts.onSettled ? { onSettled: opts.onSettled } : {},
|
|
380
1086
|
...opts.onSessionOpen ? { onSessionOpen: opts.onSessionOpen } : {},
|
|
381
|
-
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {}
|
|
1087
|
+
...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
|
|
1088
|
+
...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
|
|
1089
|
+
...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
|
|
1090
|
+
...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
|
|
1091
|
+
...opts.logger ? { logger: opts.logger } : {}
|
|
382
1092
|
})
|
|
383
1093
|
);
|
|
384
1094
|
}
|
|
385
1095
|
}
|
|
1096
|
+
const defaultMode = MPP_MODES.find((mode) => handlerMap.has(mode)) ?? (handlerMap.has("x402") ? "x402" : void 0);
|
|
386
1097
|
return (req, res, next) => {
|
|
387
1098
|
if (req.path === "/.well-known/routedock.json") {
|
|
388
|
-
res.json(
|
|
1099
|
+
res.json(signedManifest);
|
|
389
1100
|
return;
|
|
390
1101
|
}
|
|
391
|
-
if (
|
|
1102
|
+
if (req.path === "/health") {
|
|
1103
|
+
res.json({
|
|
1104
|
+
status: "ok",
|
|
1105
|
+
routedock: opts.manifest.routedock,
|
|
1106
|
+
network: opts.network,
|
|
1107
|
+
payee: opts.payee,
|
|
1108
|
+
modesActive: opts.modes,
|
|
1109
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3)
|
|
1110
|
+
});
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (handlerMap.size === 0) {
|
|
392
1114
|
next();
|
|
393
1115
|
return;
|
|
394
1116
|
}
|
|
395
1117
|
const hasX402Header = !!(req.headers["payment-signature"] || req.headers["x-payment"]);
|
|
396
1118
|
const prefersX402 = req.headers["x-preferred-mode"] === "x402";
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
} else {
|
|
401
|
-
handler = handlers[handlers.length - 1];
|
|
402
|
-
}
|
|
1119
|
+
const x402Handler = handlerMap.get("x402");
|
|
1120
|
+
const defaultHandler = defaultMode ? handlerMap.get(defaultMode) : void 0;
|
|
1121
|
+
const handler = hasX402Header || prefersX402 ? x402Handler ?? defaultHandler : defaultHandler;
|
|
403
1122
|
if (!handler) {
|
|
404
1123
|
next();
|
|
405
1124
|
return;
|
|
@@ -412,6 +1131,149 @@ function routedock(opts) {
|
|
|
412
1131
|
})).then(() => next()).catch((err) => next(err));
|
|
413
1132
|
};
|
|
414
1133
|
}
|
|
1134
|
+
var MPP_NETWORK3 = {
|
|
1135
|
+
testnet: "stellar:testnet",
|
|
1136
|
+
mainnet: "stellar:pubnet"
|
|
1137
|
+
};
|
|
1138
|
+
function decimalToStroops(amountStr) {
|
|
1139
|
+
return nulthSdk.usdcToStroops(amountStr);
|
|
1140
|
+
}
|
|
1141
|
+
async function reconcileAbandonedSessions(opts) {
|
|
1142
|
+
const stats = {
|
|
1143
|
+
orphanedCount: 0,
|
|
1144
|
+
recoveredCount: 0,
|
|
1145
|
+
skippedCount: 0,
|
|
1146
|
+
failedCount: 0,
|
|
1147
|
+
errors: []
|
|
1148
|
+
};
|
|
1149
|
+
const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
|
|
1150
|
+
const networkId = MPP_NETWORK3[opts.network];
|
|
1151
|
+
try {
|
|
1152
|
+
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);
|
|
1153
|
+
if (error) {
|
|
1154
|
+
throw new Error(`Failed to query sessions: ${error.message}`);
|
|
1155
|
+
}
|
|
1156
|
+
stats.orphanedCount = closingSessions?.length ?? 0;
|
|
1157
|
+
if (!closingSessions || closingSessions.length === 0) {
|
|
1158
|
+
return stats;
|
|
1159
|
+
}
|
|
1160
|
+
for (const session of closingSessions) {
|
|
1161
|
+
try {
|
|
1162
|
+
const {
|
|
1163
|
+
channel_id: sessionId,
|
|
1164
|
+
channel_contract: channelContract,
|
|
1165
|
+
cumulative_amount,
|
|
1166
|
+
last_signature
|
|
1167
|
+
} = session;
|
|
1168
|
+
const channelId = channelContract || sessionId;
|
|
1169
|
+
if (!channelId || !cumulative_amount || !last_signature) {
|
|
1170
|
+
stats.skippedCount++;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const closeAmount = decimalToStroops(cumulative_amount);
|
|
1174
|
+
const closeSig = Buffer.from(last_signature, "hex");
|
|
1175
|
+
const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
|
|
1176
|
+
const closeTxHash = await closeFn({
|
|
1177
|
+
channel: channelId,
|
|
1178
|
+
amount: closeAmount,
|
|
1179
|
+
signature: closeSig,
|
|
1180
|
+
feePayer: channelAuthorizer(payeeKeypair),
|
|
1181
|
+
network: networkId
|
|
1182
|
+
});
|
|
1183
|
+
const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
|
|
1184
|
+
const { error: updateError } = await opts.supabase.from("sessions").update({
|
|
1185
|
+
status: "closed",
|
|
1186
|
+
settlement_tx_hash: closeTxHash,
|
|
1187
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1188
|
+
}).eq("channel_id", sessionId);
|
|
1189
|
+
if (updateError) {
|
|
1190
|
+
throw new Error(`Failed to update session: ${updateError.message}`);
|
|
1191
|
+
}
|
|
1192
|
+
stats.recoveredCount++;
|
|
1193
|
+
if (opts.onRecovered) {
|
|
1194
|
+
await opts.onRecovered(channelId, closeTxHash, totalPaid);
|
|
1195
|
+
}
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
stats.failedCount++;
|
|
1198
|
+
stats.errors.push({
|
|
1199
|
+
channelId: session.channel_id,
|
|
1200
|
+
reason: formatMppError(err)
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return stats;
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`Session reconciliation failed: ${formatMppError(err)}`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async function runStartupReconciliation(opts, logger) {
|
|
1212
|
+
const log = resolveLogger(logger);
|
|
1213
|
+
log("info", "[SessionReconciler] Running startup reconciliation...");
|
|
1214
|
+
try {
|
|
1215
|
+
const stats = await reconcileAbandonedSessions(opts);
|
|
1216
|
+
if (stats.orphanedCount === 0) {
|
|
1217
|
+
log("info", "[SessionReconciler] No orphaned sessions found");
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
log(
|
|
1221
|
+
"info",
|
|
1222
|
+
`[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
|
|
1223
|
+
);
|
|
1224
|
+
if (stats.errors.length > 0) {
|
|
1225
|
+
log("warn", "[SessionReconciler] Reconciliation errors:");
|
|
1226
|
+
for (const err of stats.errors) {
|
|
1227
|
+
log("warn", ` - ${err.channelId}: ${err.reason}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/provider/registerProvider.ts
|
|
1236
|
+
async function registerProvider(optionsOrSupabase, manifestArg, baseUrlArg, verifiedArg) {
|
|
1237
|
+
let supabase;
|
|
1238
|
+
let manifest;
|
|
1239
|
+
let baseUrl;
|
|
1240
|
+
let verified;
|
|
1241
|
+
if ("supabase" in optionsOrSupabase) {
|
|
1242
|
+
supabase = optionsOrSupabase.supabase;
|
|
1243
|
+
manifest = optionsOrSupabase.manifest;
|
|
1244
|
+
baseUrl = optionsOrSupabase.baseUrl;
|
|
1245
|
+
verified = optionsOrSupabase.verified ?? true;
|
|
1246
|
+
} else {
|
|
1247
|
+
supabase = optionsOrSupabase;
|
|
1248
|
+
if (!manifestArg || !baseUrlArg) {
|
|
1249
|
+
throw new RouteDockManifestError("manifest and baseUrl are required for registerProvider");
|
|
1250
|
+
}
|
|
1251
|
+
manifest = manifestArg;
|
|
1252
|
+
baseUrl = baseUrlArg;
|
|
1253
|
+
verified = verifiedArg ?? true;
|
|
1254
|
+
}
|
|
1255
|
+
verifyManifestSignature(manifest);
|
|
1256
|
+
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
1257
|
+
const { error } = await supabase.from("providers").upsert(
|
|
1258
|
+
{
|
|
1259
|
+
name: manifest.name,
|
|
1260
|
+
description: manifest.description ?? null,
|
|
1261
|
+
base_url: normalizedBaseUrl,
|
|
1262
|
+
modes: manifest.modes,
|
|
1263
|
+
tags: manifest.tags ?? [],
|
|
1264
|
+
categories: manifest.categories ?? [],
|
|
1265
|
+
network: manifest.network,
|
|
1266
|
+
payee: manifest.payee,
|
|
1267
|
+
manifest,
|
|
1268
|
+
verified,
|
|
1269
|
+
registered_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1270
|
+
},
|
|
1271
|
+
{ onConflict: "base_url" }
|
|
1272
|
+
);
|
|
1273
|
+
if (error) {
|
|
1274
|
+
throw new Error(`Failed to register provider in Supabase: ${error.message}`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
415
1277
|
|
|
416
1278
|
// src/store/SessionStore.ts
|
|
417
1279
|
var SupabaseSessionStore = class {
|
|
@@ -421,12 +1283,16 @@ var SupabaseSessionStore = class {
|
|
|
421
1283
|
supabase;
|
|
422
1284
|
async get(channelId) {
|
|
423
1285
|
const { data, error } = await this.supabase.from("sessions").select("*").eq("channel_id", channelId).maybeSingle();
|
|
424
|
-
if (error)
|
|
1286
|
+
if (error) {
|
|
1287
|
+
throw new RouteDockNetworkError(`SessionStore.get failed: ${error.message}`);
|
|
1288
|
+
}
|
|
425
1289
|
if (!data) return null;
|
|
426
1290
|
return {
|
|
427
1291
|
channel_id: data.channel_id,
|
|
428
1292
|
payee: data.payee,
|
|
429
1293
|
payer: data.payer,
|
|
1294
|
+
channel_contract: data.channel_contract,
|
|
1295
|
+
network: data.network,
|
|
430
1296
|
cumulative_amount: String(data.cumulative_amount),
|
|
431
1297
|
last_signature: data.last_signature,
|
|
432
1298
|
status: data.status,
|
|
@@ -441,7 +1307,7 @@ var SupabaseSessionStore = class {
|
|
|
441
1307
|
const prev = parseFloat(existing.cumulative_amount);
|
|
442
1308
|
const next = parseFloat(state.cumulative_amount);
|
|
443
1309
|
if (next <= prev) {
|
|
444
|
-
throw new
|
|
1310
|
+
throw new RouteDockVoucherMonotonicityError(
|
|
445
1311
|
`cumulative_amount must be strictly increasing: ${next} <= ${prev}`
|
|
446
1312
|
);
|
|
447
1313
|
}
|
|
@@ -451,6 +1317,8 @@ var SupabaseSessionStore = class {
|
|
|
451
1317
|
channel_id: state.channel_id,
|
|
452
1318
|
payee: state.payee,
|
|
453
1319
|
payer: state.payer,
|
|
1320
|
+
channel_contract: state.channel_contract,
|
|
1321
|
+
network: state.network,
|
|
454
1322
|
cumulative_amount: state.cumulative_amount,
|
|
455
1323
|
last_signature: state.last_signature,
|
|
456
1324
|
status: state.status,
|
|
@@ -462,26 +1330,116 @@ var SupabaseSessionStore = class {
|
|
|
462
1330
|
);
|
|
463
1331
|
if (error) {
|
|
464
1332
|
if (error.message.includes("strictly increasing")) {
|
|
465
|
-
throw new
|
|
1333
|
+
throw new RouteDockVoucherMonotonicityError(
|
|
466
1334
|
`cumulative_amount must be strictly increasing (DB): ${error.message}`
|
|
467
1335
|
);
|
|
468
1336
|
}
|
|
469
|
-
throw new
|
|
1337
|
+
throw new RouteDockNetworkError(`SessionStore.upsert failed: ${error.message}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
async setStatus(channelId, status, settlementTxHash) {
|
|
1341
|
+
const payload = {
|
|
1342
|
+
status,
|
|
1343
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1344
|
+
};
|
|
1345
|
+
if (settlementTxHash !== void 0) {
|
|
1346
|
+
payload.settlement_tx_hash = settlementTxHash;
|
|
1347
|
+
}
|
|
1348
|
+
const { error } = await this.supabase.from("sessions").update(payload).eq("channel_id", channelId);
|
|
1349
|
+
if (error) {
|
|
1350
|
+
throw new RouteDockNetworkError(`SessionStore.setStatus failed: ${error.message}`);
|
|
470
1351
|
}
|
|
471
1352
|
}
|
|
472
1353
|
async close(channelId) {
|
|
473
|
-
|
|
474
|
-
|
|
1354
|
+
await this.setStatus(channelId, "closed");
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
// src/store/SupabaseSeenTxStore.ts
|
|
1359
|
+
var SupabaseSeenTxStore = class {
|
|
1360
|
+
constructor(supabase) {
|
|
1361
|
+
this.supabase = supabase;
|
|
1362
|
+
}
|
|
1363
|
+
supabase;
|
|
1364
|
+
async get(key) {
|
|
1365
|
+
const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
|
|
1366
|
+
if (error) {
|
|
1367
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
|
|
1368
|
+
}
|
|
1369
|
+
if (!data) return void 0;
|
|
1370
|
+
const headers = data.headers;
|
|
1371
|
+
return {
|
|
1372
|
+
txHash: data.tx_hash ?? null,
|
|
1373
|
+
...headers ? { headers } : {},
|
|
1374
|
+
createdAt: Date.parse(data.created_at)
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
async set(key, record) {
|
|
1378
|
+
const { error } = await this.supabase.from("settlements").upsert(
|
|
1379
|
+
{
|
|
1380
|
+
key,
|
|
1381
|
+
tx_hash: record.txHash,
|
|
1382
|
+
headers: record.headers ?? null,
|
|
1383
|
+
created_at: new Date(record.createdAt ?? Date.now()).toISOString()
|
|
1384
|
+
},
|
|
1385
|
+
{ onConflict: "key" }
|
|
1386
|
+
);
|
|
1387
|
+
if (error) {
|
|
1388
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Atomically increment the settlement's replay counter through the
|
|
1393
|
+
* `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
|
|
1394
|
+
* row lock gives concurrent claims distinct counts. Returns `Infinity` when
|
|
1395
|
+
* the key no longer exists, so the caller treats it as spent.
|
|
1396
|
+
*/
|
|
1397
|
+
async claimReplay(key) {
|
|
1398
|
+
const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
|
|
1399
|
+
p_key: key
|
|
1400
|
+
});
|
|
1401
|
+
if (error) {
|
|
1402
|
+
throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
|
|
1403
|
+
}
|
|
1404
|
+
if (data == null) return Number.POSITIVE_INFINITY;
|
|
1405
|
+
return data;
|
|
475
1406
|
}
|
|
476
1407
|
};
|
|
477
1408
|
|
|
1409
|
+
exports.DEFAULT_MAX_SESSION_DURATION_MS = DEFAULT_MAX_SESSION_DURATION_MS;
|
|
1410
|
+
exports.InMemorySeenTxStore = InMemorySeenTxStore;
|
|
1411
|
+
exports.MAX_SETTLEMENT_REPLAYS = MAX_SETTLEMENT_REPLAYS;
|
|
1412
|
+
exports.RouteDockChannelStateError = RouteDockChannelStateError;
|
|
1413
|
+
exports.RouteDockClientVersionError = RouteDockClientVersionError;
|
|
1414
|
+
exports.RouteDockDisputeError = RouteDockDisputeError;
|
|
478
1415
|
exports.RouteDockError = RouteDockError;
|
|
1416
|
+
exports.RouteDockFacilitatorError = RouteDockFacilitatorError;
|
|
479
1417
|
exports.RouteDockManifestError = RouteDockManifestError;
|
|
1418
|
+
exports.RouteDockNetworkError = RouteDockNetworkError;
|
|
480
1419
|
exports.RouteDockNoSupportedModeError = RouteDockNoSupportedModeError;
|
|
1420
|
+
exports.RouteDockPolicyRejectError = RouteDockPolicyRejectError;
|
|
481
1421
|
exports.RouteDockPolicyRejectedError = RouteDockPolicyRejectedError;
|
|
1422
|
+
exports.RouteDockRefundWindowError = RouteDockRefundWindowError;
|
|
482
1423
|
exports.RouteDockSessionError = RouteDockSessionError;
|
|
1424
|
+
exports.RouteDockSignatureError = RouteDockSignatureError;
|
|
1425
|
+
exports.RouteDockTrustlineError = RouteDockTrustlineError;
|
|
1426
|
+
exports.RouteDockVoucherMonotonicityError = RouteDockVoucherMonotonicityError;
|
|
1427
|
+
exports.SETTLEMENT_REPLAY_WINDOW_MS = SETTLEMENT_REPLAY_WINDOW_MS;
|
|
1428
|
+
exports.SupabaseSeenTxStore = SupabaseSeenTxStore;
|
|
483
1429
|
exports.SupabaseSessionStore = SupabaseSessionStore;
|
|
1430
|
+
exports.checkSettlementReplay = checkSettlementReplay;
|
|
484
1431
|
exports.createMppChargeHandler = createMppChargeHandler;
|
|
485
1432
|
exports.createMppSessionHandler = createMppSessionHandler;
|
|
486
1433
|
exports.createX402Handler = createX402Handler;
|
|
1434
|
+
exports.decimalToStroops = decimalToStroops;
|
|
1435
|
+
exports.getEligibleAssets = getEligibleAssets;
|
|
1436
|
+
exports.isAssetEligible = isAssetEligible;
|
|
1437
|
+
exports.isVoucherStoreValue = isVoucherStoreValue;
|
|
1438
|
+
exports.normalizeManifestAssets = normalizeManifestAssets;
|
|
1439
|
+
exports.paymentIdempotencyKey = paymentIdempotencyKey;
|
|
1440
|
+
exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
|
|
1441
|
+
exports.registerProvider = registerProvider;
|
|
1442
|
+
exports.resolveAssetContract = resolveAssetContract;
|
|
487
1443
|
exports.routedock = routedock;
|
|
1444
|
+
exports.runStartupReconciliation = runStartupReconciliation;
|
|
1445
|
+
exports.selectAsset = selectAsset;
|