@gravito/flare 4.0.1 → 4.0.2

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
@@ -4,18 +4,32 @@
4
4
 
5
5
  **Status**: v3.4.0 - Production ready with advanced features (Retries, Metrics, Batching, Timeout, Rate Limiting, Preference Driver).
6
6
 
7
- ## Features
8
-
9
- - **Zero runtime overhead**: Pure type wrappers that delegate to channel drivers
10
- - **Multi-channel delivery**: Mail, database, broadcast, Slack, SMS (Twilio & AWS SNS)
11
- - **High Performance**: Parallel channel execution and batch sending capabilities
12
- - **Reliability**: Built-in retry mechanism with exponential backoff and timeout protection
13
- - **Observability**: Comprehensive metrics with Prometheus support
14
- - **Developer Experience**: Strong typing, lifecycle hooks, and template system
15
- - **Queue support**: Works with `@gravito/stream` for async delivery with Lazy Loading
16
- - **Rate Limiting**: Channel-level rate limiting with Token Bucket algorithm
17
- - **Preference Driver**: User notification preferences with automatic channel filtering
18
- - **Middleware System**: Extensible middleware chain for custom notification processing
7
+ ## ✨ Key Features
8
+
9
+ - 🪐 **Galaxy-Ready Notifications**: Native integration with PlanetCore for universal user notification across all Satellites.
10
+ - 📡 **Multi-Channel Delivery**: Seamlessly broadcast via **Mail**, **SMS**, **Slack**, **Discord**, and **Push Notifications**.
11
+ - 🛠️ **Distributed Preference Management**: User-level notification settings that persist across the entire Galaxy.
12
+ - 🛡️ **Reliability Stack**: Built-in exponential backoff, timeout protection, and automatic retries.
13
+ - 🚀 **High Performance**: Parallel channel execution and batch sending optimized for Bun.
14
+ - ⚙️ **Queue Integration**: Offload notification delivery to `@gravito/stream` with zero configuration.
15
+
16
+ ## 🌌 Role in Galaxy Architecture
17
+
18
+ In the **Gravito Galaxy Architecture**, Flare acts as the **Communication Flux (Nervous System Extension)**.
19
+
20
+ - **Outbound Feedback**: Provides the primary mechanism for Satellites to communicate directly with users (e.g., "Your order has shipped" from the `Shop` Satellite).
21
+ - **Preference Guard**: Ensures that user privacy and communication preferences are respected globally, regardless of which Satellite initiates the notification.
22
+ - **Micro-Infrastructure Bridge**: Connects internal domain events to external communication providers (Twilio, AWS, SendGrid) without bloating Satellite logic.
23
+
24
+ ```mermaid
25
+ graph LR
26
+ S[Satellite: Order] -- "Notify" --> Flare{Flare Orbit}
27
+ Flare -->|Check| Pref[User Preferences]
28
+ Flare -->|Route| C1[Mail: SES]
29
+ Flare -->|Route| C2[SMS: Twilio]
30
+ Flare -->|Route| C3[Social: Slack]
31
+ Flare -.->|Queue| Stream[Stream Orbit]
32
+ ```
19
33
 
20
34
  ## Installation
21
35
 
@@ -276,6 +290,14 @@ OrbitFlare.configure({
276
290
  })
