@suphark/notify 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,172 +1,5 @@
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
- * ยกเลิกเมื่อ mailer หมดเวลา (timeoutMs) — provider ที่ใช้ fetch ควรส่งต่อเข้า fetch
66
- * เพื่อไม่ให้คำขอค้างต่อหลัง mailer ไป failover แล้ว (ไม่ส่งมา = provider เขียนเองรุ่นเก่า ใช้ได้ตามเดิม)
67
- */
68
- signal?: AbortSignal;
69
- }
70
- /** ประเภทความผิดพลาด — ใช้ตัดสินใจว่าจะพัก provider นานแค่ไหน */
71
- type ErrorKind = "quota" | "rate_limit" | "auth" | "network" | "server" | "invalid" | "unknown";
72
- type SkipReason = "cooldown" | "quota" | "disabled";
73
- interface Attempt {
74
- provider: string;
75
- ok: boolean;
76
- /** ข้ามเพราะ circuit เปิด/โควต้าเต็ม (ไม่ได้ลองส่งจริง) */
77
- skipped?: SkipReason;
78
- error?: string;
79
- kind?: ErrorKind;
80
- ms: number;
81
- }
82
- interface SendResult {
83
- /** id จาก provider (ถ้ามี) */
84
- id?: string;
85
- /** ชื่อ provider ที่ส่งสำเร็จ */
86
- provider: string;
87
- /** true ถ้าตัวแรกที่ควรใช้ล้ม/ถูกข้าม แล้วไปสำเร็จที่ตัวสำรอง */
88
- failedOver: boolean;
89
- attempts: Attempt[];
90
- }
91
- interface Quota {
92
- perHour?: number;
93
- perDay?: number;
94
- perMonth?: number;
95
- }
96
- /** สิ่งที่ provider ต้อง implement — เท่านี้พอ ที่เหลือ mailer จัดการ */
97
- interface Provider {
98
- readonly name: string;
99
- readonly type: string;
100
- /** from เริ่มต้นของ provider นี้ (ชนะ from ของ mailer) */
101
- readonly from?: ParsedAddress;
102
- /** true = ส่งได้แม้ไม่มี from (provider ปลายทางเติมค่าเริ่มต้นให้ เช่น hub) */
103
- readonly optionalFrom?: boolean;
104
- send(msg: NormalizedMessage): Promise<{
105
- id?: string;
106
- }>;
107
- /** ทดสอบการเชื่อมต่อ/credential (ไม่ส่งอีเมล) */
108
- verify?(): Promise<void>;
109
- /** provider ตัวนี้เป็นทางผ่านไปยังตัวอื่น (hub) — คืนรายการข้างหลัง */
110
- listProviders?(): Promise<ProviderInfo[]>;
111
- }
112
- /**
113
- * state ที่ mailer ต้องจำข้ามคำขอ (นับโควต้า, cooldown)
114
- * default เก็บใน memory ของ process; ต่อ Redis ได้เพื่อแชร์ข้ามแอป/instance (ดู createRedisStore)
115
- */
116
- interface Store {
117
- get(key: string): Promise<string | null>;
118
- /** ตั้งค่า พร้อมอายุ (วินาที) */
119
- set(key: string, value: string, ttlSec: number): Promise<void>;
120
- /** เพิ่มค่าทีละ 1 (สร้างเป็น 1 ถ้ายังไม่มี) พร้อมอายุ (วินาที) คืนค่าใหม่ */
121
- incr(key: string, ttlSec: number): Promise<number>;
122
- /** ลดค่าทีละ 1 (ไม่ต่ำกว่า 0) — คืนโควต้าที่จองไว้เมื่อส่งไม่สำเร็จ; store ที่ไม่ implement จะนับความล้มเหลวเป็นโควต้าด้วย */
123
- decr?(key: string): Promise<void>;
124
- del(key: string): Promise<void>;
125
- }
126
- type MailerEvent = {
127
- type: "sent";
128
- provider: string;
129
- id?: string;
130
- ms: number;
131
- failedOver: boolean;
132
- } | {
133
- type: "failed";
134
- provider: string;
135
- kind: ErrorKind;
136
- error: string;
137
- ms: number;
138
- } | {
139
- type: "skipped";
140
- provider: string;
141
- reason: SkipReason;
142
- } | {
143
- type: "cooldown";
144
- provider: string;
145
- kind: ErrorKind;
146
- untilMs: number;
147
- failures: number;
148
- } | {
149
- type: "recovered";
150
- provider: string;
151
- } | {
152
- type: "exhausted";
153
- attempts: Attempt[];
154
- };
155
- interface QuotaUsage {
156
- window: "hour" | "day" | "month";
157
- used: number;
158
- limit: number;
159
- }
160
- interface ProviderStatus {
161
- name: string;
162
- type: string;
163
- /** พร้อมใช้ (ไม่อยู่ใน cooldown และโควต้ายังไม่เต็ม) */
164
- available: boolean;
165
- cooldownUntil?: number;
166
- cooldownKind?: ErrorKind;
167
- failures: number;
168
- quota: QuotaUsage[];
169
- }
1
+ import { P as ParsedAddress, A as Address, a as Provider, E as ErrorKind, Q as Quota, M as Message, S as SendResult, b as ProviderStatus, c as ProviderInfo, d as Store, e as MailerEvent, f as Attempt, g as PushDevice, N as NotifyOptions, h as NotifyResultItem, i as NotifyInput } from './notify-L7JnpG0t.js';
2
+ export { j as Attachment, C as ChannelStatus, k as NOTIFY_TIMEOUT_MS, l as NormalizedMessage, m as NotifyChannel, n as NotifyResult, o as PushSubscriptionJson, p as QuotaUsage, R as Recipient, q as RecipientChannelInfo, r as RecipientInfo, s as SkipReason, t as createLink, u as deletePushSubscription, v as getChannelStatus, w as getPushConfig, x as getRecipient, y as hasNotifyKey, z as listPushDevices, B as notify, D as savePushSubscription, F as toEmails } from './notify-L7JnpG0t.js';
170
3
 
