@strav/instant 1.0.0-alpha.42

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 (56) hide show
  1. package/package.json +49 -0
  2. package/src/drivers/line/index.ts +50 -0
  3. package/src/drivers/line/liff_client.ts +126 -0
  4. package/src/drivers/line/liff_sign_in_route.ts +112 -0
  5. package/src/drivers/line/line_beacon.ts +14 -0
  6. package/src/drivers/line/line_config.ts +55 -0
  7. package/src/drivers/line/line_driver.ts +202 -0
  8. package/src/drivers/line/line_flex.ts +117 -0
  9. package/src/drivers/line/line_liff.ts +102 -0
  10. package/src/drivers/line/line_message_mapper.ts +118 -0
  11. package/src/drivers/line/line_provider.ts +33 -0
  12. package/src/drivers/line/line_rich_menu.ts +68 -0
  13. package/src/drivers/line/line_webhook.ts +168 -0
  14. package/src/drivers/messenger/index.ts +16 -0
  15. package/src/drivers/messenger/messenger_config.ts +20 -0
  16. package/src/drivers/messenger/messenger_driver.ts +151 -0
  17. package/src/drivers/messenger/messenger_message_mapper.ts +146 -0
  18. package/src/drivers/messenger/messenger_profile.ts +43 -0
  19. package/src/drivers/messenger/messenger_provider.ts +31 -0
  20. package/src/drivers/messenger/messenger_webhook.ts +165 -0
  21. package/src/drivers/telegram/index.ts +23 -0
  22. package/src/drivers/telegram/telegram_config.ts +19 -0
  23. package/src/drivers/telegram/telegram_driver.ts +121 -0
  24. package/src/drivers/telegram/telegram_message_mapper.ts +147 -0
  25. package/src/drivers/telegram/telegram_provider.ts +32 -0
  26. package/src/drivers/telegram/telegram_web_app.ts +108 -0
  27. package/src/drivers/telegram/telegram_webhook.ts +200 -0
  28. package/src/drivers/whatsapp/flows/index.ts +15 -0
  29. package/src/drivers/whatsapp/flows/whatsapp_flow_builder.ts +55 -0
  30. package/src/drivers/whatsapp/flows/whatsapp_flow_crypto.ts +81 -0
  31. package/src/drivers/whatsapp/index.ts +14 -0
  32. package/src/drivers/whatsapp/whatsapp_config.ts +35 -0
  33. package/src/drivers/whatsapp/whatsapp_driver.ts +111 -0
  34. package/src/drivers/whatsapp/whatsapp_message_mapper.ts +157 -0
  35. package/src/drivers/whatsapp/whatsapp_provider.ts +31 -0
  36. package/src/drivers/whatsapp/whatsapp_webhook.ts +170 -0
  37. package/src/drivers/zalo/index.ts +16 -0
  38. package/src/drivers/zalo/zalo_config.ts +37 -0
  39. package/src/drivers/zalo/zalo_driver.ts +130 -0
  40. package/src/drivers/zalo/zalo_message_mapper.ts +146 -0
  41. package/src/drivers/zalo/zalo_mini_app.ts +17 -0
  42. package/src/drivers/zalo/zalo_provider.ts +31 -0
  43. package/src/drivers/zalo/zalo_webhook.ts +139 -0
  44. package/src/errors.ts +122 -0
  45. package/src/index.ts +52 -0
  46. package/src/instant_capabilities.ts +49 -0
  47. package/src/instant_driver.ts +109 -0
  48. package/src/instant_manager.ts +113 -0
  49. package/src/instant_provider.ts +45 -0
  50. package/src/internal/fetch_json.ts +58 -0
  51. package/src/internal/meta/meta_graph_client.ts +51 -0
  52. package/src/internal/meta/meta_signature.ts +22 -0
  53. package/src/internal/meta/meta_webhook_challenge.ts +19 -0
  54. package/src/message.ts +65 -0
  55. package/src/types.ts +32 -0
  56. package/src/webhook_event.ts +93 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * `ZaloDriver` — `InstantDriver` for the Zalo Official Account
