@suphark/notify 0.5.0 → 0.6.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 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
@@ -94,6 +94,24 @@ const { url, code } = await createLink(user); // รับ use
94
94
  const { linked, label, manageUrl } = await getChannelStatus(user, "telegram");
95
95
  ```
96
96
 
97
+ ### ให้ hub ส่งซ้ำเอง: `deliverWithin` + `idempotencyKey` (0.6.0)
98
+
99
+ ```ts
100
+ const r = await notify({
101
+ to: user,
102
+ title: "ถึงกำหนดงาน",
103
+ body: "…",
104
+ channels: ["telegram"],
105
+ deliverWithin: 3600, // ให้ hub พยายามส่งภายใน 1 ชม.
106
+ idempotencyKey: `pi-done:reminder:${user.id}:${runId}`, // cron รันซ้ำก็ไม่เตือนซ้ำ
107
+ });
108
+ // r.results[i].status: "sent" | "queued" (hub รับไว้ส่งซ้ำเอง) | "failed" | "skipped"
109
+ ```
110
+
111
+ - นับ `queued` เป็น "รับไว้แล้ว" — แอปไม่ต้องส่งซ้ำเอง; ดูสถานะที่หน้า Logs / คิวส่งซ้ำของ Pi Notify
112
+ - `failed` = ผิดถาวร / ลองครบแล้ว / เลยเวลา — แอปตัดสินใจเอง (อย่าสลับไปอีเมลถ้าผู้ใช้ไม่ได้ขอ)
113
+ - ใช้ `idempotencyKey` ที่ผูกกับเหตุการณ์ (ไม่ใช่สุ่มใหม่ทุกครั้ง) จึงกันซ้ำได้ — ส่งเนื้อหาเดิมด้วย key เดิมซ้ำจะได้ผลเดิมกลับมา
114
+
97
115
  ### เขียนให้สั้น: ส่งแบบไม่โยน / ส่งเบื้องหลัง / `to` เป็น user object (0.5.0)
98
116
 
99
117
  `to` ของ `notify()` รับ **user object ที่มี `email`** (และ array ของมัน) ได้เลย — ไม่ต้องแกะ `.email` เอง
@@ -373,6 +391,11 @@ Pi Notify มี contract test (`apps/notify/src/test/contract/`) เก็บ b
373
391
 
374
392
  ## Changelog
375
393
 
394
+ - 0.6.0 — **hub ส่งซ้ำเอง** (เพิ่มอย่างเดียว — แอปที่ไม่ส่งตัวเลือกใหม่ได้พฤติกรรมเดิมทุกอย่าง; ต้องใช้ Pi Notify 2026-10-14+)
395
+ - `notify({ deliverWithin })` (วินาที 60–86400): รายการที่ค้างหรือพลาดชั่วคราวเข้าคิวถาวรของ hub แล้วส่งซ้ำตาม backoff (1 นาที → 5 นาที → 30 นาที → 2 ชม., สูงสุด 5 ครั้ง) · ผิดถาวรไม่ส่งซ้ำ · เลยเวลาแล้วไม่ส่ง · ไม่สลับช่องทาง
396
+ - `notify({ idempotencyKey })` → หัว `Idempotency-Key`: key+เนื้อหาเดิมได้ผลเดิม ไม่ส่งซ้ำ (จำ 7 วัน) · เนื้อหาต่างแต่ key เดิม = `invalid` (422) · คำขอเดิมยังทำอยู่ = `rate_limit` (409, `retryAfterSec` 5)
397
+ - `NotifyResultItem.status` เพิ่ม `"queued"` (+ `deliveryId`), `NotifyResult.queued?` / `NotifySafeResult.queued?` — `queued` นับเป็น "รับไว้แล้ว" ไม่ใช่ failed (`notifySafe().ok` เป็น true) · `status` ใหม่เกิดเฉพาะเมื่อแอปส่ง `deliverWithin` (ออกเป็น minor เพราะ union ของ `status` ที่แอปอาจ switch)
398
+
376
399
  - 0.5.0 — **เขียนฝั่งแอปให้สั้นลง** (เพิ่มอย่างเดียว — ฟอร์มของ 0.4.0 ใช้ได้ทุกแบบ; Pi Notify รุ่นเก่าใช้ได้ ยกเว้น `code` ที่แกะจาก url ให้เอง)
377
400
  - `notify({ to })` รับ user object `{ email }` และ array (กรอง null); ผู้รับเดี่ยวไม่มีอีเมล = `MailError` `invalid`
378
401
  - `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,