277
291
  ```
278
292
 
293
+ ## 📚 Documentation
294
+
295
+ Detailed guides and references for the Galaxy Architecture:
296
+
297
+ - [🏗️ **Architecture Overview**](./README.md) — Multi-channel notification core.
298
+ - [📡 **Notification Strategies**](./doc/NOTIFICATION_STRATEGIES.md) — **NEW**: Multi-channel routing, preferences, and queuing.
299
+ - [⚙️ **Queue Support**](#-queue-support) — Asynchronous delivery with `@gravito/stream`.
300
+
279
301
  ## API Reference
280
302
 
281
303
  ### NotificationManager
package/dist/index.cjs CHANGED
@@ -262,16 +262,23 @@ var TimeoutChannel = class {
262
262
  }
263
263
  const controller = new AbortController();
264
264
  const { signal } = controller;
265
+ let timeoutId;
266
+ let settled = false;
267
+ let handleExternalAbort;
265
268
  if (options?.signal) {
266
269
  if (options.signal.aborted) {
267
270
  throw new AbortError("Request was aborted before sending");
268
271
  }
269
- options.signal.addEventListener("abort", () => {
272
+ handleExternalAbort = () => {
270
273
  controller.abort();
271
- });
274
+ };
275
+ options.signal.addEventListener("abort", handleExternalAbort, { once: true });
272
276
  }
273
277
  const timeoutPromise = new Promise((_, reject) => {
274
- setTimeout(() => {
278
+ timeoutId = setTimeout(() => {
279
+ if (settled) {
280
+ return;
281
+ }
275
282
  if (this.config.onTimeout) {
276
283
  this.config.onTimeout(this.inner.constructor.name, notification);
277
284
  }
@@ -292,7 +299,17 @@ var TimeoutChannel = class {
292
299
  }
293
300
  throw error;
294
301
  });
295
- return Promise.race([sendPromise, timeoutPromise]);
302
+ try {
303
+ return await Promise.race([sendPromise, timeoutPromise]);
304
+ } finally {
305
+ settled = true;
306
+ if (timeoutId) {
307
+ clearTimeout(timeoutId);
308
+ }
309
+ if (options?.signal && handleExternalAbort) {
310
+ options.signal.removeEventListener("abort", handleExternalAbort);
311
+ }
312
+ }
296
313
  }
297
314
  };
298
315
 
@@ -797,7 +814,7 @@ var RateLimitMiddleware = class {
797
814
  * Create a new RateLimitMiddleware instance.
798
815
  *
799
816
  * @param config - Rate limit configuration for each channel
800
- * @param store - Optional cache store for distributed rate limiting
817
+ * @param store - Optional cache store for distributed rate limiting (future use)
801
818
  *
802
819
  * @example
803
820
  * ```typescript
@@ -814,7 +831,7 @@ var RateLimitMiddleware = class {
814
831
  */
815
832
  constructor(config, store) {
816
833
  this.config = config;
817
- this.store = store ?? new MemoryStore();
834
+ this.store = store || new MemoryStore();
818
835
  this.initializeBuckets();
819
836
  }
820
837
  /**
@@ -833,7 +850,6 @@ var RateLimitMiddleware = class {
833
850
  buckets = /* @__PURE__ */ new Map();
834
851
  /**
835
852
  * Cache store for distributed rate limiting.
836
- * 分散式限流使用的快取儲存
837
853
  */
838
854
  store;
839
855
  /**
@@ -1853,6 +1869,7 @@ var OrbitFlare = class _OrbitFlare {
1853
1869
  };
1854
1870
 
1855
1871
  // src/templates/NotificationTemplate.ts
1872
+ var import_core = require("@gravito/core");
1856
1873
  var TemplatedNotification = class extends Notification {
1857
1874
  data = {};
1858
1875
  with(data) {
@@ -1881,6 +1898,33 @@ var TemplatedNotification = class extends Notification {
1881
1898
  attachments: template.attachments
1882
1899
  };
1883
1900
  }
1901
+ /**
1902
+ * 將 Markdown 模板渲染為 HTML。
1903
+ * 先執行變數插值(`{{var}}`),再將 Markdown 轉為 HTML。
1904
+ * 內建 XSS 防護:過濾 script、iframe、event handler 等危險 HTML。
1905
+ *
1906
+ * 適用於郵件、Slack 等富文本通知頻道。
1907
+ *
1908
+ * @param template - Markdown 格式的模板字串
1909
+ * @returns 安全的 HTML 字串
1910
+ *
1911
+ * @example
1912
+ * ```typescript
1913
+ * const html = this.renderMarkdown('# Hello {{name}}')
1914
+ * await sendEmail({ htmlBody: html })
1915
+ * ```
1916
+ */
1917
+ renderMarkdown(template) {
1918
+ if (!template) {
1919
+ return "";
1920
+ }
1921
+ const interpolated = this.interpolate(template);
1922
+ const md = (0, import_core.getMarkdownAdapter)();
1923
+ const sanitizeCallbacks = (0, import_core.createHtmlRenderCallbacks)({
1924
+ html: (rawHtml) => sanitizeHtml(rawHtml)
1925
+ });
1926
+ return md.render(interpolated, sanitizeCallbacks);
1927
+ }
1884
1928
  interpolate(text) {
1885
1929
  return text.replace(/\{\{(\w+)\}\}/g, (_, key) => String(this.data[key] ?? `{{${key}}}`));
1886
1930
  }
@@ -1891,6 +1935,17 @@ var TemplatedNotification = class extends Notification {
1891
1935
  throw new Error("Notifiable does not have an email property");
1892
1936
  }
1893
1937
  };
