@strav/notification 1.0.0-alpha.33 → 1.0.0-alpha.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strav/notification",
3
- "version": "1.0.0-alpha.33",
3
+ "version": "1.0.0-alpha.35",
4
4
  "description": "Strav multi-channel notifications — NotificationManager fan-out across channel drivers (mail / database / log / webhook / broadcast / discord / sse). Manager+drivers shape; new channels register via `manager.extend(name, factory)`.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -13,6 +13,7 @@
13
13
  "./webhook": "./src/drivers/webhook/index.ts",
14
14
  "./broadcast": "./src/drivers/broadcast/index.ts",
15
15
  "./discord": "./src/drivers/discord/index.ts",
16
+ "./line": "./src/drivers/line/index.ts",
16
17
  "./sse": "./src/drivers/sse/index.ts",
17
18
  "./tenanted": "./src/drivers/database/tenanted/index.ts"
18
19
  },
@@ -27,19 +28,21 @@
27
28
  "access": "public"
28
29
  },
29
30
  "dependencies": {
30
- "@strav/kernel": "1.0.0-alpha.33"
31
+ "@strav/kernel": "1.0.0-alpha.35"
31
32
  },
32
33
  "devDependencies": {
33
- "@strav/broadcast": "1.0.0-alpha.33",
34
- "@strav/database": "1.0.0-alpha.33",
35
- "@strav/http": "1.0.0-alpha.33",
36
- "@strav/mail": "1.0.0-alpha.33"
34
+ "@strav/broadcast": "1.0.0-alpha.35",
35
+ "@strav/database": "1.0.0-alpha.35",
36
+ "@strav/http": "1.0.0-alpha.35",
37
+ "@strav/instant": "1.0.0-alpha.35",
38
+ "@strav/mail": "1.0.0-alpha.35"
37
39
  },
38
40
  "peerDependencies": {
39
- "@strav/broadcast": "1.0.0-alpha.33",
40
- "@strav/database": "1.0.0-alpha.33",
41
- "@strav/http": "1.0.0-alpha.33",
42
- "@strav/mail": "1.0.0-alpha.33",
41
+ "@strav/broadcast": "1.0.0-alpha.35",
42
+ "@strav/database": "1.0.0-alpha.35",
43
+ "@strav/http": "1.0.0-alpha.35",
44
+ "@strav/instant": "1.0.0-alpha.35",
45
+ "@strav/mail": "1.0.0-alpha.35",
43
46
  "@types/bun": ">=1.3.14"
44
47
  },
