@suphark/notify 0.9.0 → 0.10.1

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
@@ -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,11 @@ Pi Notify มี contract test (`apps/notify/src/test/contract/`) เก็บ b
487
503
 
488
504
  ## Changelog
489
505
 
506
+ - 0.10.1 — ข้อความสำรองของเทมเพลต `auth.*` ใช้ชื่อผู้ส่ง (`from` / `MAIL_FROM`) เป็นชื่อแอปในหัวเรื่องเมื่อแอปไม่ได้ส่ง `appName` (เดิมหัวเรื่องไม่มีชื่อแอป)
507
+ - 0.10.0 — **อีเมลล็อกอินมาตรฐาน + ข้อความสำรอง** (เพิ่มอย่างเดียว; ต้องใช้ Pi Notify 2026-11-11+)
508
+ - เทมเพลตมาตรฐาน `auth.magic-link` / `auth.reset-password` / `auth.verify-email` ใช้ได้ทุกแอปโดยไม่ต้องสร้าง (สร้างชื่อเดียวกันเพื่อปรับ)
509
+ - ติดต่อ hub ไม่ได้ → ส่งข้อความพื้นฐานผ่าน provider ในเครื่อง (`SendResult.fallback`, event `"fallback"`) · `Message.fallback` สำหรับเทมเพลตของแอปเอง
510
+ - `listTemplates()` / `getTemplate()` คืนเทมเพลตมาตรฐานด้วย (`builtin: true`) · hub เติม `appName` ให้ทุกเทมเพลต
490
511
  - 0.9.0 — **เทมเพลตอีเมลที่ hub** (เพิ่มอย่างเดียว; ต้องใช้ Pi Notify 2026-11-11+)
491
512
  - `sendMail({ to, template, data })` — hub render เทมเพลตของแอป (`subject` ไม่บังคับเมื่อใช้ template)
492
513
  - `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,49 @@ 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 fromName = (() => {
925
+ try {
926
+ return (message.from !== void 0 ? parseAddress(message.from) : defaultFrom)?.name;
927
+ } catch {
928
+ return void 0;
929
+ }
930
+ })();
931
+ const fallback = message.fallback ?? builtinFallback(message.template, {
932
+ ...fromName ? { appName: fromName } : {},
933
+ ...message.data
934
+ });
935
+ const hasHub = entries.some((e) => e.provider.type === "hub");
936
+ const hasLocal = entries.some((e) => e.provider.type !== "hub" && !e.disabled);
937
+ const asFallback = (fb) => ({
938
+ ...message,
939
+ template: void 0,
940
+ data: void 0,
941
+ fallback: void 0,
942
+ subject: fb.subject,
943
+ text: fb.text,
944
+ html: fb.html
945
+ });
946
+ if (!hasHub) {
947
+ if (fallback && hasLocal)
948
+ return { ...await sendVia(asFallback(fallback), "local"), fallback: true };
949
+ throw new MailError(
950
+ `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`,
951
+ { kind: "invalid" }
952
+ );
953
+ }
954
+ try {
955
+ return await sendVia(message, "hub");
956
+ } catch (err) {
957
+ const attempts = err instanceof AllProvidersFailedError ? err.attempts : [];
958
+ const invalid = err instanceof MailError && err.kind === "invalid" && attempts.length === 0 || attempts.some((a) => a.kind === "invalid");
959
+ if (!fallback || !hasLocal || invalid) throw err;
960
+ emit({ type: "fallback", template: message.template });
961
+ const r = await sendVia(asFallback(fallback), "local");
962
+ return { ...r, failedOver: true, fallback: true, attempts: [...attempts, ...r.attempts] };
963
+ }
964
+ }
965
+ async function sendVia(message, via) {
916
966
  if (!message?.to || Array.isArray(message.to) && message.to.length === 0)
917
967
  throw new MailError("Message has no recipients (to)", { kind: "invalid" });
918
968
  if (!message.template) {
@@ -921,14 +971,8 @@ function createMailer(options = {}) {
921
971
  throw new MailError("Message needs html or text", { kind: "invalid" });
922
972
  }
923
973
  let candidates = order();
924
- if (message.template) {
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
- }
974
+ if (via !== "all")
975
+ candidates = candidates.filter((e) => e.provider.type === "hub" === (via === "hub"));
932
976
  if (message.prefer) {
933
977
  const i = candidates.findIndex((e) => e.provider.name === message.prefer);
934
978
  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-Dgzxhh-U.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-Dgzxhh-U.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-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-AAB6GK3Z.js";
44
+ } from "./chunk-JN7P2CVF.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-XVD6P4HN.js";
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
@@ -2,7 +2,8 @@ import {
2
2
  notifySafe,
3
3
  notifyTopicSafe,
4
4
  sendMailSafe
5
- } from "./chunk-AAB6GK3Z.js";
5
+ } from "./chunk-JN7P2CVF.js";
6
+ import "./chunk-NNMV7FZ3.js";
6
7
 
7
8
  // src/next.ts
8
9
  import { after } from "next/server";
@@ -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";
@@ -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-XVD6P4HN.js";
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.9.0",
3
+ "version": "0.10.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "ส่งอีเมล + แจ้งเตือน Telegram ผ่าน Pi Notify hub (notify.suphark.com) หรือส่งอีเมลตรงไป Resend/SMTP/Zoho/ZeptoMail ใน API เดียว พร้อม failover อัตโนมัติ",