1938
+ var DANGEROUS_TAGS = /^<\/?(?:script|iframe|object|embed|form|input|textarea|button|select|style|link|meta|base)\b[^>]*>$/i;
1939
+ var EVENT_HANDLER_ATTRS = /\s+on\w+\s*=/i;
1940
+ function sanitizeHtml(rawHtml) {
1941
+ if (DANGEROUS_TAGS.test(rawHtml.trim())) {
1942
+ return "";
1943
+ }
1944
+ if (EVENT_HANDLER_ATTRS.test(rawHtml)) {
1945
+ return "";
1946
+ }
1947
+ return rawHtml;
1948
+ }
1894
1949
 
1895
1950
  // src/index.ts
1896
1951
  init_middleware();
package/dist/index.d.cts CHANGED
@@ -1039,14 +1039,13 @@ declare class RateLimitMiddleware implements ChannelMiddleware {
1039
1039
  private buckets;
1040
1040
  /**
1041
1041
  * Cache store for distributed rate limiting.
1042
- * 分散式限流使用的快取儲存
1043
1042
  */
1044
- private store;
1043
+ store: CacheStore;
1045
1044
  /**
1046
1045
  * Create a new RateLimitMiddleware instance.
1047
1046
  *
1048
1047
  * @param config - Rate limit configuration for each channel
1049
- * @param store - Optional cache store for distributed rate limiting
1048
+ * @param store - Optional cache store for distributed rate limiting (future use)
1050
1049
  *
1051
1050
  * @example
1052
1051
  * ```typescript
@@ -1379,7 +1378,24 @@ declare abstract class TemplatedNotification extends Notification {
1379
1378
  protected slackTemplate?(): SlackTemplate;
1380
1379
  toMail(notifiable: Notifiable): MailMessage;
1381
1380
  toSlack(_notifiable: Notifiable): SlackMessage;
1382
- private interpolate;
1381
+ /**
1382
+ * 將 Markdown 模板渲染為 HTML。
1383
+ * 先執行變數插值(`{{var}}`),再將 Markdown 轉為 HTML。
1384
+ * 內建 XSS 防護:過濾 script、iframe、event handler 等危險 HTML。
1385
+ *
1386
+ * 適用於郵件、Slack 等富文本通知頻道。
1387
+ *
1388
+ * @param template - Markdown 格式的模板字串
1389
+ * @returns 安全的 HTML 字串
1390
+ *
1391
+ * @example
1392
+ * ```typescript
1393
+ * const html = this.renderMarkdown('# Hello {{name}}')
1394
+ * await sendEmail({ htmlBody: html })
1395
+ * ```
1396
+ */
1397
+ protected renderMarkdown(template: string): string;
1398
+ protected interpolate(text: string): string;
1383
1399
  private getRecipientEmail;
1384
1400
  }
1385
1401
 