171
4
  /** แปลง "ชื่อ <a@b.com>" / "a@b.com" / { name, email } → { name, email } */
172
5
  declare function parseAddress(input: Address): ParsedAddress;
@@ -469,150 +302,133 @@ declare function classifyError(err: unknown): {
469
302
  retryAfterSec?: number;
470
303
  };
471
304
 
305
+ /**
306
+ * รูปแบบของ body ที่ใช้ร่วมทุกช่องทาง (อีเมล, Telegram, LINE ที่จะมาทีหลัง, push)
307
+ * ช่องทางที่ไม่รองรับ HTML ใช้ `toPlainText` ตัวเดียวกัน — แหล่งความจริงเดียวของ Pi Notify (hub import จากแพ็กเกจนี้)
308
+ */
309
+ type BodyFormat = "text" | "html";
310
+ /**
311
+ * ดูว่าข้อความน่าจะเป็น HTML หรือข้อความล้วน — นับเฉพาะแท็กที่รู้จัก
312
+ * ("บัตร <KBank>" หรือ "a < b" ถือเป็นข้อความล้วน; แอปที่แน่ใจให้ส่ง format มาแทนการเดา)
313
+ */
314
+ declare function looksLikeHtml(s: string): boolean;
315
+ declare function resolveFormat(body: string, format?: string): BodyFormat;
316
+ /** ตัดแท็กทั้งหมดเป็นข้อความล้วน (Telegram ใช้ส่งซ้ำเมื่อ parse HTML ไม่ผ่าน; LINE/อีเมล/push ใช้เป็นเนื้อหลัก) */
317
+ declare function toPlainText(html: string): string;
318
+
472
319
  /** เวลาพัก (วินาที) ต่อ error แต่ละแบบ ก่อนกลับมาลอง provider นั้นอีก — คูณสองทุกครั้งที่พังซ้ำ */
473
320
  declare const DEFAULT_COOLDOWN: Record<ErrorKind, number>;
474
321
 
