@fiber-pay/runtime 0.1.0-rc.1

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/dist/index.js ADDED
@@ -0,0 +1,3389 @@
1
+ // src/service.ts
2
+ import { EventEmitter as EventEmitter2 } from "events";
3
+ import { FiberRpcClient as FiberRpcClient2 } from "@fiber-pay/sdk";
4
+
5
+ // src/alerts/alert-manager.ts
6
+ import { randomUUID } from "crypto";
7
+ var AlertManager = class {
8
+ backends;
9
+ store;
10
+ listeners = [];
11
+ constructor(options) {
12
+ this.backends = options.backends;
13
+ this.store = options.store;
14
+ }
15
+ /** Register a listener that is called after every emitted alert. */
16
+ onEmit(listener) {
17
+ this.listeners.push(listener);
18
+ }
19
+ async start() {
20
+ for (const backend of this.backends) {
21
+ if (backend.start) {
22
+ await backend.start();
23
+ }
24
+ }
25
+ }
26
+ async stop() {
27
+ for (const backend of this.backends) {
28
+ if (backend.stop) {
29
+ await backend.stop();
30
+ }
31
+ }
32
+ }
33
+ async emit(input) {
34
+ const alert = {
35
+ id: randomUUID(),
36
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
37
+ type: input.type,
38
+ priority: input.priority,
39
+ source: input.source,
40
+ data: input.data
41
+ };
42
+ this.store.addAlert(alert);
43
+ await Promise.allSettled(this.backends.map((backend) => backend.send(alert)));
44
+ for (const listener of this.listeners) {
45
+ listener(alert);
46
+ }
47
+ return alert;
48
+ }
49
+ };
50
+
51
+ // src/alerts/backends/file-jsonl.ts
52
+ import { appendFileSync, mkdirSync } from "fs";
53
+ import { dirname } from "path";
54
+ var JsonlFileAlertBackend = class {
55
+ path;
56
+ constructor(path) {
57
+ this.path = path;
58
+ mkdirSync(dirname(path), { recursive: true });
59
+ }
60
+ async send(alert) {
61
+ appendFileSync(this.path, `${JSON.stringify(alert)}
62
+ `, "utf-8");
63
+ }
64
+ };
65
+
66
+ // src/alerts/format.ts
67
+ var ANSI_RESET = "\x1B[0m";
68
+ var ANSI_BOLD = "\x1B[1m";
69
+ var ANSI_DIM = "\x1B[2m";
70
+ var ANSI_RED = "\x1B[31m";
71
+ var ANSI_GREEN = "\x1B[32m";
72
+ var ANSI_YELLOW = "\x1B[33m";
73
+ var ANSI_BLUE = "\x1B[34m";
74
+ var ANSI_MAGENTA = "\x1B[35m";
75
+ var ANSI_CYAN = "\x1B[36m";
76
+ function clip(value, max = 120) {
77
+ if (value.length <= max) {
78
+ return value;
79
+ }
80
+ return `${value.slice(0, max - 1)}\u2026`;
81
+ }
82
+ function readStringField(record, key) {
83
+ const value = record[key];
84
+ return typeof value === "string" && value.length > 0 ? value : void 0;
85
+ }
86
+ function toRecord(value) {
87
+ if (!value || typeof value !== "object") {
88
+ return void 0;
89
+ }
90
+ return value;
91
+ }
92
+ function summarizeAlertData(data, withColor) {
93
+ if (data === null || data === void 0) {
94
+ return "{}";
95
+ }
96
+ if (typeof data !== "object") {
97
+ return clip(String(data), 160);
98
+ }
99
+ const record = data;
100
+ const parts = [];
101
+ const eventType = readStringField(record, "type");
102
+ if (eventType) {
103
+ parts.push(`type=${eventType}`);
104
+ }
105
+ const channel = toRecord(record.channel);
106
+ const previousChannel = toRecord(record.previousChannel);
107
+ const channelId = readStringField(record, "channelId") ?? readStringField(channel ?? {}, "channel_id") ?? readStringField(previousChannel ?? {}, "channel_id");
108
+ if (channelId) {
109
+ parts.push(`channelId=${channelId}`);
110
+ }
111
+ const paymentHash = readStringField(record, "paymentHash");
112
+ if (paymentHash) {
113
+ parts.push(`paymentHash=${paymentHash}`);
114
+ }
115
+ const invoicePaymentHash = readStringField(record, "invoicePaymentHash");
116
+ if (invoicePaymentHash) {
117
+ parts.push(`invoicePaymentHash=${invoicePaymentHash}`);
118
+ }
119
+ const peerId = readStringField(record, "peerId") ?? readStringField(channel ?? {}, "peer_id");
120
+ if (peerId) {
121
+ parts.push(`peerId=${peerId}`);
122
+ }
123
+ const jobId = readStringField(record, "jobId");
124
+ if (jobId) {
125
+ parts.push(`jobId=${jobId}`);
126
+ }
127
+ const previousState = readStringField(record, "previousState");
128
+ const currentState = readStringField(record, "currentState");
129
+ if (previousState && currentState) {
130
+ parts.push(
131
+ colorize(`state=${previousState}->${currentState}`, `${ANSI_BOLD}${ANSI_YELLOW}`, withColor)
132
+ );
133
+ }
134
+ const channelState = toRecord(channel?.state);
135
+ const previousChannelState = toRecord(previousChannel?.state);
136
+ const currentStateName = readStringField(channelState ?? {}, "state_name");
137
+ const previousStateName = readStringField(previousChannelState ?? {}, "state_name");
138
+ if (!previousState && !currentState && previousStateName && currentStateName) {
139
+ parts.push(
140
+ colorize(`state=${previousStateName}->${currentStateName}`, `${ANSI_BOLD}${ANSI_YELLOW}`, withColor)
141
+ );
142
+ } else if (!previousState && !currentState && currentStateName) {
143
+ parts.push(colorize(`state=${currentStateName}`, `${ANSI_BOLD}${ANSI_YELLOW}`, withColor));
144
+ }
145
+ const reason = readStringField(record, "reason") ?? readStringField(record, "message");
146
+ if (reason) {
147
+ parts.push(`reason=${clip(reason, 80)}`);
148
+ }
149
+ const error = readStringField(record, "error");
150
+ if (error) {
151
+ parts.push(`error=${clip(error, 80)}`);
152
+ }
153
+ if (parts.length > 0) {
154
+ return parts.join(" ");
155
+ }
156
+ return clip(JSON.stringify(record), 160);
157
+ }
158
+ function shouldUseColor(mode) {
159
+ if (mode === "always") {
160
+ return true;
161
+ }
162
+ if (mode === "never") {
163
+ return false;
164
+ }
165
+ return process.env.NO_COLOR === void 0 && process.stdout.isTTY !== false;
166
+ }
167
+ function colorize(text, color, enabled) {
168
+ if (!enabled) {
169
+ return text;
170
+ }
171
+ return `${color}${text}${ANSI_RESET}`;
172
+ }
173
+ function formatRuntimeAlert(alert, options = {}) {
174
+ const colorMode = options.color ?? "auto";
175
+ const withColor = shouldUseColor(colorMode);
176
+ const priorityLabel = alert.priority.toUpperCase().padEnd(8, " ");
177
+ const priorityColor = alert.priority === "critical" ? ANSI_RED : alert.priority === "high" ? ANSI_YELLOW : alert.priority === "medium" ? ANSI_CYAN : ANSI_GREEN;
178
+ const typeColor = alert.type.startsWith("channel_") ? ANSI_BLUE : alert.type.startsWith("payment_") || alert.type.startsWith("incoming_") || alert.type.startsWith("outgoing_") ? ANSI_MAGENTA : ANSI_CYAN;
179
+ const prefixLabel = options.prefix ?? "[fiber-runtime]";
180
+ const prefix = colorize(prefixLabel, `${ANSI_BOLD}${ANSI_CYAN}`, withColor);
181
+ const ts = colorize(alert.timestamp, ANSI_DIM, withColor);
182
+ const priority = colorize(priorityLabel, `${ANSI_BOLD}${priorityColor}`, withColor);
183
+ const type = colorize(alert.type, `${ANSI_BOLD}${typeColor}`, withColor);
184
+ const data = colorize(summarizeAlertData(alert.data, withColor), ANSI_DIM, withColor);
185
+ return `${prefix} ${ts} ${priority} ${type} ${data}`;
186
+ }
187
+
188
+ // src/alerts/backends/stdout.ts
189
+ var StdoutAlertBackend = class {
190
+ async send(alert) {
191
+ console.log(formatRuntimeAlert(alert));
192
+ }
193
+ };
194
+
195
+ // src/utils/async.ts
196
+ function sleep(ms, signal) {
197
+ return new Promise((resolve2, reject) => {
198
+ if (signal?.aborted) {
199
+ reject(new DOMException("Aborted", "AbortError"));
200
+ return;
201
+ }
202
+ const timer = setTimeout(resolve2, ms);
203
+ signal?.addEventListener(
204
+ "abort",
205
+ () => {
206
+ clearTimeout(timer);
207
+ reject(new DOMException("Aborted", "AbortError"));
208
+ },
209
+ { once: true }
210
+ );
211
+ });
212
+ }
213
+
214
+ // src/alerts/backends/webhook.ts
215
+ var WebhookAlertBackend = class {
216
+ config;
217
+ constructor(config) {
218
+ this.config = {
219
+ timeoutMs: 5e3,
220
+ headers: {},
221
+ ...config
222
+ };
223
+ }
224
+ async send(alert) {
225
+ let lastError;
226
+ for (let attempt = 0; attempt < 3; attempt += 1) {
227
+ const controller = new AbortController();
228
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
229
+ try {
230
+ const response = await fetch(this.config.url, {
231
+ method: "POST",
232
+ headers: {
233
+ "content-type": "application/json",
234
+ ...this.config.headers
235
+ },
236
+ body: JSON.stringify(alert),
237
+ signal: controller.signal
238
+ });
239
+ clearTimeout(timer);
240
+ if (response.ok) {
241
+ return;
242
+ }
243
+ lastError = new Error(`Webhook response status: ${response.status}`);
244
+ } catch (error) {
245
+ clearTimeout(timer);
246
+ lastError = error;
247
+ }
248
+ await sleep(100 * 2 ** attempt);
249
+ }
250
+ throw new Error(`Failed to deliver webhook alert: ${String(lastError)}`);
251
+ }
252
+ };
253
+
254
+ // src/alerts/backends/websocket.ts
255
+ import { createHash } from "crypto";
256
+ import http from "http";
257
+ var WebsocketAlertBackend = class {
258
+ config;
259
+ clients = /* @__PURE__ */ new Set();
260
+ server;
261
+ constructor(config) {
262
+ this.config = config;
263
+ }
264
+ async start() {
265
+ if (this.server) {
266
+ return;
267
+ }
268
+ this.server = http.createServer((req, res) => {
269
+ if (req.url === "/health") {
270
+ res.writeHead(200, { "content-type": "application/json" });
271
+ res.end(JSON.stringify({ ok: true }));
272
+ return;
273
+ }
274
+ res.writeHead(404, { "content-type": "application/json" });
275
+ res.end(JSON.stringify({ error: "Not found" }));
276
+ });
277
+ this.server.on("upgrade", (request, socket) => {
278
+ const key = request.headers["sec-websocket-key"];
279
+ if (!key || typeof key !== "string") {
280
+ socket.destroy();
281
+ return;
282
+ }
283
+ const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
284
+ const headers = [
285
+ "HTTP/1.1 101 Switching Protocols",
286
+ "Upgrade: websocket",
287
+ "Connection: Upgrade",
288
+ `Sec-WebSocket-Accept: ${accept}`
289
+ ];
290
+ socket.write(`${headers.join("\r\n")}\r
291
+ \r
292
+ `);
293
+ this.clients.add(socket);
294
+ socket.on("close", () => this.clients.delete(socket));
295
+ socket.on("end", () => this.clients.delete(socket));
296
+ socket.on("error", () => this.clients.delete(socket));
297
+ });
298
+ await new Promise((resolve2, reject) => {
299
+ this.server?.once("error", reject);
300
+ this.server?.listen(this.config.port, this.config.host, () => {
301
+ this.server?.off("error", reject);
302
+ resolve2();
303
+ });
304
+ });
305
+ }
306
+ async send(alert) {
307
+ const payload = Buffer.from(JSON.stringify(alert), "utf8");
308
+ const frame = buildWebSocketFrame(payload);
309
+ for (const client of this.clients) {
310
+ try {
311
+ client.write(frame);
312
+ } catch {
313
+ this.clients.delete(client);
314
+ }
315
+ }
316
+ }
317
+ async stop() {
318
+ for (const client of this.clients) {
319
+ client.destroy();
320
+ }
321
+ this.clients.clear();
322
+ if (!this.server) {
323
+ return;
324
+ }
325
+ const server = this.server;
326
+ this.server = void 0;
327
+ await new Promise((resolve2) => {
328
+ server.close(() => resolve2());
329
+ });
330
+ }
331
+ };
332
+ function buildWebSocketFrame(payload) {
333
+ const payloadLength = payload.length;
334
+ if (payloadLength < 126) {
335
+ return Buffer.concat([Buffer.from([129, payloadLength]), payload]);
336
+ }
337
+ if (payloadLength < 65536) {
338
+ const header2 = Buffer.alloc(4);
339
+ header2[0] = 129;
340
+ header2[1] = 126;
341
+ header2.writeUInt16BE(payloadLength, 2);
342
+ return Buffer.concat([header2, payload]);
343
+ }
344
+ const header = Buffer.alloc(10);
345
+ header[0] = 129;
346
+ header[1] = 127;
347
+ header.writeUInt32BE(0, 2);
348
+ header.writeUInt32BE(payloadLength, 6);
349
+ return Buffer.concat([header, payload]);
350
+ }
351
+
352
+ // src/config.ts
353
+ import { resolve } from "path";
354
+
355
+ // src/jobs/retry-policy.ts
356
+ var defaultPaymentRetryPolicy = {
357
+ maxRetries: 3,
358
+ baseDelayMs: 2e3,
359
+ maxDelayMs: 3e4,
360
+ backoffMultiplier: 2,
361
+ jitterMs: 500
362
+ };
363
+ function shouldRetry(error, retryCount, policy) {
364
+ if (!error.retryable) return false;
365
+ if (retryCount >= policy.maxRetries) return false;
366
+ return true;
367
+ }
368
+ function computeRetryDelay(retryCount, policy) {
369
+ const base = policy.baseDelayMs * policy.backoffMultiplier ** retryCount;
370
+ const capped = Math.min(base, policy.maxDelayMs);
371
+ const jitter = Math.floor(Math.random() * policy.jitterMs);
372
+ return capped + jitter;
373
+ }
374
+
375
+ // src/config.ts
376
+ var defaultRuntimeConfig = {
377
+ fiberRpcUrl: "http://127.0.0.1:8227",
378
+ channelPollIntervalMs: 3e3,
379
+ invoicePollIntervalMs: 2e3,
380
+ paymentPollIntervalMs: 1e3,
381
+ peerPollIntervalMs: 1e4,
382
+ healthPollIntervalMs: 5e3,
383
+ includeClosedChannels: true,
384
+ completedItemTtlSeconds: 86400,
385
+ requestTimeoutMs: 1e4,
386
+ alerts: [{ type: "stdout" }],
387
+ proxy: {
388
+ enabled: true,
389
+ listen: "127.0.0.1:8229"
390
+ },
391
+ storage: {
392
+ stateFilePath: resolve(process.cwd(), ".fiber-pay-runtime-state.json"),
393
+ flushIntervalMs: 3e4,
394
+ maxAlertHistory: 5e3
395
+ },
396
+ jobs: {
397
+ enabled: true,
398
+ dbPath: resolve(process.cwd(), ".fiber-pay-jobs.db"),
399
+ maxConcurrentJobs: 5,
400
+ schedulerIntervalMs: 500,
401
+ retryPolicy: defaultPaymentRetryPolicy
402
+ }
403
+ };
404
+ function createRuntimeConfig(input = {}) {
405
+ const config = {
406
+ ...defaultRuntimeConfig,
407
+ ...input,
408
+ proxy: {
409
+ ...defaultRuntimeConfig.proxy,
410
+ ...input.proxy
411
+ },
412
+ storage: {
413
+ ...defaultRuntimeConfig.storage,
414
+ ...input.storage
415
+ },
416
+ jobs: {
417
+ ...defaultRuntimeConfig.jobs,
418
+ ...input.jobs,
419
+ retryPolicy: {
420
+ ...defaultRuntimeConfig.jobs.retryPolicy,
421
+ ...input.jobs?.retryPolicy
422
+ }
423
+ },
424
+ alerts: input.alerts ?? defaultRuntimeConfig.alerts
425
+ };
426
+ if (!config.fiberRpcUrl) {
427
+ throw new Error("Runtime config requires fiberRpcUrl");
428
+ }
429
+ if (!config.proxy.listen) {
430
+ throw new Error("Runtime config requires proxy.listen");
431
+ }
432
+ if (config.channelPollIntervalMs <= 0 || config.invoicePollIntervalMs <= 0) {
433
+ throw new Error("Polling intervals must be > 0");
434
+ }
435
+ if (config.paymentPollIntervalMs <= 0 || config.peerPollIntervalMs <= 0) {
436
+ throw new Error("Polling intervals must be > 0");
437
+ }
438
+ if (config.healthPollIntervalMs <= 0) {
439
+ throw new Error("healthPollIntervalMs must be > 0");
440
+ }
441
+ if (config.completedItemTtlSeconds < 0) {
442
+ throw new Error("completedItemTtlSeconds must be >= 0");
443
+ }
444
+ if (config.storage.flushIntervalMs <= 0) {
445
+ throw new Error("storage.flushIntervalMs must be > 0");
446
+ }
447
+ if (config.storage.maxAlertHistory <= 0) {
448
+ throw new Error("storage.maxAlertHistory must be > 0");
449
+ }
450
+ if (!config.jobs.dbPath) {
451
+ throw new Error("jobs.dbPath is required");
452
+ }
453
+ if (config.jobs.maxConcurrentJobs <= 0) {
454
+ throw new Error("jobs.maxConcurrentJobs must be > 0");
455
+ }
456
+ if (config.jobs.schedulerIntervalMs <= 0) {
457
+ throw new Error("jobs.schedulerIntervalMs must be > 0");
458
+ }
459
+ if (config.jobs.retryPolicy.maxRetries < 0) {
460
+ throw new Error("jobs.retryPolicy.maxRetries must be >= 0");
461
+ }
462
+ if (config.jobs.retryPolicy.baseDelayMs < 0) {
463
+ throw new Error("jobs.retryPolicy.baseDelayMs must be >= 0");
464
+ }
465
+ if (config.jobs.retryPolicy.maxDelayMs < 0) {
466
+ throw new Error("jobs.retryPolicy.maxDelayMs must be >= 0");
467
+ }
468
+ return config;
469
+ }
470
+ function parseListenAddress(listen) {
471
+ const [host, portText] = listen.split(":");
472
+ const port = Number(portText);
473
+ if (!host || Number.isNaN(port) || port <= 0) {
474
+ throw new Error(`Invalid listen address: ${listen}`);
475
+ }
476
+ return { host, port };
477
+ }
478
+
479
+ // src/monitors/channel-monitor.ts
480
+ import { ChannelState } from "@fiber-pay/sdk";
481
+
482
+ // src/diff/channel-diff.ts
483
+ function diffChannels(previous, current) {
484
+ const events = [];
485
+ const prevById = new Map(previous.map((channel) => [channel.channel_id, channel]));
486
+ const currById = new Map(current.map((channel) => [channel.channel_id, channel]));
487
+ for (const [channelId, channel] of currById.entries()) {
488
+ const previousChannel = prevById.get(channelId);
489
+ if (!previousChannel) {
490
+ events.push({ type: "channel_new", channel });
491
+ continue;
492
+ }
493
+ if (channel.state.state_name !== previousChannel.state.state_name) {
494
+ events.push({
495
+ type: "channel_state_changed",
496
+ channel,
497
+ previousState: previousChannel.state.state_name,
498
+ currentState: channel.state.state_name
499
+ });
500
+ }
501
+ if (channel.local_balance !== previousChannel.local_balance || channel.remote_balance !== previousChannel.remote_balance) {
502
+ events.push({
503
+ type: "channel_balance_changed",
504
+ channel,
505
+ localBalanceBefore: previousChannel.local_balance,
506
+ localBalanceAfter: channel.local_balance,
507
+ remoteBalanceBefore: previousChannel.remote_balance,
508
+ remoteBalanceAfter: channel.remote_balance
509
+ });
510
+ }
511
+ const previousPending = new Set(previousChannel.pending_tlcs.map((tlc) => tlc.id));
512
+ const newTlcCount = channel.pending_tlcs.filter((tlc) => !previousPending.has(tlc.id)).length;
513
+ if (newTlcCount > 0) {
514
+ events.push({
515
+ type: "channel_pending_tlc_added",
516
+ channel,
517
+ previousPendingTlcCount: previousChannel.pending_tlcs.length,
518
+ newPendingTlcCount: channel.pending_tlcs.length
519
+ });
520
+ }
521
+ }
522
+ for (const [channelId, previousChannel] of prevById.entries()) {
523
+ if (!currById.has(channelId)) {
524
+ events.push({
525
+ type: "channel_disappeared",
526
+ channelId,
527
+ previousChannel
528
+ });
529
+ }
530
+ }
531
+ return events;
532
+ }
533
+
534
+ // src/monitors/base-monitor.ts
535
+ var BaseMonitor = class {
536
+ intervalMs;
537
+ hooks;
538
+ timer;
539
+ running = false;
540
+ constructor(intervalMs, hooks = {}) {
541
+ this.intervalMs = intervalMs;
542
+ this.hooks = hooks;
543
+ }
544
+ start() {
545
+ this.stop();
546
+ void this.runOnce();
547
+ this.timer = setInterval(() => {
548
+ void this.runOnce();
549
+ }, this.intervalMs);
550
+ }
551
+ stop() {
552
+ if (this.timer) {
553
+ clearInterval(this.timer);
554
+ this.timer = void 0;
555
+ }
556
+ }
557
+ async runOnce() {
558
+ if (this.running) {
559
+ return;
560
+ }
561
+ this.running = true;
562
+ try {
563
+ await this.poll();
564
+ await this.hooks.onCycleSuccess?.(this.name);
565
+ } catch (error) {
566
+ await this.hooks.onCycleError?.(error, this.name);
567
+ } finally {
568
+ this.running = false;
569
+ }
570
+ }
571
+ };
572
+
573
+ // src/monitors/channel-monitor.ts
574
+ var ChannelMonitor = class extends BaseMonitor {
575
+ get name() {
576
+ return "channel-monitor";
577
+ }
578
+ client;
579
+ store;
580
+ alerts;
581
+ config;
582
+ constructor(options) {
583
+ super(options.config.intervalMs, options.hooks);
584
+ this.client = options.client;
585
+ this.store = options.store;
586
+ this.alerts = options.alerts;
587
+ this.config = options.config;
588
+ }
589
+ async poll() {
590
+ const previous = this.store.getChannelSnapshot();
591
+ const result = await this.client.listChannels({
592
+ include_closed: this.config.includeClosedChannels
593
+ });
594
+ const current = result.channels;
595
+ const changes = diffChannels(previous, current);
596
+ for (const change of changes) {
597
+ if (change.type === "channel_new" && change.channel.state.state_name === ChannelState.NegotiatingFunding) {
598
+ await this.alerts.emit({
599
+ type: "new_inbound_channel_request",
600
+ priority: "high",
601
+ source: this.name,
602
+ data: { channelId: change.channel.channel_id, channel: change.channel }
603
+ });
604
+ }
605
+ if (change.type === "channel_state_changed") {
606
+ await this.alerts.emit({
607
+ type: "channel_state_changed",
608
+ priority: getChannelStatePriority(change.currentState),
609
+ source: this.name,
610
+ data: {
611
+ channelId: change.channel.channel_id,
612
+ previousState: change.previousState,
613
+ currentState: change.currentState,
614
+ channel: change.channel
615
+ }
616
+ });
617
+ }
618
+ if (change.type === "channel_state_changed" && change.currentState === ChannelState.ChannelReady) {
619
+ await this.alerts.emit({
620
+ type: "channel_became_ready",
621
+ priority: "medium",
622
+ source: this.name,
623
+ data: change
624
+ });
625
+ }
626
+ if (change.type === "channel_state_changed" && (change.currentState === ChannelState.ShuttingDown || change.currentState === ChannelState.Closed)) {
627
+ await this.alerts.emit({
628
+ type: "channel_closing",
629
+ priority: "high",
630
+ source: this.name,
631
+ data: change
632
+ });
633
+ }
634
+ if (change.type === "channel_disappeared") {
635
+ await this.alerts.emit({
636
+ type: "channel_state_changed",
637
+ priority: "high",
638
+ source: this.name,
639
+ data: {
640
+ channelId: change.channelId,
641
+ previousState: change.previousChannel.state.state_name,
642
+ currentState: "DISAPPEARED",
643
+ channel: change.previousChannel
644
+ }
645
+ });
646
+ await this.alerts.emit({
647
+ type: "channel_closing",
648
+ priority: "high",
649
+ source: this.name,
650
+ data: change
651
+ });
652
+ }
653
+ if (change.type === "channel_balance_changed") {
654
+ await this.alerts.emit({
655
+ type: "channel_balance_changed",
656
+ priority: "low",
657
+ source: this.name,
658
+ data: change
659
+ });
660
+ }
661
+ if (change.type === "channel_pending_tlc_added") {
662
+ await this.alerts.emit({
663
+ type: "new_pending_tlc",
664
+ priority: "medium",
665
+ source: this.name,
666
+ data: change
667
+ });
668
+ }
669
+ }
670
+ this.store.setChannelSnapshot(current);
671
+ }
672
+ };
673
+ function getChannelStatePriority(stateName) {
674
+ const normalized = stateName.toUpperCase();
675
+ const closedState = String(ChannelState.Closed).toUpperCase();
676
+ const shuttingDownState = String(ChannelState.ShuttingDown).toUpperCase();
677
+ const readyState = String(ChannelState.ChannelReady).toUpperCase();
678
+ if (normalized === closedState || normalized === "CLOSED" || normalized === shuttingDownState || normalized === "SHUTTING_DOWN") {
679
+ return "high";
680
+ }
681
+ if (normalized === readyState || normalized === "CHANNEL_READY") {
682
+ return "medium";
683
+ }
684
+ return "low";
685
+ }
686
+
687
+ // src/monitors/health-monitor.ts
688
+ var HealthMonitor = class extends BaseMonitor {
689
+ get name() {
690
+ return "health-monitor";
691
+ }
692
+ client;
693
+ alerts;
694
+ isOffline = false;
695
+ constructor(options) {
696
+ super(options.config.intervalMs, options.hooks);
697
+ this.client = options.client;
698
+ this.alerts = options.alerts;
699
+ }
700
+ async poll() {
701
+ let isHealthy = false;
702
+ let failureReason;
703
+ try {
704
+ isHealthy = await this.client.ping();
705
+ } catch (error) {
706
+ failureReason = error instanceof Error ? error.message : String(error);
707
+ isHealthy = false;
708
+ }
709
+ if (isHealthy && this.isOffline) {
710
+ this.isOffline = false;
711
+ await this.alerts.emit({
712
+ type: "node_online",
713
+ priority: "low",
714
+ source: this.name,
715
+ data: { message: "Fiber node RPC recovered" }
716
+ });
717
+ return;
718
+ }
719
+ if (!isHealthy && !this.isOffline) {
720
+ this.isOffline = true;
721
+ await this.alerts.emit({
722
+ type: "node_offline",
723
+ priority: "critical",
724
+ source: this.name,
725
+ data: {
726
+ message: failureReason ? `Fiber node ping failed: ${failureReason}` : "Fiber node ping returned false"
727
+ }
728
+ });
729
+ }
730
+ }
731
+ };
732
+
733
+ // src/monitors/tracker-utils.ts
734
+ function isExpectedTrackerError(error) {
735
+ const message = error instanceof Error ? error.message : String(error);
736
+ return /not found|does not exist|no such|temporarily unavailable|connection refused|timed out|timeout/i.test(
737
+ message
738
+ );
739
+ }
740
+
741
+ // src/monitors/invoice-tracker.ts
742
+ var InvoiceTracker = class extends BaseMonitor {
743
+ get name() {
744
+ return "invoice-tracker";
745
+ }
746
+ client;
747
+ store;
748
+ alerts;
749
+ config;
750
+ constructor(options) {
751
+ super(options.config.intervalMs, options.hooks);
752
+ this.client = options.client;
753
+ this.store = options.store;
754
+ this.alerts = options.alerts;
755
+ this.config = options.config;
756
+ }
757
+ async poll() {
758
+ const tracked = this.store.listTrackedInvoices();
759
+ for (const invoice of tracked) {
760
+ try {
761
+ const next = await this.client.getInvoice({ payment_hash: invoice.paymentHash });
762
+ const previousStatus = invoice.status;
763
+ const currentStatus = next.status;
764
+ if (currentStatus !== previousStatus) {
765
+ this.store.updateTrackedInvoice(invoice.paymentHash, currentStatus);
766
+ if (currentStatus === "Received" || currentStatus === "Paid") {
767
+ await this.alerts.emit({
768
+ type: "incoming_payment_received",
769
+ priority: "high",
770
+ source: this.name,
771
+ data: {
772
+ paymentHash: invoice.paymentHash,
773
+ previousStatus,
774
+ currentStatus,
775
+ invoice: next
776
+ }
777
+ });
778
+ }
779
+ if (currentStatus === "Expired") {
780
+ await this.alerts.emit({
781
+ type: "invoice_expired",
782
+ priority: "medium",
783
+ source: this.name,
784
+ data: {
785
+ paymentHash: invoice.paymentHash,
786
+ previousStatus,
787
+ currentStatus,
788
+ invoice: next
789
+ }
790
+ });
791
+ }
792
+ if (currentStatus === "Cancelled") {
793
+ await this.alerts.emit({
794
+ type: "invoice_cancelled",
795
+ priority: "medium",
796
+ source: this.name,
797
+ data: {
798
+ paymentHash: invoice.paymentHash,
799
+ previousStatus,
800
+ currentStatus,
801
+ invoice: next
802
+ }
803
+ });
804
+ }
805
+ }
806
+ } catch (error) {
807
+ if (isExpectedTrackerError(error)) {
808
+ continue;
809
+ }
810
+ throw error;
811
+ }
812
+ }
813
+ this.store.pruneCompleted(this.config.completedItemTtlSeconds * 1e3);
814
+ }
815
+ };
816
+
817
+ // src/monitors/payment-tracker.ts
818
+ var PaymentTracker = class extends BaseMonitor {
819
+ get name() {
820
+ return "payment-tracker";
821
+ }
822
+ client;
823
+ store;
824
+ alerts;
825
+ config;
826
+ constructor(options) {
827
+ super(options.config.intervalMs, options.hooks);
828
+ this.client = options.client;
829
+ this.store = options.store;
830
+ this.alerts = options.alerts;
831
+ this.config = options.config;
832
+ }
833
+ async poll() {
834
+ const tracked = this.store.listTrackedPayments();
835
+ for (const payment of tracked) {
836
+ try {
837
+ const next = await this.client.getPayment({ payment_hash: payment.paymentHash });
838
+ const previousStatus = payment.status;
839
+ const currentStatus = next.status;
840
+ if (currentStatus !== previousStatus) {
841
+ this.store.updateTrackedPayment(payment.paymentHash, currentStatus);
842
+ if (currentStatus === "Success") {
843
+ await this.alerts.emit({
844
+ type: "outgoing_payment_completed",
845
+ priority: "medium",
846
+ source: this.name,
847
+ data: {
848
+ paymentHash: payment.paymentHash,
849
+ previousStatus,
850
+ currentStatus,
851
+ payment: next
852
+ }
853
+ });
854
+ }
855
+ if (currentStatus === "Failed") {
856
+ await this.alerts.emit({
857
+ type: "outgoing_payment_failed",
858
+ priority: "high",
859
+ source: this.name,
860
+ data: {
861
+ paymentHash: payment.paymentHash,
862
+ previousStatus,
863
+ currentStatus,
864
+ payment: next
865
+ }
866
+ });
867
+ }
868
+ }
869
+ } catch (error) {
870
+ if (isExpectedTrackerError(error)) {
871
+ continue;
872
+ }
873
+ throw error;
874
+ }
875
+ }
876
+ this.store.pruneCompleted(this.config.completedItemTtlSeconds * 1e3);
877
+ }
878
+ };
879
+
880
+ // src/diff/peer-diff.ts
881
+ function diffPeers(previous, current) {
882
+ const events = [];
883
+ const prevById = new Map(previous.map((peer) => [peer.peer_id, peer]));
884
+ const currById = new Map(current.map((peer) => [peer.peer_id, peer]));
885
+ for (const [peerId, peer] of currById.entries()) {
886
+ if (!prevById.has(peerId)) {
887
+ events.push({ type: "peer_connected", peer });
888
+ }
889
+ }
890
+ for (const [peerId, peer] of prevById.entries()) {
891
+ if (!currById.has(peerId)) {
892
+ events.push({ type: "peer_disconnected", peer });
893
+ }
894
+ }
895
+ return events;
896
+ }
897
+
898
+ // src/monitors/peer-monitor.ts
899
+ var PeerMonitor = class extends BaseMonitor {
900
+ get name() {
901
+ return "peer-monitor";
902
+ }
903
+ client;
904
+ store;
905
+ alerts;
906
+ constructor(options) {
907
+ super(options.config.intervalMs, options.hooks);
908
+ this.client = options.client;
909
+ this.store = options.store;
910
+ this.alerts = options.alerts;
911
+ }
912
+ async poll() {
913
+ const previous = this.store.getPeerSnapshot();
914
+ const result = await this.client.listPeers();
915
+ const current = result.peers;
916
+ const changes = diffPeers(previous, current);
917
+ for (const change of changes) {
918
+ if (change.type === "peer_connected") {
919
+ await this.alerts.emit({
920
+ type: "peer_connected",
921
+ priority: "low",
922
+ source: this.name,
923
+ data: { peer: change.peer }
924
+ });
925
+ }
926
+ if (change.type === "peer_disconnected") {
927
+ await this.alerts.emit({
928
+ type: "peer_disconnected",
929
+ priority: "low",
930
+ source: this.name,
931
+ data: { peer: change.peer }
932
+ });
933
+ }
934
+ }
935
+ this.store.setPeerSnapshot(current);
936
+ }
937
+ };
938
+
939
+ // src/proxy/rpc-proxy.ts
940
+ import http2 from "http";
941
+
942
+ // src/proxy/body.ts
943
+ var MAX_REQUEST_BODY_BYTES = 1024 * 1024;
944
+ async function readRawBody(req) {
945
+ const chunks = [];
946
+ let totalBytes = 0;
947
+ return await new Promise((resolve2, reject) => {
948
+ req.on("data", (chunk) => {
949
+ totalBytes += chunk.length;
950
+ if (totalBytes > MAX_REQUEST_BODY_BYTES) {
951
+ req.destroy();
952
+ reject(new PayloadTooLargeError());
953
+ return;
954
+ }
955
+ chunks.push(chunk);
956
+ });
957
+ req.on("end", () => resolve2(Buffer.concat(chunks)));
958
+ req.on("error", reject);
959
+ });
960
+ }
961
+ var PayloadTooLargeError = class extends Error {
962
+ constructor() {
963
+ super("Payload too large");
964
+ this.name = "PayloadTooLargeError";
965
+ }
966
+ };
967
+ function isPayloadTooLargeError(error) {
968
+ return error instanceof PayloadTooLargeError;
969
+ }
970
+
971
+ // src/proxy/http-utils.ts
972
+ var CORS_HEADERS = {
973
+ "access-control-allow-origin": "*",
974
+ "access-control-allow-methods": "GET,POST,DELETE,OPTIONS",
975
+ "access-control-allow-headers": "Content-Type, Authorization"
976
+ };
977
+ var CORS_PREFLIGHT_HEADERS = {
978
+ ...CORS_HEADERS,
979
+ "access-control-max-age": "86400"
980
+ };
981
+ function writeJson(res, status, value) {
982
+ res.writeHead(status, {
983
+ "content-type": "application/json",
984
+ ...CORS_HEADERS
985
+ });
986
+ res.end(JSON.stringify(value));
987
+ }
988
+ function parseOptionalPositiveInteger(value) {
989
+ if (!value) {
990
+ return void 0;
991
+ }
992
+ const parsed = Number(value);
993
+ if (!Number.isInteger(parsed) || parsed <= 0) {
994
+ return void 0;
995
+ }
996
+ return parsed;
997
+ }
998
+
999
+ // src/proxy/json.ts
1000
+ function tryParseJson(text) {
1001
+ try {
1002
+ return JSON.parse(text);
1003
+ } catch {
1004
+ return void 0;
1005
+ }
1006
+ }
1007
+ function isObject(value) {
1008
+ return typeof value === "object" && value !== null;
1009
+ }
1010
+
1011
+ // src/proxy/jsonrpc-tracking.ts
1012
+ function collectJsonRpcMethods(requestBody) {
1013
+ const methods = /* @__PURE__ */ new Map();
1014
+ for (const item of normalizeJsonRpcRequest(requestBody)) {
1015
+ if (item.id !== void 0 && typeof item.method === "string") {
1016
+ methods.set(item.id, item.method);
1017
+ }
1018
+ }
1019
+ return methods;
1020
+ }
1021
+ function captureTrackedHashes(methodById, responseBody, handlers) {
1022
+ const responses = normalizeJsonRpcResponse(responseBody);
1023
+ for (const message of responses) {
1024
+ if (message.error || message.id === void 0) {
1025
+ continue;
1026
+ }
1027
+ const method = methodById.get(message.id);
1028
+ if (!method) {
1029
+ continue;
1030
+ }
1031
+ if (method === "new_invoice") {
1032
+ const paymentHash = extractInvoicePaymentHash(message.result);
1033
+ if (paymentHash) {
1034
+ handlers.onInvoiceTracked(paymentHash);
1035
+ }
1036
+ }
1037
+ if (method === "send_payment") {
1038
+ const paymentHash = extractPaymentHash(message.result);
1039
+ if (paymentHash) {
1040
+ handlers.onPaymentTracked(paymentHash);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ function normalizeJsonRpcRequest(body) {
1046
+ if (!body) {
1047
+ return [];
1048
+ }
1049
+ if (Array.isArray(body)) {
1050
+ return body.filter(isObject);
1051
+ }
1052
+ if (isObject(body)) {
1053
+ return [body];
1054
+ }
1055
+ return [];
1056
+ }
1057
+ function normalizeJsonRpcResponse(body) {
1058
+ if (!body) {
1059
+ return [];
1060
+ }
1061
+ if (Array.isArray(body)) {
1062
+ return body.filter(isObject);
1063
+ }
1064
+ if (isObject(body)) {
1065
+ return [body];
1066
+ }
1067
+ return [];
1068
+ }
1069
+ function extractInvoicePaymentHash(result) {
1070
+ if (!isObject(result)) {
1071
+ return void 0;
1072
+ }
1073
+ const invoice = result.invoice;
1074
+ if (!isObject(invoice)) {
1075
+ return void 0;
1076
+ }
1077
+ const data = invoice.data;
1078
+ if (!isObject(data)) {
1079
+ return void 0;
1080
+ }
1081
+ return typeof data.payment_hash === "string" ? data.payment_hash : void 0;
1082
+ }
1083
+ function extractPaymentHash(result) {
1084
+ if (!isObject(result)) {
1085
+ return void 0;
1086
+ }
1087
+ return typeof result.payment_hash === "string" ? result.payment_hash : void 0;
1088
+ }
1089
+
1090
+ // src/proxy/job-routes.ts
1091
+ async function handleJobPostEndpoint(pathname, req, res, deps) {
1092
+ let rawBody;
1093
+ try {
1094
+ rawBody = await readRawBody(req);
1095
+ } catch (error) {
1096
+ if (isPayloadTooLargeError(error)) {
1097
+ writeJson(res, 413, { error: "Request body too large" });
1098
+ return;
1099
+ }
1100
+ writeJson(res, 400, { error: "Failed to read request body" });
1101
+ return;
1102
+ }
1103
+ const body = tryParseJson(rawBody.toString("utf-8"));
1104
+ if (!isObject(body)) {
1105
+ writeJson(res, 400, { error: "Invalid JSON body" });
1106
+ return;
1107
+ }
1108
+ if (pathname === "/jobs/payment") {
1109
+ if (!deps.createPaymentJob) {
1110
+ writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
1111
+ return;
1112
+ }
1113
+ const params = body.params;
1114
+ if (!params) {
1115
+ writeJson(res, 400, { error: "Missing params for payment job" });
1116
+ return;
1117
+ }
1118
+ const options = body.options;
1119
+ const job = await deps.createPaymentJob(params, options);
1120
+ writeJson(res, 200, job);
1121
+ return;
1122
+ }
1123
+ if (pathname === "/jobs/invoice") {
1124
+ if (!deps.createInvoiceJob) {
1125
+ writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
1126
+ return;
1127
+ }
1128
+ const params = body.params;
1129
+ if (!params) {
1130
+ writeJson(res, 400, { error: "Missing params for invoice job" });
1131
+ return;
1132
+ }
1133
+ const options = body.options;
1134
+ const job = await deps.createInvoiceJob(params, options);
1135
+ writeJson(res, 200, job);
1136
+ return;
1137
+ }
1138
+ if (pathname === "/jobs/channel") {
1139
+ if (!deps.createChannelJob) {
1140
+ writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
1141
+ return;
1142
+ }
1143
+ const params = body.params;
1144
+ if (!params) {
1145
+ writeJson(res, 400, { error: "Missing params for channel job" });
1146
+ return;
1147
+ }
1148
+ const options = body.options;
1149
+ const job = await deps.createChannelJob(params, options);
1150
+ writeJson(res, 200, job);
1151
+ return;
1152
+ }
1153
+ writeJson(res, 404, { error: "Unknown jobs endpoint" });
1154
+ }
1155
+ async function handleDeleteEndpoint(req, res, deps) {
1156
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
1157
+ if (!url.pathname.startsWith("/jobs/")) {
1158
+ writeJson(res, 405, { error: "Method not allowed" });
1159
+ return;
1160
+ }
1161
+ if (!deps.cancelJob) {
1162
+ writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
1163
+ return;
1164
+ }
1165
+ const segments = url.pathname.split("/").filter(Boolean);
1166
+ const [, id] = segments;
1167
+ if (!id) {
1168
+ writeJson(res, 400, { error: "Missing job id" });
1169
+ return;
1170
+ }
1171
+ try {
1172
+ deps.cancelJob(id);
1173
+ writeJson(res, 204, {});
1174
+ } catch (error) {
1175
+ writeJson(res, 404, { error: String(error) });
1176
+ }
1177
+ }
1178
+
1179
+ // src/alerts/types.ts
1180
+ var alertTypeValues = [
1181
+ "channel_state_changed",
1182
+ "new_inbound_channel_request",
1183
+ "channel_became_ready",
1184
+ "channel_closing",
1185
+ "incoming_payment_received",
1186
+ "invoice_expired",
1187
+ "invoice_cancelled",
1188
+ "outgoing_payment_completed",
1189
+ "outgoing_payment_failed",
1190
+ "channel_balance_changed",
1191
+ "new_pending_tlc",
1192
+ "peer_connected",
1193
+ "peer_disconnected",
1194
+ "node_offline",
1195
+ "node_online",
1196
+ "payment_job_started",
1197
+ "payment_job_retrying",
1198
+ "payment_job_succeeded",
1199
+ "payment_job_failed",
1200
+ "invoice_job_started",
1201
+ "invoice_job_retrying",
1202
+ "invoice_job_succeeded",
1203
+ "invoice_job_failed",
1204
+ "channel_job_started",
1205
+ "channel_job_retrying",
1206
+ "channel_job_succeeded",
1207
+ "channel_job_failed"
1208
+ ];
1209
+ var alertPriorityOrder = {
1210
+ low: 1,
1211
+ medium: 2,
1212
+ high: 3,
1213
+ critical: 4
1214
+ };
1215
+ function isAlertPriority(value) {
1216
+ return value === "critical" || value === "high" || value === "medium" || value === "low";
1217
+ }
1218
+ function isAlertType(value) {
1219
+ return alertTypeValues.includes(value);
1220
+ }
1221
+
1222
+ // src/proxy/monitor-routes.ts
1223
+ function handleMonitorEndpoint(req, res, deps) {
1224
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
1225
+ if (url.pathname === "/jobs") {
1226
+ if (!deps.listJobs) {
1227
+ writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
1228
+ return;
1229
+ }
1230
+ const state = url.searchParams.get("state") ?? void 0;
1231
+ const type = url.searchParams.get("type");
1232
+ const limit = parseOptionalPositiveInteger(url.searchParams.get("limit"));
1233
+ const offset = parseOptionalPositiveInteger(url.searchParams.get("offset"));
1234
+ writeJson(res, 200, {
1235
+ jobs: deps.listJobs({
1236
+ state,
1237
+ type: type ?? void 0,
1238
+ limit,
1239
+ offset
1240
+ })
1241
+ });
1242
+ return;
1243
+ }
1244
+ if (url.pathname.startsWith("/jobs/")) {
1245
+ if (!deps.getJob) {
1246
+ writeJson(res, 404, { error: "Jobs are not enabled in runtime config" });
1247
+ return;
1248
+ }
1249
+ const segments = url.pathname.split("/").filter(Boolean);
1250
+ const [, id, sub] = segments;
1251
+ if (!id) {
1252
+ writeJson(res, 400, { error: "Missing job id" });
1253
+ return;
1254
+ }
1255
+ if (sub === "events") {
1256
+ if (!deps.listJobEvents) {
1257
+ writeJson(res, 404, { error: "Job events not available" });
1258
+ return;
1259
+ }
1260
+ writeJson(res, 200, { events: deps.listJobEvents(id) });
1261
+ return;
1262
+ }
1263
+ const job = deps.getJob(id);
1264
+ if (!job) {
1265
+ writeJson(res, 404, { error: "Job not found" });
1266
+ return;
1267
+ }
1268
+ writeJson(res, 200, job);
1269
+ return;
1270
+ }
1271
+ if (url.pathname === "/monitor/list_tracked_invoices") {
1272
+ writeJson(res, 200, { invoices: deps.listTrackedInvoices() });
1273
+ return;
1274
+ }
1275
+ if (url.pathname === "/monitor/list_tracked_payments") {
1276
+ writeJson(res, 200, { payments: deps.listTrackedPayments() });
1277
+ return;
1278
+ }
1279
+ if (url.pathname === "/monitor/list_alerts") {
1280
+ const limitRaw = url.searchParams.get("limit");
1281
+ const minPriorityRaw = url.searchParams.get("min_priority");
1282
+ const typeRaw = url.searchParams.get("type");
1283
+ const sourceRaw = url.searchParams.get("source");
1284
+ const limit = parseOptionalPositiveInteger(limitRaw);
1285
+ if (limitRaw !== null && limit === void 0) {
1286
+ writeJson(res, 400, {
1287
+ error: "Invalid query parameter: limit must be a positive integer"
1288
+ });
1289
+ return;
1290
+ }
1291
+ if (minPriorityRaw && !isAlertPriority(minPriorityRaw)) {
1292
+ writeJson(res, 400, {
1293
+ error: "Invalid query parameter: min_priority must be one of critical|high|medium|low"
1294
+ });
1295
+ return;
1296
+ }
1297
+ if (typeRaw && !isAlertType(typeRaw)) {
1298
+ writeJson(res, 400, {
1299
+ error: "Invalid query parameter: type is not a known alert type"
1300
+ });
1301
+ return;
1302
+ }
1303
+ const minPriority = minPriorityRaw && isAlertPriority(minPriorityRaw) ? minPriorityRaw : void 0;
1304
+ const type = typeRaw && isAlertType(typeRaw) ? typeRaw : void 0;
1305
+ writeJson(res, 200, {
1306
+ alerts: deps.listAlerts({
1307
+ limit,
1308
+ minPriority,
1309
+ type,
1310
+ source: sourceRaw ?? void 0
1311
+ })
1312
+ });
1313
+ return;
1314
+ }
1315
+ if (url.pathname === "/monitor/status") {
1316
+ writeJson(res, 200, deps.getStatus());
1317
+ return;
1318
+ }
1319
+ writeJson(res, 404, { error: "Not found" });
1320
+ }
1321
+
1322
+ // src/proxy/rpc-proxy.ts
1323
+ var RpcMonitorProxy = class {
1324
+ config;
1325
+ deps;
1326
+ server;
1327
+ constructor(config, deps) {
1328
+ this.config = config;
1329
+ this.deps = deps;
1330
+ }
1331
+ async start() {
1332
+ if (this.server) {
1333
+ return;
1334
+ }
1335
+ this.server = http2.createServer((req, res) => {
1336
+ void this.handleRequest(req, res);
1337
+ });
1338
+ const { host, port } = parseListenAddress(this.config.listen);
1339
+ await new Promise((resolve2, reject) => {
1340
+ this.server?.once("error", reject);
1341
+ this.server?.listen(port, host, () => {
1342
+ this.server?.off("error", reject);
1343
+ resolve2();
1344
+ });
1345
+ });
1346
+ }
1347
+ async stop() {
1348
+ if (!this.server) {
1349
+ return;
1350
+ }
1351
+ const server = this.server;
1352
+ this.server = void 0;
1353
+ await new Promise((resolve2) => {
1354
+ server.close(() => resolve2());
1355
+ });
1356
+ }
1357
+ async handleRequest(req, res) {
1358
+ if (req.method === "OPTIONS") {
1359
+ res.writeHead(204, CORS_PREFLIGHT_HEADERS);
1360
+ res.end();
1361
+ return;
1362
+ }
1363
+ if (req.method === "GET") {
1364
+ handleMonitorEndpoint(req, res, this.deps);
1365
+ return;
1366
+ }
1367
+ if (req.method === "DELETE") {
1368
+ await handleDeleteEndpoint(req, res, this.deps);
1369
+ return;
1370
+ }
1371
+ if (req.method !== "POST") {
1372
+ writeJson(res, 405, { error: "Method not allowed" });
1373
+ return;
1374
+ }
1375
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
1376
+ if (url.pathname.startsWith("/jobs/")) {
1377
+ await handleJobPostEndpoint(url.pathname, req, res, this.deps);
1378
+ return;
1379
+ }
1380
+ let requestBody;
1381
+ try {
1382
+ requestBody = await readRawBody(req);
1383
+ } catch (error) {
1384
+ if (isPayloadTooLargeError(error)) {
1385
+ writeJson(res, 413, { error: "Request body too large" });
1386
+ return;
1387
+ }
1388
+ writeJson(res, 400, { error: "Failed to read request body" });
1389
+ return;
1390
+ }
1391
+ const requestJson = tryParseJson(requestBody.toString("utf-8"));
1392
+ const methodById = collectJsonRpcMethods(requestJson);
1393
+ let responseText = "";
1394
+ let responseStatus = 500;
1395
+ let responseHeaders = new Headers();
1396
+ try {
1397
+ const response = await fetch(this.config.targetUrl, {
1398
+ method: "POST",
1399
+ headers: {
1400
+ "content-type": req.headers["content-type"] ?? "application/json",
1401
+ ...req.headers.authorization ? { authorization: req.headers.authorization } : {}
1402
+ },
1403
+ body: requestBody
1404
+ });
1405
+ responseStatus = response.status;
1406
+ responseHeaders = response.headers;
1407
+ responseText = await response.text();
1408
+ const responseJson = tryParseJson(responseText);
1409
+ captureTrackedHashes(methodById, responseJson, {
1410
+ onInvoiceTracked: this.deps.onInvoiceTracked,
1411
+ onPaymentTracked: this.deps.onPaymentTracked
1412
+ });
1413
+ } catch (error) {
1414
+ writeJson(res, 502, { error: `Proxy request failed: ${String(error)}` });
1415
+ return;
1416
+ }
1417
+ const contentType = responseHeaders.get("content-type") ?? "application/json";
1418
+ res.writeHead(responseStatus, {
1419
+ "content-type": contentType,
1420
+ ...CORS_HEADERS
1421
+ });
1422
+ res.end(responseText);
1423
+ }
1424
+ };
1425
+
1426
+ // src/storage/memory-store.ts
1427
+ import { mkdir, readFile, writeFile } from "fs/promises";
1428
+ import { dirname as dirname2 } from "path";
1429
+ function nowMs() {
1430
+ return Date.now();
1431
+ }
1432
+ function toRecord2(items) {
1433
+ return Object.fromEntries(items.entries());
1434
+ }
1435
+ var MemoryStore = class {
1436
+ config;
1437
+ channelSnapshot = [];
1438
+ peerSnapshot = [];
1439
+ trackedInvoices = /* @__PURE__ */ new Map();
1440
+ trackedPayments = /* @__PURE__ */ new Map();
1441
+ alerts = [];
1442
+ flushTimer;
1443
+ constructor(config) {
1444
+ this.config = config;
1445
+ }
1446
+ async load() {
1447
+ try {
1448
+ const raw = await readFile(this.config.stateFilePath, "utf-8");
1449
+ const state = JSON.parse(raw);
1450
+ this.channelSnapshot = state.channels ?? [];
1451
+ this.peerSnapshot = state.peers ?? [];
1452
+ this.trackedInvoices = new Map(Object.entries(state.trackedInvoices ?? {}));
1453
+ this.trackedPayments = new Map(Object.entries(state.trackedPayments ?? {}));
1454
+ this.alerts = state.alerts ?? [];
1455
+ } catch {
1456
+ this.channelSnapshot = [];
1457
+ this.peerSnapshot = [];
1458
+ this.trackedInvoices.clear();
1459
+ this.trackedPayments.clear();
1460
+ this.alerts = [];
1461
+ }
1462
+ }
1463
+ async flush() {
1464
+ const state = {
1465
+ channels: this.channelSnapshot,
1466
+ peers: this.peerSnapshot,
1467
+ trackedInvoices: toRecord2(this.trackedInvoices),
1468
+ trackedPayments: toRecord2(this.trackedPayments),
1469
+ alerts: this.alerts
1470
+ };
1471
+ await mkdir(dirname2(this.config.stateFilePath), { recursive: true });
1472
+ await writeFile(this.config.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
1473
+ }
1474
+ startAutoFlush() {
1475
+ this.stopAutoFlush();
1476
+ this.flushTimer = setInterval(() => {
1477
+ void this.flush();
1478
+ }, this.config.flushIntervalMs);
1479
+ }
1480
+ stopAutoFlush() {
1481
+ if (this.flushTimer) {
1482
+ clearInterval(this.flushTimer);
1483
+ this.flushTimer = void 0;
1484
+ }
1485
+ }
1486
+ pruneCompleted(ttlMs) {
1487
+ const now = nowMs();
1488
+ for (const [hash, entry] of this.trackedInvoices.entries()) {
1489
+ if (entry.completedAt && now - entry.completedAt >= ttlMs) {
1490
+ this.trackedInvoices.delete(hash);
1491
+ }
1492
+ }
1493
+ for (const [hash, entry] of this.trackedPayments.entries()) {
1494
+ if (entry.completedAt && now - entry.completedAt >= ttlMs) {
1495
+ this.trackedPayments.delete(hash);
1496
+ }
1497
+ }
1498
+ }
1499
+ getChannelSnapshot() {
1500
+ return this.channelSnapshot;
1501
+ }
1502
+ setChannelSnapshot(channels) {
1503
+ this.channelSnapshot = channels;
1504
+ }
1505
+ getPeerSnapshot() {
1506
+ return this.peerSnapshot;
1507
+ }
1508
+ setPeerSnapshot(peers) {
1509
+ this.peerSnapshot = peers;
1510
+ }
1511
+ addTrackedInvoice(hash, status = "Open") {
1512
+ const existing = this.trackedInvoices.get(hash);
1513
+ if (existing) return;
1514
+ const now = nowMs();
1515
+ this.trackedInvoices.set(hash, {
1516
+ paymentHash: hash,
1517
+ status,
1518
+ trackedAt: now,
1519
+ updatedAt: now
1520
+ });
1521
+ }
1522
+ listTrackedInvoices() {
1523
+ return [...this.trackedInvoices.values()];
1524
+ }
1525
+ getTrackedInvoice(hash) {
1526
+ return this.trackedInvoices.get(hash);
1527
+ }
1528
+ updateTrackedInvoice(hash, status) {
1529
+ const now = nowMs();
1530
+ const existing = this.trackedInvoices.get(hash);
1531
+ const next = existing ? {
1532
+ ...existing,
1533
+ status,
1534
+ updatedAt: now,
1535
+ completedAt: isTerminalInvoiceStatus(status) ? existing.completedAt ?? now : void 0
1536
+ } : {
1537
+ paymentHash: hash,
1538
+ status,
1539
+ trackedAt: now,
1540
+ updatedAt: now,
1541
+ completedAt: isTerminalInvoiceStatus(status) ? now : void 0
1542
+ };
1543
+ this.trackedInvoices.set(hash, next);
1544
+ }
1545
+ addTrackedPayment(hash, status = "Created") {
1546
+ const existing = this.trackedPayments.get(hash);
1547
+ if (existing) return;
1548
+ const now = nowMs();
1549
+ this.trackedPayments.set(hash, {
1550
+ paymentHash: hash,
1551
+ status,
1552
+ trackedAt: now,
1553
+ updatedAt: now
1554
+ });
1555
+ }
1556
+ listTrackedPayments() {
1557
+ return [...this.trackedPayments.values()];
1558
+ }
1559
+ getTrackedPayment(hash) {
1560
+ return this.trackedPayments.get(hash);
1561
+ }
1562
+ updateTrackedPayment(hash, status) {
1563
+ const now = nowMs();
1564
+ const existing = this.trackedPayments.get(hash);
1565
+ const next = existing ? {
1566
+ ...existing,
1567
+ status,
1568
+ updatedAt: now,
1569
+ completedAt: isTerminalPaymentStatus(status) ? existing.completedAt ?? now : void 0
1570
+ } : {
1571
+ paymentHash: hash,
1572
+ status,
1573
+ trackedAt: now,
1574
+ updatedAt: now,
1575
+ completedAt: isTerminalPaymentStatus(status) ? now : void 0
1576
+ };
1577
+ this.trackedPayments.set(hash, next);
1578
+ }
1579
+ addAlert(alert) {
1580
+ this.alerts.push(alert);
1581
+ if (this.alerts.length > this.config.maxAlertHistory) {
1582
+ this.alerts.splice(0, this.alerts.length - this.config.maxAlertHistory);
1583
+ }
1584
+ }
1585
+ listAlerts(filters) {
1586
+ const minPriority = filters?.minPriority;
1587
+ const type = filters?.type;
1588
+ const source = filters?.source;
1589
+ const limit = filters?.limit;
1590
+ let alerts = this.alerts;
1591
+ if (minPriority) {
1592
+ const minRank = alertPriorityOrder[minPriority];
1593
+ alerts = alerts.filter((alert) => alertPriorityOrder[alert.priority] >= minRank);
1594
+ }
1595
+ if (type) {
1596
+ alerts = alerts.filter((alert) => alert.type === type);
1597
+ }
1598
+ if (source) {
1599
+ alerts = alerts.filter((alert) => alert.source === source);
1600
+ }
1601
+ if (!limit || limit <= 0) {
1602
+ return [...alerts];
1603
+ }
1604
+ return alerts.slice(-limit);
1605
+ }
1606
+ };
1607
+ function isTerminalInvoiceStatus(status) {
1608
+ return status === "Paid" || status === "Cancelled" || status === "Expired";
1609
+ }
1610
+ function isTerminalPaymentStatus(status) {
1611
+ return status === "Success" || status === "Failed";
1612
+ }
1613
+
1614
+ // src/storage/sqlite-store.ts
1615
+ import Database from "better-sqlite3";
1616
+ import { randomUUID as randomUUID2 } from "crypto";
1617
+ import { mkdirSync as mkdirSync2 } from "fs";
1618
+ import { dirname as dirname3 } from "path";
1619
+ var MIGRATIONS = [
1620
+ {
1621
+ version: 1,
1622
+ sql: `
1623
+ CREATE TABLE IF NOT EXISTS jobs (
1624
+ id TEXT PRIMARY KEY,
1625
+ type TEXT NOT NULL,
1626
+ state TEXT NOT NULL,
1627
+ params TEXT NOT NULL,
1628
+ result TEXT,
1629
+ error TEXT,
1630
+ retry_count INTEGER NOT NULL DEFAULT 0,
1631
+ max_retries INTEGER NOT NULL DEFAULT 3,
1632
+ next_retry_at INTEGER,
1633
+ idempotency_key TEXT NOT NULL UNIQUE,
1634
+ created_at INTEGER NOT NULL,
1635
+ updated_at INTEGER NOT NULL,
1636
+ completed_at INTEGER
1637
+ );
1638
+
1639
+ CREATE INDEX IF NOT EXISTS idx_jobs_state ON jobs(state);
1640
+ CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs(type);
1641
+ CREATE INDEX IF NOT EXISTS idx_jobs_next_retry_at ON jobs(next_retry_at);
1642
+
1643
+ CREATE TABLE IF NOT EXISTS job_events (
1644
+ id TEXT PRIMARY KEY,
1645
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
1646
+ event_type TEXT NOT NULL,
1647
+ from_state TEXT,
1648
+ to_state TEXT,
1649
+ data TEXT,
1650
+ created_at INTEGER NOT NULL
1651
+ );
1652
+
1653
+ CREATE INDEX IF NOT EXISTS idx_job_events_job_id ON job_events(job_id);
1654
+ `
1655
+ }
1656
+ ];
1657
+ var SqliteJobStore = class {
1658
+ db;
1659
+ constructor(dbPath) {
1660
+ mkdirSync2(dirname3(dbPath), { recursive: true });
1661
+ this.db = new Database(dbPath);
1662
+ this.db.pragma("journal_mode = WAL");
1663
+ this.db.pragma("foreign_keys = ON");
1664
+ this.runMigrations();
1665
+ }
1666
+ // ─── Migrations ─────────────────────────────────────────────────────────────
1667
+ runMigrations() {
1668
+ this.db.exec(`
1669
+ CREATE TABLE IF NOT EXISTS schema_migrations (
1670
+ version INTEGER PRIMARY KEY,
1671
+ applied_at INTEGER NOT NULL
1672
+ );
1673
+ `);
1674
+ const applied = new Set(
1675
+ this.db.prepare("SELECT version FROM schema_migrations").all().map(
1676
+ (r) => r.version
1677
+ )
1678
+ );
1679
+ for (const migration of MIGRATIONS) {
1680
+ if (!applied.has(migration.version)) {
1681
+ this.db.transaction(() => {
1682
+ this.db.exec(migration.sql);
1683
+ this.db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)").run(
1684
+ migration.version,
1685
+ Date.now()
1686
+ );
1687
+ })();
1688
+ }
1689
+ }
1690
+ }
1691
+ // ─── Job CRUD ────────────────────────────────────────────────────────────────
1692
+ createJob(job) {
1693
+ const now = Date.now();
1694
+ const full = {
1695
+ ...job,
1696
+ id: randomUUID2(),
1697
+ createdAt: now,
1698
+ updatedAt: now
1699
+ };
1700
+ this.db.prepare(
1701
+ `INSERT INTO jobs
1702
+ (id, type, state, params, result, error, retry_count, max_retries,
1703
+ next_retry_at, idempotency_key, created_at, updated_at, completed_at)
1704
+ VALUES
1705
+ (@id, @type, @state, @params, @result, @error, @retry_count, @max_retries,
1706
+ @next_retry_at, @idempotency_key, @created_at, @updated_at, @completed_at)`
1707
+ ).run({
1708
+ id: full.id,
1709
+ type: full.type,
1710
+ state: full.state,
1711
+ params: JSON.stringify(full.params),
1712
+ result: full.result !== void 0 ? JSON.stringify(full.result) : null,
1713
+ error: full.error !== void 0 ? JSON.stringify(full.error) : null,
1714
+ retry_count: full.retryCount,
1715
+ max_retries: full.maxRetries,
1716
+ next_retry_at: full.nextRetryAt ?? null,
1717
+ idempotency_key: full.idempotencyKey,
1718
+ created_at: full.createdAt,
1719
+ updated_at: full.updatedAt,
1720
+ completed_at: full.completedAt ?? null
1721
+ });
1722
+ return full;
1723
+ }
1724
+ updateJob(id, updates) {
1725
+ const existing = this.getJob(id);
1726
+ if (!existing) throw new Error(`Job not found: ${id}`);
1727
+ const now = Date.now();
1728
+ const merged = { ...existing, ...updates, updatedAt: now };
1729
+ this.db.prepare(
1730
+ `UPDATE jobs SET
1731
+ state = @state,
1732
+ params = @params,
1733
+ result = @result,
1734
+ error = @error,
1735
+ retry_count = @retry_count,
1736
+ next_retry_at = @next_retry_at,
1737
+ updated_at = @updated_at,
1738
+ completed_at = @completed_at
1739
+ WHERE id = @id`
1740
+ ).run({
1741
+ id: merged.id,
1742
+ state: merged.state,
1743
+ params: JSON.stringify(merged.params),
1744
+ result: merged.result !== void 0 ? JSON.stringify(merged.result) : null,
1745
+ error: merged.error !== void 0 ? JSON.stringify(merged.error) : null,
1746
+ retry_count: merged.retryCount,
1747
+ next_retry_at: merged.nextRetryAt ?? null,
1748
+ updated_at: merged.updatedAt,
1749
+ completed_at: merged.completedAt ?? null
1750
+ });
1751
+ return merged;
1752
+ }
1753
+ getJob(id) {
1754
+ const row = this.db.prepare("SELECT * FROM jobs WHERE id = ?").get(id);
1755
+ return row ? this.rowToJob(row) : void 0;
1756
+ }
1757
+ getJobByIdempotencyKey(key) {
1758
+ const row = this.db.prepare("SELECT * FROM jobs WHERE idempotency_key = ?").get(key);
1759
+ return row ? this.rowToJob(row) : void 0;
1760
+ }
1761
+ listJobs(filter = {}) {
1762
+ const conditions = [];
1763
+ const params = {};
1764
+ if (filter.type) {
1765
+ conditions.push("type = @type");
1766
+ params.type = filter.type;
1767
+ }
1768
+ if (filter.state) {
1769
+ const states = Array.isArray(filter.state) ? filter.state : [filter.state];
1770
+ const placeholders = states.map((_, i) => `@state_${i}`).join(", ");
1771
+ conditions.push(`state IN (${placeholders})`);
1772
+ for (const [i, s] of states.entries()) {
1773
+ params[`state_${i}`] = s;
1774
+ }
1775
+ }
1776
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1777
+ const limit = filter.limit ? `LIMIT @limit` : "";
1778
+ const offset = filter.offset ? `OFFSET @offset` : "";
1779
+ if (filter.limit) params.limit = filter.limit;
1780
+ if (filter.offset) params.offset = filter.offset;
1781
+ const rows = this.db.prepare(`SELECT * FROM jobs ${where} ORDER BY created_at DESC ${limit} ${offset}`).all(params);
1782
+ return rows.map((r) => this.rowToJob(r));
1783
+ }
1784
+ deleteJob(id) {
1785
+ this.db.prepare("DELETE FROM jobs WHERE id = ?").run(id);
1786
+ }
1787
+ /** Return jobs that are ready to be retried right now. */
1788
+ getRetryableJobs(now = Date.now()) {
1789
+ const rows = this.db.prepare(
1790
+ `SELECT * FROM jobs
1791
+ WHERE state = 'waiting_retry' AND next_retry_at <= @now
1792
+ ORDER BY next_retry_at ASC`
1793
+ ).all({ now });
1794
+ return rows.map((r) => this.rowToJob(r));
1795
+ }
1796
+ /** Return jobs in non-terminal states (for recovery after daemon restart). */
1797
+ getInProgressJobs() {
1798
+ const rows = this.db.prepare(
1799
+ `SELECT * FROM jobs
1800
+ WHERE state IN (
1801
+ 'queued',
1802
+ 'executing',
1803
+ 'inflight',
1804
+ 'waiting_retry',
1805
+ 'invoice_created',
1806
+ 'invoice_active',
1807
+ 'invoice_received',
1808
+ 'channel_opening',
1809
+ 'channel_accepting',
1810
+ 'channel_abandoning',
1811
+ 'channel_updating',
1812
+ 'channel_awaiting_ready',
1813
+ 'channel_closing'
1814
+ )
1815
+ ORDER BY created_at ASC`
1816
+ ).all();
1817
+ return rows.map((r) => this.rowToJob(r));
1818
+ }
1819
+ // ─── Job Events ──────────────────────────────────────────────────────────────
1820
+ addJobEvent(jobId, eventType, fromState, toState, data) {
1821
+ const event = {
1822
+ id: randomUUID2(),
1823
+ jobId,
1824
+ eventType,
1825
+ fromState,
1826
+ toState,
1827
+ data,
1828
+ createdAt: Date.now()
1829
+ };
1830
+ this.db.prepare(
1831
+ `INSERT INTO job_events (id, job_id, event_type, from_state, to_state, data, created_at)
1832
+ VALUES (@id, @job_id, @event_type, @from_state, @to_state, @data, @created_at)`
1833
+ ).run({
1834
+ id: event.id,
1835
+ job_id: event.jobId,
1836
+ event_type: event.eventType,
1837
+ from_state: event.fromState ?? null,
1838
+ to_state: event.toState ?? null,
1839
+ data: event.data !== void 0 ? JSON.stringify(event.data) : null,
1840
+ created_at: event.createdAt
1841
+ });
1842
+ return event;
1843
+ }
1844
+ listJobEvents(jobId) {
1845
+ const rows = this.db.prepare("SELECT * FROM job_events WHERE job_id = ? ORDER BY created_at ASC").all(jobId);
1846
+ return rows.map((r) => ({
1847
+ id: r.id,
1848
+ jobId: r.job_id,
1849
+ eventType: r.event_type,
1850
+ fromState: r.from_state ?? void 0,
1851
+ toState: r.to_state ?? void 0,
1852
+ data: r.data ? JSON.parse(r.data) : void 0,
1853
+ createdAt: r.created_at
1854
+ }));
1855
+ }
1856
+ // ─── Lifecycle ───────────────────────────────────────────────────────────────
1857
+ close() {
1858
+ this.db.close();
1859
+ }
1860
+ // ─── Private Helpers ─────────────────────────────────────────────────────────
1861
+ rowToJob(row) {
1862
+ return {
1863
+ id: row.id,
1864
+ type: row.type,
1865
+ state: row.state,
1866
+ params: JSON.parse(row.params),
1867
+ result: row.result ? JSON.parse(row.result) : void 0,
1868
+ error: row.error ? JSON.parse(row.error) : void 0,
1869
+ retryCount: row.retry_count,
1870
+ maxRetries: row.max_retries,
1871
+ nextRetryAt: row.next_retry_at ?? void 0,
1872
+ idempotencyKey: row.idempotency_key,
1873
+ createdAt: row.created_at,
1874
+ updatedAt: row.updated_at,
1875
+ completedAt: row.completed_at ?? void 0
1876
+ };
1877
+ }
1878
+ };
1879
+
1880
+ // src/jobs/job-manager.ts
1881
+ import { EventEmitter } from "events";
1882
+ import { randomUUID as randomUUID3 } from "crypto";
1883
+
1884
+ // src/jobs/types.ts
1885
+ var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
1886
+
1887
+ // src/jobs/error-classifier.ts
1888
+ var PATTERNS = [
1889
+ // Routing / topology
1890
+ { pattern: /no path found|no route|route not found/i, category: "no_route", retryable: true },
1891
+ { pattern: /no outgoing channel|no available channel/i, category: "no_route", retryable: true },
1892
+ // Liquidity
1893
+ { pattern: /insufficient (balance|capacity|funds)/i, category: "insufficient_balance", retryable: false },
1894
+ { pattern: /amount.*too large|exceeds.*capacity/i, category: "amount_too_large", retryable: false },
1895
+ // Invoice lifecycle
1896
+ { pattern: /invoice.*expir|expir.*invoice/i, category: "invoice_expired", retryable: false },
1897
+ { pattern: /invoice.*cancel|cancel.*invoice/i, category: "invoice_cancelled", retryable: false },
1898
+ { pattern: /payment hash.*exist|payment_hash.*exist|duplicated payment hash/i, category: "invalid_payment", retryable: false },
1899
+ // Peer / connectivity
1900
+ { pattern: /peer.*offline|peer.*unreachable|peer.*disconnect/i, category: "peer_offline", retryable: true },
1901
+ { pattern: /connection.*refused|connection.*reset/i, category: "peer_offline", retryable: true },
1902
+ { pattern: /peer.*feature not found|waiting for peer to send init message/i, category: "peer_offline", retryable: true },
1903
+ { pattern: /channel.*already.*exist|duplicat(e|ed).*channel/i, category: "temporary_failure", retryable: true },
1904
+ // Timeout
1905
+ { pattern: /timeout|timed out/i, category: "timeout", retryable: true },
1906
+ // Payment validity
1907
+ { pattern: /invalid.*invoice|malformed.*invoice/i, category: "invalid_payment", retryable: false },
1908
+ { pattern: /payment.*hash.*mismatch|preimage.*invalid/i, category: "invalid_payment", retryable: false },
1909
+ // Generic temporary
1910
+ { pattern: /temporary.*failure|try again|retry/i, category: "temporary_failure", retryable: true }
1911
+ ];
1912
+ function classifyRpcError(error, failedError) {
1913
+ const raw = failedError ?? (error instanceof Error ? error.message : String(error));
1914
+ for (const { pattern, category, retryable } of PATTERNS) {
1915
+ if (pattern.test(raw)) {
1916
+ return { category, retryable, message: raw, rawError: raw };
1917
+ }
1918
+ }
1919
+ return {
1920
+ category: "unknown",
1921
+ retryable: false,
1922
+ // safe default: don't retry unknowns
1923
+ message: raw,
1924
+ rawError: raw
1925
+ };
1926
+ }
1927
+
1928
+ // src/jobs/state-machine.ts
1929
+ var PAYMENT_TRANSITIONS = [
1930
+ { from: "queued", event: "send_issued", to: "executing" },
1931
+ { from: "executing", event: "payment_inflight", to: "inflight" },
1932
+ { from: "executing", event: "payment_success", to: "succeeded" },
1933
+ { from: "executing", event: "payment_failed_retryable", to: "waiting_retry" },
1934
+ { from: "executing", event: "payment_failed_permanent", to: "failed" },
1935
+ { from: "inflight", event: "payment_success", to: "succeeded" },
1936
+ { from: "inflight", event: "payment_failed_retryable", to: "waiting_retry" },
1937
+ { from: "inflight", event: "payment_failed_permanent", to: "failed" },
1938
+ { from: "waiting_retry", event: "retry_delay_elapsed", to: "executing" },
1939
+ {
1940
+ from: ["queued", "executing", "inflight", "waiting_retry"],
1941
+ event: "cancel",
1942
+ to: "cancelled"
1943
+ }
1944
+ ];
1945
+ var INVOICE_TRANSITIONS = [
1946
+ { from: "queued", event: "send_issued", to: "executing" },
1947
+ { from: "executing", event: "invoice_created", to: "invoice_created" },
1948
+ { from: "invoice_created", event: "payment_success", to: "succeeded" },
1949
+ { from: "executing", event: "payment_failed_retryable", to: "waiting_retry" },
1950
+ { from: "invoice_created", event: "payment_failed_retryable", to: "waiting_retry" },
1951
+ { from: "invoice_active", event: "payment_failed_retryable", to: "waiting_retry" },
1952
+ { from: "invoice_received", event: "payment_failed_retryable", to: "waiting_retry" },
1953
+ { from: "waiting_retry", event: "retry_delay_elapsed", to: "executing" },
1954
+ { from: "invoice_created", event: "invoice_received", to: "invoice_received" },
1955
+ { from: "invoice_created", event: "invoice_settled", to: "invoice_settled" },
1956
+ { from: "invoice_active", event: "invoice_received", to: "invoice_received" },
1957
+ { from: "invoice_active", event: "invoice_settled", to: "invoice_settled" },
1958
+ { from: "invoice_active", event: "invoice_expired", to: "invoice_expired" },
1959
+ { from: "invoice_active", event: "invoice_cancelled", to: "invoice_cancelled" },
1960
+ { from: "invoice_received", event: "invoice_settled", to: "invoice_settled" },
1961
+ { from: ["invoice_created", "invoice_received"], event: "invoice_expired", to: "invoice_expired" },
1962
+ { from: ["invoice_created", "invoice_received"], event: "invoice_cancelled", to: "invoice_cancelled" },
1963
+ { from: ["invoice_settled", "invoice_expired", "invoice_cancelled"], event: "payment_success", to: "succeeded" },
1964
+ { from: ["invoice_expired", "invoice_cancelled"], event: "payment_failed_permanent", to: "failed" },
1965
+ { from: ["executing", "invoice_created", "invoice_active", "invoice_received"], event: "payment_failed_permanent", to: "failed" },
1966
+ {
1967
+ from: ["queued", "executing", "waiting_retry", "invoice_created", "invoice_active", "invoice_received"],
1968
+ event: "cancel",
1969
+ to: "cancelled"
1970
+ }
1971
+ ];
1972
+ var CHANNEL_TRANSITIONS = [
1973
+ { from: "queued", event: "send_issued", to: "executing" },
1974
+ { from: "executing", event: "channel_opening", to: "channel_opening" },
1975
+ { from: "executing", event: "channel_accepting", to: "channel_accepting" },
1976
+ { from: "executing", event: "channel_abandoning", to: "channel_abandoning" },
1977
+ { from: "executing", event: "channel_updating", to: "channel_updating" },
1978
+ { from: "channel_opening", event: "payment_success", to: "succeeded" },
1979
+ { from: "channel_accepting", event: "payment_success", to: "succeeded" },
1980
+ { from: "channel_abandoning", event: "payment_success", to: "succeeded" },
1981
+ { from: "channel_updating", event: "payment_success", to: "succeeded" },
1982
+ { from: "executing", event: "payment_failed_retryable", to: "waiting_retry" },
1983
+ { from: "channel_opening", event: "payment_failed_retryable", to: "waiting_retry" },
1984
+ { from: "channel_accepting", event: "payment_failed_retryable", to: "waiting_retry" },
1985
+ { from: "channel_abandoning", event: "payment_failed_retryable", to: "waiting_retry" },
1986
+ { from: "channel_updating", event: "payment_failed_retryable", to: "waiting_retry" },
1987
+ { from: "channel_awaiting_ready", event: "payment_failed_retryable", to: "waiting_retry" },
1988
+ { from: "channel_closing", event: "payment_failed_retryable", to: "waiting_retry" },
1989
+ { from: "executing", event: "payment_failed_permanent", to: "failed" },
1990
+ { from: "channel_opening", event: "payment_failed_permanent", to: "failed" },
1991
+ { from: "channel_accepting", event: "payment_failed_permanent", to: "failed" },
1992
+ { from: "channel_abandoning", event: "payment_failed_permanent", to: "failed" },
1993
+ { from: "channel_updating", event: "payment_failed_permanent", to: "failed" },
1994
+ { from: "channel_awaiting_ready", event: "payment_failed_permanent", to: "failed" },
1995
+ { from: "channel_closing", event: "payment_failed_permanent", to: "failed" },
1996
+ { from: ["executing", "channel_ready"], event: "channel_closing", to: "channel_closing" },
1997
+ { from: "waiting_retry", event: "retry_delay_elapsed", to: "executing" },
1998
+ { from: "channel_opening", event: "channel_opening", to: "channel_awaiting_ready" },
1999
+ { from: "channel_awaiting_ready", event: "channel_opening", to: "channel_awaiting_ready" },
2000
+ { from: "channel_opening", event: "channel_ready", to: "channel_ready" },
2001
+ { from: "channel_awaiting_ready", event: "channel_ready", to: "channel_ready" },
2002
+ { from: ["channel_opening", "channel_ready"], event: "channel_closed", to: "channel_closed" },
2003
+ { from: "channel_awaiting_ready", event: "channel_closed", to: "channel_closed" },
2004
+ { from: "channel_closing", event: "channel_closed", to: "channel_closed" },
2005
+ { from: "channel_closing", event: "payment_success", to: "succeeded" },
2006
+ { from: "channel_closed", event: "payment_failed_permanent", to: "failed" },
2007
+ { from: ["channel_ready", "channel_opening"], event: "channel_failed", to: "failed" },
2008
+ { from: ["channel_ready", "channel_closed"], event: "payment_success", to: "succeeded" },
2009
+ {
2010
+ from: ["queued", "executing", "waiting_retry", "channel_opening", "channel_accepting", "channel_abandoning", "channel_updating", "channel_awaiting_ready", "channel_ready", "channel_closing"],
2011
+ event: "cancel",
2012
+ to: "cancelled"
2013
+ }
2014
+ ];
2015
+ var JobStateMachine = class {
2016
+ table;
2017
+ constructor(transitions) {
2018
+ this.table = /* @__PURE__ */ new Map();
2019
+ for (const t of transitions) {
2020
+ const froms = Array.isArray(t.from) ? t.from : [t.from];
2021
+ for (const from of froms) {
2022
+ this.table.set(`${from}:${t.event}`, t.to);
2023
+ }
2024
+ }
2025
+ }
2026
+ transition(current, event) {
2027
+ return this.table.get(`${current}:${event}`) ?? null;
2028
+ }
2029
+ isTerminal(state) {
2030
+ return state === "succeeded" || state === "failed" || state === "cancelled";
2031
+ }
2032
+ };
2033
+ var paymentStateMachine = new JobStateMachine(PAYMENT_TRANSITIONS);
2034
+ var invoiceStateMachine = new JobStateMachine(INVOICE_TRANSITIONS);
2035
+ var channelStateMachine = new JobStateMachine(CHANNEL_TRANSITIONS);
2036
+
2037
+ // src/jobs/executor-utils.ts
2038
+ function transitionJobState(job, machine, event, options) {
2039
+ const nextState = machine.transition(job.state, event);
2040
+ if (!nextState) {
2041
+ throw new Error(`Invalid state transition: ${job.state} --${event}--> ?`);
2042
+ }
2043
+ const now = options?.now ?? Date.now();
2044
+ return {
2045
+ ...job,
2046
+ ...options?.patch ?? {},
2047
+ state: nextState,
2048
+ updatedAt: now,
2049
+ completedAt: machine.isTerminal(nextState) ? now : job.completedAt
2050
+ };
2051
+ }
2052
+ function applyRetryOrFail(job, classifiedError, policy, options) {
2053
+ const now = options?.now ?? Date.now();
2054
+ if (shouldRetry(classifiedError, job.retryCount, policy)) {
2055
+ const delay = computeRetryDelay(job.retryCount, policy);
2056
+ const retryTransition = options?.machine && options.retryEvent ? transitionJobState(job, options.machine, options.retryEvent, { now }) : { ...job, state: "waiting_retry", updatedAt: now };
2057
+ return {
2058
+ ...retryTransition,
2059
+ error: classifiedError,
2060
+ retryCount: job.retryCount + 1,
2061
+ nextRetryAt: now + delay
2062
+ };
2063
+ }
2064
+ const failTransition = options?.machine && options.failEvent ? transitionJobState(job, options.machine, options.failEvent, { now }) : { ...job, state: "failed", completedAt: now, updatedAt: now };
2065
+ return {
2066
+ ...failTransition,
2067
+ error: classifiedError,
2068
+ ...options?.failedPatch ?? {}
2069
+ };
2070
+ }
2071
+
2072
+ // src/jobs/executors/payment-executor.ts
2073
+ async function* runPaymentJob(job, rpc, policy, signal) {
2074
+ let current = { ...job };
2075
+ while (!paymentStateMachine.isTerminal(current.state)) {
2076
+ if (signal.aborted) {
2077
+ current = transitionJobState(current, paymentStateMachine, "cancel");
2078
+ yield current;
2079
+ return;
2080
+ }
2081
+ if (current.state === "queued") {
2082
+ current = transitionJobState(current, paymentStateMachine, "send_issued");
2083
+ yield current;
2084
+ continue;
2085
+ }
2086
+ if (current.state === "waiting_retry") {
2087
+ const delay = current.nextRetryAt ? Math.max(0, current.nextRetryAt - Date.now()) : 0;
2088
+ if (delay > 0) {
2089
+ await sleep(delay, signal);
2090
+ if (signal.aborted) {
2091
+ current = transitionJobState(current, paymentStateMachine, "cancel");
2092
+ yield current;
2093
+ return;
2094
+ }
2095
+ }
2096
+ current = transitionJobState(current, paymentStateMachine, "retry_delay_elapsed", {
2097
+ patch: { nextRetryAt: void 0 }
2098
+ });
2099
+ yield current;
2100
+ continue;
2101
+ }
2102
+ if (current.state === "executing") {
2103
+ let paymentHash;
2104
+ try {
2105
+ const sendResult = await rpc.sendPayment(current.params.sendPaymentParams);
2106
+ paymentHash = sendResult.payment_hash;
2107
+ if (sendResult.status === "Success") {
2108
+ current = transitionJobState(current, paymentStateMachine, "payment_success", {
2109
+ patch: {
2110
+ result: {
2111
+ paymentHash: sendResult.payment_hash,
2112
+ status: sendResult.status,
2113
+ fee: sendResult.fee,
2114
+ failedError: sendResult.failed_error
2115
+ }
2116
+ }
2117
+ });
2118
+ yield current;
2119
+ return;
2120
+ }
2121
+ if (sendResult.status === "Failed") {
2122
+ const classified = classifyRpcError(
2123
+ new Error(sendResult.failed_error ?? "Payment failed"),
2124
+ sendResult.failed_error
2125
+ );
2126
+ current = applyRetryOrFail(current, classified, policy, {
2127
+ failedPatch: {
2128
+ result: {
2129
+ paymentHash: sendResult.payment_hash,
2130
+ status: sendResult.status,
2131
+ fee: sendResult.fee,
2132
+ failedError: sendResult.failed_error
2133
+ }
2134
+ },
2135
+ machine: paymentStateMachine,
2136
+ retryEvent: "payment_failed_retryable",
2137
+ failEvent: "payment_failed_permanent"
2138
+ });
2139
+ yield current;
2140
+ continue;
2141
+ }
2142
+ current = transitionJobState(current, paymentStateMachine, "payment_inflight");
2143
+ if (paymentHash) {
2144
+ current = { ...current, params: { ...current.params, sendPaymentParams: { ...current.params.sendPaymentParams, payment_hash: paymentHash } } };
2145
+ }
2146
+ yield current;
2147
+ continue;
2148
+ } catch (err) {
2149
+ const classified = classifyRpcError(err);
2150
+ current = applyRetryOrFail(current, classified, policy, {
2151
+ machine: paymentStateMachine,
2152
+ retryEvent: "payment_failed_retryable",
2153
+ failEvent: "payment_failed_permanent"
2154
+ });
2155
+ yield current;
2156
+ continue;
2157
+ }
2158
+ }
2159
+ if (current.state === "inflight") {
2160
+ const hash = current.params.sendPaymentParams.payment_hash;
2161
+ if (!hash) {
2162
+ current = transitionJobState(current, paymentStateMachine, "payment_failed_permanent", {
2163
+ patch: {
2164
+ error: { category: "unknown", retryable: false, message: "No payment_hash in inflight job" }
2165
+ }
2166
+ });
2167
+ yield current;
2168
+ continue;
2169
+ }
2170
+ try {
2171
+ const pollResult = await rpc.getPayment({ payment_hash: hash });
2172
+ if (pollResult.status === "Success") {
2173
+ current = transitionJobState(current, paymentStateMachine, "payment_success", {
2174
+ patch: {
2175
+ result: {
2176
+ paymentHash: pollResult.payment_hash,
2177
+ status: pollResult.status,
2178
+ fee: pollResult.fee,
2179
+ failedError: pollResult.failed_error
2180
+ }
2181
+ }
2182
+ });
2183
+ yield current;
2184
+ return;
2185
+ }
2186
+ if (pollResult.status === "Failed") {
2187
+ const classified = classifyRpcError(
2188
+ new Error(pollResult.failed_error ?? "Payment failed"),
2189
+ pollResult.failed_error
2190
+ );
2191
+ current = applyRetryOrFail(current, classified, policy, {
2192
+ failedPatch: {
2193
+ result: {
2194
+ paymentHash: pollResult.payment_hash,
2195
+ status: pollResult.status,
2196
+ fee: pollResult.fee,
2197
+ failedError: pollResult.failed_error
2198
+ }
2199
+ },
2200
+ machine: paymentStateMachine,
2201
+ retryEvent: "payment_failed_retryable",
2202
+ failEvent: "payment_failed_permanent"
2203
+ });
2204
+ yield current;
2205
+ continue;
2206
+ }
2207
+ await sleep(POLL_INTERVAL_MS, signal);
2208
+ if (signal.aborted) {
2209
+ current = transitionJobState(current, paymentStateMachine, "cancel");
2210
+ yield current;
2211
+ return;
2212
+ }
2213
+ current = { ...current, updatedAt: Date.now() };
2214
+ continue;
2215
+ } catch (err) {
2216
+ const classified = classifyRpcError(err);
2217
+ current = applyRetryOrFail(current, classified, policy, {
2218
+ machine: paymentStateMachine,
2219
+ retryEvent: "payment_failed_retryable",
2220
+ failEvent: "payment_failed_permanent"
2221
+ });
2222
+ yield current;
2223
+ continue;
2224
+ }
2225
+ }
2226
+ break;
2227
+ }
2228
+ }
2229
+ var POLL_INTERVAL_MS = 1500;
2230
+
2231
+ // src/jobs/executors/invoice-executor.ts
2232
+ var DEFAULT_POLL_INTERVAL = 1500;
2233
+ async function* runInvoiceJob(job, rpc, policy, signal) {
2234
+ let current = { ...job };
2235
+ if (current.state === "queued") {
2236
+ current = transitionJobState(current, invoiceStateMachine, "send_issued");
2237
+ yield current;
2238
+ }
2239
+ if (current.state === "waiting_retry") {
2240
+ current = transitionJobState(current, invoiceStateMachine, "retry_delay_elapsed", {
2241
+ patch: { nextRetryAt: void 0 }
2242
+ });
2243
+ yield current;
2244
+ }
2245
+ try {
2246
+ const pollIntervalMs = current.params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL;
2247
+ if (current.params.action === "create") {
2248
+ if (!current.params.newInvoiceParams) {
2249
+ throw new Error("Invoice create job requires newInvoiceParams");
2250
+ }
2251
+ const created = await rpc.newInvoice(current.params.newInvoiceParams);
2252
+ const paymentHash = created.invoice.data.payment_hash;
2253
+ const createdStatus = "Open";
2254
+ current = {
2255
+ ...current,
2256
+ state: "invoice_created",
2257
+ result: {
2258
+ paymentHash,
2259
+ invoiceAddress: created.invoice_address,
2260
+ status: createdStatus,
2261
+ invoice: created.invoice
2262
+ },
2263
+ updatedAt: Date.now()
2264
+ };
2265
+ yield current;
2266
+ if (!current.params.waitForTerminal) {
2267
+ current = transitionJobState(current, invoiceStateMachine, "payment_success");
2268
+ yield current;
2269
+ return;
2270
+ }
2271
+ while (true) {
2272
+ if (signal.aborted) {
2273
+ current = transitionJobState(current, invoiceStateMachine, "cancel");
2274
+ yield current;
2275
+ return;
2276
+ }
2277
+ const invoice = await rpc.getInvoice({ payment_hash: paymentHash });
2278
+ if (invoice.status === "Paid") {
2279
+ current = {
2280
+ ...current,
2281
+ state: "invoice_settled",
2282
+ result: {
2283
+ paymentHash,
2284
+ invoiceAddress: invoice.invoice_address,
2285
+ status: invoice.status,
2286
+ invoice: invoice.invoice
2287
+ },
2288
+ updatedAt: Date.now()
2289
+ };
2290
+ yield current;
2291
+ current = transitionJobState(current, invoiceStateMachine, "payment_success");
2292
+ yield current;
2293
+ return;
2294
+ }
2295
+ if (invoice.status === "Received") {
2296
+ current = {
2297
+ ...current,
2298
+ state: "invoice_received",
2299
+ result: {
2300
+ paymentHash,
2301
+ invoiceAddress: invoice.invoice_address,
2302
+ status: invoice.status,
2303
+ invoice: invoice.invoice
2304
+ },
2305
+ updatedAt: Date.now()
2306
+ };
2307
+ yield current;
2308
+ } else if (invoice.status === "Cancelled") {
2309
+ current = {
2310
+ ...current,
2311
+ state: "invoice_cancelled",
2312
+ result: {
2313
+ paymentHash,
2314
+ invoiceAddress: invoice.invoice_address,
2315
+ status: invoice.status,
2316
+ invoice: invoice.invoice
2317
+ },
2318
+ completedAt: Date.now(),
2319
+ updatedAt: Date.now()
2320
+ };
2321
+ yield current;
2322
+ current = transitionJobState(current, invoiceStateMachine, "payment_failed_permanent");
2323
+ yield current;
2324
+ return;
2325
+ } else if (invoice.status === "Expired") {
2326
+ current = {
2327
+ ...current,
2328
+ state: "invoice_expired",
2329
+ result: {
2330
+ paymentHash,
2331
+ invoiceAddress: invoice.invoice_address,
2332
+ status: invoice.status,
2333
+ invoice: invoice.invoice
2334
+ },
2335
+ completedAt: Date.now(),
2336
+ updatedAt: Date.now()
2337
+ };
2338
+ yield current;
2339
+ current = transitionJobState(current, invoiceStateMachine, "payment_failed_permanent");
2340
+ yield current;
2341
+ return;
2342
+ }
2343
+ await sleep(pollIntervalMs, signal);
2344
+ }
2345
+ }
2346
+ if (current.params.action === "watch") {
2347
+ if (!current.params.getInvoicePaymentHash) {
2348
+ throw new Error("Invoice watch job requires getInvoicePaymentHash");
2349
+ }
2350
+ const paymentHash = current.params.getInvoicePaymentHash;
2351
+ while (true) {
2352
+ if (signal.aborted) {
2353
+ current = transitionJobState(current, invoiceStateMachine, "cancel");
2354
+ yield current;
2355
+ return;
2356
+ }
2357
+ const invoice = await rpc.getInvoice({ payment_hash: paymentHash });
2358
+ current = {
2359
+ ...current,
2360
+ state: invoice.status === "Paid" ? "invoice_settled" : invoice.status === "Received" ? "invoice_received" : invoice.status === "Cancelled" ? "invoice_cancelled" : invoice.status === "Expired" ? "invoice_expired" : "invoice_active",
2361
+ result: {
2362
+ paymentHash,
2363
+ invoiceAddress: invoice.invoice_address,
2364
+ status: invoice.status,
2365
+ invoice: invoice.invoice
2366
+ },
2367
+ updatedAt: Date.now()
2368
+ };
2369
+ yield current;
2370
+ if (invoice.status === "Paid") {
2371
+ current = transitionJobState(current, invoiceStateMachine, "payment_success");
2372
+ yield current;
2373
+ return;
2374
+ }
2375
+ if (invoice.status === "Cancelled" || invoice.status === "Expired") {
2376
+ current = transitionJobState(current, invoiceStateMachine, "payment_failed_permanent");
2377
+ yield current;
2378
+ return;
2379
+ }
2380
+ await sleep(pollIntervalMs, signal);
2381
+ }
2382
+ }
2383
+ if (current.params.action === "cancel") {
2384
+ if (!current.params.cancelInvoiceParams) {
2385
+ throw new Error("Invoice cancel job requires cancelInvoiceParams");
2386
+ }
2387
+ const cancelled = await rpc.cancelInvoice(current.params.cancelInvoiceParams);
2388
+ current = {
2389
+ ...current,
2390
+ state: "invoice_cancelled",
2391
+ result: {
2392
+ paymentHash: current.params.cancelInvoiceParams.payment_hash,
2393
+ invoiceAddress: cancelled.invoice_address,
2394
+ status: cancelled.status,
2395
+ invoice: cancelled.invoice
2396
+ },
2397
+ updatedAt: Date.now()
2398
+ };
2399
+ yield current;
2400
+ current = transitionJobState(current, invoiceStateMachine, "payment_success");
2401
+ yield current;
2402
+ return;
2403
+ }
2404
+ if (current.params.action === "settle") {
2405
+ if (!current.params.settleInvoiceParams) {
2406
+ throw new Error("Invoice settle job requires settleInvoiceParams");
2407
+ }
2408
+ await rpc.settleInvoice(current.params.settleInvoiceParams);
2409
+ current = {
2410
+ ...current,
2411
+ state: "invoice_settled",
2412
+ result: {
2413
+ paymentHash: current.params.settleInvoiceParams.payment_hash,
2414
+ status: "Paid"
2415
+ },
2416
+ updatedAt: Date.now()
2417
+ };
2418
+ yield current;
2419
+ current = transitionJobState(current, invoiceStateMachine, "payment_success");
2420
+ yield current;
2421
+ return;
2422
+ }
2423
+ throw new Error(`Unsupported invoice action: ${current.params.action}`);
2424
+ } catch (error) {
2425
+ const classified = classifyRpcError(error);
2426
+ current = applyRetryOrFail(current, classified, policy, {
2427
+ machine: invoiceStateMachine,
2428
+ retryEvent: "payment_failed_retryable",
2429
+ failEvent: "payment_failed_permanent"
2430
+ });
2431
+ yield current;
2432
+ }
2433
+ }
2434
+
2435
+ // src/jobs/executors/channel-executor.ts
2436
+ import { ChannelState as ChannelState2 } from "@fiber-pay/sdk";
2437
+ var DEFAULT_POLL_INTERVAL2 = 2e3;
2438
+ async function* runChannelJob(job, rpc, policy, signal) {
2439
+ let current = { ...job };
2440
+ const resumedFromRetry = job.state === "waiting_retry";
2441
+ if (current.state === "queued") {
2442
+ current = transitionJobState(current, channelStateMachine, "send_issued");
2443
+ yield current;
2444
+ }
2445
+ if (current.state === "waiting_retry") {
2446
+ current = transitionJobState(current, channelStateMachine, "retry_delay_elapsed", {
2447
+ patch: { nextRetryAt: void 0 }
2448
+ });
2449
+ yield current;
2450
+ }
2451
+ try {
2452
+ const pollIntervalMs = current.params.pollIntervalMs ?? DEFAULT_POLL_INTERVAL2;
2453
+ if (current.params.action === "open") {
2454
+ if (!current.params.openChannelParams) {
2455
+ throw new Error("Channel open job requires openChannelParams");
2456
+ }
2457
+ const targetPeerId = current.params.peerId ?? current.params.openChannelParams.peer_id;
2458
+ let temporaryChannelId = current.result?.temporaryChannelId;
2459
+ if (resumedFromRetry) {
2460
+ const existing = await findTargetChannel(rpc, targetPeerId, current.params.channelId);
2461
+ if (existing && !isClosed(existing.state.state_name)) {
2462
+ current = transitionJobState(current, channelStateMachine, "channel_opening", {
2463
+ patch: {
2464
+ result: {
2465
+ temporaryChannelId,
2466
+ channelId: existing.channel_id,
2467
+ state: existing.state.state_name
2468
+ }
2469
+ }
2470
+ });
2471
+ yield current;
2472
+ } else {
2473
+ const opened = await rpc.openChannel(current.params.openChannelParams);
2474
+ temporaryChannelId = opened.temporary_channel_id;
2475
+ current = transitionJobState(current, channelStateMachine, "channel_opening", {
2476
+ patch: {
2477
+ result: {
2478
+ temporaryChannelId,
2479
+ state: "OPENING"
2480
+ }
2481
+ }
2482
+ });
2483
+ yield current;
2484
+ }
2485
+ } else {
2486
+ const opened = await rpc.openChannel(current.params.openChannelParams);
2487
+ temporaryChannelId = opened.temporary_channel_id;
2488
+ current = transitionJobState(current, channelStateMachine, "channel_opening", {
2489
+ patch: {
2490
+ result: {
2491
+ temporaryChannelId,
2492
+ state: "OPENING"
2493
+ }
2494
+ }
2495
+ });
2496
+ yield current;
2497
+ }
2498
+ if (!current.params.waitForReady) {
2499
+ current = transitionJobState(current, channelStateMachine, "payment_success");
2500
+ yield current;
2501
+ return;
2502
+ }
2503
+ while (true) {
2504
+ if (signal.aborted) {
2505
+ current = transitionJobState(current, channelStateMachine, "cancel");
2506
+ yield current;
2507
+ return;
2508
+ }
2509
+ const channels = await rpc.listChannels({
2510
+ peer_id: targetPeerId,
2511
+ include_closed: true
2512
+ });
2513
+ const candidates = channels.channels.filter((channel) => {
2514
+ if (current.params.channelId && channel.channel_id !== current.params.channelId) return false;
2515
+ return channel.peer_id === targetPeerId;
2516
+ });
2517
+ const readyMatch = candidates.find(
2518
+ (channel) => String(channel.state.state_name).toUpperCase() === "CHANNEL_READY"
2519
+ );
2520
+ const activeMatch = candidates.find((channel) => !isClosed(channel.state.state_name));
2521
+ const closedMatch = current.params.channelId && !activeMatch && candidates.length > 0 ? candidates.find((channel) => isTerminalClosed(channel.state.state_name)) : void 0;
2522
+ if (readyMatch) {
2523
+ current = transitionJobState(current, channelStateMachine, "channel_ready", {
2524
+ patch: {
2525
+ result: {
2526
+ temporaryChannelId,
2527
+ channelId: readyMatch.channel_id,
2528
+ state: readyMatch.state.state_name
2529
+ }
2530
+ }
2531
+ });
2532
+ yield current;
2533
+ current = transitionJobState(current, channelStateMachine, "payment_success");
2534
+ yield current;
2535
+ return;
2536
+ }
2537
+ if (closedMatch) {
2538
+ current = transitionJobState(current, channelStateMachine, "channel_closed", {
2539
+ patch: {
2540
+ result: {
2541
+ temporaryChannelId,
2542
+ channelId: closedMatch.channel_id,
2543
+ state: closedMatch.state.state_name
2544
+ }
2545
+ }
2546
+ });
2547
+ yield current;
2548
+ current = transitionJobState(current, channelStateMachine, "payment_failed_permanent");
2549
+ yield current;
2550
+ return;
2551
+ }
2552
+ current = transitionJobState(current, channelStateMachine, "channel_opening");
2553
+ yield current;
2554
+ await sleep(pollIntervalMs, signal);
2555
+ }
2556
+ }
2557
+ if (current.params.action === "shutdown") {
2558
+ const shutdownParams = current.params.shutdownChannelParams;
2559
+ if (!shutdownParams) {
2560
+ throw new Error("Channel shutdown job requires shutdownChannelParams");
2561
+ }
2562
+ await rpc.shutdownChannel(shutdownParams);
2563
+ current = transitionJobState(current, channelStateMachine, "channel_closing", {
2564
+ patch: {
2565
+ result: {
2566
+ channelId: shutdownParams.channel_id,
2567
+ state: "SHUTTING_DOWN"
2568
+ }
2569
+ }
2570
+ });
2571
+ yield current;
2572
+ if (!current.params.waitForClosed) {
2573
+ current = transitionJobState(current, channelStateMachine, "payment_success");
2574
+ yield current;
2575
+ return;
2576
+ }
2577
+ while (true) {
2578
+ if (signal.aborted) {
2579
+ current = transitionJobState(current, channelStateMachine, "cancel");
2580
+ yield current;
2581
+ return;
2582
+ }
2583
+ const channels = await rpc.listChannels({ include_closed: true });
2584
+ const match = channels.channels.find((channel) => channel.channel_id === shutdownParams.channel_id);
2585
+ if (!match || isTerminalClosed(String(match.state.state_name))) {
2586
+ current = transitionJobState(current, channelStateMachine, "channel_closed", {
2587
+ patch: {
2588
+ result: {
2589
+ channelId: shutdownParams.channel_id,
2590
+ state: "CLOSED"
2591
+ }
2592
+ }
2593
+ });
2594
+ yield current;
2595
+ current = transitionJobState(current, channelStateMachine, "payment_success");
2596
+ yield current;
2597
+ return;
2598
+ }
2599
+ await sleep(pollIntervalMs, signal);
2600
+ }
2601
+ }
2602
+ if (current.params.action === "accept") {
2603
+ if (!current.params.acceptChannelParams) {
2604
+ throw new Error("Channel accept job requires acceptChannelParams");
2605
+ }
2606
+ const accepted = await rpc.acceptChannel(current.params.acceptChannelParams);
2607
+ current = transitionJobState(current, channelStateMachine, "channel_accepting", {
2608
+ patch: {
2609
+ result: {
2610
+ acceptedChannelId: accepted.channel_id,
2611
+ channelId: accepted.channel_id,
2612
+ state: "ACCEPTED"
2613
+ }
2614
+ }
2615
+ });
2616
+ yield current;
2617
+ current = transitionJobState(current, channelStateMachine, "payment_success");
2618
+ yield current;
2619
+ return;
2620
+ }
2621
+ if (current.params.action === "abandon") {
2622
+ if (!current.params.abandonChannelParams) {
2623
+ throw new Error("Channel abandon job requires abandonChannelParams");
2624
+ }
2625
+ await rpc.abandonChannel(current.params.abandonChannelParams);
2626
+ current = transitionJobState(current, channelStateMachine, "channel_abandoning", {
2627
+ patch: {
2628
+ result: {
2629
+ channelId: current.params.abandonChannelParams.channel_id,
2630
+ state: "ABANDONED"
2631
+ }
2632
+ }
2633
+ });
2634
+ yield current;
2635
+ current = transitionJobState(current, channelStateMachine, "payment_success");
2636
+ yield current;
2637
+ return;
2638
+ }
2639
+ if (current.params.action === "update") {
2640
+ if (!current.params.updateChannelParams) {
2641
+ throw new Error("Channel update job requires updateChannelParams");
2642
+ }
2643
+ await rpc.updateChannel(current.params.updateChannelParams);
2644
+ current = transitionJobState(current, channelStateMachine, "channel_updating", {
2645
+ patch: {
2646
+ result: {
2647
+ channelId: current.params.updateChannelParams.channel_id,
2648
+ state: "UPDATED"
2649
+ }
2650
+ }
2651
+ });
2652
+ yield current;
2653
+ current = transitionJobState(current, channelStateMachine, "payment_success");
2654
+ yield current;
2655
+ return;
2656
+ }
2657
+ throw new Error(`Unsupported channel action: ${current.params.action}`);
2658
+ } catch (error) {
2659
+ const classified = classifyRpcError(error);
2660
+ current = applyRetryOrFail(current, classified, policy, {
2661
+ machine: channelStateMachine,
2662
+ retryEvent: "payment_failed_retryable",
2663
+ failEvent: "payment_failed_permanent"
2664
+ });
2665
+ yield current;
2666
+ }
2667
+ }
2668
+ async function findTargetChannel(rpc, peerId, channelId) {
2669
+ const channels = await rpc.listChannels({
2670
+ peer_id: peerId,
2671
+ include_closed: true
2672
+ });
2673
+ return channels.channels.find((channel) => {
2674
+ if (channelId && channel.channel_id !== channelId) return false;
2675
+ return channel.peer_id === peerId;
2676
+ });
2677
+ }
2678
+ function isClosed(stateName) {
2679
+ return stateName === ChannelState2.Closed || stateName === "CLOSED" || stateName === ChannelState2.ShuttingDown || stateName === "SHUTTING_DOWN";
2680
+ }
2681
+ function isTerminalClosed(stateName) {
2682
+ return stateName === ChannelState2.Closed || stateName === "CLOSED";
2683
+ }
2684
+
2685
+ // src/jobs/job-manager.ts
2686
+ function stableStringify(value) {
2687
+ if (value === null || typeof value !== "object") {
2688
+ return JSON.stringify(value);
2689
+ }
2690
+ if (Array.isArray(value)) {
2691
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
2692
+ }
2693
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => `${JSON.stringify(key)}:${stableStringify(nested)}`);
2694
+ return `{${entries.join(",")}}`;
2695
+ }
2696
+ function haveSameParams(left, right) {
2697
+ return stableStringify(left) === stableStringify(right);
2698
+ }
2699
+ var JobManager = class extends EventEmitter {
2700
+ rpc;
2701
+ store;
2702
+ retryPolicy;
2703
+ schedulerIntervalMs;
2704
+ maxConcurrentJobs;
2705
+ running = false;
2706
+ schedulerTimer;
2707
+ active = /* @__PURE__ */ new Map();
2708
+ constructor(rpc, store, config = {}) {
2709
+ super();
2710
+ this.rpc = rpc;
2711
+ this.store = store;
2712
+ this.retryPolicy = config.retryPolicy ?? defaultPaymentRetryPolicy;
2713
+ this.schedulerIntervalMs = config.schedulerIntervalMs ?? 500;
2714
+ this.maxConcurrentJobs = config.maxConcurrentJobs ?? 5;
2715
+ }
2716
+ async ensurePayment(params, options = {}) {
2717
+ const idempotencyKey = options.idempotencyKey ?? params.sendPaymentParams.payment_hash ?? randomUUID3();
2718
+ const existing = this.store.getJobByIdempotencyKey(idempotencyKey);
2719
+ if (existing?.type === "payment") {
2720
+ if (!haveSameParams(existing.params, params)) {
2721
+ throw new Error(
2722
+ `Idempotency key collision with different payment params: ${idempotencyKey}. Use a new idempotency key for a new payment intent.`
2723
+ );
2724
+ }
2725
+ if (existing.state === "succeeded" || existing.state === "cancelled") {
2726
+ return existing;
2727
+ }
2728
+ if (!TERMINAL_JOB_STATES.has(existing.state) || existing.state === "failed") {
2729
+ if (existing.state === "failed" && !this.active.has(existing.id)) {
2730
+ const reset = this.store.updateJob(existing.id, {
2731
+ state: "queued",
2732
+ params,
2733
+ result: void 0,
2734
+ error: void 0,
2735
+ retryCount: 0,
2736
+ nextRetryAt: void 0,
2737
+ completedAt: void 0
2738
+ });
2739
+ this.schedule(reset);
2740
+ return reset;
2741
+ }
2742
+ return existing;
2743
+ }
2744
+ }
2745
+ const job = this.store.createJob({
2746
+ type: "payment",
2747
+ state: "queued",
2748
+ params,
2749
+ retryCount: 0,
2750
+ maxRetries: options.maxRetries ?? this.retryPolicy.maxRetries,
2751
+ idempotencyKey
2752
+ });
2753
+ this.store.addJobEvent(job.id, "created", void 0, "queued", this.buildEventData(job));
2754
+ this.emit("job:created", job);
2755
+ this.schedule(job);
2756
+ return job;
2757
+ }
2758
+ async manageInvoice(params, options = {}) {
2759
+ const idempotencyKey = options.idempotencyKey ?? deriveInvoiceKey(params) ?? randomUUID3();
2760
+ return this.createOrReuseJob("invoice", params, idempotencyKey, options.maxRetries, options.reuseTerminal);
2761
+ }
2762
+ async manageChannel(params, options = {}) {
2763
+ const idempotencyKey = options.idempotencyKey ?? deriveChannelKey(params) ?? randomUUID3();
2764
+ return this.createOrReuseJob("channel", params, idempotencyKey, options.maxRetries, options.reuseTerminal);
2765
+ }
2766
+ getJob(id) {
2767
+ return this.store.getJob(id);
2768
+ }
2769
+ listJobs(filter = {}) {
2770
+ return this.store.listJobs(filter);
2771
+ }
2772
+ cancelJob(id) {
2773
+ const job = this.getJob(id);
2774
+ if (!job) throw new Error(`Job not found: ${id}`);
2775
+ if (TERMINAL_JOB_STATES.has(job.state)) return;
2776
+ this.active.get(id)?.abortController.abort();
2777
+ if (!this.active.has(id)) {
2778
+ const updated = this.store.updateJob(id, {
2779
+ state: "cancelled",
2780
+ completedAt: Date.now()
2781
+ });
2782
+ this.store.addJobEvent(id, "cancelled", job.state, "cancelled", this.buildEventData(updated));
2783
+ this.emit("job:cancelled", updated);
2784
+ }
2785
+ }
2786
+ start() {
2787
+ if (this.running) return;
2788
+ this.running = true;
2789
+ this.recover();
2790
+ this.schedulerTimer = setInterval(() => this.tick(), this.schedulerIntervalMs);
2791
+ }
2792
+ async stop() {
2793
+ this.running = false;
2794
+ if (this.schedulerTimer) {
2795
+ clearInterval(this.schedulerTimer);
2796
+ this.schedulerTimer = void 0;
2797
+ }
2798
+ for (const [, exec] of this.active) {
2799
+ exec.abortController.abort();
2800
+ }
2801
+ await Promise.allSettled(Array.from(this.active.values()).map((e) => e.promise));
2802
+ }
2803
+ async createOrReuseJob(type, params, idempotencyKey, maxRetries, reuseTerminal) {
2804
+ const existing = this.store.getJobByIdempotencyKey(idempotencyKey);
2805
+ if (existing?.type === type) {
2806
+ if (!haveSameParams(existing.params, params)) {
2807
+ throw new Error(
2808
+ `Idempotency key collision with different ${type} params: ${idempotencyKey}. Use a new idempotency key for a new ${type} intent.`
2809
+ );
2810
+ }
2811
+ if (existing.state === "succeeded" || existing.state === "cancelled") {
2812
+ if (reuseTerminal === false) {
2813
+ const reset = this.store.updateJob(existing.id, {
2814
+ state: "queued",
2815
+ params,
2816
+ result: void 0,
2817
+ error: void 0,
2818
+ retryCount: 0,
2819
+ nextRetryAt: void 0,
2820
+ completedAt: void 0
2821
+ });
2822
+ this.store.addJobEvent(existing.id, "created", existing.state, "queued", this.buildEventData(reset));
2823
+ this.emit("job:created", reset);
2824
+ this.schedule(reset);
2825
+ return reset;
2826
+ }
2827
+ return existing;
2828
+ }
2829
+ if (!TERMINAL_JOB_STATES.has(existing.state) || existing.state === "failed") {
2830
+ if (existing.state === "failed" && !this.active.has(existing.id)) {
2831
+ const reset = this.store.updateJob(existing.id, {
2832
+ state: "queued",
2833
+ params,
2834
+ result: void 0,
2835
+ error: void 0,
2836
+ retryCount: 0,
2837
+ nextRetryAt: void 0,
2838
+ completedAt: void 0
2839
+ });
2840
+ this.schedule(reset);
2841
+ return reset;
2842
+ }
2843
+ return existing;
2844
+ }
2845
+ }
2846
+ const job = this.store.createJob({
2847
+ type,
2848
+ state: "queued",
2849
+ params,
2850
+ retryCount: 0,
2851
+ maxRetries: maxRetries ?? this.retryPolicy.maxRetries,
2852
+ idempotencyKey
2853
+ });
2854
+ this.store.addJobEvent(job.id, "created", void 0, "queued", this.buildEventData(job));
2855
+ this.emit("job:created", job);
2856
+ this.schedule(job);
2857
+ return job;
2858
+ }
2859
+ tick() {
2860
+ if (!this.running) return;
2861
+ if (this.active.size >= this.maxConcurrentJobs) return;
2862
+ const queued = this.store.listJobs({
2863
+ state: "queued",
2864
+ limit: this.maxConcurrentJobs - this.active.size
2865
+ });
2866
+ for (const job of queued) {
2867
+ if (this.active.size >= this.maxConcurrentJobs) break;
2868
+ if (!this.active.has(job.id)) {
2869
+ this.execute(job);
2870
+ }
2871
+ }
2872
+ if (this.active.size < this.maxConcurrentJobs) {
2873
+ const retryable = this.store.getRetryableJobs();
2874
+ for (const job of retryable) {
2875
+ if (this.active.size >= this.maxConcurrentJobs) break;
2876
+ if (!this.active.has(job.id)) {
2877
+ this.execute(job);
2878
+ }
2879
+ }
2880
+ }
2881
+ }
2882
+ schedule(job) {
2883
+ if (this.running && this.active.size < this.maxConcurrentJobs && !this.active.has(job.id)) {
2884
+ this.execute(job);
2885
+ }
2886
+ }
2887
+ recover() {
2888
+ const inProgress = this.store.getInProgressJobs();
2889
+ for (const job of inProgress) {
2890
+ if (this.active.size >= this.maxConcurrentJobs) break;
2891
+ let recoveredJob = job;
2892
+ if (job.type === "payment" && (job.state === "executing" || job.state === "inflight")) {
2893
+ recoveredJob = this.store.updateJob(job.id, { state: "inflight" });
2894
+ }
2895
+ this.execute(recoveredJob);
2896
+ }
2897
+ }
2898
+ execute(job) {
2899
+ const abortController = new AbortController();
2900
+ const promise = (async () => {
2901
+ try {
2902
+ const generator = job.type === "payment" ? runPaymentJob(job, this.rpc, this.retryPolicy, abortController.signal) : job.type === "invoice" ? runInvoiceJob(job, this.rpc, this.retryPolicy, abortController.signal) : runChannelJob(job, this.rpc, this.retryPolicy, abortController.signal);
2903
+ for await (const updated of generator) {
2904
+ const prev = this.getJob(updated.id);
2905
+ const fromState = prev?.state ?? job.state;
2906
+ this.store.updateJob(updated.id, updated);
2907
+ this.store.addJobEvent(
2908
+ updated.id,
2909
+ stateToEvent(updated.state),
2910
+ fromState,
2911
+ updated.state,
2912
+ this.buildEventData(updated)
2913
+ );
2914
+ this.emit("job:state_changed", updated, fromState);
2915
+ if (updated.state === "succeeded") this.emit("job:succeeded", updated);
2916
+ if (updated.state === "failed") this.emit("job:failed", updated);
2917
+ if (updated.state === "cancelled") this.emit("job:cancelled", updated);
2918
+ }
2919
+ } finally {
2920
+ this.active.delete(job.id);
2921
+ }
2922
+ })();
2923
+ this.active.set(job.id, { abortController, promise });
2924
+ }
2925
+ buildEventData(job) {
2926
+ const base = {
2927
+ type: job.type,
2928
+ idempotencyKey: job.idempotencyKey,
2929
+ retryCount: job.retryCount,
2930
+ maxRetries: job.maxRetries,
2931
+ nextRetryAt: job.nextRetryAt
2932
+ };
2933
+ if (job.error) {
2934
+ base.error = {
2935
+ category: job.error.category,
2936
+ retryable: job.error.retryable,
2937
+ message: job.error.message
2938
+ };
2939
+ }
2940
+ if (job.type === "payment") {
2941
+ const paymentJob = job;
2942
+ return {
2943
+ ...base,
2944
+ invoice: paymentJob.params.invoice,
2945
+ paymentHash: paymentJob.result?.paymentHash,
2946
+ paymentStatus: paymentJob.result?.status
2947
+ };
2948
+ }
2949
+ if (job.type === "invoice") {
2950
+ const invoiceJob = job;
2951
+ return {
2952
+ ...base,
2953
+ action: invoiceJob.params.action,
2954
+ paymentHash: invoiceJob.result?.paymentHash ?? invoiceJob.params.getInvoicePaymentHash,
2955
+ invoiceStatus: invoiceJob.result?.status
2956
+ };
2957
+ }
2958
+ const channelJob = job;
2959
+ return {
2960
+ ...base,
2961
+ action: channelJob.params.action,
2962
+ channelId: channelJob.params.channelId ?? channelJob.params.shutdownChannelParams?.channel_id ?? channelJob.result?.channelId,
2963
+ peerId: channelJob.params.peerId ?? channelJob.params.openChannelParams?.peer_id,
2964
+ channelState: channelJob.result?.state
2965
+ };
2966
+ }
2967
+ };
2968
+ function deriveInvoiceKey(params) {
2969
+ if (params.action === "watch" && params.getInvoicePaymentHash) {
2970
+ return `invoice:watch:${params.getInvoicePaymentHash}`;
2971
+ }
2972
+ if (params.action === "cancel" && params.cancelInvoiceParams?.payment_hash) {
2973
+ return `invoice:cancel:${params.cancelInvoiceParams.payment_hash}`;
2974
+ }
2975
+ if (params.action === "settle" && params.settleInvoiceParams?.payment_hash) {
2976
+ return `invoice:settle:${params.settleInvoiceParams.payment_hash}`;
2977
+ }
2978
+ if (params.action === "create" && params.newInvoiceParams?.payment_hash) {
2979
+ return `invoice:create:${params.newInvoiceParams.payment_hash}`;
2980
+ }
2981
+ return void 0;
2982
+ }
2983
+ function deriveChannelKey(params) {
2984
+ if (params.action === "open") return void 0;
2985
+ if (params.acceptChannelParams?.temporary_channel_id) {
2986
+ return `channel:accept:${params.acceptChannelParams.temporary_channel_id}`;
2987
+ }
2988
+ if (params.action === "shutdown" && params.shutdownChannelParams?.channel_id) {
2989
+ return `channel:shutdown:${params.shutdownChannelParams.channel_id}`;
2990
+ }
2991
+ if (params.action === "abandon" && params.abandonChannelParams?.channel_id) {
2992
+ return `channel:abandon:${params.abandonChannelParams.channel_id}`;
2993
+ }
2994
+ if (params.action === "update" && params.updateChannelParams?.channel_id) {
2995
+ const fingerprint = stableStringify(params.updateChannelParams);
2996
+ return `channel:update:${params.updateChannelParams.channel_id}:${fingerprint}`;
2997
+ }
2998
+ if (params.channelId) return `channel:${params.action}:${params.channelId}`;
2999
+ return void 0;
3000
+ }
3001
+ function stateToEvent(state) {
3002
+ switch (state) {
3003
+ case "executing":
3004
+ return "executing";
3005
+ case "inflight":
3006
+ return "inflight";
3007
+ case "invoice_created":
3008
+ return "invoice_created";
3009
+ case "invoice_received":
3010
+ return "invoice_received";
3011
+ case "invoice_settled":
3012
+ return "invoice_settled";
3013
+ case "invoice_expired":
3014
+ return "invoice_expired";
3015
+ case "invoice_cancelled":
3016
+ return "invoice_cancelled";
3017
+ case "channel_opening":
3018
+ return "channel_opening";
3019
+ case "channel_accepting":
3020
+ return "channel_accepting";
3021
+ case "channel_abandoning":
3022
+ return "channel_abandoning";
3023
+ case "channel_updating":
3024
+ return "channel_updating";
3025
+ case "channel_awaiting_ready":
3026
+ return "channel_awaiting_ready";
3027
+ case "channel_ready":
3028
+ return "channel_ready";
3029
+ case "channel_closing":
3030
+ return "channel_closing";
3031
+ case "channel_closed":
3032
+ return "channel_closed";
3033
+ case "waiting_retry":
3034
+ return "retry_scheduled";
3035
+ case "succeeded":
3036
+ return "succeeded";
3037
+ case "failed":
3038
+ return "failed";
3039
+ case "cancelled":
3040
+ return "cancelled";
3041
+ default:
3042
+ return "executing";
3043
+ }
3044
+ }
3045
+
3046
+ // src/service.ts
3047
+ var FiberMonitorService = class extends EventEmitter2 {
3048
+ config;
3049
+ startedAt;
3050
+ client;
3051
+ store;
3052
+ alerts;
3053
+ monitors;
3054
+ proxy;
3055
+ jobStore;
3056
+ jobManager;
3057
+ running = false;
3058
+ constructor(configInput = {}) {
3059
+ super();
3060
+ this.config = createRuntimeConfig(configInput);
3061
+ this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
3062
+ this.client = new FiberRpcClient2({
3063
+ url: this.config.fiberRpcUrl,
3064
+ timeout: this.config.requestTimeoutMs
3065
+ });
3066
+ this.store = new MemoryStore({
3067
+ stateFilePath: this.config.storage.stateFilePath,
3068
+ flushIntervalMs: this.config.storage.flushIntervalMs,
3069
+ maxAlertHistory: this.config.storage.maxAlertHistory
3070
+ });
3071
+ this.alerts = new AlertManager({
3072
+ backends: this.createAlertBackends(this.config),
3073
+ store: this.store
3074
+ });
3075
+ this.jobStore = this.config.jobs.enabled ? new SqliteJobStore(this.config.jobs.dbPath) : null;
3076
+ this.jobManager = this.jobStore ? new JobManager(this.client, this.jobStore, {
3077
+ maxConcurrentJobs: this.config.jobs.maxConcurrentJobs,
3078
+ schedulerIntervalMs: this.config.jobs.schedulerIntervalMs,
3079
+ retryPolicy: this.config.jobs.retryPolicy
3080
+ }) : null;
3081
+ this.wireJobAlerts();
3082
+ const hooks = {};
3083
+ this.monitors = [
3084
+ new ChannelMonitor({
3085
+ client: this.client,
3086
+ store: this.store,
3087
+ alerts: this.alerts,
3088
+ config: {
3089
+ intervalMs: this.config.channelPollIntervalMs,
3090
+ includeClosedChannels: this.config.includeClosedChannels
3091
+ },
3092
+ hooks
3093
+ }),
3094
+ new InvoiceTracker({
3095
+ client: this.client,
3096
+ store: this.store,
3097
+ alerts: this.alerts,
3098
+ config: {
3099
+ intervalMs: this.config.invoicePollIntervalMs,
3100
+ completedItemTtlSeconds: this.config.completedItemTtlSeconds
3101
+ },
3102
+ hooks
3103
+ }),
3104
+ new PaymentTracker({
3105
+ client: this.client,
3106
+ store: this.store,
3107
+ alerts: this.alerts,
3108
+ config: {
3109
+ intervalMs: this.config.paymentPollIntervalMs,
3110
+ completedItemTtlSeconds: this.config.completedItemTtlSeconds
3111
+ },
3112
+ hooks
3113
+ }),
3114
+ new PeerMonitor({
3115
+ client: this.client,
3116
+ store: this.store,
3117
+ alerts: this.alerts,
3118
+ config: { intervalMs: this.config.peerPollIntervalMs },
3119
+ hooks
3120
+ }),
3121
+ new HealthMonitor({
3122
+ client: this.client,
3123
+ alerts: this.alerts,
3124
+ config: { intervalMs: this.config.healthPollIntervalMs }
3125
+ })
3126
+ ];
3127
+ this.alerts.onEmit((alert) => {
3128
+ this.emit("alert", alert);
3129
+ });
3130
+ this.proxy = new RpcMonitorProxy(
3131
+ {
3132
+ listen: this.config.proxy.listen,
3133
+ targetUrl: this.config.fiberRpcUrl
3134
+ },
3135
+ {
3136
+ onInvoiceTracked: (paymentHash) => {
3137
+ this.store.addTrackedInvoice(paymentHash);
3138
+ },
3139
+ onPaymentTracked: (paymentHash) => {
3140
+ this.store.addTrackedPayment(paymentHash);
3141
+ },
3142
+ listTrackedInvoices: () => this.store.listTrackedInvoices(),
3143
+ listTrackedPayments: () => this.store.listTrackedPayments(),
3144
+ listAlerts: (filters) => this.store.listAlerts(filters),
3145
+ getStatus: () => this.getStatus(),
3146
+ createPaymentJob: this.jobManager ? (params, options) => this.jobManager.ensurePayment(params, options) : void 0,
3147
+ createInvoiceJob: this.jobManager ? (params, options) => this.jobManager.manageInvoice(params, options) : void 0,
3148
+ createChannelJob: this.jobManager ? (params, options) => this.jobManager.manageChannel(params, options) : void 0,
3149
+ getJob: this.jobManager ? (id) => this.jobManager.getJob(id) : void 0,
3150
+ listJobs: this.jobManager ? (filter) => this.jobManager.listJobs(filter) : void 0,
3151
+ cancelJob: this.jobManager ? (id) => this.jobManager.cancelJob(id) : void 0,
3152
+ listJobEvents: this.jobStore ? (jobId) => this.jobStore.listJobEvents(jobId) : void 0
3153
+ }
3154
+ );
3155
+ }
3156
+ async start() {
3157
+ if (this.running) {
3158
+ return;
3159
+ }
3160
+ await this.store.load();
3161
+ this.store.startAutoFlush();
3162
+ await this.alerts.start();
3163
+ for (const monitor of this.monitors) {
3164
+ monitor.start();
3165
+ }
3166
+ this.jobManager?.start();
3167
+ if (this.config.proxy.enabled) {
3168
+ await this.proxy.start();
3169
+ }
3170
+ this.running = true;
3171
+ }
3172
+ async stop() {
3173
+ if (!this.running) {
3174
+ return;
3175
+ }
3176
+ for (const monitor of this.monitors) {
3177
+ monitor.stop();
3178
+ }
3179
+ await this.jobManager?.stop();
3180
+ if (this.config.proxy.enabled) {
3181
+ await this.proxy.stop();
3182
+ }
3183
+ this.store.stopAutoFlush();
3184
+ await this.store.flush();
3185
+ await this.alerts.stop();
3186
+ this.jobStore?.close();
3187
+ this.running = false;
3188
+ }
3189
+ getStatus() {
3190
+ return {
3191
+ startedAt: this.startedAt,
3192
+ proxyListen: this.config.proxy.listen,
3193
+ targetUrl: this.config.fiberRpcUrl,
3194
+ running: this.running
3195
+ };
3196
+ }
3197
+ listAlerts(filters) {
3198
+ return this.store.listAlerts(filters);
3199
+ }
3200
+ listTrackedInvoices() {
3201
+ return this.store.listTrackedInvoices();
3202
+ }
3203
+ listTrackedPayments() {
3204
+ return this.store.listTrackedPayments();
3205
+ }
3206
+ trackInvoice(paymentHash) {
3207
+ this.store.addTrackedInvoice(paymentHash);
3208
+ }
3209
+ trackPayment(paymentHash) {
3210
+ this.store.addTrackedPayment(paymentHash);
3211
+ }
3212
+ createAlertBackends(config) {
3213
+ return config.alerts.map((alertConfig) => {
3214
+ if (alertConfig.type === "stdout") {
3215
+ return new StdoutAlertBackend();
3216
+ }
3217
+ if (alertConfig.type === "webhook") {
3218
+ return new WebhookAlertBackend({
3219
+ url: alertConfig.url,
3220
+ timeoutMs: alertConfig.timeoutMs,
3221
+ headers: alertConfig.headers
3222
+ });
3223
+ }
3224
+ if (alertConfig.type === "file") {
3225
+ return new JsonlFileAlertBackend(alertConfig.path);
3226
+ }
3227
+ const [host, portText] = alertConfig.listen.split(":");
3228
+ return new WebsocketAlertBackend({
3229
+ host,
3230
+ port: Number(portText)
3231
+ });
3232
+ });
3233
+ }
3234
+ wireJobAlerts() {
3235
+ if (!this.jobManager) {
3236
+ return;
3237
+ }
3238
+ this.jobManager.on("job:created", (job) => {
3239
+ this.emitJobAlert(job, "started", "low");
3240
+ this.trackJobArtifacts(job);
3241
+ });
3242
+ this.jobManager.on("job:state_changed", (job) => {
3243
+ this.trackJobArtifacts(job);
3244
+ if (job.state === "waiting_retry") {
3245
+ this.emitJobAlert(job, "retrying", "medium");
3246
+ }
3247
+ });
3248
+ this.jobManager.on("job:succeeded", (job) => {
3249
+ this.trackJobArtifacts(job);
3250
+ this.emitJobAlert(job, "succeeded", "medium");
3251
+ });
3252
+ this.jobManager.on("job:failed", (job) => {
3253
+ this.trackJobArtifacts(job);
3254
+ this.emitJobAlert(job, "failed", "high");
3255
+ });
3256
+ }
3257
+ emitJobAlert(job, lifecycle, priority) {
3258
+ const type = this.toJobAlertType(job.type, lifecycle);
3259
+ const data = this.toJobAlertData(job);
3260
+ void this.alerts.emit({
3261
+ type,
3262
+ priority,
3263
+ source: "job-manager",
3264
+ data
3265
+ });
3266
+ }
3267
+ toJobAlertType(jobType, lifecycle) {
3268
+ return `${jobType}_job_${lifecycle}`;
3269
+ }
3270
+ toJobAlertData(job) {
3271
+ const error = job.error?.message;
3272
+ if (job.type === "payment") {
3273
+ const paymentJob = job;
3274
+ return {
3275
+ jobId: paymentJob.id,
3276
+ idempotencyKey: paymentJob.idempotencyKey,
3277
+ retryCount: paymentJob.retryCount,
3278
+ error,
3279
+ fee: paymentJob.result?.fee
3280
+ };
3281
+ }
3282
+ if (job.type === "invoice") {
3283
+ const invoiceJob = job;
3284
+ return {
3285
+ jobId: invoiceJob.id,
3286
+ idempotencyKey: invoiceJob.idempotencyKey,
3287
+ retryCount: invoiceJob.retryCount,
3288
+ action: invoiceJob.params.action,
3289
+ status: invoiceJob.result?.status,
3290
+ paymentHash: this.extractInvoiceHash(invoiceJob),
3291
+ error
3292
+ };
3293
+ }
3294
+ const channelJob = job;
3295
+ return {
3296
+ jobId: channelJob.id,
3297
+ idempotencyKey: channelJob.idempotencyKey,
3298
+ retryCount: channelJob.retryCount,
3299
+ action: channelJob.params.action,
3300
+ channelId: this.extractChannelId(channelJob),
3301
+ error
3302
+ };
3303
+ }
3304
+ trackJobArtifacts(job) {
3305
+ if (job.type === "payment") {
3306
+ const paymentHash = this.extractPaymentHash(job);
3307
+ if (paymentHash) {
3308
+ this.store.addTrackedPayment(paymentHash);
3309
+ }
3310
+ return;
3311
+ }
3312
+ if (job.type === "invoice") {
3313
+ const paymentHash = this.extractInvoiceHash(job);
3314
+ if (paymentHash) {
3315
+ this.store.addTrackedInvoice(paymentHash);
3316
+ }
3317
+ }
3318
+ }
3319
+ extractPaymentHash(job) {
3320
+ return this.normalizeHash(job.result?.paymentHash ?? job.params.sendPaymentParams.payment_hash);
3321
+ }
3322
+ extractInvoiceHash(job) {
3323
+ return this.normalizeHash(
3324
+ job.result?.paymentHash ?? job.params.getInvoicePaymentHash ?? job.params.cancelInvoiceParams?.payment_hash ?? job.params.settleInvoiceParams?.payment_hash ?? job.params.newInvoiceParams?.payment_hash
3325
+ );
3326
+ }
3327
+ extractChannelId(job) {
3328
+ return this.normalizeHash(
3329
+ job.result?.channelId ?? job.result?.acceptedChannelId ?? job.params.channelId ?? job.params.shutdownChannelParams?.channel_id
3330
+ );
3331
+ }
3332
+ normalizeHash(value) {
3333
+ if (!value || !value.startsWith("0x")) {
3334
+ return void 0;
3335
+ }
3336
+ return value;
3337
+ }
3338
+ };
3339
+
3340
+ // src/bootstrap.ts
3341
+ async function startRuntimeService(configInput = {}) {
3342
+ const service = new FiberMonitorService(configInput);
3343
+ await service.start();
3344
+ let stopping = false;
3345
+ const stop = async () => {
3346
+ if (stopping) {
3347
+ return;
3348
+ }
3349
+ stopping = true;
3350
+ await service.stop();
3351
+ };
3352
+ const waitForShutdownSignal = () => {
3353
+ return new Promise((resolve2) => {
3354
+ const onSignal = (signal) => {
3355
+ process.off("SIGINT", onSignal);
3356
+ process.off("SIGTERM", onSignal);
3357
+ resolve2(signal);
3358
+ };
3359
+ process.on("SIGINT", onSignal);
3360
+ process.on("SIGTERM", onSignal);
3361
+ });
3362
+ };
3363
+ return {
3364
+ service,
3365
+ stop,
3366
+ waitForShutdownSignal
3367
+ };
3368
+ }
3369
+ export {
3370
+ FiberMonitorService,
3371
+ JobManager,
3372
+ MemoryStore,
3373
+ RpcMonitorProxy,
3374
+ SqliteJobStore,
3375
+ alertPriorityOrder,
3376
+ alertTypeValues,
3377
+ classifyRpcError,
3378
+ computeRetryDelay,
3379
+ createRuntimeConfig,
3380
+ defaultPaymentRetryPolicy,
3381
+ defaultRuntimeConfig,
3382
+ formatRuntimeAlert,
3383
+ isAlertPriority,
3384
+ isAlertType,
3385
+ paymentStateMachine,
3386
+ shouldRetry,
3387
+ startRuntimeService
3388
+ };
3389
+ //# sourceMappingURL=index.js.map