hazo_notify 5.0.1 → 5.3.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.
Files changed (124) hide show
  1. package/README.md +42 -10
  2. package/dist/components/notification_bell/index.d.ts +11 -1
  3. package/dist/components/notification_bell/index.d.ts.map +1 -1
  4. package/dist/components/notification_bell/index.js +33 -1
  5. package/dist/components/notification_bell/index.js.map +1 -1
  6. package/dist/lib/adapters/email/adapter.d.ts +2 -0
  7. package/dist/lib/adapters/email/adapter.d.ts.map +1 -1
  8. package/dist/lib/adapters/email/adapter.js +53 -22
  9. package/dist/lib/adapters/email/adapter.js.map +1 -1
  10. package/dist/lib/adapters/webhook/adapter.d.ts +10 -0
  11. package/dist/lib/adapters/webhook/adapter.d.ts.map +1 -0
  12. package/dist/lib/adapters/webhook/adapter.js +90 -0
  13. package/dist/lib/adapters/webhook/adapter.js.map +1 -0
  14. package/dist/lib/adapters/webhook/index.d.ts +8 -0
  15. package/dist/lib/adapters/webhook/index.d.ts.map +1 -0
  16. package/dist/lib/adapters/webhook/index.js +3 -0
  17. package/dist/lib/adapters/webhook/index.js.map +1 -0
  18. package/dist/lib/adapters/webhook/types.d.ts +45 -0
  19. package/dist/lib/adapters/webhook/types.d.ts.map +1 -0
  20. package/dist/lib/adapters/webhook/types.js +12 -0
  21. package/dist/lib/adapters/webhook/types.js.map +1 -0
  22. package/dist/lib/api/inbox.d.ts +32 -0
  23. package/dist/lib/api/inbox.d.ts.map +1 -1
  24. package/dist/lib/api/inbox.js +68 -0
  25. package/dist/lib/api/inbox.js.map +1 -1
  26. package/dist/lib/dispatcher/index.d.ts +2 -2
  27. package/dist/lib/dispatcher/index.d.ts.map +1 -1
  28. package/dist/lib/dispatcher/index.js +30 -8
  29. package/dist/lib/dispatcher/index.js.map +1 -1
  30. package/dist/lib/inbox/broadcaster.d.ts +44 -0
  31. package/dist/lib/inbox/broadcaster.d.ts.map +1 -0
  32. package/dist/lib/inbox/broadcaster.js +62 -0
  33. package/dist/lib/inbox/broadcaster.js.map +1 -0
  34. package/dist/lib/inbox/index.d.ts +2 -0
  35. package/dist/lib/inbox/index.d.ts.map +1 -1
  36. package/dist/lib/inbox/index.js +1 -0
  37. package/dist/lib/inbox/index.js.map +1 -1
  38. package/dist/lib/inbox/storage.d.ts.map +1 -1
  39. package/dist/lib/inbox/storage.js +8 -2
  40. package/dist/lib/inbox/storage.js.map +1 -1
  41. package/dist/lib/inbox/types.d.ts +10 -1
  42. package/dist/lib/inbox/types.d.ts.map +1 -1
  43. package/dist/lib/jobs/digest.d.ts +77 -0
  44. package/dist/lib/jobs/digest.d.ts.map +1 -0
  45. package/dist/lib/jobs/digest.js +127 -0
  46. package/dist/lib/jobs/digest.js.map +1 -0
  47. package/dist/lib/jobs/index.d.ts +2 -0
  48. package/dist/lib/jobs/index.d.ts.map +1 -1
  49. package/dist/lib/jobs/index.js +1 -0
  50. package/dist/lib/jobs/index.js.map +1 -1
  51. package/dist/lib/jobs/submit.d.ts.map +1 -1
  52. package/dist/lib/jobs/submit.js +1 -0
  53. package/dist/lib/jobs/submit.js.map +1 -1
  54. package/dist/lib/jobs/types.d.ts +1 -0
  55. package/dist/lib/jobs/types.d.ts.map +1 -1
  56. package/dist/lib/lifecycle/default_templates.d.ts +9 -0
  57. package/dist/lib/lifecycle/default_templates.d.ts.map +1 -0
  58. package/dist/lib/lifecycle/default_templates.js +196 -0
  59. package/dist/lib/lifecycle/default_templates.js.map +1 -0
  60. package/dist/lib/lifecycle/dispatch.d.ts +37 -0
  61. package/dist/lib/lifecycle/dispatch.d.ts.map +1 -0
  62. package/dist/lib/lifecycle/dispatch.js +32 -0
  63. package/dist/lib/lifecycle/dispatch.js.map +1 -0
  64. package/dist/lib/lifecycle/events.d.ts +34 -0
  65. package/dist/lib/lifecycle/events.d.ts.map +1 -0
  66. package/dist/lib/lifecycle/events.js +118 -0
  67. package/dist/lib/lifecycle/events.js.map +1 -0
  68. package/dist/lib/lifecycle/handler.d.ts +29 -0
  69. package/dist/lib/lifecycle/handler.d.ts.map +1 -0
  70. package/dist/lib/lifecycle/handler.js +145 -0
  71. package/dist/lib/lifecycle/handler.js.map +1 -0
  72. package/dist/lib/lifecycle/index.d.ts +23 -0
  73. package/dist/lib/lifecycle/index.d.ts.map +1 -0
  74. package/dist/lib/lifecycle/index.js +21 -0
  75. package/dist/lib/lifecycle/index.js.map +1 -0
  76. package/dist/lib/lifecycle/register.d.ts +29 -0
  77. package/dist/lib/lifecycle/register.d.ts.map +1 -0
  78. package/dist/lib/lifecycle/register.js +29 -0
  79. package/dist/lib/lifecycle/register.js.map +1 -0
  80. package/dist/lib/lifecycle/resolver.d.ts +22 -0
  81. package/dist/lib/lifecycle/resolver.d.ts.map +1 -0
  82. package/dist/lib/lifecycle/resolver.js +105 -0
  83. package/dist/lib/lifecycle/resolver.js.map +1 -0
  84. package/dist/lib/lifecycle/scheduler.d.ts +27 -0
  85. package/dist/lib/lifecycle/scheduler.d.ts.map +1 -0
  86. package/dist/lib/lifecycle/scheduler.js +108 -0
  87. package/dist/lib/lifecycle/scheduler.js.map +1 -0
  88. package/dist/lib/lifecycle/status.d.ts +39 -0
  89. package/dist/lib/lifecycle/status.d.ts.map +1 -0
  90. package/dist/lib/lifecycle/status.js +67 -0
  91. package/dist/lib/lifecycle/status.js.map +1 -0
  92. package/dist/lib/lifecycle/types.d.ts +165 -0
  93. package/dist/lib/lifecycle/types.d.ts.map +1 -0
  94. package/dist/lib/lifecycle/types.js +11 -0
  95. package/dist/lib/lifecycle/types.js.map +1 -0
  96. package/dist/lib/preferences/index.d.ts +3 -0
  97. package/dist/lib/preferences/index.d.ts.map +1 -0
  98. package/dist/lib/preferences/index.js +3 -0
  99. package/dist/lib/preferences/index.js.map +1 -0
  100. package/dist/lib/preferences/storage.d.ts +33 -0
  101. package/dist/lib/preferences/storage.d.ts.map +1 -0
  102. package/dist/lib/preferences/storage.js +129 -0
  103. package/dist/lib/preferences/storage.js.map +1 -0
  104. package/dist/lib/preferences/types.d.ts +59 -0
  105. package/dist/lib/preferences/types.d.ts.map +1 -0
  106. package/dist/lib/preferences/types.js +20 -0
  107. package/dist/lib/preferences/types.js.map +1 -0
  108. package/dist/lib/template_manager/db/template_repository.d.ts +4 -1
  109. package/dist/lib/template_manager/db/template_repository.d.ts.map +1 -1
  110. package/dist/lib/template_manager/db/template_repository.js +17 -1
  111. package/dist/lib/template_manager/db/template_repository.js.map +1 -1
  112. package/dist/lib/template_manager/types.d.ts +1 -0
  113. package/dist/lib/template_manager/types.d.ts.map +1 -1
  114. package/migrations/008_lifecycle_status.pg.sql +27 -0
  115. package/migrations/008_lifecycle_status.sqlite.sql +24 -0
  116. package/migrations/009_templates_locale.pg.sql +6 -0
  117. package/migrations/009_templates_locale.sqlite.sql +4 -0
  118. package/migrations/010_preferences.pg.sql +26 -0
  119. package/migrations/010_preferences.sqlite.sql +32 -0
  120. package/migrations/011_digest_status.pg.sql +21 -0
  121. package/migrations/011_digest_status.sqlite.sql +43 -0
  122. package/migrations/012_preferences_digest.pg.sql +12 -0
  123. package/migrations/012_preferences_digest.sqlite.sql +51 -0
  124. package/package.json +15 -3