322
+ /** ผู้ใช้ที่ล็อกอินอยู่ในแอป — **ต้องมาจาก session ฝั่ง server เท่านั้น** ห้ามรับอีเมลจาก client */
323
+ interface PushUser {
324
+ email: string;
325
+ /** user id ใน Pi Auth (sub) — ให้ Pi Notify ลบ subscription ตามบัญชีที่ถูกลบ */
326
+ id?: string;
327
+ }
328
+ /** รูปที่ `PushSubscription.toJSON()` ของเบราว์เซอร์ส่งมา (field optional ตาม DOM type) */
329
+ interface BrowserPushSubscription {
330
+ endpoint?: string;
331
+ keys?: Record<string, string | undefined>;
332
+ }
333
+ interface PushHandlersOptions extends NotifyOptions {
334
+ /** ดึงผู้ใช้จาก session ของแอป — คืน null/undefined = ยังไม่ล็อกอิน */
335
+ getUser: () => PushUser | null | undefined | Promise<PushUser | null | undefined>;
336
+ }
337
+ interface PushHandlers {
338
+ /** VAPID public key สำหรับ `pushManager.subscribe({ applicationServerKey })` */
339
+ getPublicKey(): Promise<string>;
340
+ subscribe(subscription: BrowserPushSubscription, opts?: {
341
+ label?: string;
342
+ }): Promise<{
343
+ ok: true;
344
+ created: boolean;
345
+ }>;
346
+ unsubscribe(endpoint: string): Promise<{
347
+ ok: true;
348
+ deleted: number;
349
+ }>;
350
+ listDevices(): Promise<{
351
+ devices: PushDevice[];
352
+ }>;
353
+ }
354
+ /**
355
+ * ชุดฟังก์ชันสำหรับ Server Actions ของ `<WebPushRow />` — แอปส่งแค่ `getUser`
356
+ * (ใน `"use server"` ห่อเป็น `export async function` ทีละตัว; ห้าม import SDK ใน Client Component)
357
+ */
358
+ declare function createPushHandlers(options: PushHandlersOptions): PushHandlers;
359
+ interface PushServiceWorkerOptions {
360
+ /** ไอคอนของการแจ้งเตือน (path ในแอป) ค่าเริ่มต้น /icon-192.png */
361
+ icon?: string;
362
+ /** ลิงก์ที่เปิดเมื่อ payload ไม่มี link ค่าเริ่มต้น / */
363
+ defaultLink?: string;
364
+ }
365
+ /** ซอร์ส service worker ที่รับ push ของ Pi Notify (payload { title, body, link? }) — เปิดเฉพาะลิงก์ใน origin ของแอป */
366
+ declare function pushServiceWorkerSource(options?: PushServiceWorkerOptions): string;
475
367
  /**
476
- * ช่องทางแจ้งเตือนที่ hub รองรับ — ผู้รับผูกแชทเองที่ hub (/me) หรือผ่านลิงก์ที่แอปขอจาก hub
477
- * `push` = web push ของเบราว์เซอร์ ผูกต่อแอป (service worker อยู่ที่โดเมนของแอปเอง)
368
+ * Route Handler ที่เสิร์ฟ service worker จากตัวแพ็กเกจเอง (same-origin, ล็อกเวอร์ชันผ่าน npm)
369
+ * @example // app/sw.js/route.ts
370
+ * export const GET = pushServiceWorker();
371
+ * ไม่แนะนำให้ importScripts จากโดเมนอื่น: service worker คุมทั้ง origin ของแอปและดักทุก request
478
372
  */