45
48
  "peerDependenciesMeta": {
@@ -52,6 +55,9 @@
52
55
  "@strav/http": {
53
56
  "optional": true
54
57
  },
58
+ "@strav/instant": {
59
+ "optional": true
60
+ },
55
61
  "@strav/mail": {
56
62
  "optional": true
57
63
  }
@@ -0,0 +1,7 @@
1
+ export type { LineChannelConfig } from './line_config.ts'
2
+ export {
3
+ LineNotificationDriver,
4
+ type LineNotificationDriverOptions,
5
+ type LineNotificationMessage,
6
+ } from './line_notification_driver.ts'
7
+ export { LineNotificationProvider } from './line_notification_provider.ts'
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Vendor-specific config shape for the LINE notification channel.
3
+ * Discriminator `driver: 'line'` selects this factory at
4
+ * `manager.use(...)` time.
5
+ *
6
+ * The LINE channel delegates to `@strav/instant`'s LINE driver under
7
+ * the hood — apps only configure which `config.instant.providers`
8
+ * entry to route through. Authentication / channel access tokens /
9
+ * webhook signature handling stay on `@strav/instant`.
10
+ */
11
+
12
+ import type { ChannelConfig } from '../../notification_config.ts'
13
+
14
+ export interface LineChannelConfig extends ChannelConfig {
15
+ driver: 'line'
16
+ /**
17
+ * Name of the `config.instant.providers` entry whose driver this
18
+ * channel should push through. Defaults to `'line'`. Apps running
19
+ * one LINE bot for support + another for marketing point each
20
+ * notification channel at the matching instance.
21
+ */
22
+ instantProvider?: string
23
+ /**
24
+ * Default display name shown when the hook returns a plain `string`.
25
+ * Apps almost never set this — LINE messages don't carry sender
26
+ * overrides the way Discord does — but it's surfaced to the hook in
27
+ * `defaults` so apps can branch on it.
28
+ */
29
+ sender?: string
30
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * `LineNotificationDriver` — sends notifications via the LINE
3
+ * Messaging API by delegating to `@strav/instant`'s LINE driver.
4
+ *
5
+ * The notification system stays out of the LINE specifics — auth,
6
+ * webhook signature, Flex builder, rich menus all live in
7
+ * `@strav/instant/line`. This channel is the thin adapter from
8
+ * `BaseNotification` → `instant.use('line').push(...)` (single
9
+ * recipient) or `.multicast(...)` (≤500 recipients per LINE limit).
10
+ *
11
+ * Reads `notification.toLine(notifiable, defaults)` for the message.
12
+ * The hook can return:
13
+ *
14
+ * - A `string` — shorthand for `{ text: <string> }` text-only push.
15
+ * - An `OutgoingMessage` (from `@strav/instant`) — full envelope
16
+ * with attachments / quick replies / Flex via `raw`.
17
+ * - A `LineNotificationMessage` envelope with an optional `to`
18
+ * override (single id or array — array triggers multicast).
19
+ *
20
+ * Recipient resolution order: hook `to` → `notifiable.lineUserIds`
21
+ * (multicast) → `notifiable.lineUserId` (single push). None resolved
22
+ * → `{ delivered: false }` with no error (same opt-out semantics as
23
+ * the mail / discord channels).
24
+ *
25
+ * Flex shortcut: pass a `flex` field on the envelope — the driver
26
+ * lifts it into `OutgoingMessage.raw` so apps don't have to know the
27
+ * `raw` plumbing. The Flex object itself comes from
28
+ * `@strav/instant/line`'s typed `flex.*` factories.
29
+ */
30
+
31
+ import type { InstantManager } from '@strav/instant'
32
+ import type { Notifiable } from '../../notifiable.ts'
33
+ import type { BaseNotification } from '../../notification.ts'
34
+ import type { NotificationDriver } from '../../notification_driver.ts'
35
+ import { NotificationDeliveryError } from '../../notification_error.ts'
36
+ import type { NotificationContext, NotificationDeliveryResult } from '../../types.ts'
37
+
38
+ /** Shape returned by `notification.toLine(notifiable, defaults)`. */
39
+ export interface LineNotificationMessage {
40
+ /** Plain-text body. */
41
+ text?: string
42
+ /** Native `@strav/instant` attachments (image / video / audio / location / sticker). */
43
+ attachments?: import('@strav/instant').Attachment[]
44
+ /** Native quick replies. */
45
+ quickReplies?: import('@strav/instant').QuickReply[]
46
+ /**
47
+ * Flex object — shortcut for `raw`. Build via `@strav/instant/line`'s
48
+ * `flex.bubble(...)` / `flex.carousel(...)` factories. The driver
49
+ * wraps it into a LINE Flex message envelope.
50
+ */
51
+ flex?: unknown
52
+ /**
53
+ * Override the recipient(s). A single string → push; an array of
54
+ * strings → multicast. When set, the driver ignores
55
+ * `notifiable.lineUserId(s)`.
56
+ */
57
+ to?: string | readonly string[]
58
+ /**
59
+ * Provider-native message object. When set, forwarded verbatim and
60
+ * supersedes the LCD fields above.
61
+ */
62
+ raw?: unknown
63
+ }
64
+
65
+ /** Hook surface — apps add `toLine(notifiable, defaults)` on their notification. */
66
+ interface LineCapableNotification extends BaseNotification {
67
+ toLine?(
68
+ notifiable: Notifiable,
69
+ defaults: { sender?: string },
70
+ ):
71
+ | string
72
+ | LineNotificationMessage
73
+ | import('@strav/instant').OutgoingMessage
74
+ | Promise<string | LineNotificationMessage | import('@strav/instant').OutgoingMessage>
75
+ }
76
+
77
+ interface NotifiableWithLine extends Notifiable {
78
+ lineUserId?: string
79
+ lineUserIds?: readonly string[]
80
+ }
81
+
82
+ export interface LineNotificationDriverOptions {
83
+ name: string
84
+ manager: InstantManager
85
+ instantProvider?: string
86
+ sender?: string
87
+ }
88
+
89
+ export class LineNotificationDriver implements NotificationDriver {
90
+ readonly name: string
91
+ private readonly manager: InstantManager
92
+ private readonly instantProvider: string
93
+ private readonly sender: string | undefined
94
+
95
+ constructor(options: LineNotificationDriverOptions) {
96
+ this.name = options.name
97
+ this.manager = options.manager
98
+ this.instantProvider = options.instantProvider ?? 'line'
99
+ this.sender = options.sender
100
+ }
101
+
102
+ async send(
103
+ notifiable: Notifiable,
104
+ notification: BaseNotification,
105
+ context: NotificationContext,
106
+ ): Promise<NotificationDeliveryResult> {
107
+ const hook = (notification as LineCapableNotification).toLine
108
+ if (typeof hook !== 'function') {
109
+ return { channel: this.name, delivered: false }
110
+ }
111
+
112
+ const defaults = this.sender !== undefined ? { sender: this.sender } : {}
113
+ const raw = await hook.call(notification, notifiable, defaults)
114
+ const envelope = normaliseEnvelope(raw)
115
+
116
+ const to = resolveRecipients(envelope.to, notifiable)
117
+ if (to === null) {
118
+ return { channel: this.name, delivered: false }
119
+ }
120
+
121
+ const outgoing = toOutgoing(envelope)
122
+ const driver = this.manager.use(this.instantProvider)
123
+
124
+ try {
125
+ const result =
126
+ typeof to === 'string'
127
+ ? await driver.push!(to, outgoing)
128
+ : await driver.multicast!(to, outgoing)
129
+ return {
130
+ channel: this.name,
131
+ delivered: !!result.accepted,
132
+ ...(result.messageId ? { reference: result.messageId } : { reference: context.id }),
133
+ }
134
+ } catch (cause) {
135
+ throw new NotificationDeliveryError(
136
+ `LineNotificationDriver: delivery via instant provider "${this.instantProvider}" failed.`,
137
+ {
138
+ context: {
139
+ channel: this.name,
140
+ notifiableId: notifiable.id,
141
+ notification: notification.constructor.name,
142
+ instantProvider: this.instantProvider,
143
+ retryable: true,
144
+ },
145
+ cause,
146
+ },
147
+ )
148
+ }
149
+ }
150
+ }
151
+
152
+ function normaliseEnvelope(
153
+ raw: string | LineNotificationMessage | import('@strav/instant').OutgoingMessage,
154
+ ): LineNotificationMessage {
155
+ if (typeof raw === 'string') return { text: raw }
156
+ return raw as LineNotificationMessage
157
+ }
158
+
159
+ function resolveRecipients(
160
+ override: string | readonly string[] | undefined,
161
+ notifiable: Notifiable,
162
+ ): string | readonly string[] | null {
163
+ if (override !== undefined) {
164
+ if (Array.isArray(override)) return override.length > 0 ? override : null
165
+ return override || null
166
+ }
167
+ const n = notifiable as NotifiableWithLine
168
+ if (n.lineUserIds && n.lineUserIds.length > 0) return n.lineUserIds
169
+ if (n.lineUserId) return n.lineUserId
170
+ return null
171
+ }
172
+
173
+ function toOutgoing(
174
+ envelope: LineNotificationMessage,
175
+ ): import('@strav/instant').OutgoingMessage {
176
+ // Flex shortcut: lift the flex object into a LINE Flex message
177
+ // envelope on `raw`, where `@strav/instant/line`'s mapper forwards
178
+ // it verbatim to the wire.
179
+ if (envelope.flex !== undefined && envelope.raw === undefined) {
180
+ return {
181
+ ...(envelope.text !== undefined ? { text: envelope.text } : {}),
182
+ raw: [{ type: 'flex', altText: envelope.text ?? 'Notification', contents: envelope.flex }],
183
+ }
184
+ }
185
+ return {
186
+ ...(envelope.text !== undefined ? { text: envelope.text } : {}),
187
+ ...(envelope.attachments !== undefined ? { attachments: envelope.attachments } : {}),
188
+ ...(envelope.quickReplies !== undefined ? { quickReplies: envelope.quickReplies } : {}),
189
+ ...(envelope.raw !== undefined ? { raw: envelope.raw } : {}),
190
+ }
191
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ServiceProvider that registers the LINE channel factory on the
3
+ * `NotificationManager`. Apps include this in their provider list
4
+ * AFTER `NotificationProvider` AND `InstantProvider` — the factory
5
+ * resolves whenever `config.notification.channels.<name>.driver ===
6
+ * 'line'` and forwards through the matching `InstantManager` instance.
7
+ */
8
+
9
+ import { InstantManager } from '@strav/instant'
10
+ import { type Application, ServiceProvider } from '@strav/kernel'
11
+ import { NotificationManager } from '../../notification_manager.ts'
12
+ import type { LineChannelConfig } from './line_config.ts'
13
+ import { LineNotificationDriver } from './line_notification_driver.ts'
14
+
15
+ export class LineNotificationProvider extends ServiceProvider {
16
+ override readonly name = 'notification.line'
17
+ override readonly dependencies = ['notification', 'instant']
18
+
19
+ override async boot(app: Application): Promise<void> {
20
+ const manager = app.resolve(NotificationManager)
21
+ const instant = app.resolve(InstantManager)
22
+ manager.extend('line', ({ instanceName, config }) => {
23
+ const cfg = config as LineChannelConfig
24
+ return new LineNotificationDriver({
25
+ name: instanceName,
26
+ manager: instant,
27
+ ...(cfg.instantProvider !== undefined ? { instantProvider: cfg.instantProvider } : {}),
28
+ ...(cfg.sender !== undefined ? { sender: cfg.sender } : {}),
29
+ })
30
+ })
31
+ }
32
+ }