@suphark/notify 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,564 @@
1
+ /** ที่อยู่อีเมล: "a@b.com" | "ชื่อ <a@b.com>" | { name, email } */
2
+ type Address = string | {
3
+ name?: string;
4
+ email: string;
5
+ };
6
+ interface Attachment {
7
+ filename: string;
8
+ /** Buffer หรือ string (utf8) — ถ้าเป็น base64 อยู่แล้วให้ระบุ encoding: "base64" */
9
+ content: Buffer | string;
10
+ contentType?: string;
11
+ encoding?: "base64" | "utf8";
12
+ }
13
+ /** ข้อความที่แอปส่งเข้ามา (ไม่ผูกกับ provider ใด ๆ) */
14
+ interface Message {
15
+ to: Address | Address[];
16
+ subject: string;
17
+ html?: string;
18
+ text?: string;
19
+ /** override from ของ provider/ค่าเริ่มต้น */
20
+ from?: Address;
21
+ cc?: Address | Address[];
22
+ bcc?: Address | Address[];
23
+ replyTo?: Address | Address[];
24
+ attachments?: Attachment[];
25
+ headers?: Record<string, string>;
26
+ /** metadata แนบไปกับอีเมล (provider ที่รองรับ เช่น Resend tags) */
27
+ tags?: Record<string, string>;
28
+ /** บังคับใช้ provider ตัวนี้ตัวเดียว (ไม่ failover) — โหมด local เท่านั้น */
29
+ provider?: string;
30
+ /** อยากให้ลองตัวนี้ก่อน (ยัง failover ตามปกติถ้าไม่ได้) — ส่งต่อไป hub ด้วย */
31
+ prefer?: string;
32
+ }
33
+ /** ข้อมูลตัวเลือกที่แอปควรรู้ — แค่ชื่อ/ประเภท/พร้อมใช้ไหม */
34
+ interface ProviderInfo {
35
+ name: string;
36
+ type: string;
37
+ available: boolean;
38
+ /** ชื่อ provider ที่เป็นทางผ่าน (เช่น "hub") เมื่อรายการนี้อยู่หลัง hub */
39
+ via?: string;
40
+ }
41
+ interface ParsedAddress {
42
+ name?: string;
43
+ email: string;
44
+ }
45
+ /** ข้อความหลัง normalize แล้ว — ทุก provider ได้รับรูปแบบนี้ */
46
+ interface NormalizedMessage {
47
+ /** ไม่มีได้เฉพาะ provider ที่ประกาศ optionalFrom (hub เติมให้เอง) */
48
+ from?: ParsedAddress;
49
+ to: ParsedAddress[];
50
+ cc: ParsedAddress[];
51
+ bcc: ParsedAddress[];
52
+ replyTo: ParsedAddress[];
53
+ subject: string;
54
+ html?: string;
55
+ text?: string;
56
+ attachments: {
57
+ filename: string;
58
+ base64: string;
59
+ contentType?: string;
60
+ }[];
61
+ headers: Record<string, string>;
62
+ tags: Record<string, string>;
63
+ prefer?: string;
64
+ }
65
+ /** ประเภทความผิดพลาด — ใช้ตัดสินใจว่าจะพัก provider นานแค่ไหน */
66
+ type ErrorKind = "quota" | "rate_limit" | "auth" | "network" | "server" | "invalid" | "unknown";
67
+ type SkipReason = "cooldown" | "quota" | "disabled";
68
+ interface Attempt {
69
+ provider: string;
70
+ ok: boolean;
71
+ /** ข้ามเพราะ circuit เปิด/โควต้าเต็ม (ไม่ได้ลองส่งจริง) */
72
+ skipped?: SkipReason;
73
+ error?: string;
74
+ kind?: ErrorKind;
75
+ ms: number;
76
+ }
77
+ interface SendResult {
78
+ /** id จาก provider (ถ้ามี) */
79
+ id?: string;
80
+ /** ชื่อ provider ที่ส่งสำเร็จ */
81
+ provider: string;
82
+ /** true ถ้าตัวแรกที่ควรใช้ล้ม/ถูกข้าม แล้วไปสำเร็จที่ตัวสำรอง */
83
+ failedOver: boolean;
84
+ attempts: Attempt[];
85
+ }
86
+ interface Quota {
87
+ perHour?: number;
88
+ perDay?: number;
89
+ perMonth?: number;
90
+ }
91
+ /** สิ่งที่ provider ต้อง implement — เท่านี้พอ ที่เหลือ mailer จัดการ */
92
+ interface Provider {
93
+ readonly name: string;
94
+ readonly type: string;
95
+ /** from เริ่มต้นของ provider นี้ (ชนะ from ของ mailer) */
96
+ readonly from?: ParsedAddress;
97
+ /** true = ส่งได้แม้ไม่มี from (provider ปลายทางเติมค่าเริ่มต้นให้ เช่น hub) */
98
+ readonly optionalFrom?: boolean;
99
+ send(msg: NormalizedMessage): Promise<{
100
+ id?: string;
101
+ }>;
102
+ /** ทดสอบการเชื่อมต่อ/credential (ไม่ส่งอีเมล) */
103
+ verify?(): Promise<void>;
104
+ /** provider ตัวนี้เป็นทางผ่านไปยังตัวอื่น (hub) — คืนรายการข้างหลัง */
105
+ listProviders?(): Promise<ProviderInfo[]>;
106
+ }
107
+ /**
108
+ * state ที่ mailer ต้องจำข้ามคำขอ (นับโควต้า, cooldown)
109
+ * default เก็บใน memory ของ process; ต่อ Redis ได้เพื่อแชร์ข้ามแอป/instance (ดู createRedisStore)
110
+ */
111
+ interface Store {
112
+ get(key: string): Promise<string | null>;
113
+ /** ตั้งค่า พร้อมอายุ (วินาที) */
114
+ set(key: string, value: string, ttlSec: number): Promise<void>;
115
+ /** เพิ่มค่าทีละ 1 (สร้างเป็น 1 ถ้ายังไม่มี) พร้อมอายุ (วินาที) คืนค่าใหม่ */
116
+ incr(key: string, ttlSec: number): Promise<number>;
117
+ /** ลดค่าทีละ 1 (ไม่ต่ำกว่า 0) — คืนโควต้าที่จองไว้เมื่อส่งไม่สำเร็จ; store ที่ไม่ implement จะนับความล้มเหลวเป็นโควต้าด้วย */
118
+ decr?(key: string): Promise<void>;
119
+ del(key: string): Promise<void>;
120
+ }
121
+ type MailerEvent = {
122
+ type: "sent";
123
+ provider: string;
124
+ id?: string;
125
+ ms: number;
126
+ failedOver: boolean;
127
+ } | {
128
+ type: "failed";
129
+ provider: string;
130
+ kind: ErrorKind;
131
+ error: string;
132
+ ms: number;
133
+ } | {
134
+ type: "skipped";
135
+ provider: string;
136
+ reason: SkipReason;
137
+ } | {
138
+ type: "cooldown";
139
+ provider: string;
140
+ kind: ErrorKind;
141
+ untilMs: number;
142
+ failures: number;
143
+ } | {
144
+ type: "recovered";
145
+ provider: string;
146
+ } | {
147
+ type: "exhausted";
148
+ attempts: Attempt[];
149
+ };
150
+ interface QuotaUsage {
151
+ window: "hour" | "day" | "month";
152
+ used: number;
153
+ limit: number;
154
+ }
155
+ interface ProviderStatus {
156
+ name: string;
157
+ type: string;
158
+ /** พร้อมใช้ (ไม่อยู่ใน cooldown และโควต้ายังไม่เต็ม) */
159
+ available: boolean;
160
+ cooldownUntil?: number;
161
+ cooldownKind?: ErrorKind;
162
+ failures: number;
163
+ quota: QuotaUsage[];
164
+ }
165
+
166
+ /** แปลง "ชื่อ <a@b.com>" / "a@b.com" / { name, email } → { name, email } */
167
+ declare function parseAddress(input: Address): ParsedAddress;
168
+ /** provider ที่ต้องมี from เสมอเรียกตัวนี้ (mailer รับประกันให้อยู่แล้ว ยกเว้น optionalFrom) */
169
+ declare function requireFrom(msg: {
170
+ from?: ParsedAddress;
171
+ }, provider: string): ParsedAddress;
172
+ declare function parseAddressList(input: Address | Address[] | undefined): ParsedAddress[];
173
+ /** { name, email } → "ชื่อ <a@b.com>" (สำหรับ API ที่รับ string) */
174
+ declare function formatAddress(a: ParsedAddress): string;
175
+
176
+ interface ConsoleOptions {
177
+ name?: string;
178
+ from?: ParsedAddress;
179
+ /** พิมพ์เนื้อหา html/text ด้วย (ค่าเริ่มต้น: แค่หัวอีเมล) */
180
+ body?: boolean;
181
+ log?: (line: string) => void;
182
+ }
183
+ /** ไม่ส่งจริง — พิมพ์ลง console (ใช้ตอน dev/test) */
184
+ declare function createConsoleProvider(opts?: ConsoleOptions): Provider;
185
+
186
+ /** Pi Notify hub ที่ deploy ไว้ — ฝังไว้เพื่อให้แอปใส่แค่ PI_NOTIFY_KEY */
187
+ declare const DEFAULT_HUB_URL = "https://notify.suphark.com";
188
+ interface HubOptions {
189
+ name?: string;
190
+ /** API key ของแอป (pn_...) ออกจากหน้า admin ของ Pi Notify */
191
+ apiKey: string;
192
+ /** override เมื่อรัน hub ที่อื่น (localhost, staging) */
193
+ url?: string;
194
+ from?: ParsedAddress;
195
+ fetch?: typeof fetch;
196
+ }
197
+ /** payload ที่ SDK ส่งให้ hub (POST /api/v1/send) — hub ใช้ schema เดียวกันตรวจ */
198
+ interface HubSendRequest {
199
+ from?: ParsedAddress;
200
+ to: ParsedAddress[];
201
+ cc?: ParsedAddress[];
202
+ bcc?: ParsedAddress[];
203
+ replyTo?: ParsedAddress[];
204
+ subject: string;
205
+ html?: string;
206
+ text?: string;
207
+ attachments?: {
208
+ filename: string;
209
+ base64: string;
210
+ contentType?: string;
211
+ }[];
212
+ headers?: Record<string, string>;
213
+ tags?: Record<string, string>;
214
+ /** ชื่อ provider ที่อยากให้ hub ลองก่อน */
215
+ prefer?: string;
216
+ }
217
+ interface HubSendResponse {
218
+ id?: string;
219
+ provider: string;
220
+ failedOver: boolean;
221
+ }
222
+ /** error body ของ hub: { error: { kind, message, attempts? } } */
223
+ interface HubErrorResponse {
224
+ error: {
225
+ kind?: ErrorKind;
226
+ message: string;
227
+ attempts?: unknown[];
228
+ };
229
+ }
230
+ /**
231
+ * ส่งผ่าน Pi Notify hub — hub เป็นคนถือ credential ของ Resend/SMTP/... และทำ failover เอง
232
+ * ถ้า hub ล่ม mailer ฝั่งแอปจะ failover ไป provider local ตัวถัดไป (ถ้าตั้งไว้)
233
+ */
234
+ declare function createHubProvider(opts: HubOptions): Provider;
235
+
236
+ interface ResendOptions {
237
+ name?: string;
238
+ apiKey: string;
239
+ from?: ParsedAddress;
240
+ /** ค่าเริ่มต้น https://api.resend.com */
241
+ baseUrl?: string;
242
+ fetch?: typeof fetch;
243
+ }
244
+ /** Resend (HTTP API) — https://resend.com/docs/api-reference/emails/send-email */
245
+ declare function createResendProvider(opts: ResendOptions): Provider;
246
+
247
+ interface SmtpOptions {
248
+ name?: string;
249
+ host: string;
250
+ /** ค่าเริ่มต้น 465 ถ้า secure, ไม่งั้น 587 */
251
+ port?: number;
252
+ /** true = TLS ทันที (465) · false = STARTTLS (587) · ไม่ระบุ = เดาจาก port */
253
+ secure?: boolean;
254
+ user?: string;
255
+ pass?: string;
256
+ from?: ParsedAddress;
257
+ /** ใช้ connection pool (เหมาะกับส่งเยอะ) */
258
+ pool?: boolean;
259
+ /** timeout ต่อการเชื่อมต่อ (ms) ค่าเริ่มต้น 15000 */
260
+ timeoutMs?: number;
261
+ /** ยอมรับ cert ที่ไม่ผ่านการตรวจ (self-signed) — ใช้เฉพาะ SMTP ภายในองค์กร */
262
+ rejectUnauthorized?: boolean;
263
+ /** ชื่อ type ที่รายงาน (smtp/zoho/gmail/...) */
264
+ type?: string;
265
+ }
266
+ /** SMTP ทั่วไป ผ่าน nodemailer — ใช้กับ SMTP บริษัท, Zoho, Gmail, Office 365 ฯลฯ */
267
+ declare function createSmtpProvider(opts: SmtpOptions): Provider;
268
+ /** preset ของผู้ให้บริการที่ใช้บ่อย — ใส่แค่ user/pass */
269
+ declare const SMTP_PRESETS: {
270
+ readonly zoho: {
271
+ readonly host: "smtp.zoho.com";
272
+ readonly port: 465;
273
+ readonly secure: true;
274
+ };
275
+ /** Zoho Mail แบบองค์กร (custom domain) — บาง region ต้องใช้ smtppro.zoho.com */
276
+ readonly zohopro: {
277
+ readonly host: "smtppro.zoho.com";
278
+ readonly port: 465;
279
+ readonly secure: true;
280
+ };
281
+ readonly gmail: {
282
+ readonly host: "smtp.gmail.com";
283
+ readonly port: 465;
284
+ readonly secure: true;
285
+ };
286
+ readonly office365: {
287
+ readonly host: "smtp.office365.com";
288
+ readonly port: 587;
289
+ readonly secure: false;
290
+ };
291
+ /** Brevo (Sendinblue) SMTP */
292
+ readonly brevo: {
293
+ readonly host: "smtp-relay.brevo.com";
294
+ readonly port: 587;
295
+ readonly secure: false;
296
+ };
297
+ /** Mailtrap sandbox (ทดสอบ) */
298
+ readonly mailtrap: {
299
+ readonly host: "sandbox.smtp.mailtrap.io";
300
+ readonly port: 2525;
301
+ readonly secure: false;
302
+ };
303
+ };
304
+ type SmtpPreset = keyof typeof SMTP_PRESETS;
305
+ declare function createPresetSmtpProvider(preset: SmtpPreset, opts: Omit<SmtpOptions, "host" | "type"> & {
306
+ host?: string;
307
+ }): Provider;
308
+
309
+ interface ZeptoMailOptions {
310
+ name?: string;
311
+ /** Send Mail token (ไม่ต้องใส่คำว่า Zoho-enczapikey) */
312
+ apiKey: string;
313
+ from?: ParsedAddress;
314
+ /** region ของบัญชี Zoho: com (default), eu, in, com.au, jp, ... หรือระบุ baseUrl เอง */
315
+ region?: string;
316
+ baseUrl?: string;
317
+ fetch?: typeof fetch;
318
+ }
319
+ /** Zoho ZeptoMail (transactional email API) — https://www.zoho.com/zeptomail/help/api/email-sending.html */
320
+ declare function createZeptoMailProvider(opts: ZeptoMailOptions): Provider;
321
+
322
+ interface Common {
323
+ /** ชื่อไว้อ้างอิงใน log/status (ค่าเริ่มต้น = type) */
324
+ name?: string;
325
+ /**
326
+ * ตัวตนผู้ส่งของ provider นี้ — ถ้า message.from ไม่อยู่ใน senderDomains จะใช้ที่อยู่นี้แทน (คงชื่อจาก message)
327
+ * ไม่ตั้ง = provider ส่งจากที่อยู่ไหนก็ได้ (hub, console) และใช้ from ของ message/mailer ตามลำดับ
328
+ */
329
+ from?: Address;
330
+ /**
331
+ * โดเมนที่ provider นี้ส่งได้ (เช่น Resend ที่ verify หลายโดเมน) — ค่าเริ่มต้น = ต้องตรงที่อยู่ของ from เป๊ะ
332
+ * (SMTP ส่วนใหญ่บังคับ From ให้ตรงบัญชีที่ล็อกอิน)
333
+ */
334
+ senderDomains?: string[];
335
+ /** เพดานที่เราตั้งเอง — ถึงแล้ว mailer จะข้ามไปตัวถัดไปโดยไม่ต้องรอ provider ตอบ error */
336
+ quota?: Quota;
337
+ /** ปิดชั่วคราวโดยไม่ต้องลบ config */
338
+ disabled?: boolean;
339
+ }
340
+ type Opt<T> = Omit<T, "name" | "from">;
341
+ type ProviderConfig = ({
342
+ type: "hub";
343
+ } & Common & Opt<HubOptions>) | ({
344
+ type: "resend";
345
+ } & Common & Opt<ResendOptions>) | ({
346
+ type: "smtp";
347
+ } & Common & Opt<SmtpOptions>) | ({
348
+ type: SmtpPreset;
349
+ } & Common & Opt<Omit<SmtpOptions, "host" | "type">> & {
350
+ host?: string;
351
+ }) | ({
352
+ type: "zeptomail";
353
+ } & Common & Opt<ZeptoMailOptions>) | ({
354
+ type: "console";
355
+ } & Common & Opt<ConsoleOptions>)
356
+ /** provider ที่เขียนเอง (implement interface Provider) */
357
+ | ({
358
+ type: "custom";
359
+ provider: Provider;
360
+ } & Common);
361
+
362
+ type Env = Record<string, string | undefined>;
363
+ /**
364
+ * อ่าน provider จาก env — รูปแบบ:
365
+ *
366
+ * MAIL_PROVIDERS=resend,company,zoho ← ลำดับ = ลำดับความสำคัญ
367
+ * MAIL_RESEND_API_KEY=re_xxx ← ชื่อที่ตรงกับ type ไม่ต้องใส่ _TYPE
368
+ * MAIL_COMPANY_TYPE=smtp ← ชื่ออื่นต้องบอก _TYPE (หรือมี _HOST → smtp)
369
+ * MAIL_COMPANY_HOST=... _PORT _SECURE _USER _PASS _FROM _QUOTA_DAY ...
370
+ *
371
+ * PI_NOTIFY_KEY=pn_xxx → ใส่ Pi Notify hub เป็นตัวแรกเสมอ (PI_NOTIFY_URL override ได้) ตัวอื่นเป็นสำรอง
372
+ * ไม่มี MAIL_PROVIDERS → เดาจาก PI_NOTIFY_KEY / RESEND_API_KEY / SMTP_HOST (zero-config)
373
+ */
374
+ declare function providersFromEnv(env?: Env): ProviderConfig[];
375
+
376
+ interface MailerOptions {
377
+ /** รายการ provider เรียงตามลำดับความสำคัญ — ไม่ใส่ = อ่านจาก env (ดู providersFromEnv) */
378
+ providers?: (ProviderConfig | Provider)[];
379
+ /** from เริ่มต้นเมื่อ message และ provider ไม่ได้ระบุ (env: MAIL_FROM) */
380
+ from?: Address;
381
+ /**
382
+ * priority (ค่าเริ่มต้น) = ใช้ตัวแรกเสมอ ตัวถัดไปเป็นสำรอง
383
+ * round-robin = หมุนเวียนทุกตัวที่พร้อมใช้ (กระจายโควต้า)
384
+ */
385
+ strategy?: "priority" | "round-robin";
386
+ /** ที่เก็บ state โควต้า/cooldown — ค่าเริ่มต้น memory; ใช้ createRedisStore เพื่อแชร์ข้ามแอป */
387
+ store?: Store;
388
+ /** เวลาพัก (วินาที) ต่อ error แต่ละแบบ (override DEFAULT_COOLDOWN บางค่าได้) */
389
+ cooldown?: Partial<Record<ErrorKind, number>>;
390
+ maxCooldownSec?: number;
391
+ /** timeout ต่อการส่ง 1 ครั้งต่อ provider (ms) ค่าเริ่มต้น 20000 */
392
+ timeoutMs?: number;
393
+ /** รับ event (sent/failed/cooldown/...) ไปต่อ logger ของแอป */
394
+ onEvent?: (e: MailerEvent) => void;
395
+ /** พิมพ์ event ลง console (env: MAIL_DEBUG=1) */
396
+ debug?: boolean;
397
+ /** env ที่ใช้อ่าน config (ค่าเริ่มต้น process.env) */
398
+ env?: Env;
399
+ }
400
+ interface Mailer {
401
+ /** ส่งอีเมล — ลองตาม provider ทีละตัวจนกว่าจะสำเร็จ; โยน AllProvidersFailedError ถ้าไม่สำเร็จเลย */
402
+ send(message: Message): Promise<SendResult>;
403
+ /** สถานะ provider ทุกตัว (cooldown, โควต้าที่ใช้ไป) */
404
+ status(): Promise<ProviderStatus[]>;
405
+ /** ตัวเลือกที่มีแบบย่อ (ชื่อ/ประเภท/พร้อมใช้) — ถ้ามี hub จะถามรายการข้างหลัง hub มาให้ด้วย */
406
+ listProviders(): Promise<ProviderInfo[]>;
407
+ /** ทดสอบการเชื่อมต่อของทุก provider (ไม่ส่งอีเมล) */
408
+ verify(): Promise<Record<string, {
409
+ ok: boolean;
410
+ error?: string;
411
+ }>>;
412
+ /** ล้าง cooldown ของ provider (ไม่ระบุ = ทุกตัว) */
413
+ reset(name?: string): Promise<void>;
414
+ /** ชื่อ provider ตามลำดับ */
415
+ readonly providers: string[];
416
+ }
417
+ declare function createMailer(options?: MailerOptions): Mailer;
418
+
419
+ /**
420
+ * ตั้งค่า mailer กลางของแอป (เรียกครั้งเดียวตอนบูต) — ไม่เรียกก็ได้ ถ้าใช้ env ล้วน
421
+ * @example configureMailer({ providers: [...], onEvent: (e) => logger.info(e) })
422
+ */
423
+ declare function configureMailer(options: MailerOptions): Mailer;
424
+ /** mailer กลาง (สร้างจาก env ครั้งแรกที่เรียก) */
425
+ declare function getMailer(): Mailer;
426
+ /**
427
+ * ทางลัดที่สั้นที่สุด — อ่าน config จาก env แล้วส่งเลย
428
+ * @example await sendMail({ to: "a@b.com", subject: "สวัสดี", html: "<p>...</p>" })
429
+ */
430
+ declare function sendMail(message: Message): Promise<SendResult>;
431
+ /**
432
+ * ตัวเลือกที่มี (ชื่อ/ประเภท/พร้อมใช้) — ใช้แสดงให้ผู้ใช้เลือกแล้วส่งด้วย { prefer: name }
433
+ * @example (await listProviders()) → [{ name: "resend", type: "resend", available: true, via: "hub" }, ...]
434
+ */
435
+ declare function listProviders(): Promise<ProviderInfo[]>;
436
+ /** ใช้ในเทสต์: ทิ้ง instance กลางเพื่อสร้างใหม่ด้วย env/options อื่น */
437
+ declare function resetMailer(): void;
438
+
439
+ declare class MailError extends Error {
440
+ readonly kind: ErrorKind;
441
+ readonly provider?: string;
442
+ readonly status?: number;
443
+ constructor(message: string, opts?: {
444
+ kind?: ErrorKind;
445
+ provider?: string;
446
+ status?: number;
447
+ cause?: unknown;
448
+ });
449
+ }
450
+ /** ส่งไม่สำเร็จเลยสักตัว — ดู attempts ว่าแต่ละตัวพังเพราะอะไร */
451
+ declare class AllProvidersFailedError extends MailError {
452
+ readonly attempts: Attempt[];
453
+ constructor(attempts: Attempt[]);
454
+ }
455
+ /** เดา ErrorKind จาก error ของ nodemailer / fetch / provider API (ไม่มีทางแม่น 100% แต่ครอบคลุมเคสหลัก) */
456
+ declare function classifyError(err: unknown): {
457
+ kind: ErrorKind;
458
+ message: string;
459
+ status?: number;
460
+ };
461
+
462
+ /** เวลาพัก (วินาที) ต่อ error แต่ละแบบ ก่อนกลับมาลอง provider นั้นอีก — คูณสองทุกครั้งที่พังซ้ำ */
463
+ declare const DEFAULT_COOLDOWN: Record<ErrorKind, number>;
464
+
465
+ /** ช่องทางแจ้งเตือนที่ hub รองรับ — ผู้รับผูกแชทเองที่ hub (/me) หรือผ่านลิงก์ที่แอปขอจาก hub */
466
+ type NotifyChannel = "telegram" | "line" | "email";
467
+ interface NotifyInput {
468
+ /** อีเมลผู้รับ (ตรงกับบัญชี Pi Auth) — ระบุได้สูงสุด 50 คน */
469
+ to: string | string[];
470
+ title: string;
471
+ /** ข้อความล้วน หรือ HTML จำกัดแท็ก (b/i/u/s/a/code/pre) — ระบุ format เมื่อไม่อยากให้ hub เดา */
472
+ body: string;
473
+ /** text = แสดงตามตัวอักษร (hub escape ให้), html = ตีความแท็ก; ไม่ระบุ = hub เดาจากเนื้อหา */
474
+ format?: "text" | "html";
475
+ /** ลิงก์ท้ายข้อความ (Telegram = ปุ่ม "เปิดลิงก์", อีเมล = ปุ่ม) */
476
+ link?: string;
477
+ /**
478
+ * ไม่ระบุ = ส่งทุกแชทที่ผู้รับผูกไว้; ไม่มีแชท → อีเมล
479
+ * ระบุ = เฉพาะที่ขอ; แชทที่ขอแต่ไม่ได้ผูก (และไม่ได้ขอ email) → skipped
480
+ */
481
+ channels?: NotifyChannel[];
482
+ /** ชื่อ provider ฝั่ง hub ที่อยากให้ลองก่อน */
483
+ prefer?: string;
484
+ }
485
+ interface NotifyResultItem {
486
+ to: string;
487
+ channel: NotifyChannel;
488
+ status: "sent" | "failed" | "skipped";
489
+ provider?: string;
490
+ id?: string;
491
+ failedOver?: boolean;
492
+ error?: string;
493
+ reason?: "no_link" | "channel_not_linked" | "no_chat_provider";
494
+ }
495
+ interface NotifyResult {
496
+ results: NotifyResultItem[];
497
+ sent: number;
498
+ failed: number;
499
+ skipped: number;
500
+ }
501
+ interface NotifyOptions {
502
+ /** ค่าเริ่มต้น PI_NOTIFY_KEY */
503
+ apiKey?: string;
504
+ /** ค่าเริ่มต้น PI_NOTIFY_URL หรือ https://notify.suphark.com */
505
+ url?: string;
506
+ fetch?: typeof fetch;
507
+ env?: Record<string, string | undefined>;
508
+ }
509
+ /** ช่องทางที่ผู้รับผูกไว้ (จาก GET /api/v1/recipients/:email) — ไม่มี chat id */
510
+ interface RecipientChannelInfo {
511
+ type: string;
512
+ label: string | null;
513
+ enabled: boolean;
514
+ disabledReason: string | null;
515
+ linkedAt: string;
516
+ }
517
+ interface RecipientInfo {
518
+ email: string;
519
+ channels: RecipientChannelInfo[];
520
+ /** หน้า hub ให้ผู้รับผูกเอง */
521
+ linkUrl: string;
522
+ }
523
+ /**
524
+ * แจ้งเตือนผู้รับผ่าน Pi Notify hub — Telegram (ที่ผู้รับผูกไว้) หรืออีเมล
525
+ * ใช้ env เดียวกับ sendMail(): PI_NOTIFY_KEY (+ PI_NOTIFY_URL)
526
+ * โยน MailError เมื่อ hub ตอบ non-2xx (502 = ไม่มีช่องทางไหนส่งสำเร็จเลย; results อยู่ใน err.cause)
527
+ */
528
+ declare function notify(input: NotifyInput, opts?: NotifyOptions): Promise<NotifyResult>;
529
+ /** ช่องทางที่ผู้รับผูกไว้ — ไว้แสดง "ผูกแล้ว/ยังไม่ผูก" ในหน้าตั้งค่าของแอป */
530
+ declare function getRecipient(email: string, opts?: NotifyOptions): Promise<RecipientInfo>;
531
+ /**
532
+ * ขอลิงก์ผูก Telegram ให้ผู้รับ (t.me/<bot>?start=<code>, หมดอายุ 10 นาที)
533
+ * แอปวางเป็นปุ่ม/QR ในหน้าตั้งค่าของตัวเอง แล้วโพล getRecipient() จนเห็น telegram
534
+ */
535
+ declare function createLink(email: string, channel?: Exclude<NotifyChannel, "email">, opts?: NotifyOptions): Promise<{
536
+ channel: string;
537
+ url: string;
538
+ expiresAt: string;
539
+ }>;
540
+
541
+ /** Store ใน memory ของ process (ค่าเริ่มต้น) — state หายเมื่อ restart และไม่แชร์ข้าม instance */
542
+ declare function createMemoryStore(): Store;
543
+
544
+ /** client ขั้นต่ำที่ต้องมี — ioredis, node-redis (v4+) และ @upstash/redis ใช้ได้เลย */
545
+ interface RedisLike {
546
+ get(key: string): Promise<string | number | null>;
547
+ set(key: string, value: string): Promise<unknown>;
548
+ incr(key: string): Promise<number>;
549
+ decr(key: string): Promise<number>;
550
+ expire(key: string, seconds: number): Promise<unknown>;
551
+ del(key: string): Promise<unknown>;
552
+ }
553
+ /**
554
+ * Store บน Redis — ใช้เมื่อรันหลาย instance หรืออยากให้หลายแอปแชร์โควต้า/cooldown ของ provider เดียวกัน
555
+ *
556
+ * @example
557
+ * import Redis from "ioredis";
558
+ * createMailer({ store: createRedisStore(new Redis(process.env.REDIS_URL)) })
559
+ */
560
+ declare function createRedisStore(client: RedisLike, opts?: {
561
+ prefix?: string;
562
+ }): Store;
563
+
564
+ export { type Address, AllProvidersFailedError, type Attachment, type Attempt, type ConsoleOptions, DEFAULT_COOLDOWN, DEFAULT_HUB_URL, type Env, type ErrorKind, type HubErrorResponse, type HubOptions, type HubSendRequest, type HubSendResponse, MailError, type Mailer, type MailerEvent, type MailerOptions, type Message, type NormalizedMessage, type NotifyChannel, type NotifyInput, type NotifyOptions, type NotifyResult, type NotifyResultItem, type ParsedAddress, type Provider, type ProviderConfig, type ProviderInfo, type ProviderStatus, type Quota, type QuotaUsage, type RecipientChannelInfo, type RecipientInfo, type RedisLike, type ResendOptions, SMTP_PRESETS, type SendResult, type SkipReason, type SmtpOptions, type SmtpPreset, type Store, type ZeptoMailOptions, classifyError, configureMailer, createConsoleProvider, createHubProvider, createLink, createMailer, createMemoryStore, createPresetSmtpProvider, createRedisStore, createResendProvider, createSmtpProvider, createZeptoMailProvider, formatAddress, getMailer, getRecipient, listProviders, notify, parseAddress, parseAddressList, providersFromEnv, requireFrom, resetMailer, sendMail };