arc402-cli 0.7.1 → 0.7.2
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/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +67 -0
- package/dist/commands/daemon.js.map +1 -1
- package/dist/daemon/config.d.ts +15 -1
- package/dist/daemon/config.d.ts.map +1 -1
- package/dist/daemon/config.js +32 -0
- package/dist/daemon/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +1 -9
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/notify.d.ts +35 -6
- package/dist/daemon/notify.d.ts.map +1 -1
- package/dist/daemon/notify.js +176 -48
- package/dist/daemon/notify.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/daemon.ts +73 -0
- package/src/daemon/config.ts +35 -0
- package/src/daemon/index.ts +2 -14
- package/src/daemon/notify.ts +199 -59
package/src/daemon/notify.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Multi-channel notification module for daemon events.
|
|
3
|
+
* Supports Telegram, Discord webhooks, generic webhooks, and email (via nodemailer).
|
|
4
4
|
*/
|
|
5
5
|
import * as https from "https";
|
|
6
6
|
import * as http from "http";
|
|
7
|
+
import type { DaemonConfig } from "./config";
|
|
7
8
|
|
|
8
9
|
export type NotifyEvent =
|
|
9
10
|
| "hire_request"
|
|
@@ -16,33 +17,36 @@ export type NotifyEvent =
|
|
|
16
17
|
| "daemon_started"
|
|
17
18
|
| "daemon_stopped";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// ─── Channel interface ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface NotificationChannel {
|
|
23
|
+
send(title: string, body: string): Promise<void>;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
// ─── HTTP helper ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function httpPost(url: string, payload: string, extraHeaders: Record<string, string>): Promise<void> {
|
|
25
29
|
return new Promise((resolve, reject) => {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
const mod = parsed.protocol === "https:" ? https : http;
|
|
32
|
+
const options = {
|
|
33
|
+
hostname: parsed.hostname,
|
|
34
|
+
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
35
|
+
path: parsed.pathname + parsed.search,
|
|
31
36
|
method: "POST",
|
|
32
37
|
headers: {
|
|
33
38
|
"Content-Type": "application/json",
|
|
34
39
|
"Content-Length": Buffer.byteLength(payload),
|
|
40
|
+
...extraHeaders,
|
|
35
41
|
},
|
|
36
42
|
};
|
|
37
|
-
const req =
|
|
38
|
-
|
|
39
|
-
res.on("data", (c: Buffer) => { data += c.toString(); });
|
|
43
|
+
const req = mod.request(options, (res) => {
|
|
44
|
+
res.resume();
|
|
40
45
|
res.on("end", () => {
|
|
41
|
-
|
|
42
|
-
if (!parsed.ok) {
|
|
43
|
-
reject(new Error(`Telegram API error: ${parsed.description}`));
|
|
44
|
-
} else {
|
|
46
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
45
47
|
resolve();
|
|
48
|
+
} else {
|
|
49
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
46
50
|
}
|
|
47
51
|
});
|
|
48
52
|
});
|
|
@@ -52,16 +56,118 @@ function telegramPost(botToken: string, method: string, body: object): Promise<v
|
|
|
52
56
|
});
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
// ─── Telegram channel ─────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export class TelegramChannel implements NotificationChannel {
|
|
62
|
+
constructor(private botToken: string, private chatId: string) {}
|
|
63
|
+
|
|
64
|
+
async send(title: string, body: string): Promise<void> {
|
|
65
|
+
const text = body ? `<b>${title}</b>\n${body}` : `<b>${title}</b>`;
|
|
66
|
+
const payload = JSON.stringify({ chat_id: this.chatId, text, parse_mode: "HTML" });
|
|
67
|
+
const options: https.RequestOptions = {
|
|
68
|
+
hostname: "api.telegram.org",
|
|
69
|
+
port: 443,
|
|
70
|
+
path: `/bot${this.botToken}/sendMessage`,
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
await new Promise<void>((resolve, reject) => {
|
|
78
|
+
const req = https.request(options, (res) => {
|
|
79
|
+
let data = "";
|
|
80
|
+
res.on("data", (c: Buffer) => { data += c.toString(); });
|
|
81
|
+
res.on("end", () => {
|
|
82
|
+
const parsed = JSON.parse(data) as { ok: boolean; description?: string };
|
|
83
|
+
if (!parsed.ok) {
|
|
84
|
+
reject(new Error(`Telegram API error: ${parsed.description}`));
|
|
85
|
+
} else {
|
|
86
|
+
resolve();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
req.on("error", reject);
|
|
91
|
+
req.write(payload);
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Discord channel ──────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export class DiscordChannel implements NotificationChannel {
|
|
100
|
+
constructor(private webhookUrl: string) {}
|
|
101
|
+
|
|
102
|
+
async send(title: string, body: string): Promise<void> {
|
|
103
|
+
const content = body ? `**${title}**\n${body}` : `**${title}**`;
|
|
104
|
+
await httpPost(this.webhookUrl, JSON.stringify({ content }), {});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Generic webhook channel ──────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export class WebhookChannel implements NotificationChannel {
|
|
111
|
+
constructor(private url: string, private headers: Record<string, string> = {}) {}
|
|
112
|
+
|
|
113
|
+
async send(title: string, body: string): Promise<void> {
|
|
114
|
+
await httpPost(
|
|
115
|
+
this.url,
|
|
116
|
+
JSON.stringify({ title, body, timestamp: new Date().toISOString() }),
|
|
117
|
+
this.headers
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Email channel (optional — requires nodemailer) ───────────────────────────
|
|
123
|
+
|
|
124
|
+
export class EmailChannel implements NotificationChannel {
|
|
125
|
+
constructor(private cfg: {
|
|
126
|
+
smtpHost: string;
|
|
127
|
+
smtpPort: number;
|
|
128
|
+
smtpUser: string;
|
|
129
|
+
smtpPass: string;
|
|
130
|
+
to: string;
|
|
131
|
+
}) {}
|
|
132
|
+
|
|
133
|
+
async send(title: string, body: string): Promise<void> {
|
|
134
|
+
// nodemailer is an optional runtime dependency — load via require to skip
|
|
135
|
+
// compile-time module resolution. Throws a clear message if not installed.
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
|
|
137
|
+
let nodemailer: any;
|
|
138
|
+
try {
|
|
139
|
+
// Using Function constructor avoids TypeScript static import analysis.
|
|
140
|
+
nodemailer = (new Function("require", "return require('nodemailer')"))(require);
|
|
141
|
+
} catch {
|
|
142
|
+
throw new Error("nodemailer is not installed. Run: npm install nodemailer");
|
|
143
|
+
}
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
145
|
+
const transport = nodemailer.createTransport({
|
|
146
|
+
host: this.cfg.smtpHost,
|
|
147
|
+
port: this.cfg.smtpPort,
|
|
148
|
+
auth: { user: this.cfg.smtpUser, pass: this.cfg.smtpPass },
|
|
149
|
+
});
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
151
|
+
await transport.sendMail({
|
|
152
|
+
from: this.cfg.smtpUser,
|
|
153
|
+
to: this.cfg.to,
|
|
154
|
+
subject: title,
|
|
155
|
+
text: body,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Notifier ─────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
55
162
|
export class Notifier {
|
|
56
|
-
private
|
|
163
|
+
private channels: NotificationChannel[];
|
|
57
164
|
private notifyFlags: Record<NotifyEvent, boolean>;
|
|
58
165
|
|
|
59
166
|
constructor(
|
|
60
|
-
|
|
61
|
-
chatId: string,
|
|
167
|
+
channels: NotificationChannel[],
|
|
62
168
|
flags: Partial<Record<NotifyEvent, boolean>> = {}
|
|
63
169
|
) {
|
|
64
|
-
this.
|
|
170
|
+
this.channels = channels;
|
|
65
171
|
this.notifyFlags = {
|
|
66
172
|
hire_request: flags.hire_request ?? true,
|
|
67
173
|
hire_accepted: flags.hire_accepted ?? true,
|
|
@@ -76,82 +182,116 @@ export class Notifier {
|
|
|
76
182
|
}
|
|
77
183
|
|
|
78
184
|
isEnabled(): boolean {
|
|
79
|
-
return this.
|
|
185
|
+
return this.channels.length > 0;
|
|
80
186
|
}
|
|
81
187
|
|
|
82
|
-
async send(event: NotifyEvent,
|
|
83
|
-
if (!this.config) return;
|
|
188
|
+
async send(event: NotifyEvent, title: string, body: string): Promise<void> {
|
|
84
189
|
if (!this.notifyFlags[event]) return;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
190
|
+
await Promise.all(
|
|
191
|
+
this.channels.map(async (ch) => {
|
|
192
|
+
try {
|
|
193
|
+
await ch.send(title, body);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
process.stderr.write(`[notify] Channel send failed: ${err}\n`);
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
);
|
|
95
199
|
}
|
|
96
200
|
|
|
97
201
|
async notifyHireRequest(hireId: string, hirerAddress: string, priceEth: string, capability: string): Promise<void> {
|
|
98
202
|
const short = hirerAddress.slice(0, 10);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`
|
|
102
|
-
`From: <code>${short}...</code>`,
|
|
203
|
+
await this.send("hire_request", "Hire Request", [
|
|
204
|
+
`ID: ${hireId}`,
|
|
205
|
+
`From: ${short}...`,
|
|
103
206
|
`Capability: ${capability || "unspecified"}`,
|
|
104
207
|
`Price: ${priceEth} ETH`,
|
|
105
208
|
``,
|
|
106
|
-
`Approve:
|
|
107
|
-
`Reject:
|
|
108
|
-
].join("\n");
|
|
109
|
-
await this.send("hire_request", msg);
|
|
209
|
+
`Approve: arc402 daemon approve ${hireId}`,
|
|
210
|
+
`Reject: arc402 daemon reject ${hireId}`,
|
|
211
|
+
].join("\n"));
|
|
110
212
|
}
|
|
111
213
|
|
|
112
214
|
async notifyHireAccepted(hireId: string, agreementId: string): Promise<void> {
|
|
113
|
-
await this.send("hire_accepted",
|
|
114
|
-
|
|
215
|
+
await this.send("hire_accepted", "Hire Accepted",
|
|
216
|
+
`ID: ${hireId}\nAgreement: ${agreementId}`
|
|
115
217
|
);
|
|
116
218
|
}
|
|
117
219
|
|
|
118
220
|
async notifyHireRejected(hireId: string, reason: string): Promise<void> {
|
|
119
|
-
await this.send("hire_rejected",
|
|
120
|
-
|
|
221
|
+
await this.send("hire_rejected", "Hire Rejected",
|
|
222
|
+
`ID: ${hireId}\nReason: ${reason}`
|
|
121
223
|
);
|
|
122
224
|
}
|
|
123
225
|
|
|
124
226
|
async notifyDelivery(agreementId: string, deliveryHash: string, userOpHash: string): Promise<void> {
|
|
125
|
-
await this.send("delivery",
|
|
126
|
-
|
|
127
|
-
|
|
227
|
+
await this.send("delivery", "Delivery Submitted", [
|
|
228
|
+
`Agreement: ${agreementId}`,
|
|
229
|
+
`Delivery hash: ${deliveryHash.slice(0, 16)}...`,
|
|
230
|
+
`UserOp: ${userOpHash.slice(0, 16)}...`,
|
|
231
|
+
].join("\n"));
|
|
128
232
|
}
|
|
129
233
|
|
|
130
234
|
async notifyDispute(agreementId: string, raisedBy: string): Promise<void> {
|
|
131
|
-
await this.send("dispute",
|
|
132
|
-
|
|
235
|
+
await this.send("dispute", "Dispute Raised",
|
|
236
|
+
`Agreement: ${agreementId}\nBy: ${raisedBy}`
|
|
133
237
|
);
|
|
134
238
|
}
|
|
135
239
|
|
|
136
240
|
async notifyChannelChallenge(channelId: string, txHash: string): Promise<void> {
|
|
137
|
-
await this.send("channel_challenge",
|
|
138
|
-
|
|
241
|
+
await this.send("channel_challenge", "Channel Challenged",
|
|
242
|
+
`Channel: ${channelId.slice(0, 16)}...\nTx: ${txHash.slice(0, 16)}...`
|
|
139
243
|
);
|
|
140
244
|
}
|
|
141
245
|
|
|
142
246
|
async notifyLowBalance(balanceEth: string, thresholdEth: string): Promise<void> {
|
|
143
|
-
await this.send("low_balance",
|
|
144
|
-
|
|
247
|
+
await this.send("low_balance", "Low Balance Alert",
|
|
248
|
+
`Current: ${balanceEth} ETH\nThreshold: ${thresholdEth} ETH`
|
|
145
249
|
);
|
|
146
250
|
}
|
|
147
251
|
|
|
148
252
|
async notifyStarted(walletAddress: string, subsystems: string[]): Promise<void> {
|
|
149
|
-
await this.send("daemon_started",
|
|
150
|
-
|
|
253
|
+
await this.send("daemon_started", "ARC-402 Daemon Started",
|
|
254
|
+
`Wallet: ${walletAddress}\nSubsystems: ${subsystems.join(", ")}`
|
|
151
255
|
);
|
|
152
256
|
}
|
|
153
257
|
|
|
154
258
|
async notifyStopped(): Promise<void> {
|
|
155
|
-
await this.send("daemon_stopped",
|
|
259
|
+
await this.send("daemon_stopped", "ARC-402 Daemon Stopped", "");
|
|
156
260
|
}
|
|
157
261
|
}
|
|
262
|
+
|
|
263
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
export function buildNotifier(config: DaemonConfig): Notifier {
|
|
266
|
+
const notif = config.notifications;
|
|
267
|
+
const channels: NotificationChannel[] = [];
|
|
268
|
+
|
|
269
|
+
if (notif.telegram_bot_token && notif.telegram_chat_id) {
|
|
270
|
+
channels.push(new TelegramChannel(notif.telegram_bot_token, notif.telegram_chat_id));
|
|
271
|
+
}
|
|
272
|
+
if (notif.discord?.webhook_url) {
|
|
273
|
+
channels.push(new DiscordChannel(notif.discord.webhook_url));
|
|
274
|
+
}
|
|
275
|
+
if (notif.webhook?.url) {
|
|
276
|
+
channels.push(new WebhookChannel(notif.webhook.url, notif.webhook.headers ?? {}));
|
|
277
|
+
}
|
|
278
|
+
if (notif.email?.smtp_host && notif.email?.smtp_user && notif.email?.to) {
|
|
279
|
+
channels.push(new EmailChannel({
|
|
280
|
+
smtpHost: notif.email.smtp_host,
|
|
281
|
+
smtpPort: notif.email.smtp_port,
|
|
282
|
+
smtpUser: notif.email.smtp_user,
|
|
283
|
+
smtpPass: notif.email.smtp_pass,
|
|
284
|
+
to: notif.email.to,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return new Notifier(channels, {
|
|
289
|
+
hire_request: notif.notify_on_hire_request,
|
|
290
|
+
hire_accepted: notif.notify_on_hire_accepted,
|
|
291
|
+
hire_rejected: notif.notify_on_hire_rejected,
|
|
292
|
+
delivery: notif.notify_on_delivery,
|
|
293
|
+
dispute: notif.notify_on_dispute,
|
|
294
|
+
channel_challenge: notif.notify_on_channel_challenge,
|
|
295
|
+
low_balance: notif.notify_on_low_balance,
|
|
296
|
+
});
|
|
297
|
+
}
|