package/README.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  A reusable component library for sending email notifications with scope-aware template management. Implements Zeptomail API integration with support for text and HTML emails, Handlebars-based email templates with multi-tenant resolution, multiple attachments, and comprehensive security features.
4
4
 
5
- **Version**: 4.0.2
5
+ **Version**: 5.3.0
6
6
 
7
- > **⚠ Readers on v4:** Many examples below still show the v3.x API surface (`send_email`, `hazo_notify/emailer`, `channels: { in_app, email, banner }`, direct `template_html` / `template_text` access). **v4.0.0 introduced a channel-pluggable architecture** — outbound sends go through `dispatch()` → inbox row + per-channel delivery row → `EmailChannel`/`TelegramChannel` adapter, with a multi-channel worker fleet. For the current v4 API see:
7
+ > **⚠ Readers on older versions:** Many examples below still show the v3.x API surface (`send_email`, `hazo_notify/emailer`, `channels: { in_app, email, banner }`, direct `template_html` / `template_text` access). **v4.0.0 introduced a channel-pluggable architecture** — outbound sends go through `dispatch()` → inbox row + per-channel delivery row → `EmailChannel`/`TelegramChannel` adapter, with a multi-channel worker fleet. For the current API see:
8
8
  >
9
- > - [`TECHDOC.md`](./TECHDOC.md) — v4 architecture and API reference (channel registry, dispatch flow, worker fleet, adapters, schema)
9
+ > - [`TECHDOC.md`](./TECHDOC.md) — v4+ architecture and API reference (channel registry, dispatch flow, worker fleet, adapters, schema)
10
10
  > - [`SETUP_CHECKLIST.md`](./SETUP_CHECKLIST.md) §"v4.0.0 Migration" — migrations 005/006/007, dispatch rewrite, boot changes
