@suphark/notify 0.6.0 → 0.7.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @suphark/notify
2
2
 
3
- SDK ส่งอีเมล + แจ้งเตือน (Telegram / Web Push) ของแอปทุกตัว — ค่าเริ่มต้นส่งผ่าน **Pi Notify hub** (`https://notify.suphark.com`) ซึ่งเป็นคนถือ credential ของ Resend/SMTP/Zoho/Telegram bot ทำ failover นับโควต้า เก็บ log และเก็บว่าผู้รับผูกแชทไหนไว้ แอปใส่แค่ key ตัวเดียว
3
+ SDK ส่งอีเมล + แจ้งเตือน (Telegram / LINE / Web Push) ของแอปทุกตัว — ค่าเริ่มต้นส่งผ่าน **Pi Notify hub** (`https://notify.suphark.com`) ซึ่งเป็นคนถือ credential ของ Resend/SMTP/Zoho/Telegram bot/LINE OA ทำ failover นับโควต้า เก็บ log และเก็บว่าผู้รับผูกแชทไหนไว้ แอปใส่แค่ key ตัวเดียว
4
4
 
5
5
  > 📖 **คู่มือการใช้งานและสูตรการเขียนโค้ดฉบับเต็ม:** ดูที่ [docs/guide.md](../../docs/guide.md)
6
6
 
@@ -94,6 +94,43 @@ const { url, code } = await createLink(user); // รับ use
94
94
  const { linked, label, manageUrl } = await getChannelStatus(user, "telegram");
95
95
  ```
96
96
 
97
+ ### LINE (0.7.0)
98
+
99
+ ใช้เหมือน Telegram ทุกอย่าง — ผู้รับผูก LINE ของตัวเองครั้งเดียว (ที่ `/me` ของ hub หรือปุ่มในแอป) แล้วส่งด้วยอีเมล:
100
+
101
+ ```ts
102
+ const { url, code } = await createLink(user, "line");
103
+ // url เปิดแชทกับ OA พร้อมพิมพ์ "ผูก Pi Notify <code>" ไว้ — ผู้ใช้กดส่ง (ยังไม่เป็นเพื่อนกับ OA ให้เพิ่มเพื่อนด้วย)
104
+ const { linked } = await getChannelStatus(user, "line");
105
+ await notify({ to: user, title: "PO รออนุมัติ", body: "…", link, channels: ["line"] });
106
+ ```
107
+
108
+ - ข้อความ LINE เป็น text ล้วน: หัวเรื่อง / เนื้อหา / ลิงก์บรรทัดสุดท้าย (กดได้) — body ที่เป็น HTML ถูกแปลงเป็นข้อความ, ยาวเกิน 5,000 ตัดเนื้อหาแต่คงลิงก์
109
+ - ผู้ใช้บล็อก OA → hub ปิดการผูกให้เอง (ปลดบล็อกแล้วกลับมาเอง) · พิมพ์ "หยุดแจ้งเตือน" ในแชท OA = เลิกรับ
110
+
111
+ ### ส่งเข้ากลุ่ม: `notifyTopic()` (0.7.0)
112
+
113
+ แอปไม่ต้องถือ group id — admin เชิญ OA/bot เข้ากลุ่ม LINE/Telegram (hub เห็นกลุ่มเองจาก webhook) แล้วผูกกลุ่มกับ **topic ของแอป** ที่หน้า Topics ของ Pi Notify
114
+
115
+ ```ts
116
+ import { notifyTopic } from "@suphark/notify";
117
+
118
+ const r = await notifyTopic({
119
+ topic: "po-overdue", // topic ของแอปนี้ (a-z 0-9 . _ -)
120
+ title: "PO เกินกำหนด 3 รายการ",
121
+ body: "PO-2611-001, …",
122
+ link: "https://app.example.com/po?status=overdue",
123
+ deliverWithin: 3600, // ใช้ได้เหมือน notify()
124
+ idempotencyKey: `po-overdue:${date}`,
125
+ });
126
+ // r.results = [{ to: "topic:po-overdue", channel: "line", status: "sent", target: "ทีมจัดซื้อ" }, …] (รายกลุ่ม)
127
+ ```
128
+
129
+ - topic ไม่พบ → `MailError` `invalid` (404) · topic ถูกปิด/ยังไม่ผูกกลุ่ม → `status: "skipped", reason: "no_target"` (ไม่โยน)
130
+ - `channels: ["line"]` = เฉพาะกลุ่ม LINE ของ topic · ไม่ระบุ = ทุกกลุ่ม
131
+ - แบบไม่โยน/เบื้องหลัง: `notifyTopicSafe()` และ `notifyTopicLater()` จาก `@suphark/notify/next`
132
+ - LINE คิดโควต้าตามข้อความ push — ยอดของเดือนดูที่หน้า Providers ของ Pi Notify
133
+
97
134
  ### ให้ hub ส่งซ้ำเอง: `deliverWithin` + `idempotencyKey` (0.6.0)
98
135
 
99
136
  ```ts