package/dist/index.d.ts CHANGED
@@ -1039,14 +1039,13 @@ declare class RateLimitMiddleware implements ChannelMiddleware {
1039
1039
  private buckets;
1040
1040
  /**
1041
1041
  * Cache store for distributed rate limiting.
1042
- * 分散式限流使用的快取儲存
1043
1042
  */
1044
- private store;
1043
+ store: CacheStore;
1045
1044
  /**
1046
1045
  * Create a new RateLimitMiddleware instance.
1047
1046
  *
1048
1047
  * @param config - Rate limit configuration for each channel
1049
- * @param store - Optional cache store for distributed rate limiting
1048
+ * @param store - Optional cache store for distributed rate limiting (future use)
1050
1049
  *
1051
1050
  * @example
1052
1051
  * ```typescript
@@ -1379,7 +1378,24 @@ declare abstract class TemplatedNotification extends Notification {
1379
1378
  protected slackTemplate?(): SlackTemplate;
1380
1379
  toMail(notifiable: Notifiable): MailMessage;
1381
1380
  toSlack(_notifiable: Notifiable): SlackMessage;
1382
- private interpolate;
1381
+ /**
1382
+ * 將 Markdown 模板渲染為 HTML。
1383
+ * 先執行變數插值(`{{var}}`),再將 Markdown 轉為 HTML。
1384
+ * 內建 XSS 防護:過濾 script、iframe、event handler 等危險 HTML。
1385
+ *
1386
+ * 適用於郵件、Slack 等富文本通知頻道。
1387
+ *
1388
+ * @param template - Markdown 格式的模板字串
1389
+ * @returns 安全的 HTML 字串
1390
+ *
1391
+ * @example
1392
+ * ```typescript
1393
+ * const html = this.renderMarkdown('# Hello {{name}}')
1394
+ * await sendEmail({ htmlBody: html })
1395
+ * ```
1396
+ */
1397
+ protected renderMarkdown(template: string): string;
1398
+ protected interpolate(text: string): string;
1383
1399
  private getRecipientEmail;
1384
1400
  }
1385
1401
 
package/dist/index.js CHANGED
@@ -220,16 +220,23 @@ var TimeoutChannel = class {
220
220
  }
221
221
  const controller = new AbortController();
222
222
  const { signal } = controller;
223
+ let timeoutId;
224
+ let settled = false;
225
+ let handleExternalAbort;
223
226
  if (options?.signal) {
224
227
  if (options.signal.aborted) {
225
228
  throw new AbortError("Request was aborted before sending");
226
229
  }
227
- options.signal.addEventListener("abort", () => {
230
+ handleExternalAbort = () => {
228
231
  controller.abort();
229
- });
232
+ };
233
+ options.signal.addEventListener("abort", handleExternalAbort, { once: true });
230
234
  }