11
- > - [`CHANGE_LOG.md`](./CHANGE_LOG.md) — full breaking-change manifest for v4.0.0
11
+ > - [`CHANGE_LOG.md`](./CHANGE_LOG.md) — full breaking-change manifest and release history
12
12
  >
13
- > A full v4-aware rewrite of this README is tracked for v4.0.2. The examples that follow remain accurate for v3.x consumers; v4 consumers should read the docs above.
13
+ > A full v5-aware rewrite of this README is planned. The examples that follow remain accurate for v3.x consumers; v5 consumers should read the docs above.
14
14
 
15
15
  ## Features
16
16
 
@@ -29,9 +29,14 @@ A reusable component library for sending email notifications with scope-aware te
29
29
  - **Default Templates**: Built-in welcome, verification, password reset, and signature templates
30
30
  - **Caching**: Template compilation caching for performance
31
31
 
32
+ ### Dispatch & Preferences
33
+ - **`dispatch()`**: Fan-out a single event to multiple users across any mix of registered channels (email, Telegram, webhook, …). Creates one inbox row and one delivery row per (user × channel).
34
+ - **Preference matrix** (v5.2.0+): Per-user, per-channel delivery preferences via `hazo_notify/preferences`. Modes: `'on'` (immediate), `'off'` (suppress), `'digest'` (daily batch).
35
+ - **Daily digest mode** (v5.3.0+): `mode: 'digest'` writes delivery rows as `status = 'digesting'`. A scheduled `DIGEST_FLUSH_JOB_TYPE` job batches them into one digest email per (user × channel) per run.
36
+
32
37
  ### General
