@suphark/notify 0.5.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/LICENSE +21 -21
- package/README.md +66 -1
- package/dist/{chunk-FL5ZV3S2.js → chunk-2RXN5HXT.js} +22 -4
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -1
- package/dist/next.d.ts +8 -2
- package/dist/next.js +17 -1
- package/dist/{notify-L7JnpG0t.d.ts → notify-D4auDRfk.d.ts} +47 -6
- package/package.json +7 -10
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Suphark
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suphark
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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,61 @@ 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
|
+
|
|
134
|
+
### ให้ hub ส่งซ้ำเอง: `deliverWithin` + `idempotencyKey` (0.6.0)
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const r = await notify({
|
|
138
|
+
to: user,
|
|
139
|
+
title: "ถึงกำหนดงาน",
|
|
140
|
+
body: "…",
|
|
141
|
+
channels: ["telegram"],
|
|
142
|
+
deliverWithin: 3600, // ให้ hub พยายามส่งภายใน 1 ชม.
|
|
143
|
+
idempotencyKey: `pi-done:reminder:${user.id}:${runId}`, // cron รันซ้ำก็ไม่เตือนซ้ำ
|
|
144
|
+
});
|
|
145
|
+
// r.results[i].status: "sent" | "queued" (hub รับไว้ส่งซ้ำเอง) | "failed" | "skipped"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- นับ `queued` เป็น "รับไว้แล้ว" — แอปไม่ต้องส่งซ้ำเอง; ดูสถานะที่หน้า Logs / คิวส่งซ้ำของ Pi Notify
|
|
149
|
+
- `failed` = ผิดถาวร / ลองครบแล้ว / เลยเวลา — แอปตัดสินใจเอง (อย่าสลับไปอีเมลถ้าผู้ใช้ไม่ได้ขอ)
|
|
150
|
+
- ใช้ `idempotencyKey` ที่ผูกกับเหตุการณ์ (ไม่ใช่สุ่มใหม่ทุกครั้ง) จึงกันซ้ำได้ — ส่งเนื้อหาเดิมด้วย key เดิมซ้ำจะได้ผลเดิมกลับมา
|
|
151
|
+
|
|
97
152
|
### เขียนให้สั้น: ส่งแบบไม่โยน / ส่งเบื้องหลัง / `to` เป็น user object (0.5.0)
|
|
98
153
|
|
|
99
154
|
`to` ของ `notify()` รับ **user object ที่มี `email`** (และ array ของมัน) ได้เลย — ไม่ต้องแกะ `.email` เอง
|
|
@@ -373,6 +428,16 @@ Pi Notify มี contract test (`apps/notify/src/test/contract/`) เก็บ b
|
|
|
373
428
|
|
|
374
429
|
## Changelog
|
|
375
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
|
+
|
|
436
|
+
- 0.6.0 — **hub ส่งซ้ำเอง** (เพิ่มอย่างเดียว — แอปที่ไม่ส่งตัวเลือกใหม่ได้พฤติกรรมเดิมทุกอย่าง; ต้องใช้ Pi Notify 2026-10-14+)
|
|
437
|
+
- `notify({ deliverWithin })` (วินาที 60–86400): รายการที่ค้างหรือพลาดชั่วคราวเข้าคิวถาวรของ hub แล้วส่งซ้ำตาม backoff (1 นาที → 5 นาที → 30 นาที → 2 ชม., สูงสุด 5 ครั้ง) · ผิดถาวรไม่ส่งซ้ำ · เลยเวลาแล้วไม่ส่ง · ไม่สลับช่องทาง
|
|
438
|
+
- `notify({ idempotencyKey })` → หัว `Idempotency-Key`: key+เนื้อหาเดิมได้ผลเดิม ไม่ส่งซ้ำ (จำ 7 วัน) · เนื้อหาต่างแต่ key เดิม = `invalid` (422) · คำขอเดิมยังทำอยู่ = `rate_limit` (409, `retryAfterSec` 5)
|
|
439
|
+
- `NotifyResultItem.status` เพิ่ม `"queued"` (+ `deliveryId`), `NotifyResult.queued?` / `NotifySafeResult.queued?` — `queued` นับเป็น "รับไว้แล้ว" ไม่ใช่ failed (`notifySafe().ok` เป็น true) · `status` ใหม่เกิดเฉพาะเมื่อแอปส่ง `deliverWithin` (ออกเป็น minor เพราะ union ของ `status` ที่แอปอาจ switch)
|
|
440
|
+
|
|
376
441
|
- 0.5.0 — **เขียนฝั่งแอปให้สั้นลง** (เพิ่มอย่างเดียว — ฟอร์มของ 0.4.0 ใช้ได้ทุกแบบ; Pi Notify รุ่นเก่าใช้ได้ ยกเว้น `code` ที่แกะจาก url ให้เอง)
|
|
377
442
|
- `notify({ to })` รับ user object `{ email }` และ array (กรอง null); ผู้รับเดี่ยวไม่มีอีเมล = `MailError` `invalid`
|
|
378
443
|
- `notifySafe()` / `sendMailSafe()` ไม่โยน; ไม่มี key/provider = `disabled`
|
|
@@ -1101,6 +1101,7 @@ async function hubRequest(opts, path, init) {
|
|
|
1101
1101
|
method: init.method ?? "GET",
|
|
1102
1102
|
headers: {
|
|
1103
1103
|
Authorization: `Bearer ${apiKey}`,
|
|
1104
|
+
...init.headers,
|
|
1104
1105
|
...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
1105
1106
|
},
|
|
1106
1107
|
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
@@ -1127,14 +1128,23 @@ async function hubRequest(opts, path, init) {
|
|
|
1127
1128
|
}
|
|
1128
1129
|
return data;
|
|
1129
1130
|
}
|
|
1130
|
-
async function
|
|
1131
|
+
async function postNotify(body, idempotencyKey, opts) {
|
|
1131
1132
|
const data = await hubRequest(opts, "/api/v1/notify", {
|
|
1132
1133
|
method: "POST",
|
|
1133
|
-
body
|
|
1134
|
+
body,
|
|
1135
|
+
headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
|
|
1134
1136
|
timeoutMs: NOTIFY_TIMEOUT_MS.notify
|
|
1135
1137
|
});
|
|
1136
1138
|
return { results: data.results, ...data.summary };
|
|
1137
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
|
+
}
|
|
1138
1148
|
async function getRecipient(email, opts = {}) {
|
|
1139
1149
|
return hubRequest(
|
|
1140
1150
|
opts,
|
|
@@ -1213,13 +1223,19 @@ function resetSafeWarnings() {
|
|
|
1213
1223
|
}
|
|
1214
1224
|
var messageOf = (err) => err instanceof Error ? err.message : String(err);
|
|
1215
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) {
|
|
1216
1232
|
const none = { sent: 0, failed: 0, skipped: 0, results: [] };
|
|
1217
1233
|
if (!hasNotifyKey(opts)) {
|
|
1218
|
-
warnDisabled(
|
|
1234
|
+
warnDisabled(what, opts.env ?? process.env);
|
|
1219
1235
|
return { ok: false, disabled: true, ...none };
|
|
1220
1236
|
}
|
|
1221
1237
|
try {
|
|
1222
|
-
const r = await
|
|
1238
|
+
const r = await run();
|
|
1223
1239
|
return { ok: r.failed === 0, ...r };
|
|
1224
1240
|
} catch (err) {
|
|
1225
1241
|
const body = err instanceof MailError ? err.cause : void 0;
|
|
@@ -1277,6 +1293,7 @@ export {
|
|
|
1277
1293
|
NOTIFY_TIMEOUT_MS,
|
|
1278
1294
|
hasNotifyKey,
|
|
1279
1295
|
notify,
|
|
1296
|
+
notifyTopic,
|
|
1280
1297
|
getRecipient,
|
|
1281
1298
|
createLink,
|
|
1282
1299
|
getChannelStatus,
|
|
@@ -1286,5 +1303,6 @@ export {
|
|
|
1286
1303
|
listPushDevices,
|
|
1287
1304
|
resetSafeWarnings,
|
|
1288
1305
|
notifySafe,
|
|
1306
|
+
notifyTopicSafe,
|
|
1289
1307
|
sendMailSafe
|
|
1290
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-
|
|
2
|
-
export {
|
|
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;
|
|
@@ -403,6 +403,8 @@ interface NotifySafeResult {
|
|
|
403
403
|
failed: number;
|
|
404
404
|
skipped: number;
|
|
405
405
|
deferred?: number;
|
|
406
|
+
/** (0.6.0) hub รับไว้ส่งซ้ำเอง — ไม่นับเป็นความล้มเหลว */
|
|
407
|
+
queued?: number;
|
|
406
408
|
/** ผลรายผู้รับ/ช่องทาง (มีแม้ Pi Notify ตอบ 502 — ดึงจาก body) */
|
|
407
409
|
results: NotifyResultItem[];
|
|
408
410
|
/** ข้อความ error (ไม่มีอีเมลผู้รับ/เนื้อหา) */
|
|
@@ -418,6 +420,8 @@ declare function resetSafeWarnings(): void;
|
|
|
418
420
|
* ไม่มี PI_NOTIFY_KEY → { ok: false, disabled: true } ไม่ส่ง
|
|
419
421
|
*/
|
|
420
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>;
|
|
421
425
|
interface SendMailSafeResult {
|
|
422
426
|
ok: boolean;
|
|
423
427
|
provider?: string;
|
|
@@ -453,4 +457,5 @@ declare function createRedisStore(client: RedisLike, opts?: {
|
|
|
453
457
|
prefix?: string;
|
|
454
458
|
}): Store;
|
|
455
459
|
|
|
456
|
-
export { Address, AllProvidersFailedError, Attempt,
|
|
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-
|
|
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 {
|
|
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,
|
|
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-
|
|
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
|
};
|
|
@@ -204,11 +204,35 @@ interface NotifyInput {
|
|
|
204
204
|
channels?: NotifyChannel[];
|
|
205
205
|
/** ชื่อ provider ฝั่ง hub ที่อยากให้ลองก่อน */
|
|
206
206
|
prefer?: string;
|
|
207
|
+
/**
|
|
208
|
+
* (0.6.0) วินาที 60–86400 — ให้ hub รับผิดชอบส่งให้ถึงภายในเวลานี้: รายการที่ค้างหรือพลาดชั่วคราว (network/5xx/429)
|
|
209
|
+
* hub เก็บในคิวแล้วส่งซ้ำเองตาม backoff (ผลเป็น `status: "queued"` นับเป็น "รับไว้แล้ว") · ผิดถาวร (ผู้รับบล็อก ฯลฯ) ไม่ส่งซ้ำ
|
|
210
|
+
* เลยเวลาแล้วไม่ส่ง · ไม่สลับช่องทาง (ขอ telegram จะไม่ตกไปอีเมล) · ไม่ระบุ = พฤติกรรมเดิม (แอปส่งซ้ำเอง)
|
|
211
|
+
*/
|
|
212
|
+
deliverWithin?: number;
|
|
213
|
+
/**
|
|
214
|
+
* (0.6.0) กันส่งซ้ำ — ส่งเป็นหัว `Idempotency-Key` (1–200 ตัวอักษร ต่อแอป) เรียกซ้ำด้วย key และเนื้อหาเดิม
|
|
215
|
+
* ได้ผลเดิมโดย hub ไม่ส่งอะไรอีก (เนื้อหาต่างแต่ key เดิม = MailError `invalid` 422) · จำไว้ 7 วัน
|
|
216
|
+
* ควรเป็นค่าที่ผูกกับเหตุการณ์ เช่น `pi-done:reminder:<userId>:<runId>`
|
|
217
|
+
*/
|
|
218
|
+
idempotencyKey?: string;
|
|
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")[];
|
|
207
229
|
}
|
|
208
230
|
interface NotifyResultItem {
|
|
231
|
+
/** อีเมลผู้รับ หรือ "topic:<key>" เมื่อส่งด้วย notifyTopic() */
|
|
209
232
|
to: string;
|
|
210
233
|
channel: NotifyChannel;
|
|
211
|
-
|
|
234
|
+
/** `queued` (0.6.0) = hub รับไว้ส่งซ้ำเอง — เกิดเฉพาะเมื่อแอปส่ง `deliverWithin` */
|
|
235
|
+
status: "sent" | "failed" | "skipped" | "queued";
|
|
212
236
|
provider?: string;
|
|
213
237
|
id?: string;
|
|
214
238
|
failedOver?: boolean;
|
|
@@ -216,10 +240,15 @@ interface NotifyResultItem {
|
|
|
216
240
|
/**
|
|
217
241
|
* app_daily_cap = แอปส่งอีเมลแทนแชทครบเพดานรายวันที่ตั้งใน Pi Notify แล้ว
|
|
218
242
|
* no_push_provider = hub ยังไม่ได้ตั้ง provider web push
|
|
243
|
+
* no_target (0.7.0) = topic ถูกปิด หรือยังไม่มีกลุ่มที่ใช้ได้ผูกอยู่
|
|
219
244
|
*/
|
|
220
|
-
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";
|
|
221
246
|
/** (status failed) คำขอหมดงบเวลาก่อนได้เริ่มส่งรายการนี้ — ส่งซ้ำเฉพาะรายการนี้ได้ */
|
|
222
247
|
deferred?: boolean;
|
|
248
|
+
/** (0.6.0) id ของรายการในคิวส่งซ้ำของ hub — มีเมื่อ status เป็น queued */
|
|
249
|
+
deliveryId?: string;
|
|
250
|
+
/** (0.7.0) notifyTopic(): ชื่อกลุ่ม/ปลายทางของรายการนี้ */
|
|
251
|
+
target?: string;
|
|
223
252
|
}
|
|
224
253
|
interface NotifyResult {
|
|
225
254
|
results: NotifyResultItem[];
|
|
@@ -228,6 +257,8 @@ interface NotifyResult {
|
|
|
228
257
|
skipped: number;
|
|
229
258
|
/** จำนวนที่ยังไม่ได้ส่งเพราะหมดงบเวลา (นับซ้อนอยู่ใน failed; Pi Notify รุ่นเก่าไม่ส่งมา) */
|
|
230
259
|
deferred?: number;
|
|
260
|
+
/** (0.6.0) จำนวนที่ hub รับไว้ส่งซ้ำเอง (ไม่นับใน failed) — Pi Notify รุ่นเก่าไม่ส่งมา */
|
|
261
|
+
queued?: number;
|
|
231
262
|
}
|
|
232
263
|
interface NotifyOptions {
|
|
233
264
|
/** ค่าเริ่มต้น PI_NOTIFY_KEY */
|
|
@@ -279,16 +310,25 @@ interface RecipientInfo {
|
|
|
279
310
|
/** มี PI_NOTIFY_KEY (ใน opts หรือ env) หรือไม่ — ให้ helper แบบ safe/later ใช้ตัดสินว่า "ปิดอยู่" */
|
|
280
311
|
declare function hasNotifyKey(opts?: NotifyOptions): boolean;
|
|
281
312
|
/**
|
|
282
|
-
* แจ้งเตือนผู้รับผ่าน Pi Notify hub — Telegram
|
|
313
|
+
* แจ้งเตือนผู้รับผ่าน Pi Notify hub — แชทที่ผู้รับผูกไว้ (Telegram / LINE / web push) หรืออีเมล
|
|
283
314
|
* ใช้ env เดียวกับ sendMail(): PI_NOTIFY_KEY (+ PI_NOTIFY_URL)
|
|
284
315
|
* โยน MailError เมื่อ hub ตอบ non-2xx (502 = ไม่มีช่องทางไหนส่งสำเร็จเลย; results อยู่ใน err.cause)
|
|
285
316
|
*/
|
|
286
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>;
|
|
287
325
|
/** ช่องทางที่ผู้รับผูกไว้ — ไว้แสดง "ผูกแล้ว/ยังไม่ผูก" ในหน้าตั้งค่าของแอป */
|
|
288
326
|
declare function getRecipient(email: Recipient, opts?: NotifyOptions): Promise<RecipientInfo>;
|
|
289
327
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
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>" เอง
|
|
292
332
|
* `opts.piUserId` = user id ใน Pi Auth (sub) ของผู้ใช้ — ให้ Pi Notify ลบการผูกเมื่อบัญชีถูกลบ
|
|
293
333
|
*/
|
|
294
334
|
declare function createLink(email: Recipient, channel?: Exclude<NotifyChannel, "email">, opts?: NotifyOptions & {
|
|
@@ -337,4 +377,5 @@ declare function listPushDevices(email: string, opts?: NotifyOptions): Promise<{
|
|
|
337
377
|
devices: PushDevice[];
|
|
338
378
|
}>;
|
|
339
379
|
|
|
340
|
-
export {
|
|
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,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/notify",
|
|
3
|
-
"version": "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 อัตโนมัติ",
|
|
7
|
-
"main": "./dist/index.js",
|
|
8
|
-
"module": "./dist/index.js",
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
10
7
|
"exports": {
|
|
11
8
|
".": {
|
|
12
9
|
"types": "./dist/index.d.ts",
|
|
@@ -39,7 +36,7 @@
|
|
|
39
36
|
"prepack": "pnpm run build"
|
|
40
37
|
},
|
|
41
38
|
"dependencies": {
|
|
42
|
-
"nodemailer": "^10.
|
|
39
|
+
"nodemailer": "^10.2.1"
|
|
43
40
|
},
|
|
44
41
|
"peerDependencies": {
|
|
45
42
|
"next": ">=15"
|
|
@@ -50,14 +47,14 @@
|
|
|
50
47
|
}
|
|
51
48
|
},
|
|
52
49
|
"devDependencies": {
|
|
53
|
-
"@biomejs/biome": "^2.
|
|
54
|
-
"@pi/
|
|
50
|
+
"@biomejs/biome": "^2.6.2",
|
|
51
|
+
"@pi/tsconfig": "workspace:*",
|
|
55
52
|
"@types/node": "^24.13.4",
|
|
56
53
|
"@types/nodemailer": "^8.0.1",
|
|
57
|
-
"next": "^16.
|
|
58
|
-
"tsup": "^
|
|
54
|
+
"next": "^16.4.2",
|
|
55
|
+
"tsup": "^9.0.0",
|
|
59
56
|
"typescript": "^6.0.3",
|
|
60
|
-
"vitest": "^
|
|
57
|
+
"vitest": "^5.0.5"
|
|
61
58
|
},
|
|
62
59
|
"license": "MIT",
|
|
63
60
|
"repository": {
|