@@ -1128,9 +1129,11 @@ async function hubRequest(opts, path, init) {
1128
1129
  return data;
1129
1130
  }
1130
1131
  async function notify(input, opts = {}) {
1132
+ const { idempotencyKey, ...body } = input;
1131
1133
  const data = await hubRequest(opts, "/api/v1/notify", {
1132
1134
  method: "POST",
1133
- body: { ...input, to: toEmails(input.to) },
1135
+ body: { ...body, to: toEmails(input.to) },
1136
+ headers: idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0,
1134
1137
  timeoutMs: NOTIFY_TIMEOUT_MS.notify
1135
1138
  });
1136
1139
  return { results: data.results, ...data.summary };
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-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';
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';
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 (ไม่มีอีเมลผู้รับ/เนื้อหา) */
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  sendMail,
38
38
  sendMailSafe,
39
39
  toEmails
40
- } from "./chunk-FL5ZV3S2.js";
40
+ } from "./chunk-AQWFHD5T.js";
41
41
 
42
42
  // src/format.ts
43
43
  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;
package/dist/next.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { N as NotifyOptions, i as NotifyInput, M as Message } from './notify-L7JnpG0t.js';
1
+ import { N as NotifyOptions, i as NotifyInput, M as Message } from './notify-BM6FBQVP.js';
2
2
 
3
3
  /**
4
4
  * ส่งหลังตอบ response แล้ว (`after()` ของ Next.js) — entry แยก `@suphark/notify/next` เพราะ core ของ SDK
package/dist/next.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  notifySafe,
3
3
  sendMailSafe
4
- } from "./chunk-FL5ZV3S2.js";
4
+ } from "./chunk-AQWFHD5T.js";
5
5
 
6
6
  // src/next.ts
7
7
  import { after } from "next/server";
@@ -204,11 +204,24 @@ 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;
207
219
  }
208
220
  interface NotifyResultItem {
209
221
  to: string;
210
222
  channel: NotifyChannel;
211
- status: "sent" | "failed" | "skipped";
223
+ /** `queued` (0.6.0) = hub รับไว้ส่งซ้ำเอง — เกิดเฉพาะเมื่อแอปส่ง `deliverWithin` */
224
+ status: "sent" | "failed" | "skipped" | "queued";
212
225
  provider?: string;
213
226
  id?: string;
214
227
  failedOver?: boolean;
@@ -220,6 +233,8 @@ interface NotifyResultItem {
220
233
  reason?: "no_link" | "channel_not_linked" | "no_chat_provider" | "no_push_provider" | "app_daily_cap";
221
234
  /** (status failed) คำขอหมดงบเวลาก่อนได้เริ่มส่งรายการนี้ — ส่งซ้ำเฉพาะรายการนี้ได้ */
222
235
  deferred?: boolean;
236
+ /** (0.6.0) id ของรายการในคิวส่งซ้ำของ hub — มีเมื่อ status เป็น queued */
237
+ deliveryId?: string;
223
238
  }
224
239
  interface NotifyResult {
225
240
  results: NotifyResultItem[];
@@ -228,6 +243,8 @@ interface NotifyResult {
228
243
  skipped: number;
229
244
  /** จำนวนที่ยังไม่ได้ส่งเพราะหมดงบเวลา (นับซ้อนอยู่ใน failed; Pi Notify รุ่นเก่าไม่ส่งมา) */
230
245
  deferred?: number;
246
+ /** (0.6.0) จำนวนที่ hub รับไว้ส่งซ้ำเอง (ไม่นับใน failed) — Pi Notify รุ่นเก่าไม่ส่งมา */
247
+ queued?: number;
231
248
  }
232
249
  interface NotifyOptions {
233
250
  /** ค่าเริ่มต้น PI_NOTIFY_KEY */
package/package.json CHANGED
@@ -1,12 +1,9 @@
1
1
  {
2
2
  "name": "@suphark/notify",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -51,7 +48,7 @@
51
48
  },
52
49
  "devDependencies": {
53
50
  "@biomejs/biome": "^2.5.15",
54
- "@pi/config": "workspace:*",
51
+ "@pi/tsconfig": "workspace:*",
55
52
  "@types/node": "^24.13.4",
56
53
  "@types/nodemailer": "^8.0.1",
57
54
  "next": "^16.3.8",