33
- - **Test app** (v4.0.2+): Self-contained Next.js demo at `test-app/` mirroring the layout of `hazo_jobs/test-app/`. Runs on port 3020 (`npm run dev:test-app`) and covers every shipping surface — emailer, telegram, dispatch, templates, inbox/bell/banner, worker, jobs admin. Excluded from the published tarball.
34
- - **hazo_jobs integration** (v4.0.2+): New `hazo_notify/jobs` subpath exporting `createInboxFlushHandler` and `submitInboxFlushJobs` so consumers can drive the inbox-flush worker from their own [`hazo_jobs`](https://www.npmjs.com/package/hazo_jobs) queue. `hazo_jobs` is an optional peer dep.
38
+ - **Test app** (v4.0.2+): Self-contained Next.js demo at `test-app/` mirroring the layout of `hazo_jobs/test-app/`. Runs on port 3020 (`npm run dev:test-app`) and covers every shipping surface — emailer, telegram, dispatch, templates, inbox/bell/banner, worker, preferences, digest, jobs admin. Excluded from the published tarball.
39
+ - **hazo_jobs integration** (v4.0.2+): `hazo_notify/jobs` subpath exports `createInboxFlushHandler`, `submitInboxFlushJobs`, and (v5.3.0+) `DIGEST_FLUSH_JOB_TYPE` + `createDigestFlushHandler`. `hazo_jobs` is an optional peer dep.
35
40
  - **TypeScript**: Fully typed with TypeScript
36
41
  - **Testing**: Test coverage with Jest
37
42
 
@@ -1049,9 +1054,11 @@ The test-app is excluded from the published npm tarball.
1049
1054
 
1050
1055
  ### hazo_jobs integration (`hazo_notify/jobs`)
1051
1056
 
1052
- v4.0.2 ships a new subpath export for projects that want to drive the inbox
1053
- flush worker through their own [`hazo_jobs`](https://www.npmjs.com/package/hazo_jobs)
1054
- queue rather than running the bundled `startInboxWorker`:
1057
+ v4.0.2 ships a subpath export for projects that want to drive the inbox flush
1058
+ worker through their own [`hazo_jobs`](https://www.npmjs.com/package/hazo_jobs)
1059
+ queue rather than running the bundled `startInboxWorker`.
1060
+
1061
+ #### Inbox flush (immediate delivery)
1055
1062
 
1056
1063
  ```ts
1057
1064
  import {
@@ -1078,6 +1085,31 @@ const proc = createWorkerProcess({
1078
1085
  await submitInboxFlushJobs(jobsClient, { channels: ["email", "telegram"] });
1079
1086
  ```
1080
1087
 
1088
+ #### Daily digest flush (v5.3.0+)
1089
+
1090
+ Users with `mode: 'digest'` preference get delivery rows written as
1091
+ `status = 'digesting'` — skipped by the inbox flush worker. Register the
1092
+ digest handler and schedule a daily job to batch them:
1093
+
1094
+ ```ts
1095
+ import {
1096
+ DIGEST_FLUSH_JOB_TYPE,
1097
+ createDigestFlushHandler,
1098
+ } from "hazo_notify/jobs";
1099
+
1100
+ scheduler.register(
1101
+ DIGEST_FLUSH_JOB_TYPE,
1102
+ createDigestFlushHandler({
1103
+ resolve_recipient: async (user_id) => lookupEmail(user_id),
1104
+ template_name: "digest_daily", // default system template; override as needed
1105
+ }),
1106
+ );
1107
+ ```
1108
+
1109
+ The `digest_daily` template is seeded automatically by `sync_system_templates()`.
1110
+ Variables available in the template: `{{digest_count}}`, `{{channel_id}}`,
1111
+ `{{digest_date}}` (YYYY-MM-DD), `{{user_id}}`.
1112
+
1081
1113
  `hazo_jobs` is an **optional** peer dependency — the package only declares the
1082
1114
  structural types it needs and never imports `hazo_jobs` at runtime. See
1083
1115
  `test-app/worker.ts` for a full worker example.
@@ -51,6 +51,16 @@ export interface NotificationBellProps {
51
51
  components: NotificationBellComponents;
52
52
  onItemClick?: (item: InboxItem) => void;
53
53
  bellIcon?: ReactNode;
54
+ /**
55
+ * Enable real-time push via SSE. When enabled the bell opens an EventSource
56
+ * to `eventSourceUrl` and refreshes the unread count on `notification.created`
57
+ * events. Falls back to polling if the connection drops.
58
+ */
59
+ realtime?: {
60
+ enabled: boolean;
61
+ /** URL of the SSE stream endpoint. Default: `${apiBasePath}/stream` */
62
+ eventSourceUrl?: string;
63
+ };
54
64
  }
55
- export declare function NotificationBell({ apiBasePath, pollIntervalMs, components: C, onItemClick, bellIcon, }: NotificationBellProps): import("react/jsx-runtime").JSX.Element;
65
+ export declare function NotificationBell({ apiBasePath, pollIntervalMs, components: C, onItemClick, bellIcon, realtime, }: NotificationBellProps): import("react/jsx-runtime").JSX.Element;
56
66
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/notification_bell/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAoC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI7F,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5F,YAAY,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAC,CAAC;IAC3G,mBAAmB,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,mBAAmB,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,gBAAgB,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAC;IAChF,KAAK,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,0BAA0B,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,WAAkC,EAClC,cAAgC,EAChC,UAAU,EAAE,CAAC,EACb,WAAW,EACX,QAAQ,GACT,EAAE,qBAAqB,2CAiFvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/notification_bell/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAoC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI7F,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5F,YAAY,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAC,CAAC;IAC3G,mBAAmB,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,mBAAmB,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,gBAAgB,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAC;IAChF,KAAK,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,0BAA0B,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,uEAAuE;QACvE,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,WAAkC,EAClC,cAAgC,EAChC,UAAU,EAAE,CAAC,EACb,WAAW,EACX,QAAQ,EACR,QAAQ,GACT,EAAE,qBAAqB,2CAoHvB"}
@@ -9,7 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  */
10
10
  import { useEffect, useState, useCallback } from "react";
11
11
  const DEFAULT_POLL_MS = 30000;
12
- export function NotificationBell({ apiBasePath = "/api/notifications", pollIntervalMs = DEFAULT_POLL_MS, components: C, onItemClick, bellIcon, }) {
12
+ export function NotificationBell({ apiBasePath = "/api/notifications", pollIntervalMs = DEFAULT_POLL_MS, components: C, onItemClick, bellIcon, realtime, }) {
13
13
  const [unread, setUnread] = useState(0);
14
14
  const [open, setOpen] = useState(false);
15
15
  const [items, setItems] = useState([]);
@@ -56,6 +56,38 @@ export function NotificationBell({ apiBasePath = "/api/notifications", pollInter
56
56
  }, [fetchCount, pollIntervalMs]);
57
57
  useEffect(() => { if (open)
58
58
  fetchList(); }, [open, fetchList]);
59
+ // Real-time SSE subscription
60
+ useEffect(() => {
61
+ if (!realtime?.enabled)
62
+ return;
63
+ if (typeof EventSource === "undefined")
64
+ return;
65
+ const url = realtime.eventSourceUrl ?? `${apiBasePath}/stream`;
66
+ let es = null;
67
+ let reconnect_timer = null;
68
+ function connect() {
69
+ es = new EventSource(url, { withCredentials: true });
70
+ es.addEventListener('notification.created', () => {
71
+ // Refresh unread count immediately on new notification
72
+ fetchCount();
73
+ // If dropdown is open, refresh the list too
74
+ if (open)
75
+ fetchList();
76
+ });
77
+ es.onerror = () => {
78
+ es?.close();
79
+ es = null;
80
+ // Back-off reconnect: try again in 5 s
81
+ reconnect_timer = setTimeout(connect, 5000);
82
+ };
83
+ }
84
+ connect();
85
+ return () => {
86
+ es?.close();
87
+ if (reconnect_timer)
88
+ clearTimeout(reconnect_timer);
89
+ };
90
+ }, [realtime?.enabled, realtime?.eventSourceUrl, apiBasePath, open, fetchCount, fetchList]);
59
91
  const handleItem = async (item) => {
60
92
  await fetch(`${apiBasePath}/${item.id}/read`, { method: "POST", credentials: "include" });
61
93
  setUnread((n) => Math.max(0, n - 1));
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/notification_bell/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/notification_bell/index.tsx"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@ import type { ChannelAdapter, ChannelCapabilities, ChannelSendContext, ChannelSe
11
11
  import type { EmailerConfig, EmailSendResponse, EmailAttachment } from "./types.js";
12
12
  /** Payload shape for the email channel. */
13
13
  export interface EmailPayload {
14
+ /** Explicit recipient email. When provided, validated on dispatch. At send time, recipient is resolved via resolveRecipient. */
15
+ to?: string;
14
16
  subject: string;
15
17
  body_text?: string;
16
18
  body_html?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/email/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAMpF,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAOhE;AAED,4DAA4D;AAC5D,qBAAa,YAAa,YAAW,cAAc,CAAC,YAAY,CAAC;IAgBnD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAfjC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAaxC;gBAE2B,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,aAAa,CAAA;KAAO;IAElE,QAAQ,CAAC,CAAC,EAAE,YAAY,GAAG,uBAAuB;IAW5C,IAAI,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAgCjF;AAID,OAAO,QAAQ,yBAAyB,CAAC;IACvC,UAAU,iBAAiB;QACzB,KAAK,EAAE,YAAY,CAAC;KACrB;CACF"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/email/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAOpF,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,gIAAgI;IAChI,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAOhE;AAED,4DAA4D;AAC5D,qBAAa,YAAa,YAAW,cAAc,CAAC,YAAY,CAAC;IAgBnD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAfjC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAaxC;gBAE2B,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,aAAa,CAAA;KAAO;IAElE,QAAQ,CAAC,CAAC,EAAE,YAAY,GAAG,uBAAuB;IAc5C,IAAI,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAyDjF;AAID,OAAO,QAAQ,yBAAyB,CAAC;IACvC,UAAU,iBAAiB;QACzB,KAAK,EAAE,YAAY,CAAC;KACrB;CACF"}
@@ -10,6 +10,7 @@
10
10
  import { load_emailer_config } from "./config.js";
11
11
  import { get_email_provider } from "./providers/index.js";
12
12
  import { wrapEnvelope } from "./envelope.js";
13
+ import { validate_email_address } from "./utils/validation.js";
13
14
  /**
14
15
  * Returns true if the send failure is retryable (transient), false if permanent.
15
16
  *
@@ -51,6 +52,9 @@ export class EmailChannel {
51
52
  }
52
53
  validate(p) {
53
54
  const errors = [];
55
+ if (p.to !== undefined && !validate_email_address(p.to)) {
56
+ errors.push("to: invalid email address");
57
+ }
54
58
  if (!p.subject || p.subject.length === 0) {
55
59
  errors.push("subject required");
56
60
  }
@@ -60,28 +64,55 @@ export class EmailChannel {
60
64
  return errors.length > 0 ? { ok: false, errors } : { ok: true };
61
65
  }
62
66
  async send(p, ctx) {
63
- const config = this.opts.config ?? load_emailer_config();
64
- const provider = get_email_provider(config);
65
- const body_text = ctx.rendered_bodies.text ?? p.body_text ?? "";
66
- const body_html = ctx.rendered_bodies.html ?? p.body_html;
67
- // ScopeBranding and OrgBranding are structurally identical cast is safe.
68
- const { text, html } = wrapEnvelope({
69
- body_text,
70
- body_html,
71
- branding: ctx.branding,
72
- });
73
- const r = await provider.send_email({
74
- to: ctx.recipient,
75
- subject: p.subject,
76
- content: { text, html },
77
- attachments: p.attachments,
78
- reply_to: p.reply_to,
79
- cc: p.cc,
80
- bcc: p.bcc,
81
- }, config, ctx.logger);
82
- return r.success
83
- ? { ok: true, message_id: r.message_id }
84
- : { ok: false, error: r.error ?? "send_failed", retryable: classifyEmailError(r) };
67
+ try {
68
+ // Wrap config loading so that missing config / missing API key → permanent failure
69
+ // (retryable: false) rather than an unhandled exception that the flush worker
70
+ // conservatively treats as retryable. An infinite retry loop on a bad config
71
+ // is far more harmful than an immediate permanent failure.
72
+ let config;
73
+ try {
74
+ config = this.opts.config ?? load_emailer_config();
75
+ }
76
+ catch (e) {
77
+ return {
78
+ ok: false,
79
+ error: `email_config_error: ${e instanceof Error ? e.message : String(e)}`,
80
+ retryable: false,
81
+ };
82
+ }
83
+ const provider = get_email_provider(config);
84
+ const body_text = ctx.rendered_bodies.text ?? p.body_text ?? "";
85
+ const body_html = ctx.rendered_bodies.html ?? p.body_html;
86
+ // ScopeBranding and OrgBranding are structurally identical — cast is safe.
87
+ const { text, html } = wrapEnvelope({
88
+ body_text,
89
+ body_html,
90
+ branding: ctx.branding,
91
+ });
92
+ const r = await provider.send_email({
93
+ to: ctx.recipient,
94
+ subject: p.subject,
95
+ content: { text, html },
96
+ attachments: p.attachments,
97
+ reply_to: p.reply_to,
98
+ cc: p.cc,
99
+ bcc: p.bcc,
100
+ }, config, ctx.logger);
101
+ return r.success
102
+ ? { ok: true, message_id: r.message_id }
103
+ : { ok: false, error: r.error ?? "send_failed", retryable: classifyEmailError(r) };
104
+ }
105
+ catch (e) {
106
+ // Any unhandled exception (network error thrown from provider, provider bug,
107
+ // etc.) must be a permanent failure — never retryable. Without this outer
108
+ // catch the flush worker conservatively marks the delivery as retryable,
109
+ // causing infinite retry loops that never reach a final state.
110
+ return {
111
+ ok: false,
112
+ error: `send_error: ${e instanceof Error ? e.message : String(e)}`,
113
+ retryable: false,
114
+ };
115
+ }
85
116
  }
86
117
  }
87
118
  //# sourceMappingURL=adapter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../../../src/lib/adapters/email/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../../../src/lib/adapters/email/adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ChannelAdapter, ChannelCapabilities, ChannelSendContext, ChannelSendResult, ChannelValidationResult } from '../../channels/types.js';
2
+ import type { WebhookPayload, WebhookChannelOptions } from './types.js';
3
+ export declare class WebhookChannel implements ChannelAdapter<WebhookPayload> {
4
+ private readonly opts;
5
+ readonly capabilities: ChannelCapabilities;
6
+ constructor(opts: WebhookChannelOptions);
7
+ validate(_payload: WebhookPayload): ChannelValidationResult;
8
+ send(payload: WebhookPayload, ctx: ChannelSendContext): Promise<ChannelSendResult>;
9
+ }
10
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/webhook/adapter.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,EACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAmB,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEzF,qBAAa,cAAe,YAAW,cAAc,CAAC,cAAc,CAAC;IAGvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;gBAEd,IAAI,EAAE,qBAAqB;IAgBxD,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,uBAAuB;IAIrD,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAoEzF"}
@@ -0,0 +1,90 @@
1
+ // src/lib/adapters/webhook/adapter.ts
2
+ /**
3
+ * Webhook channel adapter.
4
+ *
5
+ * POSTs a signed JSON envelope to a consumer-supplied URL per scope_id.
6
+ * Retries on 5xx and 429 (Retry-After header honoured).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import { createHmac } from 'node:crypto';
11
+ export class WebhookChannel {
12
+ constructor(opts) {
13
+ this.opts = opts;
14
+ this.capabilities = {
15
+ channel_id: opts.channel_id ?? 'webhook',
16
+ display_name: 'Webhook',
17
+ template_body_keys: [],
18
+ max_text_length: null,
19
+ splits_long_messages: false,
20
+ supports_explicit_recipient: false,
21
+ retry: {
22
+ max_attempts: 3,
23
+ backoff_ms: (n, hint) => hint ?? Math.min(60000, 2000 * Math.pow(2, Math.max(0, n - 1))),
24
+ },
25
+ };
26
+ }
27
+ validate(_payload) {
28
+ return { ok: true };
29
+ }
30
+ async send(payload, ctx) {
31
+ const url = await this.opts.resolveUrl(ctx.scope_id);
32
+ if (!url) {
33
+ return {
34
+ ok: false,
35
+ error: `no_url: no webhook URL configured for scope '${ctx.scope_id}'`,
36
+ retryable: false,
37
+ };
38
+ }
39
+ const envelope = {
40
+ inbox_id: ctx.inbox_id,
41
+ user_id: ctx.user_id,
42
+ scope_id: ctx.scope_id,
43
+ rendered_bodies: ctx.rendered_bodies,
44
+ metadata: payload.metadata ?? {},
45
+ timestamp: new Date().toISOString(),
46
+ };
47
+ const body = JSON.stringify(envelope);
48
+ const headers = {
49
+ 'Content-Type': 'application/json',
50
+ };
51
+ if (this.opts.secret) {
52
+ const sig = createHmac('sha256', this.opts.secret).update(body).digest('hex');
53
+ headers['X-Hazo-Signature'] = `sha256=${sig}`;
54
+ }
55
+ const controller = new AbortController();
56
+ const timeout = setTimeout(() => controller.abort(), this.opts.timeout_ms ?? 30000);
57
+ try {
58
+ const res = await fetch(url, {
59
+ method: 'POST',
60
+ headers,
61
+ body,
62
+ signal: controller.signal,
63
+ });
64
+ if (res.ok) {
65
+ return { ok: true };
66
+ }
67
+ const retry_after_raw = res.headers.get('Retry-After');
68
+ const retry_after_ms = retry_after_raw
69
+ ? Number(retry_after_raw) * 1000
70
+ : undefined;
71
+ return {
72
+ ok: false,
73
+ error: `HTTP ${res.status}`,
74
+ retryable: res.status === 429 || res.status >= 500,
75
+ retry_after_ms,
76
+ };
77
+ }
78
+ catch (err) {
79
+ return {
80
+ ok: false,
81
+ error: err instanceof Error ? err.message : String(err),
82
+ retryable: true,
83
+ };
84
+ }
85
+ finally {
86
+ clearTimeout(timeout);
87
+ }
88
+ }
89
+ }
90
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../../../src/lib/adapters/webhook/adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export { WebhookChannel } from './adapter.js';
2
+ export type { WebhookPayload, WebhookEnvelope, WebhookChannelOptions, } from './types.js';
3
+ declare module 'hazo_notify/channels' {
4
+ interface ChannelPayloadMap {
5
+ webhook: import('./types.js').WebhookPayload;
6
+ }
7
+ }
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/webhook/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,YAAY,EACV,cAAc,EACd,eAAe,EACf,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,OAAO,QAAQ,sBAAsB,CAAC;IACpC,UAAU,iBAAiB;QACzB,OAAO,EAAE,OAAO,YAAY,EAAE,cAAc,CAAC;KAC9C;CACF"}
@@ -0,0 +1,3 @@
1
+ // src/lib/adapters/webhook/index.ts
2
+ export { WebhookChannel } from './adapter.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/adapters/webhook/index.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Types for the webhook channel adapter.
3
+ *
4
+ * The webhook adapter POSTs a signed JSON envelope to a per-scope URL.
5
+ * URL management is the consumer's responsibility — the adapter calls
6
+ * `resolveUrl(scope_id)` at send time.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ /** channel_payloads entry for the webhook channel. */
11
+ export interface WebhookPayload {
12
+ /** Extra metadata merged into the envelope `metadata` field. */
13
+ metadata?: Record<string, unknown>;
14
+ }
15
+ /** Shape of the JSON body POSTed to the webhook endpoint. */
16
+ export interface WebhookEnvelope {
17
+ inbox_id: string;
18
+ user_id: string;
19
+ scope_id: string;
20
+ rendered_bodies: Record<string, string>;
21
+ metadata: Record<string, unknown>;
22
+ timestamp: string;
23
+ }
24
+ /** Constructor options for WebhookChannel. */
25
+ export interface WebhookChannelOptions {
26
+ /**
27
+ * Stable channel ID (default: 'webhook'). Override to register multiple
28
+ * webhook channels with different IDs (e.g. 'webhook_slack', 'webhook_crm').
29
+ */
30
+ channel_id?: string;
31
+ /**
32
+ * Resolves the destination URL for a given scope_id. Return null to skip
33
+ * the send (delivery will be marked failed with reason "no_url").
34
+ */
35
+ resolveUrl: (scope_id: string) => Promise<string | null>;
36
+ /**
37
+ * HMAC-SHA256 signing secret. When provided, adds header:
38
+ * X-Hazo-Signature: sha256=<hex>
39
+ * The signature covers the raw JSON body bytes.
40
+ */
41
+ secret?: string;
42
+ /** Request timeout in ms (default: 30_000). */
43
+ timeout_ms?: number;
44
+ }
45
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/lib/adapters/webhook/types.ts"],"names":[],"mappings":"AACA;;;;;;;;GAQG;AAEH,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzD;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
@@ -0,0 +1,12 @@
1
+ // src/lib/adapters/webhook/types.ts
2
+ /**
3
+ * Types for the webhook channel adapter.
4
+ *
5
+ * The webhook adapter POSTs a signed JSON envelope to a per-scope URL.
6
+ * URL management is the consumer's responsibility — the adapter calls
7
+ * `resolveUrl(scope_id)` at send time.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/lib/adapters/webhook/types.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC;;;;;;;;GAQG"}
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
+ import type { InboxBroadcastEvent } from "../inbox/broadcaster.js";
9
10
  interface SimpleRequest {
10
11
  url: string;
11
12
  }
@@ -29,5 +30,36 @@ export declare function createInboxRoutes(opts: CreateInboxRoutesOptions): {
29
30
  }): Promise<SimpleResponse>;
30
31
  markAllReadHandler(req: SimpleRequest): Promise<SimpleResponse>;
31
32
  };
33
+ export interface CreateInboxStreamRouteOptions {
34
+ /** Extract authenticated user_id from the request. Return null to reject. */
35
+ getAuth: (req: Request) => Promise<{
36
+ user_id: string;
37
+ } | null>;
38
+ /**
39
+ * Subscribe a listener for a user_id. Returns an unsubscribe function.
40
+ * Wire this to the subscribe returned by createInboxBroadcaster().
41
+ */
42
+ subscribe: (user_id: string, fn: (event: InboxBroadcastEvent) => void) => () => void;
43
+ /** Heartbeat interval in ms (default 20 000). Keeps proxies alive. */
44
+ heartbeat_ms?: number;
45
+ }
46
+ /**
47
+ * Returns a Next.js-compatible GET handler for `text/event-stream`.
48
+ *
49
+ * Consumer mounting:
50
+ * ```ts
51
+ * // app/api/notifications/stream/route.ts
52
+ * import { createInboxStreamRoute } from 'hazo_notify/api/inbox';
53
+ * import { subscribe } from '@/lib/notify-init';
54
+ * export const GET = createInboxStreamRoute({ getAuth, subscribe });
55
+ * ```
56
+ *
57
+ * Events pushed to clients:
58
+ * event: notification.created\ndata: {"user_id":"…","inbox_id":"…"}\n\n
59
+ *
60
+ * The client (NotificationBell with realtime enabled) listens for
61
+ * `notification.created` and refreshes its unread count on receipt.
62
+ */
63
+ export declare function createInboxStreamRoute(opts: CreateInboxStreamRouteOptions): (req: Request) => Promise<Response>;
32
64
  export {};
33
65
  //# sourceMappingURL=inbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../../src/lib/api/inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,UAAU,aAAa;IAAG,GAAG,EAAE,MAAM,CAAA;CAAE;AACvC,UAAU,cAAc;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE;AAMrE,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CACxF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB;qBAErC,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;4BAiBhC,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;yBAQ9D,aAAa,OACb;QAAE,MAAM,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAC9B,OAAO,CAAC,cAAc,CAAC;4BAOI,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;EAOxE"}
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../../../src/lib/api/inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAEnE,UAAU,aAAa;IAAG,GAAG,EAAE,MAAM,CAAA;CAAE;AACvC,UAAU,cAAc;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE;AAMrE,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CACxF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB;qBAErC,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;4BAiBhC,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;yBAQ9D,aAAa,OACb;QAAE,MAAM,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAC9B,OAAO,CAAC,cAAc,CAAC;4BAOI,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;EAOxE;AAID,MAAM,WAAW,6BAA6B;IAC5C,6EAA6E;IAC7E,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC/D;;;OAGG;IACH,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACrF,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,6BAA6B,IAG9C,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwD3D"}