@@ -391,6 +428,11 @@ Pi Notify มี contract test (`apps/notify/src/test/contract/`) เก็บ b
391
428
 
392
429
  ## Changelog
393
430
 
431
+ - 0.7.0 — **LINE + ส่งเข้ากลุ่มด้วย topic** (เพิ่มอย่างเดียว; ต้องใช้ Pi Notify 2026-11-11+)
432
+ - `createLink(email, "line")` / `getChannelStatus(email, "line")` / `notify({ channels: ["line"] })` — ช่องทาง LINE รายคน
433
+ - `notifyTopic({ topic, title, body, … })` + `notifyTopicSafe()` + `notifyTopicLater()` (`/next`) — ส่งเข้าทุกกลุ่ม LINE/Telegram ที่ admin ผูกกับ topic ของแอป
434
+ - `NotifyResultItem.target?` (ชื่อกลุ่ม) และ `reason` เพิ่ม `"no_target"` (ออกเป็น minor เพราะ union ของ `reason` ที่แอปอาจ switch)
435
+
394
436
  - 0.6.0 — **hub ส่งซ้ำเอง** (เพิ่มอย่างเดียว — แอปที่ไม่ส่งตัวเลือกใหม่ได้พฤติกรรมเดิมทุกอย่าง; ต้องใช้ Pi Notify 2026-10-14+)
395
437
  - `notify({ deliverWithin })` (วินาที 60–86400): รายการที่ค้างหรือพลาดชั่วคราวเข้าคิวถาวรของ hub แล้วส่งซ้ำตาม backoff (1 นาที → 5 นาที → 30 นาที → 2 ชม., สูงสุด 5 ครั้ง) · ผิดถาวรไม่ส่งซ้ำ · เลยเวลาแล้วไม่ส่ง · ไม่สลับช่องทาง
396
438
  - `notify({ idempotencyKey })` → หัว `Idempotency-Key`: key+เนื้อหาเดิมได้ผลเดิม ไม่ส่งซ้ำ (จำ 7 วัน) · เนื้อหาต่างแต่ key เดิม = `invalid` (422) · คำขอเดิมยังทำอยู่ = `rate_limit` (409, `retryAfterSec` 5)
@@ -1128,16 +1128,23 @@ async function hubRequest(opts, path, init) {
1128
1128
  }
1129
1129
  return data;
1130
1130
  }