231
235
  const timeoutPromise = new Promise((_, reject) => {
232
- setTimeout(() => {
236
+ timeoutId = setTimeout(() => {
237
+ if (settled) {
238
+ return;
239
+ }
233
240
  if (this.config.onTimeout) {
234
241
  this.config.onTimeout(this.inner.constructor.name, notification);
235
242
  }
@@ -250,7 +257,17 @@ var TimeoutChannel = class {
250
257
  }
251
258
  throw error;
252
259
  });
253
- return Promise.race([sendPromise, timeoutPromise]);
260
+ try {
261
+ return await Promise.race([sendPromise, timeoutPromise]);
262
+ } finally {
263
+ settled = true;
264
+ if (timeoutId) {
265
+ clearTimeout(timeoutId);
266
+ }
267
+ if (options?.signal && handleExternalAbort) {
268
+ options.signal.removeEventListener("abort", handleExternalAbort);
269
+ }
270
+ }
254
271
  }
255
272
  };
256
273
 
@@ -755,7 +772,7 @@ var RateLimitMiddleware = class {
755
772
  * Create a new RateLimitMiddleware instance.
756
773
  *
757
774
  * @param config - Rate limit configuration for each channel
758
- * @param store - Optional cache store for distributed rate limiting
775
+ * @param store - Optional cache store for distributed rate limiting (future use)
759
776
  *
760
777
  * @example
761
778
  * ```typescript
@@ -772,7 +789,7 @@ var RateLimitMiddleware = class {
772
789
  */
773
790
  constructor(config, store) {
774
791
  this.config = config;
775
- this.store = store ?? new MemoryStore();
792
+ this.store = store || new MemoryStore();
776
793
  this.initializeBuckets();
777
794
  }
778
795
  /**
@@ -791,7 +808,6 @@ var RateLimitMiddleware = class {
791
808
  buckets = /* @__PURE__ */ new Map();
792
809
  /**
793
810
  * Cache store for distributed rate limiting.
794
- * 分散式限流使用的快取儲存
795
811
  */
796
812
  store;
797
813
  /**
@@ -1811,6 +1827,7 @@ var OrbitFlare = class _OrbitFlare {
1811
1827
  };
1812
1828
 
1813
1829
  // src/templates/NotificationTemplate.ts
1830
+ import { createHtmlRenderCallbacks, getMarkdownAdapter } from "@gravito/core";
1814
1831
  var TemplatedNotification = class extends Notification {
1815
1832
  data = {};
1816
1833
  with(data) {
@@ -1839,6 +1856,33 @@ var TemplatedNotification = class extends Notification {
1839
1856
  attachments: template.attachments
1840
1857
  };
1841
1858
  }
1859
+ /**
1860
+ * 將 Markdown 模板渲染為 HTML。
1861
+ * 先執行變數插值(`{{var}}`),再將 Markdown 轉為 HTML。
1862
+ * 內建 XSS 防護:過濾 script、iframe、event handler 等危險 HTML。
1863
+ *
1864
+ * 適用於郵件、Slack 等富文本通知頻道。
1865
+ *
1866
+ * @param template - Markdown 格式的模板字串
1867
+ * @returns 安全的 HTML 字串
1868
+ *
1869
+ * @example
1870
+ * ```typescript
1871
+ * const html = this.renderMarkdown('# Hello {{name}}')
1872
+ * await sendEmail({ htmlBody: html })
1873
+ * ```
1874
+ */
1875
+ renderMarkdown(template) {
1876
+ if (!template) {
1877
+ return "";
1878
+ }
1879
+ const interpolated = this.interpolate(template);
1880
+ const md = getMarkdownAdapter();
1881
+ const sanitizeCallbacks = createHtmlRenderCallbacks({
1882
+ html: (rawHtml) => sanitizeHtml(rawHtml)
1883
+ });
1884
+ return md.render(interpolated, sanitizeCallbacks);
1885
+ }
1842
1886
  interpolate(text) {
1843
1887
  return text.replace(/\{\{(\w+)\}\}/g, (_, key) => String(this.data[key] ?? `{{${key}}}`));
1844
1888
  }
@@ -1849,6 +1893,17 @@ var TemplatedNotification = class extends Notification {
1849
1893
  throw new Error("Notifiable does not have an email property");
1850
1894
  }
1851
1895
  };
1896
+ var DANGEROUS_TAGS = /^<\/?(?:script|iframe|object|embed|form|input|textarea|button|select|style|link|meta|base)\b[^>]*>$/i;
1897
+ var EVENT_HANDLER_ATTRS = /\s+on\w+\s*=/i;
1898
+ function sanitizeHtml(rawHtml) {
1899
+ if (DANGEROUS_TAGS.test(rawHtml.trim())) {
1900
+ return "";
1901
+ }
1902
+ if (EVENT_HANDLER_ATTRS.test(rawHtml)) {
1903
+ return "";
1904
+ }
1905
+ return rawHtml;
1906
+ }
1852
1907
 
1853
1908
  // src/index.ts
1854
1909
  init_middleware();
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@gravito/flare",
3
- "version": "4.0.1",
3
+ "sideEffects": false,
4
+ "version": "4.0.2",
4
5
  "publishConfig": {
5
6
  "access": "public"
6
7
  },
@@ -23,6 +24,7 @@
23
24
  ],
24
25
  "scripts": {
25
26
  "build": "bun run build.ts",
27
+ "build:dts": "bun run build.ts --dts-only",
26
28
  "test": "bun test --timeout=10000",
27
29
  "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
28
30
  "test:coverage": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
@@ -42,12 +44,12 @@
42
44
  "author": "Carl Lee <carllee0520@gmail.com>",
43
45
  "license": "MIT",
44
46
  "dependencies": {
45
- "@gravito/core": "^1.6.1",
47
+ "@gravito/core": "^2.0.6",
46
48
  "@aws-sdk/client-sns": "^3.734.0"
47
49
  },
48
50
  "peerDependencies": {
49
- "@gravito/stream": "^2.0.2",
50
- "@gravito/signal": "^3.0.4",
51
+ "@gravito/stream": "^2.1.0",
52
+ "@gravito/signal": "^3.1.0",
51
53
  "@gravito/radiance": "^1.0.4"
52
54
  },
53
55
  "devDependencies": {