479
- type NotifyChannel = "telegram" | "line" | "push" | "email";
480
- interface NotifyInput {
481
- /** อีเมลผู้รับ (ตรงกับบัญชี Pi Auth) — ระบุได้สูงสุด 50 คน */
482
- to: string | string[];
373
+ declare function pushServiceWorker(options?: PushServiceWorkerOptions): () => Response;
374
+
375
+ interface NotifyEmailInput {
483
376
  title: string;
484
- /** ข้อความล้วน หรือ HTML จำกัดแท็ก (b/i/u/s/a/code/pre) — ระบุ format เมื่อไม่อยากให้ hub เดา */
485
377
  body: string;
486
- /** text = แสดงตามตัวอักษร (hub escape ให้), html = ตีความแท็ก; ไม่ระบุ = hub เดาจากเนื้อหา */
487
- format?: "text" | "html";
488
- /** ลิงก์ท้ายข้อความ (Telegram = ปุ่ม "เปิดลิงก์", อีเมล = ปุ่ม) */
378
+ /** ไม่ระบุ = เดาจากเนื้อหา (นับเฉพาะแท็กที่รู้จัก) */
379
+ format?: "text" | "html" | (string & {});
489
380
  link?: string;
490
- /**
491
- * ไม่ระบุ = ส่งทุกแชทที่ผู้รับผูกไว้; ไม่มีแชท → อีเมล
492
- * ระบุ = เฉพาะที่ขอ; แชทที่ขอแต่ไม่ได้ผูก (และไม่ได้ขอ email) → skipped
493
- */
494
- channels?: NotifyChannel[];
495
- /** ชื่อ provider ฝั่ง hub ที่อยากให้ลองก่อน */
496
- prefer?: string;
381
+ /** ข้อความบนปุ่ม/หน้าลิงก์ในอีเมล ค่าเริ่มต้น "เปิดลิงก์" */
382
+ linkLabel?: string;
383
+ /** ชื่อแอป แสดงท้ายอีเมล */
384
+ appName?: string;
385
+ /** สีปุ่ม (hex เท่านั้น — ค่าอื่นถูกเมินกัน CSS injection) ค่าเริ่มต้น #1d4ed8 */
386
+ accent?: string;
497
387
  }
498
- interface NotifyResultItem {
499
- to: string;
500
- channel: NotifyChannel;
501
- status: "sent" | "failed" | "skipped";
502
- provider?: string;
503
- id?: string;
504
- failedOver?: boolean;
505
- error?: string;
506
- /**
507
- * app_daily_cap = แอปส่งอีเมลแทนแชทครบเพดานรายวันที่ตั้งใน Pi Notify แล้ว
508
- * no_push_provider = hub ยังไม่ได้ตั้ง provider web push
509
- */
510
- reason?: "no_link" | "channel_not_linked" | "no_chat_provider" | "no_push_provider" | "app_daily_cap";
511
- /** (status failed) คำขอหมดงบเวลาก่อนได้เริ่มส่งรายการนี้ — ส่งซ้ำเฉพาะรายการนี้ได้ */
512
- deferred?: boolean;
513
- }
514
- interface NotifyResult {
515
- results: NotifyResultItem[];
388
+ /**
389
+ * แปลงแจ้งเตือน (title/body/link) เป็นอีเมล — layout เดียวกับที่ Pi Notify ใช้ส่งแทนแชท (หัวข้อ ย่อหน้า ปุ่มเดียว)
390
+ * ใช้คู่ sendMail เมื่อแอปอยากได้หน้าตาเดียวกันโดยไม่ต้องเขียน HTML เอง:
391
+ * `sendMail({ to, ...renderNotifyEmail({ title, body, link }) })` — body เป็นข้อความล้วนเสมอ ถูก escape ให้
392
+ */
393
+ declare function renderNotifyEmail(input: NotifyEmailInput): {
394
+ subject: string;
395
+ text: string;
396
+ html: string;
397
+ };
398
+
399
+ interface NotifySafeResult {
400
+ /** ไม่มี error และไม่มีรายการ failed — skipped (เช่นยังไม่ผูกช่องทาง) ไม่นับเป็นความล้มเหลว */
401
+ ok: boolean;
516
402
  sent: number;
517
403
  failed: number;
518
404
  skipped: number;
519
- /** จำนวนที่ยังไม่ได้ส่งเพราะหมดงบเวลา (นับซ้อนอยู่ใน failed; Pi Notify รุ่นเก่าไม่ส่งมา) */
520
405
  deferred?: number;
406
+ /** ผลรายผู้รับ/ช่องทาง (มีแม้ Pi Notify ตอบ 502 — ดึงจาก body) */
407
+ results: NotifyResultItem[];
408
+ /** ข้อความ error (ไม่มีอีเมลผู้รับ/เนื้อหา) */
409
+ error?: string;
410
+ /** ไม่มี PI_NOTIFY_KEY — แอปตั้งใจปิดแจ้งเตือน (dev/test) จึงไม่ส่งและไม่ถือเป็น error */
411
+ disabled?: boolean;
521
412
  }
522
- interface NotifyOptions {
523
- /** ค่าเริ่มต้น PI_NOTIFY_KEY */
524
- apiKey?: string;
525
- /** ค่าเริ่มต้น PI_NOTIFY_URL หรือ https://notify.suphark.com */
526
- url?: string;
527
- fetch?: typeof fetch;
528
- env?: Record<string, string | undefined>;
529
- /**
530
- * เวลารอ Pi Notify สูงสุด (ms) หมดแล้วโยน MailError kind "network"
531
- * ค่าเริ่มต้น notify() 65000 (ยาวกว่า maxDuration 60 วิของ /api/v1/notify เล็กน้อย) · getRecipient()/createLink() 10000
532
- */
533
- timeoutMs?: number;
534
- }
535
- /** เวลารอเริ่มต้นของแต่ละคำสั่ง (ms) */
536
- declare const NOTIFY_TIMEOUT_MS: {
537
- readonly notify: 65000;
538
- readonly recipient: 10000;
539
- };
540
- /** subscription จากเบราว์เซอร์ (`PushSubscription.toJSON()`) */
541
- interface PushSubscriptionJson {
542
- endpoint: string;
543
- keys: {
544
- p256dh: string;
545
- auth: string;
546
- };
547
- }
548
- interface PushDevice {
549
- id: string;
550
- label: string | null;
551
- enabled: boolean;
552
- /** ท่อนท้ายของ endpoint ไว้แยกเครื่อง (ไม่คืนค่าเต็ม) */
553
- endpointTail: string;
554
- }
555
- /** ช่องทางที่ผู้รับผูกไว้ (จาก GET /api/v1/recipients/:email) — ไม่มี chat id */
556
- interface RecipientChannelInfo {
557
- type: string;
558
- label: string | null;
559
- enabled: boolean;
560
- disabledReason: string | null;
561
- linkedAt: string;
562
- }
563
- interface RecipientInfo {
564
- email: string;
565
- channels: RecipientChannelInfo[];
566
- /** หน้า hub ให้ผู้รับผูกเอง */
567
- linkUrl: string;
568
- }
569
- /**
570
- * แจ้งเตือนผู้รับผ่าน Pi Notify hub — Telegram (ที่ผู้รับผูกไว้) หรืออีเมล
571
- * ใช้ env เดียวกับ sendMail(): PI_NOTIFY_KEY (+ PI_NOTIFY_URL)
572
- * โยน MailError เมื่อ hub ตอบ non-2xx (502 = ไม่มีช่องทางไหนส่งสำเร็จเลย; results อยู่ใน err.cause)
573
- */
574
- declare function notify(input: NotifyInput, opts?: NotifyOptions): Promise<NotifyResult>;
575
- /** ช่องทางที่ผู้รับผูกไว้ — ไว้แสดง "ผูกแล้ว/ยังไม่ผูก" ในหน้าตั้งค่าของแอป */
576
- declare function getRecipient(email: string, opts?: NotifyOptions): Promise<RecipientInfo>;
577
- /**
578
- * ขอลิงก์ผูก Telegram ให้ผู้รับ (t.me/<bot>?start=<code>, หมดอายุ 10 นาที)
579
- * แอปวางเป็นปุ่ม/QR ในหน้าตั้งค่าของตัวเอง แล้วโพล getRecipient() จนเห็น telegram
580
- * `opts.piUserId` = user id ใน Pi Auth (sub) ของผู้ใช้ — ให้ Pi Notify ลบการผูกเมื่อบัญชีถูกลบ
581
- */
582
- declare function createLink(email: string, channel?: Exclude<NotifyChannel, "email">, opts?: NotifyOptions & {
583
- piUserId?: string;
584
- }): Promise<{
585
- channel: string;
586
- url: string;
587
- expiresAt: string;
588
- }>;
589
- /**
590
- * VAPID public key ของ hub — ใช้เป็น `applicationServerKey` ตอน `pushManager.subscribe()`
591
- * (ค่าสาธารณะ แคชฝั่งแอปได้) · hub ยังไม่ได้ตั้ง provider push → MailError kind "server"
592
- */
593
- declare function getPushConfig(opts?: NotifyOptions): Promise<{
594
- publicKey: string;
595
- }>;
413
+ /** ใช้ในเทสต์: ให้คำเตือน "ปิดอยู่" ครั้งเดียวของโหมด dev/test ออกมาอีกรอบ */
414
+ declare function resetSafeWarnings(): void;
596
415
  /**
597
- * เก็บ subscription ของเบราว์เซอร์หนึ่งเครื่องไว้ที่ hub (ผูกกับแอปที่เรียกเสมอ)
598
- * เรียกซ้ำด้วย endpoint เดิมได้ = อัปเดตกุญแจ/เปิดใช้คืน · `piUserId` ให้ hub ลบตามบัญชีที่ถูกลบได้
416
+ * notify() ที่ไม่โยนเด็ดขาด — ไว้ใช้ตรงที่การแจ้งเตือนเป็นแค่ผลพลอยได้ของงานหลัก (best-effort)
417
+ * ต้องรู้ผลแน่ชัดหรืออยากให้ cron ลองใหม่ → ใช้ notify() ที่โยน
418
+ * ไม่มี PI_NOTIFY_KEY → { ok: false, disabled: true } ไม่ส่ง
599
419
  */
600
- declare function savePushSubscription(email: string, subscription: PushSubscriptionJson, opts?: NotifyOptions & {
601
- label?: string;
602
- piUserId?: string;
603
- }): Promise<{
604
- ok: true;
605
- created: boolean;
606
- }>;
607
- /** ผู้ใช้ปิดแจ้งเตือนที่เครื่องนั้น (ลบได้เฉพาะ subscription ของแอปตัวเอง) */
608
- declare function deletePushSubscription(email: string, endpoint: string, opts?: NotifyOptions): Promise<{
609
- ok: true;
610
- deleted: number;
611
- }>;
612
- /** เครื่องที่ผู้รับเปิดแจ้งเตือนไว้กับแอปนี้ — ไว้แสดงรายการในหน้าตั้งค่า */
613
- declare function listPushDevices(email: string, opts?: NotifyOptions): Promise<{
614
- devices: PushDevice[];
615
- }>;
420
+ declare function notifySafe(input: NotifyInput, opts?: NotifyOptions): Promise<NotifySafeResult>;
421
+ interface SendMailSafeResult {
422
+ ok: boolean;
423
+ provider?: string;
424
+ id?: string;
425
+ failedOver?: boolean;
426
+ error?: string;
427
+ /** ไม่มี provider ให้ส่งเลย (ไม่ได้ตั้ง PI_NOTIFY_KEY หรือ provider อื่น) — แอปตั้งใจปิดอีเมล (dev/test) */
428
+ disabled?: boolean;
429
+ }
430
+ /** sendMail() ที่ไม่โยนเด็ดขาด — ไม่มี provider เลย → { ok: false, disabled: true } */
431
+ declare function sendMailSafe(message: Message): Promise<SendMailSafeResult>;
616
432
 
617
433
  /** Store ใน memory ของ process (ค่าเริ่มต้น) — state หายเมื่อ restart และไม่แชร์ข้าม instance */
618
434
  declare function createMemoryStore(): Store;
@@ -637,4 +453,4 @@ declare function createRedisStore(client: RedisLike, opts?: {
637
453
  prefix?: string;
638
454
  }): Store;
639
455
 
640
- 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, NOTIFY_TIMEOUT_MS, type NormalizedMessage, type NotifyChannel, type NotifyInput, type NotifyOptions, type NotifyResult, type NotifyResultItem, type ParsedAddress, type Provider, type ProviderConfig, type ProviderInfo, type ProviderStatus, type PushDevice, type PushSubscriptionJson, 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, deletePushSubscription, formatAddress, getMailer, getPushConfig, getRecipient, listProviders, listPushDevices, notify, parseAddress, parseAddressList, providersFromEnv, requireFrom, resetMailer, savePushSubscription, sendMail };
456
+ export { Address, AllProvidersFailedError, Attempt, type BodyFormat, type BrowserPushSubscription, type ConsoleOptions, DEFAULT_COOLDOWN, DEFAULT_HUB_URL, type Env, ErrorKind, type HubErrorResponse, type HubOptions, type HubSendRequest, type HubSendResponse, MailError, type Mailer, MailerEvent, type MailerOptions, Message, type NotifyEmailInput, NotifyInput, NotifyOptions, NotifyResultItem, type NotifySafeResult, ParsedAddress, Provider, type ProviderConfig, ProviderInfo, ProviderStatus, PushDevice, type PushHandlers, type PushHandlersOptions, type PushServiceWorkerOptions, type PushUser, Quota, type RedisLike, type ResendOptions, SMTP_PRESETS, type SendMailSafeResult, SendResult, type SmtpOptions, type SmtpPreset, Store, type ZeptoMailOptions, classifyError, configureMailer, createConsoleProvider, createHubProvider, createMailer, createMemoryStore, createPresetSmtpProvider, createPushHandlers, createRedisStore, createResendProvider, createSmtpProvider, createZeptoMailProvider, formatAddress, getMailer, listProviders, looksLikeHtml, notifySafe, parseAddress, parseAddressList, providersFromEnv, pushServiceWorker, pushServiceWorkerSource, renderNotifyEmail, requireFrom, resetMailer, resetSafeWarnings, resolveFormat, sendMail, sendMailSafe, toPlainText };