@gravito/flare 4.0.1 → 5.0.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 -12
- package/dist/index.cjs +155 -26
- package/dist/index.d.cts +75 -6
- package/dist/index.d.ts +75 -6
- package/dist/index.js +153 -26
- package/package.json +7 -5
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
|
-
- **
|
|
10
|
-
- **Multi-
|
|
11
|
-
- **
|
|
12
|
-
- **Reliability**: Built-in
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
@@ -200,6 +200,8 @@ __export(index_exports, {
|
|
|
200
200
|
AbortError: () => AbortError,
|
|
201
201
|
BroadcastChannel: () => BroadcastChannel,
|
|
202
202
|
DatabaseChannel: () => DatabaseChannel,
|
|
203
|
+
FlareError: () => FlareError,
|
|
204
|
+
FlareErrorCodes: () => FlareErrorCodes,
|
|
203
205
|
LazyNotification: () => LazyNotification,
|
|
204
206
|
MailChannel: () => MailChannel,
|
|
205
207
|
MemoryStore: () => MemoryStore,
|
|
@@ -225,6 +227,37 @@ __export(index_exports, {
|
|
|
225
227
|
});
|
|
226
228
|
module.exports = __toCommonJS(index_exports);
|
|
227
229
|
|
|
230
|
+
// src/errors/FlareError.ts
|
|
231
|
+
var import_core = require("@gravito/core");
|
|
232
|
+
var FlareError = class extends import_core.InfrastructureException {
|
|
233
|
+
/**
|
|
234
|
+
* Creates a new FlareError instance.
|
|
235
|
+
*
|
|
236
|
+
* @param code - The error code.
|
|
237
|
+
* @param message - The error message.
|
|
238
|
+
* @param options - Optional infrastructure exception options.
|
|
239
|
+
*/
|
|
240
|
+
constructor(code, message, options = {}) {
|
|
241
|
+
const status = options.retryable ? 503 : 500;
|
|
242
|
+
super(status, code, { message, ...options });
|
|
243
|
+
this.name = "FlareError";
|
|
244
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// src/errors/codes.ts
|
|
249
|
+
var FlareErrorCodes = {
|
|
250
|
+
RATE_LIMIT_EXCEEDED: "flare.rate_limit_exceeded",
|
|
251
|
+
SERIALIZATION_FAILED: "flare.serialization_failed",
|
|
252
|
+
TEMPLATE_NOT_DEFINED: "flare.template_not_defined",
|
|
253
|
+
NOTIFIABLE_MISSING_EMAIL: "flare.notifiable_missing_email",
|
|
254
|
+
INVALID_CONFIG: "flare.invalid_config",
|
|
255
|
+
NOTIFICATION_METHOD_NOT_IMPLEMENTED: "flare.notification_method_not_implemented",
|
|
256
|
+
UNSUPPORTED_PROVIDER: "flare.unsupported_provider",
|
|
257
|
+
CREDENTIALS_MISSING: "flare.credentials_missing",
|
|
258
|
+
SEND_FAILED: "flare.send_failed"
|
|
259
|
+
};
|
|
260
|
+
|
|
228
261
|
// src/channels/TimeoutChannel.ts
|
|
229
262
|
var TimeoutError = class extends Error {
|
|
230
263
|
constructor(message) {
|
|
@@ -262,16 +295,23 @@ var TimeoutChannel = class {
|
|
|
262
295
|
}
|
|
263
296
|
const controller = new AbortController();
|
|
264
297
|
const { signal } = controller;
|
|
298
|
+
let timeoutId;
|
|
299
|
+
let settled = false;
|
|
300
|
+
let handleExternalAbort;
|
|
265
301
|
if (options?.signal) {
|
|
266
302
|
if (options.signal.aborted) {
|
|
267
303
|
throw new AbortError("Request was aborted before sending");
|
|
268
304
|
}
|
|
269
|
-
|
|
305
|
+
handleExternalAbort = () => {
|
|
270
306
|
controller.abort();
|
|
271
|
-
}
|
|
307
|
+
};
|
|
308
|
+
options.signal.addEventListener("abort", handleExternalAbort, { once: true });
|
|
272
309
|
}
|
|
273
310
|
const timeoutPromise = new Promise((_, reject) => {
|
|
274
|
-
setTimeout(() => {
|
|
311
|
+
timeoutId = setTimeout(() => {
|
|
312
|
+
if (settled) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
275
315
|
if (this.config.onTimeout) {
|
|
276
316
|
this.config.onTimeout(this.inner.constructor.name, notification);
|
|
277
317
|
}
|
|
@@ -292,7 +332,17 @@ var TimeoutChannel = class {
|
|
|
292
332
|
}
|
|
293
333
|
throw error;
|
|
294
334
|
});
|
|
295
|
-
|
|
335
|
+
try {
|
|
336
|
+
return await Promise.race([sendPromise, timeoutPromise]);
|
|
337
|
+
} finally {
|
|
338
|
+
settled = true;
|
|
339
|
+
if (timeoutId) {
|
|
340
|
+
clearTimeout(timeoutId);
|
|
341
|
+
}
|
|
342
|
+
if (options?.signal && handleExternalAbort) {
|
|
343
|
+
options.signal.removeEventListener("abort", handleExternalAbort);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
296
346
|
}
|
|
297
347
|
};
|
|
298
348
|
|
|
@@ -305,7 +355,10 @@ var BroadcastChannel = class {
|
|
|
305
355
|
const innerChannel = {
|
|
306
356
|
send: async (notification, notifiable, _options) => {
|
|
307
357
|
if (!notification.toBroadcast) {
|
|
308
|
-
throw new
|
|
358
|
+
throw new FlareError(
|
|
359
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
360
|
+
"Notification does not implement toBroadcast method"
|
|
361
|
+
);
|
|
309
362
|
}
|
|
310
363
|
const broadcastNotification = notification.toBroadcast(notifiable);
|
|
311
364
|
const notifiableId = notifiable.getNotifiableId();
|
|
@@ -339,7 +392,10 @@ var DatabaseChannel = class {
|
|
|
339
392
|
const innerChannel = {
|
|
340
393
|
send: async (notification, notifiable, _options) => {
|
|
341
394
|
if (!notification.toDatabase) {
|
|
342
|
-
throw new
|
|
395
|
+
throw new FlareError(
|
|
396
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
397
|
+
"Notification does not implement toDatabase method"
|
|
398
|
+
);
|
|
343
399
|
}
|
|
344
400
|
const dbNotification = notification.toDatabase(notifiable);
|
|
345
401
|
await this.dbService.insertNotification({
|
|
@@ -371,7 +427,10 @@ var MailChannel = class {
|
|
|
371
427
|
const innerChannel = {
|
|
372
428
|
send: async (notification, notifiable, _options) => {
|
|
373
429
|
if (!notification.toMail) {
|
|
374
|
-
throw new
|
|
430
|
+
throw new FlareError(
|
|
431
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
432
|
+
"Notification does not implement toMail method"
|
|
433
|
+
);
|
|
375
434
|
}
|
|
376
435
|
const message = notification.toMail(notifiable);
|
|
377
436
|
await this.mailService.send(message);
|
|
@@ -397,7 +456,10 @@ var SlackChannel = class {
|
|
|
397
456
|
const innerChannel = {
|
|
398
457
|
send: async (notification, notifiable, options) => {
|
|
399
458
|
if (!notification.toSlack) {
|
|
400
|
-
throw new
|
|
459
|
+
throw new FlareError(
|
|
460
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
461
|
+
"Notification does not implement toSlack method"
|
|
462
|
+
);
|
|
401
463
|
}
|
|
402
464
|
const slackMessage = notification.toSlack(notifiable);
|
|
403
465
|
const response = await fetch(this.config.webhookUrl, {
|
|
@@ -416,7 +478,10 @@ var SlackChannel = class {
|
|
|
416
478
|
// Pass AbortSignal to fetch
|
|
417
479
|
});
|
|
418
480
|
if (!response.ok) {
|
|
419
|
-
throw new
|
|
481
|
+
throw new FlareError(
|
|
482
|
+
FlareErrorCodes.SEND_FAILED,
|
|
483
|
+
`Failed to send Slack notification: ${response.statusText}`
|
|
484
|
+
);
|
|
420
485
|
}
|
|
421
486
|
}
|
|
422
487
|
};
|
|
@@ -440,7 +505,10 @@ var SmsChannel = class {
|
|
|
440
505
|
const innerChannel = {
|
|
441
506
|
send: async (notification, notifiable, options) => {
|
|
442
507
|
if (!notification.toSms) {
|
|
443
|
-
throw new
|
|
508
|
+
throw new FlareError(
|
|
509
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
510
|
+
"Notification does not implement toSms method"
|
|
511
|
+
);
|
|
444
512
|
}
|
|
445
513
|
const smsMessage = notification.toSms(notifiable);
|
|
446
514
|
switch (this.config.provider) {
|
|
@@ -451,7 +519,10 @@ var SmsChannel = class {
|
|
|
451
519
|
await this.sendViaAwsSns(smsMessage, options?.signal);
|
|
452
520
|
break;
|
|
453
521
|
default:
|
|
454
|
-
throw new
|
|
522
|
+
throw new FlareError(
|
|
523
|
+
FlareErrorCodes.UNSUPPORTED_PROVIDER,
|
|
524
|
+
`Unsupported SMS provider: ${this.config.provider}`
|
|
525
|
+
);
|
|
455
526
|
}
|
|
456
527
|
}
|
|
457
528
|
};
|
|
@@ -470,7 +541,10 @@ var SmsChannel = class {
|
|
|
470
541
|
*/
|
|
471
542
|
async sendViaTwilio(message, signal) {
|
|
472
543
|
if (!this.config.apiKey || !this.config.apiSecret) {
|
|
473
|
-
throw new
|
|
544
|
+
throw new FlareError(
|
|
545
|
+
FlareErrorCodes.CREDENTIALS_MISSING,
|
|
546
|
+
"Twilio API key and secret are required"
|
|
547
|
+
);
|
|
474
548
|
}
|
|
475
549
|
const accountSid = this.config.apiKey;
|
|
476
550
|
const authToken = this.config.apiSecret;
|
|
@@ -493,7 +567,7 @@ var SmsChannel = class {
|
|
|
493
567
|
);
|
|
494
568
|
if (!response.ok) {
|
|
495
569
|
const error = await response.text();
|
|
496
|
-
throw new
|
|
570
|
+
throw new FlareError(FlareErrorCodes.SEND_FAILED, `Failed to send SMS via Twilio: ${error}`);
|
|
497
571
|
}
|
|
498
572
|
}
|
|
499
573
|
/**
|
|
@@ -507,7 +581,8 @@ var SmsChannel = class {
|
|
|
507
581
|
SNSClient = awsSns.SNSClient;
|
|
508
582
|
PublishCommand = awsSns.PublishCommand;
|
|
509
583
|
} catch {
|
|
510
|
-
throw new
|
|
584
|
+
throw new FlareError(
|
|
585
|
+
FlareErrorCodes.UNSUPPORTED_PROVIDER,
|
|
511
586
|
"AWS SNS SMS requires @aws-sdk/client-sns. Install it with: bun add @aws-sdk/client-sns"
|
|
512
587
|
);
|
|
513
588
|
}
|
|
@@ -538,7 +613,10 @@ var SmsChannel = class {
|
|
|
538
613
|
await client.send(command, { abortSignal: signal });
|
|
539
614
|
} catch (error) {
|
|
540
615
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
541
|
-
throw new
|
|
616
|
+
throw new FlareError(
|
|
617
|
+
FlareErrorCodes.SEND_FAILED,
|
|
618
|
+
`Failed to send SMS via AWS SNS: ${err.message}`
|
|
619
|
+
);
|
|
542
620
|
}
|
|
543
621
|
}
|
|
544
622
|
};
|
|
@@ -797,7 +875,7 @@ var RateLimitMiddleware = class {
|
|
|
797
875
|
* Create a new RateLimitMiddleware instance.
|
|
798
876
|
*
|
|
799
877
|
* @param config - Rate limit configuration for each channel
|
|
800
|
-
* @param store - Optional cache store for distributed rate limiting
|
|
878
|
+
* @param store - Optional cache store for distributed rate limiting (future use)
|
|
801
879
|
*
|
|
802
880
|
* @example
|
|
803
881
|
* ```typescript
|
|
@@ -814,7 +892,7 @@ var RateLimitMiddleware = class {
|
|
|
814
892
|
*/
|
|
815
893
|
constructor(config, store) {
|
|
816
894
|
this.config = config;
|
|
817
|
-
this.store = store
|
|
895
|
+
this.store = store || new MemoryStore();
|
|
818
896
|
this.initializeBuckets();
|
|
819
897
|
}
|
|
820
898
|
/**
|
|
@@ -833,7 +911,6 @@ var RateLimitMiddleware = class {
|
|
|
833
911
|
buckets = /* @__PURE__ */ new Map();
|
|
834
912
|
/**
|
|
835
913
|
* Cache store for distributed rate limiting.
|
|
836
|
-
* 分散式限流使用的快取儲存
|
|
837
914
|
*/
|
|
838
915
|
store;
|
|
839
916
|
/**
|
|
@@ -891,7 +968,8 @@ var RateLimitMiddleware = class {
|
|
|
891
968
|
if (bucket) {
|
|
892
969
|
const allowed = bucket.tryConsume();
|
|
893
970
|
if (!allowed) {
|
|
894
|
-
throw new
|
|
971
|
+
throw new FlareError(
|
|
972
|
+
FlareErrorCodes.RATE_LIMIT_EXCEEDED,
|
|
895
973
|
`Rate limit exceeded for channel '${channel}' (${window}ly limit). Please try again later.`
|
|
896
974
|
);
|
|
897
975
|
}
|
|
@@ -1194,7 +1272,8 @@ function checkSerializable(obj, path = "") {
|
|
|
1194
1272
|
function assertSerializable(obj) {
|
|
1195
1273
|
const result = checkSerializable(obj);
|
|
1196
1274
|
if (!result.serializable) {
|
|
1197
|
-
throw new
|
|
1275
|
+
throw new FlareError(
|
|
1276
|
+
FlareErrorCodes.SERIALIZATION_FAILED,
|
|
1198
1277
|
`\u7269\u4EF6\u5305\u542B\u4E0D\u53EF\u5E8F\u5217\u5316\u7684\u5C6C\u6027:
|
|
1199
1278
|
\u554F\u984C\u8DEF\u5F91: ${result.problematicPaths.join(", ")}
|
|
1200
1279
|
\u8A73\u7D30\u8CC7\u8A0A:
|
|
@@ -1713,24 +1792,30 @@ var OrbitFlare = class _OrbitFlare {
|
|
|
1713
1792
|
if (options.enableSlack) {
|
|
1714
1793
|
const slack = options.channels?.slack;
|
|
1715
1794
|
if (!slack?.webhookUrl) {
|
|
1716
|
-
throw new
|
|
1795
|
+
throw new FlareError(
|
|
1796
|
+
FlareErrorCodes.INVALID_CONFIG,
|
|
1717
1797
|
"[OrbitFlare] Slack channel enabled but webhookUrl not provided. Configure channels.slack.webhookUrl or set enableSlack to false."
|
|
1718
1798
|
);
|
|
1719
1799
|
}
|
|
1720
1800
|
if (!this.isValidUrl(slack.webhookUrl)) {
|
|
1721
|
-
throw new
|
|
1801
|
+
throw new FlareError(
|
|
1802
|
+
FlareErrorCodes.INVALID_CONFIG,
|
|
1803
|
+
`[OrbitFlare] Invalid Slack webhook URL: ${slack.webhookUrl}`
|
|
1804
|
+
);
|
|
1722
1805
|
}
|
|
1723
1806
|
}
|
|
1724
1807
|
if (options.enableSms) {
|
|
1725
1808
|
const sms = options.channels?.sms;
|
|
1726
1809
|
if (!sms?.provider) {
|
|
1727
|
-
throw new
|
|
1810
|
+
throw new FlareError(
|
|
1811
|
+
FlareErrorCodes.INVALID_CONFIG,
|
|
1728
1812
|
"[OrbitFlare] SMS channel enabled but provider not specified. Configure channels.sms.provider or set enableSms to false."
|
|
1729
1813
|
);
|
|
1730
1814
|
}
|
|
1731
1815
|
const supportedProviders = ["twilio", "aws-sns"];
|
|
1732
1816
|
if (!supportedProviders.includes(sms.provider)) {
|
|
1733
|
-
throw new
|
|
1817
|
+
throw new FlareError(
|
|
1818
|
+
FlareErrorCodes.UNSUPPORTED_PROVIDER,
|
|
1734
1819
|
`[OrbitFlare] Unsupported SMS provider: ${sms.provider}. Supported providers: ${supportedProviders.join(", ")}`
|
|
1735
1820
|
);
|
|
1736
1821
|
}
|
|
@@ -1853,6 +1938,7 @@ var OrbitFlare = class _OrbitFlare {
|
|
|
1853
1938
|
};
|
|
1854
1939
|
|
|
1855
1940
|
// src/templates/NotificationTemplate.ts
|
|
1941
|
+
var import_core2 = require("@gravito/core");
|
|
1856
1942
|
var TemplatedNotification = class extends Notification {
|
|
1857
1943
|
data = {};
|
|
1858
1944
|
with(data) {
|
|
@@ -1872,7 +1958,7 @@ var TemplatedNotification = class extends Notification {
|
|
|
1872
1958
|
// Auto-implement toSlack
|
|
1873
1959
|
toSlack(_notifiable) {
|
|
1874
1960
|
if (!this.slackTemplate) {
|
|
1875
|
-
throw new
|
|
1961
|
+
throw new FlareError(FlareErrorCodes.TEMPLATE_NOT_DEFINED, "slackTemplate not defined");
|
|
1876
1962
|
}
|
|
1877
1963
|
const template = this.slackTemplate();
|
|
1878
1964
|
return {
|
|
@@ -1881,6 +1967,33 @@ var TemplatedNotification = class extends Notification {
|
|
|
1881
1967
|
attachments: template.attachments
|
|
1882
1968
|
};
|
|
1883
1969
|
}
|
|
1970
|
+
/**
|
|
1971
|
+
* 將 Markdown 模板渲染為 HTML。
|
|
1972
|
+
* 先執行變數插值(`{{var}}`),再將 Markdown 轉為 HTML。
|
|
1973
|
+
* 內建 XSS 防護:過濾 script、iframe、event handler 等危險 HTML。
|
|
1974
|
+
*
|
|
1975
|
+
* 適用於郵件、Slack 等富文本通知頻道。
|
|
1976
|
+
*
|
|
1977
|
+
* @param template - Markdown 格式的模板字串
|
|
1978
|
+
* @returns 安全的 HTML 字串
|
|
1979
|
+
*
|
|
1980
|
+
* @example
|
|
1981
|
+
* ```typescript
|
|
1982
|
+
* const html = this.renderMarkdown('# Hello {{name}}')
|
|
1983
|
+
* await sendEmail({ htmlBody: html })
|
|
1984
|
+
* ```
|
|
1985
|
+
*/
|
|
1986
|
+
renderMarkdown(template) {
|
|
1987
|
+
if (!template) {
|
|
1988
|
+
return "";
|
|
1989
|
+
}
|
|
1990
|
+
const interpolated = this.interpolate(template);
|
|
1991
|
+
const md = (0, import_core2.getMarkdownAdapter)();
|
|
1992
|
+
const sanitizeCallbacks = (0, import_core2.createHtmlRenderCallbacks)({
|
|
1993
|
+
html: (rawHtml) => sanitizeHtml(rawHtml)
|
|
1994
|
+
});
|
|
1995
|
+
return md.render(interpolated, sanitizeCallbacks);
|
|
1996
|
+
}
|
|
1884
1997
|
interpolate(text) {
|
|
1885
1998
|
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => String(this.data[key] ?? `{{${key}}}`));
|
|
1886
1999
|
}
|
|
@@ -1888,9 +2001,23 @@ var TemplatedNotification = class extends Notification {
|
|
|
1888
2001
|
if ("email" in notifiable && typeof notifiable.email === "string") {
|
|
1889
2002
|
return notifiable.email;
|
|
1890
2003
|
}
|
|
1891
|
-
throw new
|
|
2004
|
+
throw new FlareError(
|
|
2005
|
+
FlareErrorCodes.NOTIFIABLE_MISSING_EMAIL,
|
|
2006
|
+
"Notifiable does not have an email property"
|
|
2007
|
+
);
|
|
1892
2008
|
}
|
|
1893
2009
|
};
|
|
2010
|
+
var DANGEROUS_TAGS = /^<\/?(?:script|iframe|object|embed|form|input|textarea|button|select|style|link|meta|base)\b[^>]*>$/i;
|
|
2011
|
+
var EVENT_HANDLER_ATTRS = /\s+on\w+\s*=/i;
|
|
2012
|
+
function sanitizeHtml(rawHtml) {
|
|
2013
|
+
if (DANGEROUS_TAGS.test(rawHtml.trim())) {
|
|
2014
|
+
return "";
|
|
2015
|
+
}
|
|
2016
|
+
if (EVENT_HANDLER_ATTRS.test(rawHtml)) {
|
|
2017
|
+
return "";
|
|
2018
|
+
}
|
|
2019
|
+
return rawHtml;
|
|
2020
|
+
}
|
|
1894
2021
|
|
|
1895
2022
|
// src/index.ts
|
|
1896
2023
|
init_middleware();
|
|
@@ -1949,6 +2076,8 @@ var LazyNotification = class extends Notification {
|
|
|
1949
2076
|
AbortError,
|
|
1950
2077
|
BroadcastChannel,
|
|
1951
2078
|
DatabaseChannel,
|
|
2079
|
+
FlareError,
|
|
2080
|
+
FlareErrorCodes,
|
|
1952
2081
|
LazyNotification,
|
|
1953
2082
|
MailChannel,
|
|
1954
2083
|
MemoryStore,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PlanetCore, GravitoOrbit } from '@gravito/core';
|
|
1
|
+
import { InfrastructureException, InfrastructureExceptionOptions, PlanetCore, GravitoOrbit } from '@gravito/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Notification system type definitions.
|
|
@@ -404,6 +404,59 @@ declare class BroadcastChannel implements NotificationChannel {
|
|
|
404
404
|
send(notification: Notification, notifiable: Notifiable, options?: AbortableSendOptions): Promise<void>;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
/**
|
|
408
|
+
* @fileoverview Flare error codes
|
|
409
|
+
*
|
|
410
|
+
* Namespaced error codes for the Flare notification module.
|
|
411
|
+
*
|
|
412
|
+
* @module @gravito/flare/errors
|
|
413
|
+
*/
|
|
414
|
+
/**
|
|
415
|
+
* Error codes for Flare module operations.
|
|
416
|
+
* Follows dot-separated namespace convention.
|
|
417
|
+
*/
|
|
418
|
+
declare const FlareErrorCodes: {
|
|
419
|
+
readonly RATE_LIMIT_EXCEEDED: "flare.rate_limit_exceeded";
|
|
420
|
+
readonly SERIALIZATION_FAILED: "flare.serialization_failed";
|
|
421
|
+
readonly TEMPLATE_NOT_DEFINED: "flare.template_not_defined";
|
|
422
|
+
readonly NOTIFIABLE_MISSING_EMAIL: "flare.notifiable_missing_email";
|
|
423
|
+
readonly INVALID_CONFIG: "flare.invalid_config";
|
|
424
|
+
readonly NOTIFICATION_METHOD_NOT_IMPLEMENTED: "flare.notification_method_not_implemented";
|
|
425
|
+
readonly UNSUPPORTED_PROVIDER: "flare.unsupported_provider";
|
|
426
|
+
readonly CREDENTIALS_MISSING: "flare.credentials_missing";
|
|
427
|
+
readonly SEND_FAILED: "flare.send_failed";
|
|
428
|
+
};
|
|
429
|
+
type FlareErrorCode = (typeof FlareErrorCodes)[keyof typeof FlareErrorCodes];
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @fileoverview Flare error types
|
|
433
|
+
*
|
|
434
|
+
* @module @gravito/flare/errors
|
|
435
|
+
*/
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Base error class for Flare module.
|
|
439
|
+
*
|
|
440
|
+
* Provides structured error handling with error codes and messages.
|
|
441
|
+
* Extends InfrastructureException for unified error handling across Gravito.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```typescript
|
|
445
|
+
* throw new FlareError('flare.send_failed', 'Failed to send notification')
|
|
446
|
+
* ```
|
|
447
|
+
* @public
|
|
448
|
+
*/
|
|
449
|
+
declare class FlareError extends InfrastructureException {
|
|
450
|
+
/**
|
|
451
|
+
* Creates a new FlareError instance.
|
|
452
|
+
*
|
|
453
|
+
* @param code - The error code.
|
|
454
|
+
* @param message - The error message.
|
|
455
|
+
* @param options - Optional infrastructure exception options.
|
|
456
|
+
*/
|
|
457
|
+
constructor(code: FlareErrorCode, message: string, options?: InfrastructureExceptionOptions);
|
|
458
|
+
}
|
|
459
|
+
|
|
407
460
|
/**
|
|
408
461
|
* Database channel 配置選項。
|
|
409
462
|
*/
|
|
@@ -1039,14 +1092,13 @@ declare class RateLimitMiddleware implements ChannelMiddleware {
|
|
|
1039
1092
|
private buckets;
|
|
1040
1093
|
/**
|
|
1041
1094
|
* Cache store for distributed rate limiting.
|
|
1042
|
-
* 分散式限流使用的快取儲存
|
|
1043
1095
|
*/
|
|
1044
|
-
|
|
1096
|
+
store: CacheStore;
|
|
1045
1097
|
/**
|
|
1046
1098
|
* Create a new RateLimitMiddleware instance.
|
|
1047
1099
|
*
|
|
1048
1100
|
* @param config - Rate limit configuration for each channel
|
|
1049
|
-
* @param store - Optional cache store for distributed rate limiting
|
|
1101
|
+
* @param store - Optional cache store for distributed rate limiting (future use)
|
|
1050
1102
|
*
|
|
1051
1103
|
* @example
|
|
1052
1104
|
* ```typescript
|
|
@@ -1379,7 +1431,24 @@ declare abstract class TemplatedNotification extends Notification {
|
|
|
1379
1431
|
protected slackTemplate?(): SlackTemplate;
|
|
1380
1432
|
toMail(notifiable: Notifiable): MailMessage;
|
|
1381
1433
|
toSlack(_notifiable: Notifiable): SlackMessage;
|
|
1382
|
-
|
|
1434
|
+
/**
|
|
1435
|
+
* 將 Markdown 模板渲染為 HTML。
|
|
1436
|
+
* 先執行變數插值(`{{var}}`),再將 Markdown 轉為 HTML。
|
|
1437
|
+
* 內建 XSS 防護:過濾 script、iframe、event handler 等危險 HTML。
|
|
1438
|
+
*
|
|
1439
|
+
* 適用於郵件、Slack 等富文本通知頻道。
|
|
1440
|
+
*
|
|
1441
|
+
* @param template - Markdown 格式的模板字串
|
|
1442
|
+
* @returns 安全的 HTML 字串
|
|
1443
|
+
*
|
|
1444
|
+
* @example
|
|
1445
|
+
* ```typescript
|
|
1446
|
+
* const html = this.renderMarkdown('# Hello {{name}}')
|
|
1447
|
+
* await sendEmail({ htmlBody: html })
|
|
1448
|
+
* ```
|
|
1449
|
+
*/
|
|
1450
|
+
protected renderMarkdown(template: string): string;
|
|
1451
|
+
protected interpolate(text: string): string;
|
|
1383
1452
|
private getRecipientEmail;
|
|
1384
1453
|
}
|
|
1385
1454
|
|
|
@@ -1866,4 +1935,4 @@ declare class TokenBucket {
|
|
|
1866
1935
|
private refill;
|
|
1867
1936
|
}
|
|
1868
1937
|
|
|
1869
|
-
export { AbortError, type BatchResult, BroadcastChannel, type BroadcastNotification, type CacheStore, type ChannelFailurePayload, type ChannelHookPayload, type ChannelMiddleware, type ChannelRateLimitConfig, type ChannelSuccessPayload, DatabaseChannel, type DatabaseNotification, type FlareHookEvent, type FlareHookPayloads, type FlareHooks, type HookEmitter, LazyNotification, MailChannel, type MailChannelConfig, type MailMessage, type MailTemplate, MemoryStore, type MetricsSummary, MiddlewarePriority, type MiddlewarePriorityValue, type Notifiable, Notification, type NotificationBatchCompletePayload, type NotificationBatchStartPayload, type NotificationChannel, type NotificationCompletePayload, type NotificationHookPayload, NotificationManager, type NotificationMetric, NotificationMetricsCollector, type NotificationPreference, type NotificationResult, OrbitFlare, type OrbitFlareOptions, PreferenceMiddleware, type RateLimitConfig, RateLimitMiddleware, type RetryConfig, type SendOptions, type SendResult, type SerializationCheckResult, type ShouldQueue, type ShouldRetry, SlackChannel, type SlackChannelConfig, type SlackMessage, type SlackTemplate, SmsChannel, type SmsChannelConfig, type SmsMessage, type TemplateData, TemplatedNotification, TimeoutChannel, type TimeoutConfig, TimeoutError, TokenBucket, assertSerializable, checkSerializable, createHookEmitter, deepDeserialize, deepSerialize, toPrometheusFormat };
|
|
1938
|
+
export { AbortError, type BatchResult, BroadcastChannel, type BroadcastNotification, type CacheStore, type ChannelFailurePayload, type ChannelHookPayload, type ChannelMiddleware, type ChannelRateLimitConfig, type ChannelSuccessPayload, DatabaseChannel, type DatabaseNotification, FlareError, type FlareErrorCode, FlareErrorCodes, type FlareHookEvent, type FlareHookPayloads, type FlareHooks, type HookEmitter, LazyNotification, MailChannel, type MailChannelConfig, type MailMessage, type MailTemplate, MemoryStore, type MetricsSummary, MiddlewarePriority, type MiddlewarePriorityValue, type Notifiable, Notification, type NotificationBatchCompletePayload, type NotificationBatchStartPayload, type NotificationChannel, type NotificationCompletePayload, type NotificationHookPayload, NotificationManager, type NotificationMetric, NotificationMetricsCollector, type NotificationPreference, type NotificationResult, OrbitFlare, type OrbitFlareOptions, PreferenceMiddleware, type RateLimitConfig, RateLimitMiddleware, type RetryConfig, type SendOptions, type SendResult, type SerializationCheckResult, type ShouldQueue, type ShouldRetry, SlackChannel, type SlackChannelConfig, type SlackMessage, type SlackTemplate, SmsChannel, type SmsChannelConfig, type SmsMessage, type TemplateData, TemplatedNotification, TimeoutChannel, type TimeoutConfig, TimeoutError, TokenBucket, assertSerializable, checkSerializable, createHookEmitter, deepDeserialize, deepSerialize, toPrometheusFormat };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PlanetCore, GravitoOrbit } from '@gravito/core';
|
|
1
|
+
import { InfrastructureException, InfrastructureExceptionOptions, PlanetCore, GravitoOrbit } from '@gravito/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Notification system type definitions.
|
|
@@ -404,6 +404,59 @@ declare class BroadcastChannel implements NotificationChannel {
|
|
|
404
404
|
send(notification: Notification, notifiable: Notifiable, options?: AbortableSendOptions): Promise<void>;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
/**
|
|
408
|
+
* @fileoverview Flare error codes
|
|
409
|
+
*
|
|
410
|
+
* Namespaced error codes for the Flare notification module.
|
|
411
|
+
*
|
|
412
|
+
* @module @gravito/flare/errors
|
|
413
|
+
*/
|
|
414
|
+
/**
|
|
415
|
+
* Error codes for Flare module operations.
|
|
416
|
+
* Follows dot-separated namespace convention.
|
|
417
|
+
*/
|
|
418
|
+
declare const FlareErrorCodes: {
|
|
419
|
+
readonly RATE_LIMIT_EXCEEDED: "flare.rate_limit_exceeded";
|
|
420
|
+
readonly SERIALIZATION_FAILED: "flare.serialization_failed";
|
|
421
|
+
readonly TEMPLATE_NOT_DEFINED: "flare.template_not_defined";
|
|
422
|
+
readonly NOTIFIABLE_MISSING_EMAIL: "flare.notifiable_missing_email";
|
|
423
|
+
readonly INVALID_CONFIG: "flare.invalid_config";
|
|
424
|
+
readonly NOTIFICATION_METHOD_NOT_IMPLEMENTED: "flare.notification_method_not_implemented";
|
|
425
|
+
readonly UNSUPPORTED_PROVIDER: "flare.unsupported_provider";
|
|
426
|
+
readonly CREDENTIALS_MISSING: "flare.credentials_missing";
|
|
427
|
+
readonly SEND_FAILED: "flare.send_failed";
|
|
428
|
+
};
|
|
429
|
+
type FlareErrorCode = (typeof FlareErrorCodes)[keyof typeof FlareErrorCodes];
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @fileoverview Flare error types
|
|
433
|
+
*
|
|
434
|
+
* @module @gravito/flare/errors
|
|
435
|
+
*/
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Base error class for Flare module.
|
|
439
|
+
*
|
|
440
|
+
* Provides structured error handling with error codes and messages.
|
|
441
|
+
* Extends InfrastructureException for unified error handling across Gravito.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```typescript
|
|
445
|
+
* throw new FlareError('flare.send_failed', 'Failed to send notification')
|
|
446
|
+
* ```
|
|
447
|
+
* @public
|
|
448
|
+
*/
|
|
449
|
+
declare class FlareError extends InfrastructureException {
|
|
450
|
+
/**
|
|
451
|
+
* Creates a new FlareError instance.
|
|
452
|
+
*
|
|
453
|
+
* @param code - The error code.
|
|
454
|
+
* @param message - The error message.
|
|
455
|
+
* @param options - Optional infrastructure exception options.
|
|
456
|
+
*/
|
|
457
|
+
constructor(code: FlareErrorCode, message: string, options?: InfrastructureExceptionOptions);
|
|
458
|
+
}
|
|
459
|
+
|
|
407
460
|
/**
|
|
408
461
|
* Database channel 配置選項。
|
|
409
462
|
*/
|
|
@@ -1039,14 +1092,13 @@ declare class RateLimitMiddleware implements ChannelMiddleware {
|
|
|
1039
1092
|
private buckets;
|
|
1040
1093
|
/**
|
|
1041
1094
|
* Cache store for distributed rate limiting.
|
|
1042
|
-
* 分散式限流使用的快取儲存
|
|
1043
1095
|
*/
|
|
1044
|
-
|
|
1096
|
+
store: CacheStore;
|
|
1045
1097
|
/**
|
|
1046
1098
|
* Create a new RateLimitMiddleware instance.
|
|
1047
1099
|
*
|
|
1048
1100
|
* @param config - Rate limit configuration for each channel
|
|
1049
|
-
* @param store - Optional cache store for distributed rate limiting
|
|
1101
|
+
* @param store - Optional cache store for distributed rate limiting (future use)
|
|
1050
1102
|
*
|
|
1051
1103
|
* @example
|
|
1052
1104
|
* ```typescript
|
|
@@ -1379,7 +1431,24 @@ declare abstract class TemplatedNotification extends Notification {
|
|
|
1379
1431
|
protected slackTemplate?(): SlackTemplate;
|
|
1380
1432
|
toMail(notifiable: Notifiable): MailMessage;
|
|
1381
1433
|
toSlack(_notifiable: Notifiable): SlackMessage;
|
|
1382
|
-
|
|
1434
|
+
/**
|
|
1435
|
+
* 將 Markdown 模板渲染為 HTML。
|
|
1436
|
+
* 先執行變數插值(`{{var}}`),再將 Markdown 轉為 HTML。
|
|
1437
|
+
* 內建 XSS 防護:過濾 script、iframe、event handler 等危險 HTML。
|
|
1438
|
+
*
|
|
1439
|
+
* 適用於郵件、Slack 等富文本通知頻道。
|
|
1440
|
+
*
|
|
1441
|
+
* @param template - Markdown 格式的模板字串
|
|
1442
|
+
* @returns 安全的 HTML 字串
|
|
1443
|
+
*
|
|
1444
|
+
* @example
|
|
1445
|
+
* ```typescript
|
|
1446
|
+
* const html = this.renderMarkdown('# Hello {{name}}')
|
|
1447
|
+
* await sendEmail({ htmlBody: html })
|
|
1448
|
+
* ```
|
|
1449
|
+
*/
|
|
1450
|
+
protected renderMarkdown(template: string): string;
|
|
1451
|
+
protected interpolate(text: string): string;
|
|
1383
1452
|
private getRecipientEmail;
|
|
1384
1453
|
}
|
|
1385
1454
|
|
|
@@ -1866,4 +1935,4 @@ declare class TokenBucket {
|
|
|
1866
1935
|
private refill;
|
|
1867
1936
|
}
|
|
1868
1937
|
|
|
1869
|
-
export { AbortError, type BatchResult, BroadcastChannel, type BroadcastNotification, type CacheStore, type ChannelFailurePayload, type ChannelHookPayload, type ChannelMiddleware, type ChannelRateLimitConfig, type ChannelSuccessPayload, DatabaseChannel, type DatabaseNotification, type FlareHookEvent, type FlareHookPayloads, type FlareHooks, type HookEmitter, LazyNotification, MailChannel, type MailChannelConfig, type MailMessage, type MailTemplate, MemoryStore, type MetricsSummary, MiddlewarePriority, type MiddlewarePriorityValue, type Notifiable, Notification, type NotificationBatchCompletePayload, type NotificationBatchStartPayload, type NotificationChannel, type NotificationCompletePayload, type NotificationHookPayload, NotificationManager, type NotificationMetric, NotificationMetricsCollector, type NotificationPreference, type NotificationResult, OrbitFlare, type OrbitFlareOptions, PreferenceMiddleware, type RateLimitConfig, RateLimitMiddleware, type RetryConfig, type SendOptions, type SendResult, type SerializationCheckResult, type ShouldQueue, type ShouldRetry, SlackChannel, type SlackChannelConfig, type SlackMessage, type SlackTemplate, SmsChannel, type SmsChannelConfig, type SmsMessage, type TemplateData, TemplatedNotification, TimeoutChannel, type TimeoutConfig, TimeoutError, TokenBucket, assertSerializable, checkSerializable, createHookEmitter, deepDeserialize, deepSerialize, toPrometheusFormat };
|
|
1938
|
+
export { AbortError, type BatchResult, BroadcastChannel, type BroadcastNotification, type CacheStore, type ChannelFailurePayload, type ChannelHookPayload, type ChannelMiddleware, type ChannelRateLimitConfig, type ChannelSuccessPayload, DatabaseChannel, type DatabaseNotification, FlareError, type FlareErrorCode, FlareErrorCodes, type FlareHookEvent, type FlareHookPayloads, type FlareHooks, type HookEmitter, LazyNotification, MailChannel, type MailChannelConfig, type MailMessage, type MailTemplate, MemoryStore, type MetricsSummary, MiddlewarePriority, type MiddlewarePriorityValue, type Notifiable, Notification, type NotificationBatchCompletePayload, type NotificationBatchStartPayload, type NotificationChannel, type NotificationCompletePayload, type NotificationHookPayload, NotificationManager, type NotificationMetric, NotificationMetricsCollector, type NotificationPreference, type NotificationResult, OrbitFlare, type OrbitFlareOptions, PreferenceMiddleware, type RateLimitConfig, RateLimitMiddleware, type RetryConfig, type SendOptions, type SendResult, type SerializationCheckResult, type ShouldQueue, type ShouldRetry, SlackChannel, type SlackChannelConfig, type SlackMessage, type SlackTemplate, SmsChannel, type SmsChannelConfig, type SmsMessage, type TemplateData, TemplatedNotification, TimeoutChannel, type TimeoutConfig, TimeoutError, TokenBucket, assertSerializable, checkSerializable, createHookEmitter, deepDeserialize, deepSerialize, toPrometheusFormat };
|
package/dist/index.js
CHANGED
|
@@ -183,6 +183,37 @@ var init_PreferenceMiddleware = __esm({
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
// src/errors/FlareError.ts
|
|
187
|
+
import { InfrastructureException } from "@gravito/core";
|
|
188
|
+
var FlareError = class extends InfrastructureException {
|
|
189
|
+
/**
|
|
190
|
+
* Creates a new FlareError instance.
|
|
191
|
+
*
|
|
192
|
+
* @param code - The error code.
|
|
193
|
+
* @param message - The error message.
|
|
194
|
+
* @param options - Optional infrastructure exception options.
|
|
195
|
+
*/
|
|
196
|
+
constructor(code, message, options = {}) {
|
|
197
|
+
const status = options.retryable ? 503 : 500;
|
|
198
|
+
super(status, code, { message, ...options });
|
|
199
|
+
this.name = "FlareError";
|
|
200
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/errors/codes.ts
|
|
205
|
+
var FlareErrorCodes = {
|
|
206
|
+
RATE_LIMIT_EXCEEDED: "flare.rate_limit_exceeded",
|
|
207
|
+
SERIALIZATION_FAILED: "flare.serialization_failed",
|
|
208
|
+
TEMPLATE_NOT_DEFINED: "flare.template_not_defined",
|
|
209
|
+
NOTIFIABLE_MISSING_EMAIL: "flare.notifiable_missing_email",
|
|
210
|
+
INVALID_CONFIG: "flare.invalid_config",
|
|
211
|
+
NOTIFICATION_METHOD_NOT_IMPLEMENTED: "flare.notification_method_not_implemented",
|
|
212
|
+
UNSUPPORTED_PROVIDER: "flare.unsupported_provider",
|
|
213
|
+
CREDENTIALS_MISSING: "flare.credentials_missing",
|
|
214
|
+
SEND_FAILED: "flare.send_failed"
|
|
215
|
+
};
|
|
216
|
+
|
|
186
217
|
// src/channels/TimeoutChannel.ts
|
|
187
218
|
var TimeoutError = class extends Error {
|
|
188
219
|
constructor(message) {
|
|
@@ -220,16 +251,23 @@ var TimeoutChannel = class {
|
|
|
220
251
|
}
|
|
221
252
|
const controller = new AbortController();
|
|
222
253
|
const { signal } = controller;
|
|
254
|
+
let timeoutId;
|
|
255
|
+
let settled = false;
|
|
256
|
+
let handleExternalAbort;
|
|
223
257
|
if (options?.signal) {
|
|
224
258
|
if (options.signal.aborted) {
|
|
225
259
|
throw new AbortError("Request was aborted before sending");
|
|
226
260
|
}
|
|
227
|
-
|
|
261
|
+
handleExternalAbort = () => {
|
|
228
262
|
controller.abort();
|
|
229
|
-
}
|
|
263
|
+
};
|
|
264
|
+
options.signal.addEventListener("abort", handleExternalAbort, { once: true });
|
|
230
265
|
}
|
|
231
266
|
const timeoutPromise = new Promise((_, reject) => {
|
|
232
|
-
setTimeout(() => {
|
|
267
|
+
timeoutId = setTimeout(() => {
|
|
268
|
+
if (settled) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
233
271
|
if (this.config.onTimeout) {
|
|
234
272
|
this.config.onTimeout(this.inner.constructor.name, notification);
|
|
235
273
|
}
|
|
@@ -250,7 +288,17 @@ var TimeoutChannel = class {
|
|
|
250
288
|
}
|
|
251
289
|
throw error;
|
|
252
290
|
});
|
|
253
|
-
|
|
291
|
+
try {
|
|
292
|
+
return await Promise.race([sendPromise, timeoutPromise]);
|
|
293
|
+
} finally {
|
|
294
|
+
settled = true;
|
|
295
|
+
if (timeoutId) {
|
|
296
|
+
clearTimeout(timeoutId);
|
|
297
|
+
}
|
|
298
|
+
if (options?.signal && handleExternalAbort) {
|
|
299
|
+
options.signal.removeEventListener("abort", handleExternalAbort);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
254
302
|
}
|
|
255
303
|
};
|
|
256
304
|
|
|
@@ -263,7 +311,10 @@ var BroadcastChannel = class {
|
|
|
263
311
|
const innerChannel = {
|
|
264
312
|
send: async (notification, notifiable, _options) => {
|
|
265
313
|
if (!notification.toBroadcast) {
|
|
266
|
-
throw new
|
|
314
|
+
throw new FlareError(
|
|
315
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
316
|
+
"Notification does not implement toBroadcast method"
|
|
317
|
+
);
|
|
267
318
|
}
|
|
268
319
|
const broadcastNotification = notification.toBroadcast(notifiable);
|
|
269
320
|
const notifiableId = notifiable.getNotifiableId();
|
|
@@ -297,7 +348,10 @@ var DatabaseChannel = class {
|
|
|
297
348
|
const innerChannel = {
|
|
298
349
|
send: async (notification, notifiable, _options) => {
|
|
299
350
|
if (!notification.toDatabase) {
|
|
300
|
-
throw new
|
|
351
|
+
throw new FlareError(
|
|
352
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
353
|
+
"Notification does not implement toDatabase method"
|
|
354
|
+
);
|
|
301
355
|
}
|
|
302
356
|
const dbNotification = notification.toDatabase(notifiable);
|
|
303
357
|
await this.dbService.insertNotification({
|
|
@@ -329,7 +383,10 @@ var MailChannel = class {
|
|
|
329
383
|
const innerChannel = {
|
|
330
384
|
send: async (notification, notifiable, _options) => {
|
|
331
385
|
if (!notification.toMail) {
|
|
332
|
-
throw new
|
|
386
|
+
throw new FlareError(
|
|
387
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
388
|
+
"Notification does not implement toMail method"
|
|
389
|
+
);
|
|
333
390
|
}
|
|
334
391
|
const message = notification.toMail(notifiable);
|
|
335
392
|
await this.mailService.send(message);
|
|
@@ -355,7 +412,10 @@ var SlackChannel = class {
|
|
|
355
412
|
const innerChannel = {
|
|
356
413
|
send: async (notification, notifiable, options) => {
|
|
357
414
|
if (!notification.toSlack) {
|
|
358
|
-
throw new
|
|
415
|
+
throw new FlareError(
|
|
416
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
417
|
+
"Notification does not implement toSlack method"
|
|
418
|
+
);
|
|
359
419
|
}
|
|
360
420
|
const slackMessage = notification.toSlack(notifiable);
|
|
361
421
|
const response = await fetch(this.config.webhookUrl, {
|
|
@@ -374,7 +434,10 @@ var SlackChannel = class {
|
|
|
374
434
|
// Pass AbortSignal to fetch
|
|
375
435
|
});
|
|
376
436
|
if (!response.ok) {
|
|
377
|
-
throw new
|
|
437
|
+
throw new FlareError(
|
|
438
|
+
FlareErrorCodes.SEND_FAILED,
|
|
439
|
+
`Failed to send Slack notification: ${response.statusText}`
|
|
440
|
+
);
|
|
378
441
|
}
|
|
379
442
|
}
|
|
380
443
|
};
|
|
@@ -398,7 +461,10 @@ var SmsChannel = class {
|
|
|
398
461
|
const innerChannel = {
|
|
399
462
|
send: async (notification, notifiable, options) => {
|
|
400
463
|
if (!notification.toSms) {
|
|
401
|
-
throw new
|
|
464
|
+
throw new FlareError(
|
|
465
|
+
FlareErrorCodes.NOTIFICATION_METHOD_NOT_IMPLEMENTED,
|
|
466
|
+
"Notification does not implement toSms method"
|
|
467
|
+
);
|
|
402
468
|
}
|
|
403
469
|
const smsMessage = notification.toSms(notifiable);
|
|
404
470
|
switch (this.config.provider) {
|
|
@@ -409,7 +475,10 @@ var SmsChannel = class {
|
|
|
409
475
|
await this.sendViaAwsSns(smsMessage, options?.signal);
|
|
410
476
|
break;
|
|
411
477
|
default:
|
|
412
|
-
throw new
|
|
478
|
+
throw new FlareError(
|
|
479
|
+
FlareErrorCodes.UNSUPPORTED_PROVIDER,
|
|
480
|
+
`Unsupported SMS provider: ${this.config.provider}`
|
|
481
|
+
);
|
|
413
482
|
}
|
|
414
483
|
}
|
|
415
484
|
};
|
|
@@ -428,7 +497,10 @@ var SmsChannel = class {
|
|
|
428
497
|
*/
|
|
429
498
|
async sendViaTwilio(message, signal) {
|
|
430
499
|
if (!this.config.apiKey || !this.config.apiSecret) {
|
|
431
|
-
throw new
|
|
500
|
+
throw new FlareError(
|
|
501
|
+
FlareErrorCodes.CREDENTIALS_MISSING,
|
|
502
|
+
"Twilio API key and secret are required"
|
|
503
|
+
);
|
|
432
504
|
}
|
|
433
505
|
const accountSid = this.config.apiKey;
|
|
434
506
|
const authToken = this.config.apiSecret;
|
|
@@ -451,7 +523,7 @@ var SmsChannel = class {
|
|
|
451
523
|
);
|
|
452
524
|
if (!response.ok) {
|
|
453
525
|
const error = await response.text();
|
|
454
|
-
throw new
|
|
526
|
+
throw new FlareError(FlareErrorCodes.SEND_FAILED, `Failed to send SMS via Twilio: ${error}`);
|
|
455
527
|
}
|
|
456
528
|
}
|
|
457
529
|
/**
|
|
@@ -465,7 +537,8 @@ var SmsChannel = class {
|
|
|
465
537
|
SNSClient = awsSns.SNSClient;
|
|
466
538
|
PublishCommand = awsSns.PublishCommand;
|
|
467
539
|
} catch {
|
|
468
|
-
throw new
|
|
540
|
+
throw new FlareError(
|
|
541
|
+
FlareErrorCodes.UNSUPPORTED_PROVIDER,
|
|
469
542
|
"AWS SNS SMS requires @aws-sdk/client-sns. Install it with: bun add @aws-sdk/client-sns"
|
|
470
543
|
);
|
|
471
544
|
}
|
|
@@ -496,7 +569,10 @@ var SmsChannel = class {
|
|
|
496
569
|
await client.send(command, { abortSignal: signal });
|
|
497
570
|
} catch (error) {
|
|
498
571
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
499
|
-
throw new
|
|
572
|
+
throw new FlareError(
|
|
573
|
+
FlareErrorCodes.SEND_FAILED,
|
|
574
|
+
`Failed to send SMS via AWS SNS: ${err.message}`
|
|
575
|
+
);
|
|
500
576
|
}
|
|
501
577
|
}
|
|
502
578
|
};
|
|
@@ -755,7 +831,7 @@ var RateLimitMiddleware = class {
|
|
|
755
831
|
* Create a new RateLimitMiddleware instance.
|
|
756
832
|
*
|
|
757
833
|
* @param config - Rate limit configuration for each channel
|
|
758
|
-
* @param store - Optional cache store for distributed rate limiting
|
|
834
|
+
* @param store - Optional cache store for distributed rate limiting (future use)
|
|
759
835
|
*
|
|
760
836
|
* @example
|
|
761
837
|
* ```typescript
|
|
@@ -772,7 +848,7 @@ var RateLimitMiddleware = class {
|
|
|
772
848
|
*/
|
|
773
849
|
constructor(config, store) {
|
|
774
850
|
this.config = config;
|
|
775
|
-
this.store = store
|
|
851
|
+
this.store = store || new MemoryStore();
|
|
776
852
|
this.initializeBuckets();
|
|
777
853
|
}
|
|
778
854
|
/**
|
|
@@ -791,7 +867,6 @@ var RateLimitMiddleware = class {
|
|
|
791
867
|
buckets = /* @__PURE__ */ new Map();
|
|
792
868
|
/**
|
|
793
869
|
* Cache store for distributed rate limiting.
|
|
794
|
-
* 分散式限流使用的快取儲存
|
|
795
870
|
*/
|
|
796
871
|
store;
|
|
797
872
|
/**
|
|
@@ -849,7 +924,8 @@ var RateLimitMiddleware = class {
|
|
|
849
924
|
if (bucket) {
|
|
850
925
|
const allowed = bucket.tryConsume();
|
|
851
926
|
if (!allowed) {
|
|
852
|
-
throw new
|
|
927
|
+
throw new FlareError(
|
|
928
|
+
FlareErrorCodes.RATE_LIMIT_EXCEEDED,
|
|
853
929
|
`Rate limit exceeded for channel '${channel}' (${window}ly limit). Please try again later.`
|
|
854
930
|
);
|
|
855
931
|
}
|
|
@@ -1152,7 +1228,8 @@ function checkSerializable(obj, path = "") {
|
|
|
1152
1228
|
function assertSerializable(obj) {
|
|
1153
1229
|
const result = checkSerializable(obj);
|
|
1154
1230
|
if (!result.serializable) {
|
|
1155
|
-
throw new
|
|
1231
|
+
throw new FlareError(
|
|
1232
|
+
FlareErrorCodes.SERIALIZATION_FAILED,
|
|
1156
1233
|
`\u7269\u4EF6\u5305\u542B\u4E0D\u53EF\u5E8F\u5217\u5316\u7684\u5C6C\u6027:
|
|
1157
1234
|
\u554F\u984C\u8DEF\u5F91: ${result.problematicPaths.join(", ")}
|
|
1158
1235
|
\u8A73\u7D30\u8CC7\u8A0A:
|
|
@@ -1671,24 +1748,30 @@ var OrbitFlare = class _OrbitFlare {
|
|
|
1671
1748
|
if (options.enableSlack) {
|
|
1672
1749
|
const slack = options.channels?.slack;
|
|
1673
1750
|
if (!slack?.webhookUrl) {
|
|
1674
|
-
throw new
|
|
1751
|
+
throw new FlareError(
|
|
1752
|
+
FlareErrorCodes.INVALID_CONFIG,
|
|
1675
1753
|
"[OrbitFlare] Slack channel enabled but webhookUrl not provided. Configure channels.slack.webhookUrl or set enableSlack to false."
|
|
1676
1754
|
);
|
|
1677
1755
|
}
|
|
1678
1756
|
if (!this.isValidUrl(slack.webhookUrl)) {
|
|
1679
|
-
throw new
|
|
1757
|
+
throw new FlareError(
|
|
1758
|
+
FlareErrorCodes.INVALID_CONFIG,
|
|
1759
|
+
`[OrbitFlare] Invalid Slack webhook URL: ${slack.webhookUrl}`
|
|
1760
|
+
);
|
|
1680
1761
|
}
|
|
1681
1762
|
}
|
|
1682
1763
|
if (options.enableSms) {
|
|
1683
1764
|
const sms = options.channels?.sms;
|
|
1684
1765
|
if (!sms?.provider) {
|
|
1685
|
-
throw new
|
|
1766
|
+
throw new FlareError(
|
|
1767
|
+
FlareErrorCodes.INVALID_CONFIG,
|
|
1686
1768
|
"[OrbitFlare] SMS channel enabled but provider not specified. Configure channels.sms.provider or set enableSms to false."
|
|
1687
1769
|
);
|
|
1688
1770
|
}
|
|
1689
1771
|
const supportedProviders = ["twilio", "aws-sns"];
|
|
1690
1772
|
if (!supportedProviders.includes(sms.provider)) {
|
|
1691
|
-
throw new
|
|
1773
|
+
throw new FlareError(
|
|
1774
|
+
FlareErrorCodes.UNSUPPORTED_PROVIDER,
|
|
1692
1775
|
`[OrbitFlare] Unsupported SMS provider: ${sms.provider}. Supported providers: ${supportedProviders.join(", ")}`
|
|
1693
1776
|
);
|
|
1694
1777
|
}
|
|
@@ -1811,6 +1894,7 @@ var OrbitFlare = class _OrbitFlare {
|
|
|
1811
1894
|
};
|
|
1812
1895
|
|
|
1813
1896
|
// src/templates/NotificationTemplate.ts
|
|
1897
|
+
import { createHtmlRenderCallbacks, getMarkdownAdapter } from "@gravito/core";
|
|
1814
1898
|
var TemplatedNotification = class extends Notification {
|
|
1815
1899
|
data = {};
|
|
1816
1900
|
with(data) {
|
|
@@ -1830,7 +1914,7 @@ var TemplatedNotification = class extends Notification {
|
|
|
1830
1914
|
// Auto-implement toSlack
|
|
1831
1915
|
toSlack(_notifiable) {
|
|
1832
1916
|
if (!this.slackTemplate) {
|
|
1833
|
-
throw new
|
|
1917
|
+
throw new FlareError(FlareErrorCodes.TEMPLATE_NOT_DEFINED, "slackTemplate not defined");
|
|
1834
1918
|
}
|
|
1835
1919
|
const template = this.slackTemplate();
|
|
1836
1920
|
return {
|
|
@@ -1839,6 +1923,33 @@ var TemplatedNotification = class extends Notification {
|
|
|
1839
1923
|
attachments: template.attachments
|
|
1840
1924
|
};
|
|
1841
1925
|
}
|
|
1926
|
+
/**
|
|
1927
|
+
* 將 Markdown 模板渲染為 HTML。
|
|
1928
|
+
* 先執行變數插值(`{{var}}`),再將 Markdown 轉為 HTML。
|
|
1929
|
+
* 內建 XSS 防護:過濾 script、iframe、event handler 等危險 HTML。
|
|
1930
|
+
*
|
|
1931
|
+
* 適用於郵件、Slack 等富文本通知頻道。
|
|
1932
|
+
*
|
|
1933
|
+
* @param template - Markdown 格式的模板字串
|
|
1934
|
+
* @returns 安全的 HTML 字串
|
|
1935
|
+
*
|
|
1936
|
+
* @example
|
|
1937
|
+
* ```typescript
|
|
1938
|
+
* const html = this.renderMarkdown('# Hello {{name}}')
|
|
1939
|
+
* await sendEmail({ htmlBody: html })
|
|
1940
|
+
* ```
|
|
1941
|
+
*/
|
|
1942
|
+
renderMarkdown(template) {
|
|
1943
|
+
if (!template) {
|
|
1944
|
+
return "";
|
|
1945
|
+
}
|
|
1946
|
+
const interpolated = this.interpolate(template);
|
|
1947
|
+
const md = getMarkdownAdapter();
|
|
1948
|
+
const sanitizeCallbacks = createHtmlRenderCallbacks({
|
|
1949
|
+
html: (rawHtml) => sanitizeHtml(rawHtml)
|
|
1950
|
+
});
|
|
1951
|
+
return md.render(interpolated, sanitizeCallbacks);
|
|
1952
|
+
}
|
|
1842
1953
|
interpolate(text) {
|
|
1843
1954
|
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => String(this.data[key] ?? `{{${key}}}`));
|
|
1844
1955
|
}
|
|
@@ -1846,9 +1957,23 @@ var TemplatedNotification = class extends Notification {
|
|
|
1846
1957
|
if ("email" in notifiable && typeof notifiable.email === "string") {
|
|
1847
1958
|
return notifiable.email;
|
|
1848
1959
|
}
|
|
1849
|
-
throw new
|
|
1960
|
+
throw new FlareError(
|
|
1961
|
+
FlareErrorCodes.NOTIFIABLE_MISSING_EMAIL,
|
|
1962
|
+
"Notifiable does not have an email property"
|
|
1963
|
+
);
|
|
1850
1964
|
}
|
|
1851
1965
|
};
|
|
1966
|
+
var DANGEROUS_TAGS = /^<\/?(?:script|iframe|object|embed|form|input|textarea|button|select|style|link|meta|base)\b[^>]*>$/i;
|
|
1967
|
+
var EVENT_HANDLER_ATTRS = /\s+on\w+\s*=/i;
|
|
1968
|
+
function sanitizeHtml(rawHtml) {
|
|
1969
|
+
if (DANGEROUS_TAGS.test(rawHtml.trim())) {
|
|
1970
|
+
return "";
|
|
1971
|
+
}
|
|
1972
|
+
if (EVENT_HANDLER_ATTRS.test(rawHtml)) {
|
|
1973
|
+
return "";
|
|
1974
|
+
}
|
|
1975
|
+
return rawHtml;
|
|
1976
|
+
}
|
|
1852
1977
|
|
|
1853
1978
|
// src/index.ts
|
|
1854
1979
|
init_middleware();
|
|
@@ -1906,6 +2031,8 @@ export {
|
|
|
1906
2031
|
AbortError,
|
|
1907
2032
|
BroadcastChannel,
|
|
1908
2033
|
DatabaseChannel,
|
|
2034
|
+
FlareError,
|
|
2035
|
+
FlareErrorCodes,
|
|
1909
2036
|
LazyNotification,
|
|
1910
2037
|
MailChannel,
|
|
1911
2038
|
MemoryStore,
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravito/flare",
|
|
3
|
-
"
|
|
3
|
+
"sideEffects": false,
|
|
4
|
+
"version": "5.0.0",
|
|
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,13 +44,13 @@
|
|
|
42
44
|
"author": "Carl Lee <carllee0520@gmail.com>",
|
|
43
45
|
"license": "MIT",
|
|
44
46
|
"dependencies": {
|
|
45
|
-
"@gravito/core": "^
|
|
47
|
+
"@gravito/core": "^2.0.6",
|
|
46
48
|
"@aws-sdk/client-sns": "^3.734.0"
|
|
47
49
|
},
|
|
48
50
|
"peerDependencies": {
|
|
49
|
-
"@gravito/stream": "^
|
|
50
|
-
"@gravito/signal": "^
|
|
51
|
-
"@gravito/radiance": "^
|
|
51
|
+
"@gravito/stream": "^3.0.0",
|
|
52
|
+
"@gravito/signal": "^4.0.0",
|
|
53
|
+
"@gravito/radiance": "^2.0.0"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"bun-types": "latest",
|