3
+ * Messaging API. Uses `fetch` (no SDK dependency).
4
+ *
5
+ * Zalo's access token is short-lived. The driver re-uses
6
+ * `config.accessToken` as-is and lets the app refresh via
7
+ * `tokenStore`; refresh handling is out of scope for the
8
+ * driver itself (apps wire a cron + `tokenStore.save`).
9
+ */
10
+
11
+ import { InstantProviderError } from '../../errors.ts'
12
+ import type { InstantCapability } from '../../instant_capabilities.ts'
13
+ import type { InstantDriver, UserProfile, WebhookOps } from '../../instant_driver.ts'
14
+ import type { OutgoingMessage, SendResult } from '../../message.ts'
15
+ import { fetchJson } from '../../internal/fetch_json.ts'
16
+ import type { ZaloProviderConfig } from './zalo_config.ts'
17
+ import { toZaloEnvelope, type ZaloMessageType } from './zalo_message_mapper.ts'
18
+ import { parseZaloWebhook, verifyZaloSignature } from './zalo_webhook.ts'
19
+
20
+ const DEFAULT_CAPABILITIES: ReadonlySet<InstantCapability> = new Set<InstantCapability>([
21
+ 'send.text',
22
+ 'send.image',
23
+ 'send.video',
24
+ 'send.audio',
25
+ 'send.file',
26
+ 'send.location',
27
+ 'send.sticker',
28
+ 'send.template',
29
+ 'send.templateParams',
30
+ 'send.quickReplies',
31
+ 'push',
32
+ 'profile',
33
+ 'miniApp',
34
+ 'webhook.signature',
35
+ 'webhook.parse',
36
+ ])
37
+
38
+ const ENDPOINT_PATH: Record<ZaloMessageType, string> = {
39
+ cs: '/v3.0/oa/message/cs',
40
+ transaction: '/v3.0/oa/message/transaction',
41
+ zns: '/v2.0/oa/message/zns',
42
+ }
43
+
44
+ export interface ZaloDriverOptions {
45
+ instanceName: string
46
+ config: ZaloProviderConfig
47
+ }
48
+
49
+ export class ZaloDriver implements InstantDriver {
50
+ readonly name = 'zalo'
51
+ readonly instanceName: string
52
+ readonly capabilities = DEFAULT_CAPABILITIES
53
+ readonly webhook: WebhookOps
54
+ private accessToken: string
55
+ private readonly base: string
56
+
57
+ constructor(options: ZaloDriverOptions) {
58
+ const { instanceName, config } = options
59
+ for (const field of ['oaId', 'accessToken', 'appId', 'appSecret'] as const) {
60
+ if (!config[field]) {
61
+ throw new InstantProviderError(
62
+ `ZaloDriver: \`${field}\` is required for provider "${instanceName}".`,
63
+ { provider: 'zalo', operation: 'init', status: 500 },
64
+ )
65
+ }
66
+ }
67
+ this.instanceName = instanceName
68
+ this.accessToken = config.accessToken
69
+ this.base = config.apiBaseURL ?? 'https://openapi.zalo.me'
70
+ const appId = config.appId
71
+ const appSecret = config.appSecret
72
+ this.webhook = {
73
+ verifySignature: (rawBody) => verifyZaloSignature(rawBody, { appId, appSecret }),
74
+ parse: (rawBody) => parseZaloWebhook(rawBody),
75
+ }
76
+ }
77
+
78
+ setAccessToken(token: string): void {
79
+ this.accessToken = token
80
+ }
81
+
82
+ async send(to: string, message: OutgoingMessage): Promise<SendResult> {
83
+ return this.push(to, message)
84
+ }
85
+
86
+ async push(to: string, message: OutgoingMessage): Promise<SendResult> {
87
+ const envelope = toZaloEnvelope(to, message)
88
+ const response = await this.invoke<{ data?: { message_id?: string }; error?: number; message?: string }>(
89
+ ENDPOINT_PATH[envelope.endpoint],
90
+ envelope.body,
91
+ `send.${envelope.endpoint}`,
92
+ )
93
+ if (typeof response.error === 'number' && response.error !== 0) {
94
+ throw new InstantProviderError(`Zalo API returned error ${response.error}: ${response.message ?? ''}`.trim(), {
95
+ provider: 'zalo',
96
+ operation: `send.${envelope.endpoint}`,
97
+ context: { error: response.error, message: response.message },
98
+ })
99
+ }
100
+ return {
101
+ provider: 'zalo',
102
+ accepted: true,
103
+ ...(response.data?.message_id ? { messageId: response.data.message_id } : {}),
104
+ raw: response,
105
+ }
106
+ }
107
+
108
+ async profile(userId: string): Promise<UserProfile> {
109
+ const response = await this.invoke<{
110
+ data?: { user_id?: string; display_name?: string; avatar?: string }
111
+ }>('/v2.0/oa/getprofile', { user_id: userId }, 'profile')
112
+ const r = response.data ?? {}
113
+ return {
114
+ userId: r.user_id ?? userId,
115
+ ...(r.display_name ? { displayName: r.display_name } : {}),
116
+ ...(r.avatar ? { pictureUrl: r.avatar } : {}),
117
+ raw: response,
118
+ }
119
+ }
120
+
121
+ private invoke<T>(path: string, body: Record<string, unknown>, operation: string): Promise<T> {
122
+ return fetchJson<T>(`${this.base}${path}`, {
123
+ method: 'POST',
124
+ headers: { access_token: this.accessToken, 'Content-Type': 'application/json' },
125
+ body: JSON.stringify(body),
126
+ provider: 'zalo',
127
+ operation,
128
+ })
129
+ }
130
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Map `OutgoingMessage` → Zalo OA message body.
3
+ *
4
+ * Two endpoints:
5
+ * - `/v3.0/oa/message/cs` — customer-service (within 24h
6
+ * reply window). Default for `send`/`push`.
7
+ * - `/v3.0/oa/message/transaction` — transactional sends
8
+ * outside the window. Requires `raw.zns`-style template
9
+ * approval.
10
+ *
11
+ * ZNS (Zalo Notification Service) sends go through a different
12
+ * URL path; the driver handles routing. The mapper produces the
13
+ * body only.
14
+ *
15
+ * Provider-native escape hatches via `raw`:
16
+ * - `raw.zns?: { template_id, template_data }` → ZNS template send
17
+ * - `raw.messageType?: 'cs' | 'transaction'` (default 'cs')
18
+ */
19
+
20
+ import { InstantProviderError } from '../../errors.ts'
21
+ import type { Attachment, OutgoingMessage, QuickReply } from '../../message.ts'
22
+
23
+ export type ZaloMessageType = 'cs' | 'transaction' | 'zns'
24
+
25
+ export interface ZaloMessageEnvelope {
26
+ endpoint: ZaloMessageType
27
+ body: Record<string, unknown>
28
+ }
29
+
30
+ interface ZaloRawExtras {
31
+ zns?: { template_id: string; template_data: Record<string, string>; tracking_id?: string }
32
+ messageType?: 'cs' | 'transaction'
33
+ }
34
+
35
+ export function toZaloEnvelope(to: string, message: OutgoingMessage): ZaloMessageEnvelope {
36
+ const raw = (message.raw ?? {}) as ZaloRawExtras
37
+
38
+ if (raw.zns) {
39
+ return {
40
+ endpoint: 'zns',
41
+ body: {
42
+ phone: to,
43
+ template_id: raw.zns.template_id,
44
+ template_data: raw.zns.template_data,
45
+ ...(raw.zns.tracking_id ? { tracking_id: raw.zns.tracking_id } : {}),
46
+ },
47
+ }
48
+ }
49
+
50
+ const endpoint: ZaloMessageType = raw.messageType ?? 'cs'
51
+ const recipient = { user_id: to }
52
+ const attachment = message.attachments?.[0]
53
+ const quickReplies = message.quickReplies
54
+
55
+ if (attachment) {
56
+ return {
57
+ endpoint,
58
+ body: { recipient, message: attachmentBody(attachment, message.text, quickReplies) },
59
+ }
60
+ }
61
+
62
+ if (quickReplies && quickReplies.length > 0) {
63
+ return {
64
+ endpoint,
65
+ body: {
66
+ recipient,
67
+ message: {
68
+ text: message.text ?? '',
69
+ attachment: { type: 'template', payload: listTemplate(quickReplies) },
70
+ },
71
+ },
72
+ }
73
+ }
74
+
75
+ if (!message.text) {
76
+ throw new InstantProviderError(
77
+ 'Zalo send requires `text`, an attachment, or `raw.zns`.',
78
+ { provider: 'zalo', operation: 'send', status: 400 },
79
+ )
80
+ }
81
+
82
+ return { endpoint, body: { recipient, message: { text: message.text } } }
83
+ }
84
+
85
+ function attachmentBody(
86
+ a: Attachment,
87
+ text: string | undefined,
88
+ quickReplies: QuickReply[] | undefined,
89
+ ): Record<string, unknown> {
90
+ const baseText = text ?? ''
91
+ switch (a.type) {
92
+ case 'image':
93
+ return {
94
+ text: baseText,
95
+ attachment: { type: 'template', payload: { template_type: 'media', elements: [{ media_type: 'image', url: a.url }] } },
96
+ }
97
+ case 'video':
98
+ return {
99
+ text: baseText,
100
+ attachment: { type: 'template', payload: { template_type: 'media', elements: [{ media_type: 'video', url: a.url }] } },
101
+ }
102
+ case 'file':
103
+ return {
104
+ text: baseText,
105
+ attachment: { type: 'file', payload: { url: a.url } },
106
+ }
107
+ case 'audio':
108
+ // Zalo OA has no first-class audio bubble — surface as a file.
109
+ return { text: baseText, attachment: { type: 'file', payload: { url: a.url } } }
110
+ case 'location':
111
+ return {
112
+ text: baseText || a.title || 'Location',
113
+ attachment: {
114
+ type: 'template',
115
+ payload: { template_type: 'request_user_info', elements: [{ id: 'location', text: a.title, address: a.address }] },
116
+ },
117
+ }
118
+ case 'sticker':
119
+ return { text: baseText, sticker: { id: a.stickerId } }
120
+ default:
121
+ return quickReplies
122
+ ? { text: baseText, attachment: { type: 'template', payload: listTemplate(quickReplies) } }
123
+ : { text: baseText }
124
+ }
125
+ }
126
+
127
+ function listTemplate(quickReplies: QuickReply[]): Record<string, unknown> {
128
+ return {
129
+ template_type: 'list',
130
+ elements: quickReplies.map((qr) => {
131
+ const element: Record<string, unknown> = { title: qr.label }
132
+ switch (qr.action.type) {
133
+ case 'postback':
134
+ element['default_action'] = { type: 'oa.query.hide', payload: qr.action.data }
135
+ break
136
+ case 'uri':
137
+ element['default_action'] = { type: 'oa.open.url', url: qr.action.uri }
138
+ break
139
+ case 'message':
140
+ element['default_action'] = { type: 'oa.query.show', payload: qr.action.text }
141
+ break
142
+ }
143
+ return element
144
+ }),
145
+ }
146
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Zalo Mini App helpers.
3
+ *
4
+ * `miniAppDeepLink(appId, path, params)` builds a deep link
5
+ * that opens a Zalo Mini App on Android/iOS. Send via a list
6
+ * template with an `oa.open.url` action.
7
+ */
8
+
9
+ export function miniAppDeepLink(
10
+ appId: string,
11
+ path: string,
12
+ params: Record<string, string> = {},
13
+ ): string {
14
+ const search = new URLSearchParams(params).toString()
15
+ const safePath = path.startsWith('/') ? path : `/${path}`
16
+ return `https://zalo.me/s/${encodeURIComponent(appId)}${safePath}${search ? `?${search}` : ''}`
17
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `ZaloInstantProvider` — `ServiceProvider` that registers
3
+ * the Zalo OA driver factory on the `InstantManager`.
4
+ */
5
+
6
+ import { type Application, ServiceProvider } from '@strav/kernel'
7
+ import { InstantConfigError } from '../../errors.ts'
8
+ import { InstantManager } from '../../instant_manager.ts'
9
+ import type { ZaloProviderConfig } from './zalo_config.ts'
10
+ import { ZaloDriver } from './zalo_driver.ts'
11
+
12
+ export class ZaloInstantProvider extends ServiceProvider {
13
+ override readonly name = 'instant-zalo'
14
+ override readonly dependencies = ['instant']
15
+
16
+ override register(app: Application): void {
17
+ const manager = app.resolve(InstantManager)
18
+ manager.extend('zalo', ({ instanceName, config }) => {
19
+ const cfg = config as ZaloProviderConfig
20
+ for (const field of ['oaId', 'accessToken', 'appId', 'appSecret'] as const) {
21
+ if (!cfg[field]) {
22
+ throw new InstantConfigError(
23
+ `ZaloInstantProvider: \`${field}\` is required for provider "${instanceName}".`,
24
+ { context: { instanceName, field } },
25
+ )
26
+ }
27
+ }
28
+ return new ZaloDriver({ instanceName, config: cfg })
29
+ })
30
+ }
31
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Zalo OA webhook — signature + parse.
3
+ *
4
+ * Signature: Zalo OA posts a `mac` field inside the JSON body
5
+ * computed as `HMAC_SHA256(<appId><event_name><timestamp>, appSecret)`
6
+ * (hex). Apps verify by recomputing and constant-time comparing.
7
+ *
8
+ * ⚠️ The exact signature scheme has shifted between Zalo OA
9
+ * versions. The verifier is isolated to one function — if Zalo
10
+ * ships a v2 scheme, swap the formula here. The driver passes
11
+ * the raw body so the verifier can re-parse and read the
12
+ * relevant fields.
13
+ *
14
+ * Parse: maps `event_name` to the framework's WebhookEvent
15
+ * union. Anything unknown → UnknownEvent.
16
+ */
17
+
18
+ import { createHmac, timingSafeEqual } from 'node:crypto'
19
+ import { WebhookSignatureError } from '../../errors.ts'
20
+ import type {
21
+ FollowEvent,
22
+ LocationMessageEvent,
23
+ MediaMessageEvent,
24
+ PostbackEvent,
25
+ StickerMessageEvent,
26
+ TextMessageEvent,
27
+ UnknownEvent,
28
+ WebhookEvent,
29
+ WebhookEventBase,
30
+ } from '../../webhook_event.ts'
31
+
32
+ export interface ZaloSignatureOptions {
33
+ appId: string
34
+ appSecret: string
35
+ }
36
+
37
+ interface ZaloWebhookBody {
38
+ app_id?: string
39
+ event_name?: string
40
+ timestamp?: string
41
+ sender?: { id: string }
42
+ recipient?: { id: string }
43
+ message?: {
44
+ msg_id?: string
45
+ text?: string
46
+ attachments?: Array<{ type: string; payload?: Record<string, unknown> }>
47
+ }
48
+ mac?: string
49
+ }
50
+
51
+ export function verifyZaloSignature(rawBody: string, options: ZaloSignatureOptions): boolean {
52
+ let parsed: ZaloWebhookBody
53
+ try {
54
+ parsed = JSON.parse(rawBody) as ZaloWebhookBody
55
+ } catch {
56
+ return false
57
+ }
58
+ const { mac, event_name, timestamp } = parsed
59
+ if (!mac || !event_name || !timestamp) return false
60
+ if (parsed.app_id && parsed.app_id !== options.appId) return false
61
+ const data = `${options.appId}${event_name}${timestamp}`
62
+ const expected = createHmac('sha256', options.appSecret).update(data).digest('hex')
63
+ if (mac.length !== expected.length) return false
64
+ return timingSafeEqual(Buffer.from(mac, 'hex'), Buffer.from(expected, 'hex'))
65
+ }
66
+
67
+ export function parseZaloWebhook(rawBody: string): WebhookEvent[] {
68
+ let payload: ZaloWebhookBody
69
+ try {
70
+ payload = JSON.parse(rawBody) as ZaloWebhookBody
71
+ } catch (cause) {
72
+ throw new WebhookSignatureError('Zalo webhook body is not valid JSON.', { cause })
73
+ }
74
+ const event = mapEvent(payload)
75
+ return event ? [event] : []
76
+ }
77
+
78
+ function mapEvent(p: ZaloWebhookBody): WebhookEvent | undefined {
79
+ if (!p.event_name) return undefined
80
+ const senderId = p.sender?.id
81
+ if (!senderId) return undefined
82
+ const base: WebhookEventBase = {
83
+ provider: 'zalo',
84
+ userId: senderId,
85
+ timestamp: p.timestamp ? new Date(Number.parseInt(p.timestamp, 10)) : new Date(),
86
+ source: 'user',
87
+ raw: p,
88
+ }
89
+ const messageId = p.message?.msg_id ?? ''
90
+
91
+ switch (p.event_name) {
92
+ case 'user_send_text': {
93
+ const e: TextMessageEvent = { ...base, type: 'message.text', text: p.message?.text ?? '', messageId }
94
+ return e
95
+ }
96
+ case 'user_send_image':
97
+ return { ...base, type: 'message.image', messageId } as MediaMessageEvent
98
+ case 'user_send_gif':
99
+ case 'user_send_video':
100
+ return { ...base, type: 'message.video', messageId } as MediaMessageEvent
101
+ case 'user_send_audio':
102
+ return { ...base, type: 'message.audio', messageId } as MediaMessageEvent
103
+ case 'user_send_file':
104
+ return { ...base, type: 'message.file', messageId } as MediaMessageEvent
105
+ case 'user_send_sticker': {
106
+ const e: StickerMessageEvent = { ...base, type: 'message.sticker', messageId }
107
+ return e
108
+ }
109
+ case 'user_send_location': {
110
+ const att = p.message?.attachments?.[0]?.payload as { coordinates?: { latitude: number; longitude: number } } | undefined
111
+ if (!att?.coordinates) return { ...base, type: 'unknown' }
112
+ const e: LocationMessageEvent = {
113
+ ...base,
114
+ type: 'message.location',
115
+ messageId,
116
+ latitude: att.coordinates.latitude,
117
+ longitude: att.coordinates.longitude,
118
+ }
119
+ return e
120
+ }
121
+ case 'user_click_chatnow':
122
+ case 'user_submit_info':
123
+ case 'user_send_link': {
124
+ const data = (p.message?.attachments?.[0]?.payload?.['payload'] as string | undefined) ?? p.message?.text ?? ''
125
+ const e: PostbackEvent = { ...base, type: 'postback', data }
126
+ return e
127
+ }
128
+ case 'follow': {
129
+ const e: FollowEvent = { ...base, type: 'follow' }
130
+ return e
131
+ }
132
+ case 'unfollow': {
133
+ const e: FollowEvent = { ...base, type: 'unfollow' }
134
+ return e
135
+ }
136
+ }
137
+ const fallback: UnknownEvent = { ...base, type: 'unknown' }
138
+ return fallback
139
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `InstantError` hierarchy — typed wrappers for failures across
3
+ * the instant-messaging stack. Vendor-native errors (LINE API
4
+ * failures, WhatsApp rejections) are preserved on `.cause` so
5
+ * apps can still `instanceof` the underlying type for retry /
6
+ * recovery logic; the wrapping just gives the framework a
7
+ * consistent `StravError` for the standard exception handler.
8
+ *
9
+ * Subclasses:
10
+ *
11
+ * - `InstantConfigError` — `config.instant` missing required
12
+ * fields. Thrown at boot from `InstantProvider`.
13
+ *
14
+ * - `ProviderUnsupportedError` — driver doesn't implement the
15
+ * requested operation (e.g. `messenger.flex(...)`). Thrown
16
+ * synchronously so apps fail fast.
17
+ *
18
+ * - `UnknownProviderError` — `instant.use('x')` for a name not
19
+ * configured. 400 — usually a config bug.
20
+ *
21
+ * - `WebhookSignatureError` — signature header missing or
22
+ * doesn't verify. Webhook route returns 400; LINE retries.
23
+ *
24
+ * - `InstantProviderError` — generic wrapper around a vendor
25
+ * exception that doesn't map to a more specific subclass.
26
+ * Preserves `.cause`; default status 502.
27
+ */
28
+
29
+ import { StravError } from '@strav/kernel'
30
+
31
+ export class InstantError extends StravError {
32
+ constructor(
33
+ message: string,
34
+ options: {
35
+ code?: string
36
+ status?: number
37
+ context?: Record<string, unknown>
38
+ cause?: unknown
39
+ } = {},
40
+ ) {
41
+ super(
42
+ message,
43
+ { code: options.code ?? 'instant.error', status: options.status ?? 500 },
44
+ {
45
+ ...(options.context ? { context: options.context } : {}),
46
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
47
+ },
48
+ )
49
+ }
50
+ }
51
+
52
+ export class InstantConfigError extends InstantError {
53
+ constructor(message: string, options: { context?: Record<string, unknown> } = {}) {
54
+ super(message, {
55
+ code: 'instant.config',
56
+ status: 500,
57
+ ...(options.context ? { context: options.context } : {}),
58
+ })
59
+ }
60
+ }
61
+
62
+ export class UnknownProviderError extends InstantError {
63
+ constructor(name: string, available: readonly string[]) {
64
+ super(
65
+ `Instant provider "${name}" is not configured. Available: ${available.join(', ') || '<none>'}.`,
66
+ {
67
+ code: 'instant.unknown_provider',
68
+ status: 400,
69
+ context: { requested: name, available },
70
+ },
71
+ )
72
+ }
73
+ }
74
+
75
+ export class ProviderUnsupportedError extends InstantError {
76
+ constructor(provider: string, operation: string, options: { reason?: string } = {}) {
77
+ const trailer = options.reason ? ` ${options.reason}` : ''
78
+ super(`Instant provider "${provider}" does not support "${operation}".${trailer}`, {
79
+ code: 'instant.provider_unsupported',
80
+ status: 400,
81
+ context: { provider, operation, ...(options.reason ? { reason: options.reason } : {}) },
82
+ })
83
+ }
84
+ }
85
+
86
+ export class WebhookSignatureError extends InstantError {
87
+ constructor(
88
+ message: string,
89
+ options: { context?: Record<string, unknown>; cause?: unknown } = {},
90
+ ) {
91
+ super(message, {
92
+ code: 'instant.webhook_signature',
93
+ status: 400,
94
+ ...(options.context ? { context: options.context } : {}),
95
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
96
+ })
97
+ }
98
+ }
99
+
100
+ export class InstantProviderError extends InstantError {
101
+ constructor(
102
+ message: string,
103
+ options: {
104
+ provider: string
105
+ operation: string
106
+ context?: Record<string, unknown>
107
+ cause?: unknown
108
+ status?: number
109
+ },
110
+ ) {
111
+ super(message, {
112
+ code: 'instant.provider_error',
113
+ status: options.status ?? 502,
114
+ context: {
115
+ provider: options.provider,
116
+ operation: options.operation,
117
+ ...(options.context ?? {}),
118
+ },
119
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
120
+ })
121
+ }
122
+ }
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ // Public API of `@strav/instant`.
2
+ //
3
+ // V1: provider-agnostic instant-messaging abstraction —
4
+ // normalized `OutgoingMessage` + `WebhookEvent` + multi-provider
5
+ // routing. The LINE driver ships as a subpath at
6
+ // `@strav/instant/line` with Flex builder, rich menus, beacons,
7
+ // and LIFF ID-token verification. WhatsApp + Messenger drivers
8
+ // come in follow-up slices.
9
+
10
+ export {
11
+ InstantConfigError,
12
+ InstantError,
13
+ InstantProviderError,
14
+ ProviderUnsupportedError,
15
+ UnknownProviderError,
16
+ WebhookSignatureError,
17
+ } from './errors.ts'
18
+ export type { InstantCapability } from './instant_capabilities.ts'
19
+ export type {
20
+ InstantDriver,
21
+ InstantDriverFactory,
22
+ UserProfile,
23
+ WebhookOps,
24
+ } from './instant_driver.ts'
25
+ export {
26
+ InstantManager,
27
+ type InstantManagerOptions,
28
+ } from './instant_manager.ts'
29
+ export { InstantProvider } from './instant_provider.ts'
30
+ export type {
31
+ Attachment,
32
+ OutgoingMessage,
33
+ QuickReply,
34
+ SendResult,
35
+ } from './message.ts'
36
+ export type {
37
+ InstantConfig,
38
+ ProviderConfig,
39
+ } from './types.ts'
40
+ export type {
41
+ BeaconEvent,
42
+ FollowEvent,
43
+ JoinEvent,
44
+ LocationMessageEvent,
45
+ MediaMessageEvent,
46
+ PostbackEvent,
47
+ StickerMessageEvent,
48
+ TextMessageEvent,
49
+ UnknownEvent,
50
+ WebhookEvent,
51
+ WebhookEventBase,
52
+ } from './webhook_event.ts'
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `InstantCapability` — granular feature flags every driver declares
3
+ * in `driver.capabilities`. Apps that build provider-neutral flows
4
+ * branch on capability before calling:
5
+ *
6
+ * if (instant.use('line').capabilities.has('send.flex')) { ... }
7
+ *
8
+ * Granularity is intentionally fine — one flag per non-trivial
9
+ * surface, not one per group — so e.g. WhatsApp can support
10
+ * `send.template` without claiming `send.flex`, and LINE can
11
+ * support `richMenu` and `beacon` without any analogue elsewhere.
12
+ *
13
+ * Drivers omit a capability when they can't fulfil it faithfully.
14
+ * Apps reach into provider-specific subpath imports
15
+ * (`@strav/instant/line`) when they need behaviour that doesn't
16
+ * map to a common capability.
17
+ */
18
+
19
+ export type InstantCapability =
20
+ // outbound content shapes
21
+ | 'send.text'
22
+ | 'send.image'
23
+ | 'send.video'
24
+ | 'send.audio'
25
+ | 'send.file'
26
+ | 'send.location'
27
+ | 'send.sticker'
28
+ | 'send.quickReplies'
29
+ | 'send.template'
30
+ | 'send.templateParams'
31
+ | 'send.interactive'
32
+ | 'send.flex'
33
+ // outbound endpoints
34
+ | 'reply'
35
+ | 'push'
36
+ | 'multicast'
37
+ | 'broadcast'
38
+ // identity + relationship
39
+ | 'profile'
40
+ | 'loadingIndicator'
41
+ // platform-specific surfaces
42
+ | 'richMenu'
43
+ | 'beacon'
44
+ | 'liff'
45
+ | 'miniApp'
46
+ | 'persistentMenu'
47
+ // inbound
48
+ | 'webhook.signature'
49
+ | 'webhook.parse'