1131
- async function notify(input, opts = {}) {
1132
- const { idempotencyKey, ...body } = input;
1131
+ async function postNotify(body, idempotencyKey, opts) {
1133
1132
  const data = await hubRequest(opts, "/api/v1/notify", {
1134
1133
  method: "POST",
1135
- body: { ...body, to: toEmails(input.to) },
1134
+ body,
1136
1135
  headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
1137
1136
  timeoutMs: NOTIFY_TIMEOUT_MS.notify
1138
1137
  });
1139
1138
  return { results: data.results, ...data.summary };
1140
1139
  }
1140
+ async function notify(input, opts = {}) {
1141
+ const { idempotencyKey, ...body } = input;
1142
+ return postNotify({ ...body, to: toEmails(input.to) }, idempotencyKey, opts);
1143
+ }
1144
+ async function notifyTopic(input, opts = {}) {
1145
+ const { idempotencyKey, ...body } = input;
1146
+ return postNotify(body, idempotencyKey, opts);
1147
+ }
1141
1148
  async function getRecipient(email, opts = {}) {
1142
1149
  return hubRequest(
1143
1150
  opts,
@@ -1216,13 +1223,19 @@ function resetSafeWarnings() {
1216
1223
  }
1217
1224
  var messageOf = (err) => err instanceof Error ? err.message : String(err);
1218
1225
  async function notifySafe(input, opts = {}) {
1226
+ return safely("notify()", () => notify(input, opts), opts);
1227
+ }
1228
+ async function notifyTopicSafe(input, opts = {}) {
1229
+ return safely("notifyTopic()", () => notifyTopic(input, opts), opts);
1230
+ }
1231
+ async function safely(what, run, opts) {
1219
1232
  const none = { sent: 0, failed: 0, skipped: 0, results: [] };
1220
1233
  if (!hasNotifyKey(opts)) {
1221
- warnDisabled("notify()", opts.env ?? process.env);
1234
+ warnDisabled(what, opts.env ?? process.env);
1222
1235
  return { ok: false, disabled: true, ...none };
1223
1236
  }
1224
1237
  try {
1225
- const r = await notify(input, opts);
1238
+ const r = await run();
1226
1239
  return { ok: r.failed === 0, ...r };
1227
1240
  } catch (err) {
1228
1241
  const body = err instanceof MailError ? err.cause : void 0;
@@ -1280,6 +1293,7 @@ export {
1280
1293
  NOTIFY_TIMEOUT_MS,
1281
1294
  hasNotifyKey,
1282
1295
  notify,
1296
+ notifyTopic,
1283
1297
  getRecipient,
1284
1298
  createLink,
1285
1299
  getChannelStatus,
@@ -1289,5 +1303,6 @@ export {
1289
1303
  listPushDevices,
1290
1304
  resetSafeWarnings,
1291
1305
  notifySafe,
1306
+ notifyTopicSafe,
1292
1307
  sendMailSafe
1293
1308
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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-BM6FBQVP.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-BM6FBQVP.js';
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, j as NotifyTopicInput } from './notify-D4auDRfk.js';
2
+ export { k as Attachment, C as ChannelStatus, l as NOTIFY_TIMEOUT_MS, m as NormalizedMessage, n as NotifyChannel, o as NotifyResult, p as PushSubscriptionJson, q as QuotaUsage, R as Recipient, r as RecipientChannelInfo, s as RecipientInfo, t as SkipReason, u as createLink, v as deletePushSubscription, w as getChannelStatus, x as getPushConfig, y as getRecipient, z as hasNotifyKey, B as listPushDevices, D as notify, F as notifyTopic, G as savePushSubscription, H as toEmails } from './notify-D4auDRfk.js';
3
3
 
4
4
  /** แปลง "ชื่อ <a@b.com>" / "a@b.com" / { name, email } → { name, email } */
5
5
  declare function parseAddress(input: Address): ParsedAddress;
@@ -420,6 +420,8 @@ declare function resetSafeWarnings(): void;
420
420
  * ไม่มี PI_NOTIFY_KEY → { ok: false, disabled: true } ไม่ส่ง
421
421
  */
422
422
  declare function notifySafe(input: NotifyInput, opts?: NotifyOptions): Promise<NotifySafeResult>;
423
+ /** (0.7.0) notifyTopic() ที่ไม่โยนเด็ดขาด (ดู notifySafe) — topic ไม่พบก็คืน { ok: false, error } */
424
+ declare function notifyTopicSafe(input: NotifyTopicInput, opts?: NotifyOptions): Promise<NotifySafeResult>;
423
425
  interface SendMailSafeResult {
424
426
  ok: boolean;
425
427
  provider?: string;
@@ -455,4 +457,5 @@ declare function createRedisStore(client: RedisLike, opts?: {
455
457
  prefix?: string;
456
458
  }): Store;
457
459
 
458
- 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 };
460
+ export { Address, AllProvidersFailedError, Attempt, DEFAULT_COOLDOWN, DEFAULT_HUB_URL, ErrorKind, MailError, MailerEvent, Message, NotifyInput, NotifyOptions, NotifyResultItem, NotifyTopicInput, ParsedAddress, Provider, ProviderInfo, ProviderStatus, PushDevice, Quota, SMTP_PRESETS, SendResult, Store, classifyError, configureMailer, createConsoleProvider, createHubProvider, createMailer, createMemoryStore, createPresetSmtpProvider, createPushHandlers, createRedisStore, createResendProvider, createSmtpProvider, createZeptoMailProvider, formatAddress, getMailer, listProviders, looksLikeHtml, notifySafe, notifyTopicSafe, parseAddress, parseAddressList, providersFromEnv, pushServiceWorker, pushServiceWorkerSource, renderNotifyEmail, requireFrom, resetMailer, resetSafeWarnings, resolveFormat, sendMail, sendMailSafe, toPlainText };
461
+ export type { BodyFormat, BrowserPushSubscription, ConsoleOptions, Env, HubErrorResponse, HubOptions, HubSendRequest, HubSendResponse, Mailer, MailerOptions, NotifyEmailInput, NotifySafeResult, ProviderConfig, PushHandlers, PushHandlersOptions, PushServiceWorkerOptions, PushUser, RedisLike, ResendOptions, SendMailSafeResult, SmtpOptions, SmtpPreset, ZeptoMailOptions };
package/dist/index.js CHANGED
@@ -27,6 +27,8 @@ import {
27
27
  listPushDevices,
28
28
  notify,
29
29
  notifySafe,
30
+ notifyTopic,
31
+ notifyTopicSafe,
30
32
  parseAddress,
31
33
  parseAddressList,
32
34
  providersFromEnv,
@@ -37,7 +39,7 @@ import {
37
39
  sendMail,
38
40
  sendMailSafe,
39
41
  toEmails
40
- } from "./chunk-AQWFHD5T.js";
42
+ } from "./chunk-2RXN5HXT.js";
41
43
 
42
44
  // src/format.ts
43
45
  var KNOWN_TAG_RE = /<\/?(b|strong|i|em|u|ins|s|strike|del|a|code|pre|p|br|div|span|ul|ol|li|h[1-6]|table|tr|td)(?=[\s/>])[^>]*>/i;
@@ -219,6 +221,8 @@ export {
219
221
  looksLikeHtml,
220
222
  notify,
221
223
  notifySafe,
224
+ notifyTopic,
225
+ notifyTopicSafe,
222
226
  parseAddress,
223
227
  parseAddressList,
224
228
  providersFromEnv,
package/dist/next.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { N as NotifyOptions, i as NotifyInput, M as Message } from './notify-BM6FBQVP.js';
1
+ import { NotifyOptions, NotifyInput, NotifyTopicInput, Message } from './index.js';
2
2
 
3
3
  /**
4
4
  * ส่งหลังตอบ response แล้ว (`after()` ของ Next.js) — entry แยก `@suphark/notify/next` เพราะ core ของ SDK
@@ -19,10 +19,16 @@ declare const LATER_TIMEOUT_MS = 20000;
19
19
  * @example notifyLater({ to: user, title: "บันทึกแล้ว", body: "ระบบอัปเดตงานแล้ว" });
20
20
  */
21
21
  declare function notifyLater(input: NotifyInput, opts?: LaterOptions): void;
22
+ /**
23
+ * (0.7.0) notifyTopic() เบื้องหลัง (ดู notifyLater) — คืน void
24
+ * @example notifyTopicLater({ topic: "po-overdue", title: "PO เกินกำหนด", body: "…" });
25
+ */
26
+ declare function notifyTopicLater(input: NotifyTopicInput, opts?: LaterOptions): void;
22
27
  /**
23
28
  * sendMail() เบื้องหลัง (ดู notifyLater) — คืน void
24
29
  * @example sendMailLater({ to: user.email, subject: "ยินดีต้อนรับ", html: "<p>…</p>" });
25
30
  */
26
31
  declare function sendMailLater(message: Message, opts?: Pick<LaterOptions, "onError">): void;
27
32
 
28
- export { LATER_TIMEOUT_MS, type LaterOptions, notifyLater, sendMailLater };
33
+ export { LATER_TIMEOUT_MS, notifyLater, notifyTopicLater, sendMailLater };
34
+ export type { LaterOptions };
package/dist/next.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  notifySafe,
3
+ notifyTopicSafe,
3
4
  sendMailSafe
4
- } from "./chunk-AQWFHD5T.js";
5
+ } from "./chunk-2RXN5HXT.js";
5
6
 
6
7
  // src/next.ts
7
8
  import { after } from "next/server";
@@ -27,6 +28,20 @@ function notifyLater(input, opts = {}) {
27
28
  report("notify()", r.error ?? (r.failed > 0 ? `${r.failed} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E25\u0E49\u0E21\u0E40\u0E2B\u0E25\u0E27` : void 0), onError);
28
29
  });
29
30
  }
31
+ function notifyTopicLater(input, opts = {}) {
32
+ const { onError, ...rest } = opts;
33
+ runLater(async () => {
34
+ const r = await notifyTopicSafe(input, {
35
+ timeoutMs: LATER_TIMEOUT_MS,
36
+ ...rest
37
+ });
38
+ report(
39
+ "notifyTopic()",
40
+ r.error ?? (r.failed > 0 ? `${r.failed} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E25\u0E49\u0E21\u0E40\u0E2B\u0E25\u0E27` : void 0),
41
+ onError
42
+ );
43
+ });
44
+ }
30
45
  function sendMailLater(message, opts = {}) {
31
46
  runLater(async () => {
32
47
  const r = await sendMailSafe(message);
@@ -36,5 +51,6 @@ function sendMailLater(message, opts = {}) {
36
51
  export {
37
52
  LATER_TIMEOUT_MS,
38
53
  notifyLater,
54
+ notifyTopicLater,
39
55
  sendMailLater
40
56
  };
@@ -217,7 +217,18 @@ interface NotifyInput {
217
217
  */
218
218
  idempotencyKey?: string;
219
219
  }
220
+ /**
221
+ * (0.7.0) ส่งเข้า "topic" ของแอป — กลุ่ม/ห้อง LINE หรือกลุ่ม Telegram ที่ admin ผูกไว้ที่หน้า Topics ของ Pi Notify
222
+ * แอปไม่ต้องรู้ group id: เชิญ OA/bot เข้ากลุ่ม → hub เห็นกลุ่ม → admin ผูกกับ topic
223
+ */
224
+ interface NotifyTopicInput extends Omit<NotifyInput, "to" | "channels" | "prefer"> {
225
+ /** ชื่อ topic ของแอปนี้ (a-z 0-9 . _ -) เช่น "po-overdue" — ไม่พบ = MailError invalid (404) */
226
+ topic: string;
227
+ /** ไม่ระบุ = ทุกปลายทางของ topic · ระบุ = เฉพาะแพลตฟอร์มนี้ */
228
+ channels?: ("line" | "telegram")[];
229
+ }
220
230
  interface NotifyResultItem {
231
+ /** อีเมลผู้รับ หรือ "topic:<key>" เมื่อส่งด้วย notifyTopic() */
221
232
  to: string;
222
233
  channel: NotifyChannel;
223
234
  /** `queued` (0.6.0) = hub รับไว้ส่งซ้ำเอง — เกิดเฉพาะเมื่อแอปส่ง `deliverWithin` */
@@ -229,12 +240,15 @@ interface NotifyResultItem {
229
240
  /**
230
241
  * app_daily_cap = แอปส่งอีเมลแทนแชทครบเพดานรายวันที่ตั้งใน Pi Notify แล้ว
231
242
  * no_push_provider = hub ยังไม่ได้ตั้ง provider web push
243
+ * no_target (0.7.0) = topic ถูกปิด หรือยังไม่มีกลุ่มที่ใช้ได้ผูกอยู่
232
244
  */
233
- reason?: "no_link" | "channel_not_linked" | "no_chat_provider" | "no_push_provider" | "app_daily_cap";
245
+ reason?: "no_link" | "channel_not_linked" | "no_chat_provider" | "no_push_provider" | "app_daily_cap" | "no_target";
234
246
  /** (status failed) คำขอหมดงบเวลาก่อนได้เริ่มส่งรายการนี้ — ส่งซ้ำเฉพาะรายการนี้ได้ */
235
247
  deferred?: boolean;
236
248
  /** (0.6.0) id ของรายการในคิวส่งซ้ำของ hub — มีเมื่อ status เป็น queued */
237
249
  deliveryId?: string;
250
+ /** (0.7.0) notifyTopic(): ชื่อกลุ่ม/ปลายทางของรายการนี้ */
251
+ target?: string;
238
252
  }
239
253
  interface NotifyResult {
240
254
  results: NotifyResultItem[];
@@ -296,16 +310,25 @@ interface RecipientInfo {
296
310
  /** มี PI_NOTIFY_KEY (ใน opts หรือ env) หรือไม่ — ให้ helper แบบ safe/later ใช้ตัดสินว่า "ปิดอยู่" */
297
311
  declare function hasNotifyKey(opts?: NotifyOptions): boolean;
298
312
  /**
299
- * แจ้งเตือนผู้รับผ่าน Pi Notify hub — Telegram (ที่ผู้รับผูกไว้) หรืออีเมล
313
+ * แจ้งเตือนผู้รับผ่าน Pi Notify hub — แชทที่ผู้รับผูกไว้ (Telegram / LINE / web push) หรืออีเมล
300
314
  * ใช้ env เดียวกับ sendMail(): PI_NOTIFY_KEY (+ PI_NOTIFY_URL)
301
315
  * โยน MailError เมื่อ hub ตอบ non-2xx (502 = ไม่มีช่องทางไหนส่งสำเร็จเลย; results อยู่ใน err.cause)
302
316
  */
303
317
  declare function notify(input: NotifyInput, opts?: NotifyOptions): Promise<NotifyResult>;
318
+ /**
319
+ * (0.7.0) ส่งเข้า topic ของแอป — hub กระจายไปทุกกลุ่ม LINE/Telegram ที่ admin ผูกไว้ (ผลรายกลุ่มมี `target`)
320
+ * topic ไม่พบ → MailError invalid (404) · topic ปิด/ยังไม่ผูกกลุ่ม → ผล skipped reason "no_target" (ไม่โยน)
321
+ * ใช้ deliverWithin / idempotencyKey ได้เหมือน notify()
322
+ * @example await notifyTopic({ topic: "po-overdue", title: "PO เกินกำหนด 3 รายการ", body: "…", link });
323
+ */
324
+ declare function notifyTopic(input: NotifyTopicInput, opts?: NotifyOptions): Promise<NotifyResult>;
304
325
  /** ช่องทางที่ผู้รับผูกไว้ — ไว้แสดง "ผูกแล้ว/ยังไม่ผูก" ในหน้าตั้งค่าของแอป */
305
326
  declare function getRecipient(email: Recipient, opts?: NotifyOptions): Promise<RecipientInfo>;
306
327
  /**
307
- * ขอลิงก์ผูก Telegram ให้ผู้รับ (t.me/<bot>?start=<code>, หมดอายุ 10 นาที)
308
- * แอปวางเป็นปุ่ม/QR ในหน้าตั้งค่าของตัวเอง แล้วโพล getRecipient() จนเห็น telegram
328
+ * ขอลิงก์ผูกแชทให้ผู้รับ (หมดอายุ 10 นาที) — แอปวางเป็นปุ่ม/QR ในหน้าตั้งค่าของตัวเอง แล้วโพล getRecipient() จนเห็นช่องทางโผล่
329
+ * - telegram: `t.me/<bot>?start=<code>` (ผู้ใช้กด Start)
330
+ * - line (0.7.0): `line.me/R/oaMessage/@<oa>/?ผูก Pi Notify <code>` — เปิดแชท OA พร้อมพิมพ์ข้อความไว้ ผู้ใช้กดส่ง
331
+ * (ยังไม่เป็นเพื่อนกับ OA ต้องเพิ่มเพื่อนด้วยจึงจะรับ push ได้) · `code` ใช้แสดงให้พิมพ์ "ผูก Pi Notify <code>" เอง
309
332
  * `opts.piUserId` = user id ใน Pi Auth (sub) ของผู้ใช้ — ให้ Pi Notify ลบการผูกเมื่อบัญชีถูกลบ
310
333
  */
311
334
  declare function createLink(email: Recipient, channel?: Exclude<NotifyChannel, "email">, opts?: NotifyOptions & {
@@ -354,4 +377,5 @@ declare function listPushDevices(email: string, opts?: NotifyOptions): Promise<{
354
377
  devices: PushDevice[];
355
378
  }>;
356
379
 
357
- export { type Address as A, notify as B, type ChannelStatus as C, savePushSubscription as D, type ErrorKind as E, toEmails as F, type Message as M, type NotifyOptions as N, type ParsedAddress as P, type Quota as Q, type Recipient as R, type SendResult as S, type Provider as a, type ProviderStatus as b, type ProviderInfo as c, type Store as d, type MailerEvent as e, type Attempt as f, type PushDevice as g, type NotifyResultItem as h, type NotifyInput as i, type Attachment as j, NOTIFY_TIMEOUT_MS as k, type NormalizedMessage as l, type NotifyChannel as m, type NotifyResult as n, type PushSubscriptionJson as o, type QuotaUsage as p, type RecipientChannelInfo as q, type RecipientInfo as r, type SkipReason as s, createLink as t, deletePushSubscription as u, getChannelStatus as v, getPushConfig as w, getRecipient as x, hasNotifyKey as y, listPushDevices as z };
380
+ export { listPushDevices as B, notify as D, notifyTopic as F, savePushSubscription as G, toEmails as H, NOTIFY_TIMEOUT_MS as l, createLink as u, deletePushSubscription as v, getChannelStatus as w, getPushConfig as x, getRecipient as y, hasNotifyKey as z };
381
+ export type { Address as A, ChannelStatus as C, ErrorKind as E, Message as M, NotifyOptions as N, ParsedAddress as P, Quota as Q, Recipient as R, SendResult as S, Provider as a, ProviderStatus as b, ProviderInfo as c, Store as d, MailerEvent as e, Attempt as f, PushDevice as g, NotifyResultItem as h, NotifyInput as i, NotifyTopicInput as j, Attachment as k, NormalizedMessage as m, NotifyChannel as n, NotifyResult as o, PushSubscriptionJson as p, QuotaUsage as q, RecipientChannelInfo as r, RecipientInfo as s, SkipReason as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suphark/notify",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "ส่งอีเมล + แจ้งเตือน Telegram ผ่าน Pi Notify hub (notify.suphark.com) หรือส่งอีเมลตรงไป Resend/SMTP/Zoho/ZeptoMail ใน API เดียว พร้อม failover อัตโนมัติ",
@@ -36,7 +36,7 @@
36
36
  "prepack": "pnpm run build"
37
37
  },
38
38
  "dependencies": {
39
- "nodemailer": "^10.0.14"
39
+ "nodemailer": "^10.2.1"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "next": ">=15"
@@ -47,14 +47,14 @@
47
47
  }
48
48
  },
49
49
  "devDependencies": {
50
- "@biomejs/biome": "^2.5.15",
50
+ "@biomejs/biome": "^2.6.2",
51
51
  "@pi/tsconfig": "workspace:*",
52
52
  "@types/node": "^24.13.4",
53
53
  "@types/nodemailer": "^8.0.1",
54
- "next": "^16.3.8",
55
- "tsup": "^8.5.1",
54
+ "next": "^16.4.2",
55
+ "tsup": "^9.0.0",
56
56
  "typescript": "^6.0.3",
57
- "vitest": "^4.1.11"
57
+ "vitest": "^5.0.5"
58
58
  },
59
59
  "license": "MIT",
60
60
  "repository": {