@suphark/notify 0.9.0 → 0.10.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 +20 -0
- package/dist/{chunk-AAB6GK3Z.js → chunk-CWZP54ZN.js} +42 -8
- package/dist/{chunk-XVD6P4HN.js → chunk-NNMV7FZ3.js} +92 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +8 -2
- package/dist/next.js +2 -1
- package/dist/{notify-Dgzxhh-U.d.ts → notify-D-PhiDzT.d.ts} +16 -0
- package/dist/template.d.ts +19 -2
- package/dist/template.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,22 @@ await sendMail({ to, template: "po-approved", data: { name, po: { no: "PO-9" },
|
|
|
72
72
|
- render เองฝั่งแอป (เช่นหน้าดูตัวอย่าง): `import { renderEmailTemplate } from "@suphark/notify/template"` — ตัวเดียวกับที่ hub ใช้ ไม่พึ่ง node
|
|
73
73
|
- `template` ส่งได้เฉพาะผ่าน hub — provider ในเครื่องไม่ถูกใช้ (ไม่มีทางสำรองเมื่อ hub ล่ม) อีเมลสำคัญที่ต้องมีสำรองให้ render เองแล้วส่ง `html`
|
|
74
74
|
|
|
75
|
+
### อีเมลล็อกอินมาตรฐาน + ข้อความสำรองเมื่อ Pi Notify ล่ม (0.10.0)
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
await sendMail({
|
|
79
|
+
to: user.email,
|
|
80
|
+
template: "auth.magic-link", // หรือ auth.reset-password / auth.verify-email
|
|
81
|
+
data: { url: magicLinkUrl, expiresIn: "15 นาที" }, // appName ไม่ต้องส่ง — hub เติมชื่อแอปให้
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- ใช้ได้ทุกแอปทันทีโดยไม่ต้องสร้างเทมเพลต (ฉบับสวยอยู่ที่ Pi Notify) — อยากปรับข้อความเฉพาะแอป สร้างเทมเพลตชื่อเดียวกันทับ (คอนโซล: แอป → เทมเพลตอีเมล → "ปรับสำหรับแอป")
|
|
86
|
+
- **ติดต่อ Pi Notify ไม่ได้** (ล่ม/เครือข่าย/ไม่ตอบ) และแอปตั้ง provider ในเครื่องไว้ (SMTP/Resend) → SDK ส่งข้อความพื้นฐาน (หัวเรื่อง + ลิงก์ + อายุลิงก์) ผ่าน provider ในเครื่องแทน ผล `fallback: true` — ผู้ใช้ยังล็อกอินได้
|
|
87
|
+
- Pi Notify ตอบว่าข้อมูลผิด (เช่นไม่ส่ง `url`) → โยน error ไม่ส่งข้อความสำรอง
|
|
88
|
+
- เทมเพลตของแอปเองใส่ข้อความสำรองได้: `sendMail({ template: "po-approved", data, fallback: { subject, text, html? } })`
|
|
89
|
+
- ข้อความที่ส่งทางสำรองไม่มีใน log ของ Pi Notify (ดูจาก `onEvent` type `"fallback"`)
|
|
90
|
+
|
|
75
91
|
### อีเมลที่ต้องถึงแน่: `deliverWithin` + `idempotencyKey` กับ `sendMail` (0.8.0)
|
|
76
92
|
|
|
77
93
|
```ts
|
|
@@ -487,6 +503,10 @@ Pi Notify มี contract test (`apps/notify/src/test/contract/`) เก็บ b
|
|
|
487
503
|
|
|
488
504
|
## Changelog
|
|
489
505
|
|
|
506
|
+
- 0.10.0 — **อีเมลล็อกอินมาตรฐาน + ข้อความสำรอง** (เพิ่มอย่างเดียว; ต้องใช้ Pi Notify 2026-11-11+)
|
|
507
|
+
- เทมเพลตมาตรฐาน `auth.magic-link` / `auth.reset-password` / `auth.verify-email` ใช้ได้ทุกแอปโดยไม่ต้องสร้าง (สร้างชื่อเดียวกันเพื่อปรับ)
|
|
508
|
+
- ติดต่อ hub ไม่ได้ → ส่งข้อความพื้นฐานผ่าน provider ในเครื่อง (`SendResult.fallback`, event `"fallback"`) · `Message.fallback` สำหรับเทมเพลตของแอปเอง
|
|
509
|
+
- `listTemplates()` / `getTemplate()` คืนเทมเพลตมาตรฐานด้วย (`builtin: true`) · hub เติม `appName` ให้ทุกเทมเพลต
|
|
490
510
|
- 0.9.0 — **เทมเพลตอีเมลที่ hub** (เพิ่มอย่างเดียว; ต้องใช้ Pi Notify 2026-11-11+)
|
|
491
511
|
- `sendMail({ to, template, data })` — hub render เทมเพลตของแอป (`subject` ไม่บังคับเมื่อใช้ template)
|
|
492
512
|
- `listTemplates` / `getTemplate` / `saveTemplate` / `deleteTemplate` / `previewTemplate` — จัดการเทมเพลตจากแอป
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
builtinFallback
|
|
3
|
+
} from "./chunk-NNMV7FZ3.js";
|
|
4
|
+
|
|
1
5
|
// src/address.ts
|
|
2
6
|
var NAMED_RE = /^\s*(?:"?([^"<]*?)"?\s*)?<([^<>\s]+@[^<>\s]+)>\s*$/;
|
|
3
7
|
var EMAIL_RE = /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/;
|
|
@@ -807,6 +811,9 @@ function debugLogger(e) {
|
|
|
807
811
|
`${tag} ${e.provider} cooldown [${e.kind}] until ${new Date(e.untilMs).toISOString()} (failures=${e.failures})`
|
|
808
812
|
);
|
|
809
813
|
break;
|
|
814
|
+
case "fallback":
|
|
815
|
+
console.warn(`${tag} hub unreachable \u2192 sent fallback text for template ${e.template}`);
|
|
816
|
+
break;
|
|
810
817
|
case "recovered":
|
|
811
818
|
console.log(`${tag} ${e.provider} recovered`);
|
|
812
819
|
break;
|
|
@@ -913,6 +920,39 @@ function createMailer(options = {}) {
|
|
|
913
920
|
return pick;
|
|
914
921
|
}
|
|
915
922
|
async function send(message) {
|
|
923
|
+
if (!message?.template) return sendVia(message, "all");
|
|
924
|
+
const fallback = message.fallback ?? builtinFallback(message.template, message.data);
|
|
925
|
+
const hasHub = entries.some((e) => e.provider.type === "hub");
|
|
926
|
+
const hasLocal = entries.some((e) => e.provider.type !== "hub" && !e.disabled);
|
|
927
|
+
const asFallback = (fb) => ({
|
|
928
|
+
...message,
|
|
929
|
+
template: void 0,
|
|
930
|
+
data: void 0,
|
|
931
|
+
fallback: void 0,
|
|
932
|
+
subject: fb.subject,
|
|
933
|
+
text: fb.text,
|
|
934
|
+
html: fb.html
|
|
935
|
+
});
|
|
936
|
+
if (!hasHub) {
|
|
937
|
+
if (fallback && hasLocal)
|
|
938
|
+
return { ...await sendVia(asFallback(fallback), "local"), fallback: true };
|
|
939
|
+
throw new MailError(
|
|
940
|
+
`template "${message.template}" \u0E15\u0E49\u0E2D\u0E07\u0E2A\u0E48\u0E07\u0E1C\u0E48\u0E32\u0E19 Pi Notify hub \u2014 \u0E15\u0E31\u0E49\u0E07 PI_NOTIFY_KEY`,
|
|
941
|
+
{ kind: "invalid" }
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
return await sendVia(message, "hub");
|
|
946
|
+
} catch (err) {
|
|
947
|
+
const attempts = err instanceof AllProvidersFailedError ? err.attempts : [];
|
|
948
|
+
const invalid = err instanceof MailError && err.kind === "invalid" && attempts.length === 0 || attempts.some((a) => a.kind === "invalid");
|
|
949
|
+
if (!fallback || !hasLocal || invalid) throw err;
|
|
950
|
+
emit({ type: "fallback", template: message.template });
|
|
951
|
+
const r = await sendVia(asFallback(fallback), "local");
|
|
952
|
+
return { ...r, failedOver: true, fallback: true, attempts: [...attempts, ...r.attempts] };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async function sendVia(message, via) {
|
|
916
956
|
if (!message?.to || Array.isArray(message.to) && message.to.length === 0)
|
|
917
957
|
throw new MailError("Message has no recipients (to)", { kind: "invalid" });
|
|
918
958
|
if (!message.template) {
|
|
@@ -921,14 +961,8 @@ function createMailer(options = {}) {
|
|
|
921
961
|
throw new MailError("Message needs html or text", { kind: "invalid" });
|
|
922
962
|
}
|
|
923
963
|
let candidates = order();
|
|
924
|
-
if (
|
|
925
|
-
candidates = candidates.filter((e) => e.provider.type === "hub");
|
|
926
|
-
if (candidates.length === 0)
|
|
927
|
-
throw new MailError(
|
|
928
|
-
`template "${message.template}" \u0E15\u0E49\u0E2D\u0E07\u0E2A\u0E48\u0E07\u0E1C\u0E48\u0E32\u0E19 Pi Notify hub \u2014 \u0E15\u0E31\u0E49\u0E07 PI_NOTIFY_KEY`,
|
|
929
|
-
{ kind: "invalid" }
|
|
930
|
-
);
|
|
931
|
-
}
|
|
964
|
+
if (via !== "all")
|
|
965
|
+
candidates = candidates.filter((e) => e.provider.type === "hub" === (via === "hub"));
|
|
932
966
|
if (message.prefer) {
|
|
933
967
|
const i = candidates.findIndex((e) => e.provider.name === message.prefer);
|
|
934
968
|
if (i > 0) candidates = [candidates[i], ...candidates.filter((_, j) => j !== i)];
|
|
@@ -178,6 +178,93 @@ function renderEmailTemplate(t, data, opts = {}) {
|
|
|
178
178
|
const text = t.text?.trim() ? renderTemplate(t.text, data, { escape: false }) : toPlainText(body);
|
|
179
179
|
return { subject, html, text };
|
|
180
180
|
}
|
|
181
|
+
var BUILTIN_TEMPLATE_KEYS = [
|
|
182
|
+
"auth.magic-link",
|
|
183
|
+
"auth.reset-password",
|
|
184
|
+
"auth.verify-email"
|
|
185
|
+
];
|
|
186
|
+
function isBuiltinTemplate(key) {
|
|
187
|
+
return BUILTIN_TEMPLATE_KEYS.includes(key);
|
|
188
|
+
}
|
|
189
|
+
var AUTH_VARIABLES = [
|
|
190
|
+
{
|
|
191
|
+
name: "url",
|
|
192
|
+
label: "\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E17\u0E35\u0E48\u0E1C\u0E39\u0E49\u0E43\u0E0A\u0E49\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E14",
|
|
193
|
+
sample: "https://app.example.com/auth/verify?token=abc",
|
|
194
|
+
required: true
|
|
195
|
+
},
|
|
196
|
+
{ name: "appName", label: "\u0E0A\u0E37\u0E48\u0E2D\u0E41\u0E2D\u0E1B (hub \u0E40\u0E15\u0E34\u0E21\u0E43\u0E2B\u0E49\u0E16\u0E49\u0E32\u0E44\u0E21\u0E48\u0E2A\u0E48\u0E07)", sample: "ESTAR Supply Chain" },
|
|
197
|
+
{ name: "expiresIn", label: "\u0E2D\u0E32\u0E22\u0E38\u0E25\u0E34\u0E07\u0E01\u0E4C \u0E40\u0E0A\u0E48\u0E19 15 \u0E19\u0E32\u0E17\u0E35", sample: "15 \u0E19\u0E32\u0E17\u0E35" }
|
|
198
|
+
];
|
|
199
|
+
var BUTTON = "display:inline-block;background:#1d4ed8;color:#ffffff;text-decoration:none;padding:12px 20px;border-radius:8px;font-weight:600";
|
|
200
|
+
function authHtml(heading, lead, button, note) {
|
|
201
|
+
return `<h2 style="margin:0 0 12px;font-size:20px">${heading}</h2>
|
|
202
|
+
<p style="margin:0 0 20px">${lead}</p>
|
|
203
|
+
<p style="margin:0 0 20px"><a href="{{url}}" style="${BUTTON}">${button}</a></p>
|
|
204
|
+
<p style="margin:0 0 8px;font-size:13px;color:#475569">${note}{{#if expiresIn}} \u0E25\u0E34\u0E07\u0E01\u0E4C\u0E2B\u0E21\u0E14\u0E2D\u0E32\u0E22\u0E38\u0E43\u0E19 {{expiresIn}}{{/if}}</p>
|
|
205
|
+
<p style="margin:0 0 8px;font-size:13px;color:#475569">\u0E16\u0E49\u0E32\u0E1B\u0E38\u0E48\u0E21\u0E01\u0E14\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49 \u0E43\u0E2B\u0E49\u0E04\u0E31\u0E14\u0E25\u0E2D\u0E01\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E19\u0E35\u0E49\u0E44\u0E1B\u0E40\u0E1B\u0E34\u0E14\u0E43\u0E19\u0E40\u0E1A\u0E23\u0E32\u0E27\u0E4C\u0E40\u0E0B\u0E2D\u0E23\u0E4C:<br><span style="word-break:break-all">{{url}}</span></p>
|
|
206
|
+
<p style="margin:16px 0 0;font-size:13px;color:#475569">\u0E16\u0E49\u0E32\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E40\u0E1B\u0E47\u0E19\u0E04\u0E19\u0E02\u0E2D \u0E44\u0E21\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E17\u0E33\u0E2D\u0E30\u0E44\u0E23 \u2014 \u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E22\u0E31\u0E07\u0E1B\u0E25\u0E2D\u0E14\u0E20\u0E31\u0E22</p>`;
|
|
207
|
+
}
|
|
208
|
+
var BUILTIN_TEMPLATES = {
|
|
209
|
+
"auth.magic-link": {
|
|
210
|
+
name: "\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E39\u0E48\u0E23\u0E30\u0E1A\u0E1A (\u0E21\u0E32\u0E15\u0E23\u0E10\u0E32\u0E19)",
|
|
211
|
+
subject: "\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E39\u0E48\u0E23\u0E30\u0E1A\u0E1A{{#if appName}} {{appName}}{{/if}}",
|
|
212
|
+
html: authHtml(
|
|
213
|
+
"\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E39\u0E48\u0E23\u0E30\u0E1A\u0E1A{{#if appName}} {{appName}}{{/if}}",
|
|
214
|
+
"\u0E01\u0E14\u0E1B\u0E38\u0E48\u0E21\u0E14\u0E49\u0E32\u0E19\u0E25\u0E48\u0E32\u0E07\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E39\u0E48\u0E23\u0E30\u0E1A\u0E1A \u0E44\u0E21\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E43\u0E0A\u0E49\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19",
|
|
215
|
+
"\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E39\u0E48\u0E23\u0E30\u0E1A\u0E1A",
|
|
216
|
+
"\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E19\u0E35\u0E49\u0E43\u0E0A\u0E49\u0E44\u0E14\u0E49\u0E04\u0E23\u0E31\u0E49\u0E07\u0E40\u0E14\u0E35\u0E22\u0E27"
|
|
217
|
+
),
|
|
218
|
+
variables: AUTH_VARIABLES
|
|
219
|
+
},
|
|
220
|
+
"auth.reset-password": {
|
|
221
|
+
name: "\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48 (\u0E21\u0E32\u0E15\u0E23\u0E10\u0E32\u0E19)",
|
|
222
|
+
subject: "\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48{{#if appName}} {{appName}}{{/if}}",
|
|
223
|
+
html: authHtml(
|
|
224
|
+
"\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48",
|
|
225
|
+
"\u0E21\u0E35\u0E04\u0E33\u0E02\u0E2D\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48\u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A\u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13{{#if appName}} \u0E43\u0E19 {{appName}}{{/if}} \u0E01\u0E14\u0E1B\u0E38\u0E48\u0E21\u0E14\u0E49\u0E32\u0E19\u0E25\u0E48\u0E32\u0E07\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48",
|
|
226
|
+
"\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48",
|
|
227
|
+
"\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E19\u0E35\u0E49\u0E43\u0E0A\u0E49\u0E44\u0E14\u0E49\u0E04\u0E23\u0E31\u0E49\u0E07\u0E40\u0E14\u0E35\u0E22\u0E27"
|
|
228
|
+
),
|
|
229
|
+
variables: AUTH_VARIABLES
|
|
230
|
+
},
|
|
231
|
+
"auth.verify-email": {
|
|
232
|
+
name: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25 (\u0E21\u0E32\u0E15\u0E23\u0E10\u0E32\u0E19)",
|
|
233
|
+
subject: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13{{#if appName}} \u2014 {{appName}}{{/if}}",
|
|
234
|
+
html: authHtml(
|
|
235
|
+
"\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13",
|
|
236
|
+
"\u0E01\u0E14\u0E1B\u0E38\u0E48\u0E21\u0E14\u0E49\u0E32\u0E19\u0E25\u0E48\u0E32\u0E07\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E27\u0E48\u0E32\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E19\u0E35\u0E49\u0E40\u0E1B\u0E47\u0E19\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13{{#if appName}} \u0E41\u0E25\u0E30\u0E40\u0E23\u0E34\u0E48\u0E21\u0E43\u0E0A\u0E49 {{appName}}{{/if}}",
|
|
237
|
+
"\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25",
|
|
238
|
+
"\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E04\u0E23\u0E31\u0E49\u0E07\u0E40\u0E14\u0E35\u0E22\u0E27\u0E01\u0E47\u0E1E\u0E2D"
|
|
239
|
+
),
|
|
240
|
+
variables: AUTH_VARIABLES
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
var FALLBACK_SUBJECT = {
|
|
244
|
+
"auth.magic-link": "\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E39\u0E48\u0E23\u0E30\u0E1A\u0E1A",
|
|
245
|
+
"auth.reset-password": "\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48",
|
|
246
|
+
"auth.verify-email": "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25"
|
|
247
|
+
};
|
|
248
|
+
function builtinFallback(key, data) {
|
|
249
|
+
if (!isBuiltinTemplate(key)) return null;
|
|
250
|
+
const url = typeof data?.url === "string" ? data.url : "";
|
|
251
|
+
if (!url) return null;
|
|
252
|
+
const app = typeof data?.appName === "string" && data.appName ? data.appName : "";
|
|
253
|
+
const expires = typeof data?.expiresIn === "string" && data.expiresIn ? data.expiresIn : "";
|
|
254
|
+
const subject = `${FALLBACK_SUBJECT[key]}${app ? ` ${app}` : ""}`;
|
|
255
|
+
const lines = [
|
|
256
|
+
`${FALLBACK_SUBJECT[key]}${app ? ` (${app})` : ""}`,
|
|
257
|
+
"",
|
|
258
|
+
"\u0E40\u0E1B\u0E34\u0E14\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E19\u0E35\u0E49:",
|
|
259
|
+
url,
|
|
260
|
+
"",
|
|
261
|
+
...expires ? [`\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E2B\u0E21\u0E14\u0E2D\u0E32\u0E22\u0E38\u0E43\u0E19 ${expires}`] : [],
|
|
262
|
+
"\u0E16\u0E49\u0E32\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E40\u0E1B\u0E47\u0E19\u0E04\u0E19\u0E02\u0E2D \u0E44\u0E21\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E17\u0E33\u0E2D\u0E30\u0E44\u0E23"
|
|
263
|
+
];
|
|
264
|
+
const text = lines.join("\n");
|
|
265
|
+
const html = `<p>${escapeHtml(lines[0])}</p><p><a href="${escapeHtml(url)}">${escapeHtml(url)}</a></p>${expires ? `<p>\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E2B\u0E21\u0E14\u0E2D\u0E32\u0E22\u0E38\u0E43\u0E19 ${escapeHtml(expires)}</p>` : ""}<p>\u0E16\u0E49\u0E32\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E40\u0E1B\u0E47\u0E19\u0E04\u0E19\u0E02\u0E2D \u0E44\u0E21\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E17\u0E33\u0E2D\u0E30\u0E44\u0E23</p>`;
|
|
266
|
+
return { subject, text, html };
|
|
267
|
+
}
|
|
181
268
|
|
|
182
269
|
export {
|
|
183
270
|
looksLikeHtml,
|
|
@@ -189,5 +276,9 @@ export {
|
|
|
189
276
|
templateVariableNames,
|
|
190
277
|
sampleData,
|
|
191
278
|
wrapEmailLayout,
|
|
192
|
-
renderEmailTemplate
|
|
279
|
+
renderEmailTemplate,
|
|
280
|
+
BUILTIN_TEMPLATE_KEYS,
|
|
281
|
+
isBuiltinTemplate,
|
|
282
|
+
BUILTIN_TEMPLATES,
|
|
283
|
+
builtinFallback
|
|
193
284
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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-
|
|
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 RequestStatus, u as SkipReason, v as createLink, w as deletePushSubscription, x as getChannelStatus, y as getPushConfig, z as getRecipient, B as getRequestStatus, D as hasNotifyKey, F as listPushDevices, G as notify, H as notifyTopic, I as savePushSubscription, J as toEmails } from './notify-
|
|
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-D-PhiDzT.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 RequestStatus, u as SkipReason, v as createLink, w as deletePushSubscription, x as getChannelStatus, y as getPushConfig, z as getRecipient, B as getRequestStatus, D as hasNotifyKey, F as listPushDevices, G as notify, H as notifyTopic, I as savePushSubscription, J as toEmails } from './notify-D-PhiDzT.js';
|
|
3
3
|
import { TemplateVariable } from './template.js';
|
|
4
|
-
export { TemplateError, renderEmailTemplate } from './template.js';
|
|
4
|
+
export { BUILTIN_TEMPLATE_KEYS, BuiltinTemplateKey, TemplateError, builtinFallback, isBuiltinTemplate, renderEmailTemplate } from './template.js';
|
|
5
5
|
|
|
6
6
|
/** แปลง "ชื่อ <a@b.com>" / "a@b.com" / { name, email } → { name, email } */
|
|
7
7
|
declare function parseAddress(input: Address): ParsedAddress;
|
|
@@ -479,6 +479,8 @@ interface EmailTemplateInfo {
|
|
|
479
479
|
enabled: boolean;
|
|
480
480
|
version: number;
|
|
481
481
|
updatedAt: string;
|
|
482
|
+
/** (0.10.0) true = เทมเพลตมาตรฐานของ Pi Notify (auth.*) ที่แอปยังไม่ได้สร้างทับ */
|
|
483
|
+
builtin?: boolean;
|
|
482
484
|
}
|
|
483
485
|
interface EmailTemplateInput {
|
|
484
486
|
name: string;
|
package/dist/index.js
CHANGED
|
@@ -41,14 +41,17 @@ import {
|
|
|
41
41
|
sendMail,
|
|
42
42
|
sendMailSafe,
|
|
43
43
|
toEmails
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-CWZP54ZN.js";
|
|
45
45
|
import {
|
|
46
|
+
BUILTIN_TEMPLATE_KEYS,
|
|
46
47
|
TemplateError,
|
|
48
|
+
builtinFallback,
|
|
49
|
+
isBuiltinTemplate,
|
|
47
50
|
looksLikeHtml,
|
|
48
51
|
renderEmailTemplate,
|
|
49
52
|
resolveFormat,
|
|
50
53
|
toPlainText
|
|
51
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-NNMV7FZ3.js";
|
|
52
55
|
|
|
53
56
|
// src/push.ts
|
|
54
57
|
function createPushHandlers(options) {
|
|
@@ -227,12 +230,14 @@ async function previewTemplate(key, input = {}, opts = {}) {
|
|
|
227
230
|
}
|
|
228
231
|
export {
|
|
229
232
|
AllProvidersFailedError,
|
|
233
|
+
BUILTIN_TEMPLATE_KEYS,
|
|
230
234
|
DEFAULT_COOLDOWN,
|
|
231
235
|
DEFAULT_HUB_URL,
|
|
232
236
|
MailError,
|
|
233
237
|
NOTIFY_TIMEOUT_MS,
|
|
234
238
|
SMTP_PRESETS,
|
|
235
239
|
TemplateError,
|
|
240
|
+
builtinFallback,
|
|
236
241
|
classifyError,
|
|
237
242
|
configureMailer,
|
|
238
243
|
createConsoleProvider,
|
|
@@ -256,6 +261,7 @@ export {
|
|
|
256
261
|
getRequestStatus,
|
|
257
262
|
getTemplate,
|
|
258
263
|
hasNotifyKey,
|
|
264
|
+
isBuiltinTemplate,
|
|
259
265
|
listProviders,
|
|
260
266
|
listPushDevices,
|
|
261
267
|
listTemplates,
|
package/dist/next.js
CHANGED
|
@@ -47,6 +47,15 @@ interface Message {
|
|
|
47
47
|
template?: string;
|
|
48
48
|
/** ค่าตัวแปรของเทมเพลต (`{{name}}`, `{{#each items}}` …) */
|
|
49
49
|
data?: Record<string, unknown>;
|
|
50
|
+
/**
|
|
51
|
+
* (0.10.0) ข้อความสำรองเมื่อติดต่อ Pi Notify ไม่ได้ — ส่งผ่าน provider ในเครื่อง (SMTP/Resend)
|
|
52
|
+
* เทมเพลตมาตรฐาน `auth.*` มีข้อความสำรองในตัวแล้ว ไม่ต้องใส่
|
|
53
|
+
*/
|
|
54
|
+
fallback?: {
|
|
55
|
+
subject: string;
|
|
56
|
+
text: string;
|
|
57
|
+
html?: string;
|
|
58
|
+
};
|
|
50
59
|
}
|
|
51
60
|
/** ข้อมูลตัวเลือกที่แอปควรรู้ — แค่ชื่อ/ประเภท/พร้อมใช้ไหม */
|
|
52
61
|
interface ProviderInfo {
|
|
@@ -113,6 +122,8 @@ interface SendResult {
|
|
|
113
122
|
attempts: Attempt[];
|
|
114
123
|
/** (0.8.0) hub รับไว้ส่งซ้ำเอง (ส่ง deliverWithin) — ยังไม่ถึงผู้รับ, `id` = deliveryId ในคิวของ hub */
|
|
115
124
|
queued?: boolean;
|
|
125
|
+
/** (0.10.0) ส่งข้อความสำรองของเทมเพลตผ่าน provider ในเครื่อง เพราะติดต่อ Pi Notify ไม่ได้ */
|
|
126
|
+
fallback?: boolean;
|
|
116
127
|
}
|
|
117
128
|
interface Quota {
|
|
118
129
|
perHour?: number;
|
|
@@ -178,6 +189,11 @@ type MailerEvent = {
|
|
|
178
189
|
} | {
|
|
179
190
|
type: "exhausted";
|
|
180
191
|
attempts: Attempt[];
|
|
192
|
+
}
|
|
193
|
+
/** (0.10.0) ติดต่อ hub ไม่ได้ จึงส่งข้อความสำรองของเทมเพลตผ่าน provider ในเครื่อง */
|
|
194
|
+
| {
|
|
195
|
+
type: "fallback";
|
|
196
|
+
template: string;
|
|
181
197
|
};
|
|
182
198
|
interface QuotaUsage {
|
|
183
199
|
window: "hour" | "day" | "month";
|
package/dist/template.d.ts
CHANGED
|
@@ -74,6 +74,23 @@ declare function renderEmailTemplate(t: EmailTemplateSource, data: Data, opts?:
|
|
|
74
74
|
html: string;
|
|
75
75
|
text: string;
|
|
76
76
|
};
|
|
77
|
+
/** ชื่อเทมเพลตมาตรฐาน — ทุกแอปใช้ได้โดยไม่ต้องสร้าง (สร้างชื่อเดียวกันใน Pi Notify เพื่อปรับเฉพาะแอป) */
|
|
78
|
+
declare const BUILTIN_TEMPLATE_KEYS: readonly ["auth.magic-link", "auth.reset-password", "auth.verify-email"];
|
|
79
|
+
type BuiltinTemplateKey = (typeof BUILTIN_TEMPLATE_KEYS)[number];
|
|
80
|
+
declare function isBuiltinTemplate(key: string): key is BuiltinTemplateKey;
|
|
81
|
+
/** ฉบับเต็ม (hub ใช้เมื่อแอปไม่ได้สร้างเทมเพลตชื่อนี้เอง) */
|
|
82
|
+
declare const BUILTIN_TEMPLATES: Record<BuiltinTemplateKey, EmailTemplateSource & {
|
|
83
|
+
name: string;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* ข้อความสำรองแบบพื้นฐาน (ไม่สวย แต่ใช้ล็อกอินได้) — SDK ส่งผ่าน provider ในเครื่องเมื่อติดต่อ Pi Notify ไม่ได้
|
|
87
|
+
* คืน null เมื่อไม่ใช่เทมเพลตมาตรฐานหรือไม่มี url
|
|
88
|
+
*/
|
|
89
|
+
declare function builtinFallback(key: string, data: Record<string, unknown> | undefined): {
|
|
90
|
+
subject: string;
|
|
91
|
+
text: string;
|
|
92
|
+
html: string;
|
|
93
|
+
} | null;
|
|
77
94
|
|
|
78
|
-
export { TemplateError, parseTemplate, renderEmailTemplate, renderTemplate, sampleData, templateVariableNames, wrapEmailLayout };
|
|
79
|
-
export type { EmailTemplateSource, TemplateVariable };
|
|
95
|
+
export { BUILTIN_TEMPLATES, BUILTIN_TEMPLATE_KEYS, TemplateError, builtinFallback, isBuiltinTemplate, parseTemplate, renderEmailTemplate, renderTemplate, sampleData, templateVariableNames, wrapEmailLayout };
|
|
96
|
+
export type { BuiltinTemplateKey, EmailTemplateSource, TemplateVariable };
|
package/dist/template.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BUILTIN_TEMPLATES,
|
|
3
|
+
BUILTIN_TEMPLATE_KEYS,
|
|
2
4
|
TemplateError,
|
|
5
|
+
builtinFallback,
|
|
6
|
+
isBuiltinTemplate,
|
|
3
7
|
parseTemplate,
|
|
4
8
|
renderEmailTemplate,
|
|
5
9
|
renderTemplate,
|
|
6
10
|
sampleData,
|
|
7
11
|
templateVariableNames,
|
|
8
12
|
wrapEmailLayout
|
|
9
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-NNMV7FZ3.js";
|
|
10
14
|
export {
|
|
15
|
+
BUILTIN_TEMPLATES,
|
|
16
|
+
BUILTIN_TEMPLATE_KEYS,
|
|
11
17
|
TemplateError,
|
|
18
|
+
builtinFallback,
|
|
19
|
+
isBuiltinTemplate,
|
|
12
20
|
parseTemplate,
|
|
13
21
|
renderEmailTemplate,
|
|
14
22
|
renderTemplate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/notify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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 อัตโนมัติ",
|