@suphark/notify 0.3.1 → 0.4.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 +34 -0
- package/dist/index.d.ts +53 -5
- package/dist/index.js +27 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,38 @@ const { channels } = await getRecipient(user.email); // [{ type: "telegram
|
|
|
85
85
|
|
|
86
86
|
หรือส่งผู้ใช้ไปผูกเองที่ `https://notify.suphark.com/me` (ล็อกอิน Pi Auth) — ผูกครั้งเดียวใช้ได้ทุกแอป
|
|
87
87
|
|
|
88
|
+
### แจ้งเตือนบนเบราว์เซอร์ (web push)
|
|
89
|
+
|
|
90
|
+
hub ถือคู่กุญแจ VAPID ให้ (ไม่ต้องใช้ Firebase) — แอปเหลือแค่ service worker ของตัวเองกับปุ่มขอสิทธิ์
|
|
91
|
+
**subscription ผูกกับแอปที่เรียกเสมอ** เพราะ service worker อยู่ที่โดเมนของแอป ใช้ข้ามแอปไม่ได้
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { getPushConfig, savePushSubscription, deletePushSubscription, listPushDevices } from "@suphark/notify";
|
|
95
|
+
|
|
96
|
+
// ฝั่ง client: ขอสิทธิ์แล้วส่ง subscription ไปเก็บที่ hub ผ่าน server action ของแอป
|
|
97
|
+
const { publicKey } = await getPushConfig(); // VAPID public key (ค่าสาธารณะ)
|
|
98
|
+
const reg = await navigator.serviceWorker.register("/sw.js");
|
|
99
|
+
const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: publicKey });
|
|
100
|
+
await savePushSubscription(user.email, sub.toJSON(), { label: "Chrome บน Windows", piUserId: user.id });
|
|
101
|
+
|
|
102
|
+
await listPushDevices(user.email); // { devices: [{ id, label, enabled, endpointTail }] }
|
|
103
|
+
await deletePushSubscription(user.email, sub.endpoint); // ผู้ใช้ปิดที่เครื่องนั้น
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
ส่งด้วย `notify({ channels: ["push"] })` (ไม่ระบุ channels = ส่งทุกช่องทางที่ผูกไว้ รวม push ทุกเครื่อง)
|
|
107
|
+
service worker ของแอปจะได้ payload JSON `{ title, body, link? }` — `body` เป็นข้อความล้วนเสมอ:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
self.addEventListener("push", (event) => {
|
|
111
|
+
const { title, body, link } = event.data.json();
|
|
112
|
+
event.waitUntil(self.registration.showNotification(title, { body, data: { link } }));
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- ไม่มีเครื่องไหนเปิดไว้ → `status: "skipped", reason: "channel_not_linked"` · hub ยังไม่ตั้ง provider → `no_push_provider`
|
|
117
|
+
- เบราว์เซอร์ถอนสิทธิ์ (404/410) → hub ปิด subscription นั้นให้เอง ไม่ต้องตามลบ
|
|
118
|
+
- เปลี่ยนคู่กุญแจ VAPID ที่ hub = ผู้ใช้ทุกคนต้องกดอนุญาตใหม่ (subscription ผูกกับกุญแจที่ใช้ตอน subscribe)
|
|
119
|
+
|
|
88
120
|
## โหมด local (ไม่ผ่าน hub)
|
|
89
121
|
|
|
90
122
|
ไม่มี `PI_NOTIFY_KEY` = SDK ส่งตรงด้วย provider ที่ตั้งใน env ของแอปเอง (engine เดียวกับที่ hub ใช้)
|
|
@@ -226,6 +258,8 @@ Pi Notify มี contract test (`apps/notify/src/test/contract/`) เก็บ b
|
|
|
226
258
|
|
|
227
259
|
## Changelog
|
|
228
260
|
|
|
261
|
+
- 0.4.0 — **web push ผ่าน hub**: `getPushConfig()`, `savePushSubscription()`, `deletePushSubscription()`, `listPushDevices()` และ `notify({ channels: ["push"] })`; hub ถือคู่กุญแจ VAPID (มาตรฐาน ไม่ใช้ Firebase) subscription ผูกต่อแอป · `NotifyChannel` เพิ่ม `"push"` และ `reason` เพิ่ม `"no_push_provider"` (เพิ่มอย่างเดียว แต่ `NotifyChannel` เป็น type ที่แอปใช้ในลายเซ็นของตัวเอง จึงออกเป็น minor ตามกติกาข้อ 4)
|
|
262
|
+
|
|
229
263
|
- 0.3.1 (additive ทั้งหมด — แอปที่ตั้ง `^0.3.0` รับได้เลย, ใช้ร่วมกับ Pi Notify ที่ deploy อยู่ได้ทันที)
|
|
230
264
|
- **timeout**: `notify()` 65 วิ, `getRecipient()`/`createLink()` 10 วิ (`NotifyOptions.timeoutMs`, ค่าเริ่มต้นที่ `NOTIFY_TIMEOUT_MS`) — เดิมไม่มี timeout เลย
|
|
231
265
|
- **abort**: mailer หมด `timeoutMs` แล้วยกเลิก fetch ของ provider `hub` ผ่าน `NormalizedMessage.signal` (field ใหม่ optional) ไม่ปล่อยคำขอค้าง
|
package/dist/index.d.ts
CHANGED
|
@@ -472,8 +472,11 @@ declare function classifyError(err: unknown): {
|
|
|
472
472
|
/** เวลาพัก (วินาที) ต่อ error แต่ละแบบ ก่อนกลับมาลอง provider นั้นอีก — คูณสองทุกครั้งที่พังซ้ำ */
|
|
473
473
|
declare const DEFAULT_COOLDOWN: Record<ErrorKind, number>;
|
|
474
474
|
|
|
475
|
-
/**
|
|
476
|
-
|
|
475
|
+
/**
|
|
476
|
+
* ช่องทางแจ้งเตือนที่ hub รองรับ — ผู้รับผูกแชทเองที่ hub (/me) หรือผ่านลิงก์ที่แอปขอจาก hub
|
|
477
|
+
* `push` = web push ของเบราว์เซอร์ ผูกต่อแอป (service worker อยู่ที่โดเมนของแอปเอง)
|
|
478
|
+
*/
|
|
479
|
+
type NotifyChannel = "telegram" | "line" | "push" | "email";
|
|
477
480
|
interface NotifyInput {
|
|
478
481
|
/** อีเมลผู้รับ (ตรงกับบัญชี Pi Auth) — ระบุได้สูงสุด 50 คน */
|
|
479
482
|
to: string | string[];
|
|
@@ -500,8 +503,11 @@ interface NotifyResultItem {
|
|
|
500
503
|
id?: string;
|
|
501
504
|
failedOver?: boolean;
|
|
502
505
|
error?: string;
|
|
503
|
-
/**
|
|
504
|
-
|
|
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";
|
|
505
511
|
/** (status failed) คำขอหมดงบเวลาก่อนได้เริ่มส่งรายการนี้ — ส่งซ้ำเฉพาะรายการนี้ได้ */
|
|
506
512
|
deferred?: boolean;
|
|
507
513
|
}
|
|
@@ -531,6 +537,21 @@ declare const NOTIFY_TIMEOUT_MS: {
|
|
|
531
537
|
readonly notify: 65000;
|
|
532
538
|
readonly recipient: 10000;
|
|
533
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
|
+
}
|
|
534
555
|
/** ช่องทางที่ผู้รับผูกไว้ (จาก GET /api/v1/recipients/:email) — ไม่มี chat id */
|
|
535
556
|
interface RecipientChannelInfo {
|
|
536
557
|
type: string;
|
|
@@ -565,6 +586,33 @@ declare function createLink(email: string, channel?: Exclude<NotifyChannel, "ema
|
|
|
565
586
|
url: string;
|
|
566
587
|
expiresAt: string;
|
|
567
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
|
+
}>;
|
|
596
|
+
/**
|
|
597
|
+
* เก็บ subscription ของเบราว์เซอร์หนึ่งเครื่องไว้ที่ hub (ผูกกับแอปที่เรียกเสมอ)
|
|
598
|
+
* เรียกซ้ำด้วย endpoint เดิมได้ = อัปเดตกุญแจ/เปิดใช้คืน · `piUserId` ให้ hub ลบตามบัญชีที่ถูกลบได้
|
|
599
|
+
*/
|
|
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
|
+
}>;
|
|
568
616
|
|
|
569
617
|
/** Store ใน memory ของ process (ค่าเริ่มต้น) — state หายเมื่อ restart และไม่แชร์ข้าม instance */
|
|
570
618
|
declare function createMemoryStore(): Store;
|
|
@@ -589,4 +637,4 @@ declare function createRedisStore(client: RedisLike, opts?: {
|
|
|
589
637
|
prefix?: string;
|
|
590
638
|
}): Store;
|
|
591
639
|
|
|
592
|
-
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 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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1122,6 +1122,29 @@ async function createLink(email, channel = "telegram", opts = {}) {
|
|
|
1122
1122
|
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1123
1123
|
});
|
|
1124
1124
|
}
|
|
1125
|
+
async function getPushConfig(opts = {}) {
|
|
1126
|
+
return hubRequest(opts, "/api/v1/push/config", { timeoutMs: NOTIFY_TIMEOUT_MS.recipient });
|
|
1127
|
+
}
|
|
1128
|
+
async function savePushSubscription(email, subscription, opts = {}) {
|
|
1129
|
+
const { label, piUserId, ...rest } = opts;
|
|
1130
|
+
return hubRequest(rest, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1131
|
+
method: "POST",
|
|
1132
|
+
body: { subscription, ...label ? { label } : {}, ...piUserId ? { piUserId } : {} },
|
|
1133
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
async function deletePushSubscription(email, endpoint, opts = {}) {
|
|
1137
|
+
return hubRequest(opts, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1138
|
+
method: "DELETE",
|
|
1139
|
+
body: { endpoint },
|
|
1140
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
async function listPushDevices(email, opts = {}) {
|
|
1144
|
+
return hubRequest(opts, `/api/v1/recipients/${encodeURIComponent(email)}/push`, {
|
|
1145
|
+
timeoutMs: NOTIFY_TIMEOUT_MS.recipient
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1125
1148
|
|
|
1126
1149
|
// src/stores/redis.ts
|
|
1127
1150
|
function createRedisStore(client, opts = {}) {
|
|
@@ -1169,15 +1192,19 @@ export {
|
|
|
1169
1192
|
createResendProvider,
|
|
1170
1193
|
createSmtpProvider,
|
|
1171
1194
|
createZeptoMailProvider,
|
|
1195
|
+
deletePushSubscription,
|
|
1172
1196
|
formatAddress,
|
|
1173
1197
|
getMailer,
|
|
1198
|
+
getPushConfig,
|
|
1174
1199
|
getRecipient,
|
|
1175
1200
|
listProviders,
|
|
1201
|
+
listPushDevices,
|
|
1176
1202
|
notify,
|
|
1177
1203
|
parseAddress,
|
|
1178
1204
|
parseAddressList,
|
|
1179
1205
|
providersFromEnv,
|
|
1180
1206
|
requireFrom,
|
|
1181
1207
|
resetMailer,
|
|
1208
|
+
savePushSubscription,
|
|
1182
1209
|
sendMail
|
|
1183
1210
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/notify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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 อัตโนมัติ",
|