@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.
- package/package.json +49 -0
- package/src/drivers/line/index.ts +50 -0
- package/src/drivers/line/liff_client.ts +126 -0
- package/src/drivers/line/liff_sign_in_route.ts +112 -0
- package/src/drivers/line/line_beacon.ts +14 -0
- package/src/drivers/line/line_config.ts +55 -0
- package/src/drivers/line/line_driver.ts +202 -0
- package/src/drivers/line/line_flex.ts +117 -0
- package/src/drivers/line/line_liff.ts +102 -0
- package/src/drivers/line/line_message_mapper.ts +118 -0
- package/src/drivers/line/line_provider.ts +33 -0
- package/src/drivers/line/line_rich_menu.ts +68 -0
- package/src/drivers/line/line_webhook.ts +168 -0
- package/src/drivers/messenger/index.ts +16 -0
- package/src/drivers/messenger/messenger_config.ts +20 -0
- package/src/drivers/messenger/messenger_driver.ts +151 -0
- package/src/drivers/messenger/messenger_message_mapper.ts +146 -0
- package/src/drivers/messenger/messenger_profile.ts +43 -0
- package/src/drivers/messenger/messenger_provider.ts +31 -0
- package/src/drivers/messenger/messenger_webhook.ts +165 -0
- package/src/drivers/telegram/index.ts +23 -0
- package/src/drivers/telegram/telegram_config.ts +19 -0
- package/src/drivers/telegram/telegram_driver.ts +121 -0
- package/src/drivers/telegram/telegram_message_mapper.ts +147 -0
- package/src/drivers/telegram/telegram_provider.ts +32 -0
- package/src/drivers/telegram/telegram_web_app.ts +108 -0
- package/src/drivers/telegram/telegram_webhook.ts +200 -0
- package/src/drivers/whatsapp/flows/index.ts +15 -0
- package/src/drivers/whatsapp/flows/whatsapp_flow_builder.ts +55 -0
- package/src/drivers/whatsapp/flows/whatsapp_flow_crypto.ts +81 -0
- package/src/drivers/whatsapp/index.ts +14 -0
- package/src/drivers/whatsapp/whatsapp_config.ts +35 -0
- package/src/drivers/whatsapp/whatsapp_driver.ts +111 -0
- package/src/drivers/whatsapp/whatsapp_message_mapper.ts +157 -0
- package/src/drivers/whatsapp/whatsapp_provider.ts +31 -0
- package/src/drivers/whatsapp/whatsapp_webhook.ts +170 -0
- package/src/drivers/zalo/index.ts +16 -0
- package/src/drivers/zalo/zalo_config.ts +37 -0
- package/src/drivers/zalo/zalo_driver.ts +130 -0
- package/src/drivers/zalo/zalo_message_mapper.ts +146 -0
- package/src/drivers/zalo/zalo_mini_app.ts +17 -0
- package/src/drivers/zalo/zalo_provider.ts +31 -0
- package/src/drivers/zalo/zalo_webhook.ts +139 -0
- package/src/errors.ts +122 -0
- package/src/index.ts +52 -0
- package/src/instant_capabilities.ts +49 -0
- package/src/instant_driver.ts +109 -0
- package/src/instant_manager.ts +113 -0
- package/src/instant_provider.ts +45 -0
- package/src/internal/fetch_json.ts +58 -0
- package/src/internal/meta/meta_graph_client.ts +51 -0
- package/src/internal/meta/meta_signature.ts +22 -0
- package/src/internal/meta/meta_webhook_challenge.ts +19 -0
- package/src/message.ts +65 -0
- package/src/types.ts +32 -0
- package/src/webhook_event.ts +93 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `InstantDriver` — the driver contract every adapter implements.
|
|
3
|
+
*
|
|
4
|
+
* One driver represents a configured provider instance
|
|
5
|
+
* (`config.instant.providers['line']`). The manager holds one
|
|
6
|
+
* driver per configured name and routes send / reply / webhook
|
|
7
|
+
* calls into it.
|
|
8
|
+
*
|
|
9
|
+
* Methods drivers don't support throw `ProviderUnsupportedError`
|
|
10
|
+
* synchronously. The driver's `capabilities` set declares the
|
|
11
|
+
* supported surfaces — apps that branch on capability avoid the
|
|
12
|
+
* throw by checking first.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { InstantCapability } from './instant_capabilities.ts'
|
|
16
|
+
import type { OutgoingMessage, SendResult } from './message.ts'
|
|
17
|
+
import type { WebhookEvent } from './webhook_event.ts'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Inbound webhook verification + parsing. Drivers that lack a
|
|
21
|
+
* webhook surface (rare for an instant-messaging provider)
|
|
22
|
+
* declare neither `webhook.signature` nor `webhook.parse` and
|
|
23
|
+
* throw `ProviderUnsupportedError` from these methods.
|
|
24
|
+
*/
|
|
25
|
+
export interface WebhookOps {
|
|
26
|
+
/**
|
|
27
|
+
* Verify the provider signature against the raw request body.
|
|
28
|
+
* Returns `true` when the signature matches. Drivers throw
|
|
29
|
+
* `WebhookSignatureError` only when the header is missing /
|
|
30
|
+
* malformed; a clean mismatch returns `false` so the route
|
|
31
|
+
* can decide how to respond.
|
|
32
|
+
*/
|
|
33
|
+
verifySignature(rawBody: string, signature: string | null | undefined): boolean
|
|
34
|
+
/**
|
|
35
|
+
* Parse a verified raw body into the framework's normalized
|
|
36
|
+
* `WebhookEvent` union. Drivers translate every event variant
|
|
37
|
+
* they can recognise; unknown shapes map to `{ type: 'unknown', raw }`.
|
|
38
|
+
*/
|
|
39
|
+
parse(rawBody: string): WebhookEvent[]
|
|
40
|
+
/**
|
|
41
|
+
* Optional GET-handshake responder used by webhook providers
|
|
42
|
+
* that require URL verification before they post events
|
|
43
|
+
* (Meta Cloud / Send API). Receivers call this with the
|
|
44
|
+
* query params; the driver returns the challenge value to
|
|
45
|
+
* echo back when the verify token matches, or `null` to
|
|
46
|
+
* reject. Drivers without a handshake omit this method.
|
|
47
|
+
*/
|
|
48
|
+
verifyChallenge?(params: Record<string, string | undefined>): string | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Optional profile lookup. LINE / Messenger / WhatsApp all expose
|
|
53
|
+
* a "get user profile by id" endpoint with diverging fields;
|
|
54
|
+
* the driver returns whatever it can fill.
|
|
55
|
+
*/
|
|
56
|
+
export interface UserProfile {
|
|
57
|
+
userId: string
|
|
58
|
+
displayName?: string
|
|
59
|
+
pictureUrl?: string
|
|
60
|
+
statusMessage?: string
|
|
61
|
+
language?: string
|
|
62
|
+
raw?: unknown
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface InstantDriver {
|
|
66
|
+
/** Driver identifier — matches the `driver:` discriminator in `ProviderConfig`. */
|
|
67
|
+
readonly name: string
|
|
68
|
+
/** App-chosen instance name (`config.instant.providers[name]`). */
|
|
69
|
+
readonly instanceName: string
|
|
70
|
+
/** Declared feature set. Apps check this to branch around `ProviderUnsupportedError`. */
|
|
71
|
+
readonly capabilities: ReadonlySet<InstantCapability>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Send a message to a single recipient. Whether this uses the
|
|
75
|
+
* provider's "push" endpoint, "send" endpoint, or a reply
|
|
76
|
+
* window is provider-defined; the common shape is "I have a
|
|
77
|
+
* recipient id, deliver this message."
|
|
78
|
+
*/
|
|
79
|
+
send(to: string, message: OutgoingMessage): Promise<SendResult>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reply to an inbound event within the provider's reply
|
|
83
|
+
* window. `replyToken` is provider-issued (LINE reply token,
|
|
84
|
+
* WhatsApp context message id, …).
|
|
85
|
+
*/
|
|
86
|
+
reply?(replyToken: string, message: OutgoingMessage): Promise<SendResult>
|
|
87
|
+
|
|
88
|
+
/** Push to a recipient outside a reply window. Distinct from `send` only on providers that distinguish. */
|
|
89
|
+
push?(to: string, message: OutgoingMessage): Promise<SendResult>
|
|
90
|
+
|
|
91
|
+
/** Multicast — same message to many recipients. */
|
|
92
|
+
multicast?(to: readonly string[], message: OutgoingMessage): Promise<SendResult>
|
|
93
|
+
|
|
94
|
+
/** Broadcast — same message to every follower. */
|
|
95
|
+
broadcast?(message: OutgoingMessage): Promise<SendResult>
|
|
96
|
+
|
|
97
|
+
/** Fetch a profile from the provider. */
|
|
98
|
+
profile?(userId: string): Promise<UserProfile>
|
|
99
|
+
|
|
100
|
+
readonly webhook: WebhookOps
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Factory the manager invokes for each configured provider. */
|
|
104
|
+
export type InstantDriverFactory = (config: {
|
|
105
|
+
/** App-chosen instance name (`'line'`, `'line-marketing'`, …). */
|
|
106
|
+
instanceName: string
|
|
107
|
+
/** Provider-config object with `driver:` + driver-specific fields. */
|
|
108
|
+
config: Record<string, unknown> & { driver: string }
|
|
109
|
+
}) => InstantDriver
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `InstantManager` — the facade apps use for instant-messaging
|
|
3
|
+
* workflows.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the manager-pattern shared with `@strav/payment` and
|
|
6
|
+
* `@strav/notification`:
|
|
7
|
+
*
|
|
8
|
+
* - **Drivers.** Apps declare providers in
|
|
9
|
+
* `config.instant.providers`. The manager constructs each
|
|
10
|
+
* driver lazily on first `use(name)` + memoizes. Adapter
|
|
11
|
+
* packages register their factories via
|
|
12
|
+
* `manager.extend(name, factory)`.
|
|
13
|
+
*
|
|
14
|
+
* - **Default routing.** `manager.send(to, msg)` routes to the
|
|
15
|
+
* default driver. `manager.use('marketing').send(...)`
|
|
16
|
+
* targets a named one.
|
|
17
|
+
*
|
|
18
|
+
* - **Webhooks.** `manager.verify(provider, rawBody, sig)` and
|
|
19
|
+
* `manager.parseWebhook(provider, rawBody)` delegate into the
|
|
20
|
+
* driver's `WebhookOps`. Apps wire these into their HTTP
|
|
21
|
+
* route (one route per provider).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { InstantConfigError, UnknownProviderError } from './errors.ts'
|
|
25
|
+
import type { InstantDriver, InstantDriverFactory } from './instant_driver.ts'
|
|
26
|
+
import type { OutgoingMessage, SendResult } from './message.ts'
|
|
27
|
+
import type { InstantConfig, ProviderConfig } from './types.ts'
|
|
28
|
+
import type { WebhookEvent } from './webhook_event.ts'
|
|
29
|
+
|
|
30
|
+
export interface InstantManagerOptions {
|
|
31
|
+
config: InstantConfig
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class InstantManager {
|
|
35
|
+
readonly config: InstantConfig
|
|
36
|
+
|
|
37
|
+
private readonly drivers = new Map<string, InstantDriver>()
|
|
38
|
+
private readonly extensions = new Map<string, InstantDriverFactory>()
|
|
39
|
+
|
|
40
|
+
constructor(options: InstantManagerOptions) {
|
|
41
|
+
const { config } = options
|
|
42
|
+
if (!config.providers[config.default]) {
|
|
43
|
+
throw new InstantConfigError(
|
|
44
|
+
`InstantManager: default provider "${config.default}" is not configured.`,
|
|
45
|
+
{
|
|
46
|
+
context: {
|
|
47
|
+
default: config.default,
|
|
48
|
+
available: Object.keys(config.providers),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
this.config = config
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Driver routing ──────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/** Resolve a driver by app-chosen instance name (or the default when omitted). */
|
|
59
|
+
use(name?: string): InstantDriver {
|
|
60
|
+
const key = name ?? this.config.default
|
|
61
|
+
const cached = this.drivers.get(key)
|
|
62
|
+
if (cached) return cached
|
|
63
|
+
|
|
64
|
+
const cfg = this.config.providers[key]
|
|
65
|
+
if (!cfg) {
|
|
66
|
+
throw new UnknownProviderError(key, Object.keys(this.config.providers))
|
|
67
|
+
}
|
|
68
|
+
const ext = this.extensions.get(cfg.driver)
|
|
69
|
+
if (!ext) {
|
|
70
|
+
throw new InstantConfigError(
|
|
71
|
+
`InstantManager: unknown driver "${cfg.driver}" for provider "${key}". Register it via \`manager.extend("${cfg.driver}", factory)\` or install the matching adapter package.`,
|
|
72
|
+
{ context: { driver: cfg.driver, available: [...this.extensions.keys()] } },
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
const driver = ext({
|
|
76
|
+
instanceName: key,
|
|
77
|
+
config: cfg as ProviderConfig & { driver: string },
|
|
78
|
+
})
|
|
79
|
+
this.drivers.set(key, driver)
|
|
80
|
+
return driver
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register a driver factory. Adapter packages call this from
|
|
85
|
+
* their ServiceProvider's `register()` step.
|
|
86
|
+
*/
|
|
87
|
+
extend(driverName: string, factory: InstantDriverFactory): void {
|
|
88
|
+
this.extensions.set(driverName, factory)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Hand-wire a driver instance under an app-chosen name (tests / one-offs). */
|
|
92
|
+
useDriver(instanceName: string, driver: InstantDriver): void {
|
|
93
|
+
this.drivers.set(instanceName, driver)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Convenience delegates to the default driver ─────────────────────
|
|
97
|
+
|
|
98
|
+
send(to: string, message: OutgoingMessage): Promise<SendResult> {
|
|
99
|
+
return this.use().send(to, message)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Webhook routing ─────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/** Verify a webhook signature using the named provider's driver. */
|
|
105
|
+
verify(provider: string, rawBody: string, signature: string | null | undefined): boolean {
|
|
106
|
+
return this.use(provider).webhook.verifySignature(rawBody, signature)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Parse a verified raw webhook body into the framework's normalized event union. */
|
|
110
|
+
parseWebhook(provider: string, rawBody: string): WebhookEvent[] {
|
|
111
|
+
return this.use(provider).webhook.parse(rawBody)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `InstantProvider` — `ServiceProvider` that wires `InstantManager`
|
|
3
|
+
* into the container from `config.instant`.
|
|
4
|
+
*
|
|
5
|
+
* Adapter packages register their drivers separately via their
|
|
6
|
+
* own ServiceProvider (e.g. `LineInstantProvider`). Apps list
|
|
7
|
+
* the adapter providers AFTER `InstantProvider` in
|
|
8
|
+
* `bootstrap/providers.ts` — `register()` runs in declaration
|
|
9
|
+
* order, then `boot()` runs in the same order. Adapter
|
|
10
|
+
* `register()` calls `manager.extend(driver, factory)`; this
|
|
11
|
+
* provider's `boot()` eagerly resolves so config errors surface
|
|
12
|
+
* at startup.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { type Application, ConfigRepository, ServiceProvider } from '@strav/kernel'
|
|
16
|
+
import { InstantConfigError } from './errors.ts'
|
|
17
|
+
import { InstantManager } from './instant_manager.ts'
|
|
18
|
+
import type { InstantConfig } from './types.ts'
|
|
19
|
+
|
|
20
|
+
export class InstantProvider extends ServiceProvider {
|
|
21
|
+
override readonly name = 'instant'
|
|
22
|
+
override readonly dependencies = ['config']
|
|
23
|
+
|
|
24
|
+
override register(app: Application): void {
|
|
25
|
+
app.singleton(InstantManager, (c) => {
|
|
26
|
+
const raw = c.resolve(ConfigRepository).get('instant') as InstantConfig | undefined
|
|
27
|
+
if (!raw) {
|
|
28
|
+
throw new InstantConfigError(
|
|
29
|
+
'InstantProvider: `config.instant` is missing. Add `config/instant.ts` with at least one provider.',
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
if (!raw.providers || Object.keys(raw.providers).length === 0) {
|
|
33
|
+
throw new InstantConfigError(
|
|
34
|
+
'InstantProvider: `config.instant.providers` is empty. Configure at least one provider.',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
return new InstantManager({ config: raw })
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override boot(app: Application): void {
|
|
42
|
+
// Force-resolve so config errors surface at boot, not on first send().
|
|
43
|
+
app.resolve(InstantManager)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny `fetch` wrapper that returns parsed JSON or throws
|
|
3
|
+
* `InstantProviderError` with the provider/operation/status
|
|
4
|
+
* stamped on it. Used by every driver so vendor REST failures
|
|
5
|
+
* surface with the same shape regardless of which messenger
|
|
6
|
+
* tripped.
|
|
7
|
+
*
|
|
8
|
+
* The body comes back as `T` without runtime validation —
|
|
9
|
+
* callers know the endpoint shape and the framework's error
|
|
10
|
+
* handler is happy with whatever shape `.cause` carries.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { InstantProviderError } from '../errors.ts'
|
|
14
|
+
|
|
15
|
+
export interface FetchJsonOptions {
|
|
16
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
17
|
+
headers?: Record<string, string>
|
|
18
|
+
body?: string | Uint8Array | FormData
|
|
19
|
+
provider: string
|
|
20
|
+
operation: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function fetchJson<T>(url: string, options: FetchJsonOptions): Promise<T> {
|
|
24
|
+
const { provider, operation, method = 'GET', headers, body } = options
|
|
25
|
+
let response: Response
|
|
26
|
+
try {
|
|
27
|
+
response = await fetch(url, { method, headers, body })
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
throw new InstantProviderError(`${provider} ${operation} request failed`, {
|
|
30
|
+
provider,
|
|
31
|
+
operation,
|
|
32
|
+
cause,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
const text = await response.text()
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new InstantProviderError(
|
|
38
|
+
`${provider} ${operation} returned HTTP ${response.status}`,
|
|
39
|
+
{
|
|
40
|
+
provider,
|
|
41
|
+
operation,
|
|
42
|
+
status: 502,
|
|
43
|
+
context: { httpStatus: response.status, body: text.slice(0, 1024) },
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
if (text.length === 0) return undefined as T
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(text) as T
|
|
50
|
+
} catch (cause) {
|
|
51
|
+
throw new InstantProviderError(`${provider} ${operation} returned non-JSON body`, {
|
|
52
|
+
provider,
|
|
53
|
+
operation,
|
|
54
|
+
cause,
|
|
55
|
+
context: { body: text.slice(0, 256) },
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin typed wrapper around `graph.facebook.com/v<x>`.
|
|
3
|
+
* Shared by WhatsApp Cloud and Messenger Send API.
|
|
4
|
+
*
|
|
5
|
+
* Operations stamp `provider` + `operation` onto the
|
|
6
|
+
* `InstantProviderError` thrown by `fetchJson` so callers
|
|
7
|
+
* don't need to wrap again.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fetchJson } from '../fetch_json.ts'
|
|
11
|
+
|
|
12
|
+
export interface MetaGraphClientOptions {
|
|
13
|
+
accessToken: string
|
|
14
|
+
apiVersion?: string
|
|
15
|
+
provider: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MetaGraphClient {
|
|
19
|
+
private readonly base: string
|
|
20
|
+
private readonly accessToken: string
|
|
21
|
+
private readonly provider: string
|
|
22
|
+
|
|
23
|
+
constructor(options: MetaGraphClientOptions) {
|
|
24
|
+
const version = options.apiVersion ?? 'v20.0'
|
|
25
|
+
this.base = `https://graph.facebook.com/${version}`
|
|
26
|
+
this.accessToken = options.accessToken
|
|
27
|
+
this.provider = options.provider
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
post<T>(path: string, body: unknown, operation: string): Promise<T> {
|
|
31
|
+
return fetchJson<T>(`${this.base}${path}`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(body),
|
|
38
|
+
provider: this.provider,
|
|
39
|
+
operation,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get<T>(path: string, operation: string): Promise<T> {
|
|
44
|
+
return fetchJson<T>(`${this.base}${path}`, {
|
|
45
|
+
method: 'GET',
|
|
46
|
+
headers: { Authorization: `Bearer ${this.accessToken}` },
|
|
47
|
+
provider: this.provider,
|
|
48
|
+
operation,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta's `X-Hub-Signature-256` verifier — used by both WhatsApp
|
|
3
|
+
* Cloud and Messenger. The header is `sha256=<hex>` of an
|
|
4
|
+
* HMAC-SHA256 over the raw request body keyed on the App
|
|
5
|
+
* Secret. Compared in constant time to defeat timing oracles.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
9
|
+
|
|
10
|
+
export function verifyMetaSignature(
|
|
11
|
+
rawBody: string,
|
|
12
|
+
header: string | null | undefined,
|
|
13
|
+
appSecret: string,
|
|
14
|
+
): boolean {
|
|
15
|
+
if (!header) return false
|
|
16
|
+
const prefix = 'sha256='
|
|
17
|
+
if (!header.startsWith(prefix)) return false
|
|
18
|
+
const provided = header.slice(prefix.length)
|
|
19
|
+
const expected = createHmac('sha256', appSecret).update(rawBody).digest('hex')
|
|
20
|
+
if (provided.length !== expected.length) return false
|
|
21
|
+
return timingSafeEqual(Buffer.from(provided, 'hex'), Buffer.from(expected, 'hex'))
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta's GET-handshake responder. Used by WhatsApp Cloud and
|
|
3
|
+
* Messenger Send API webhook verification.
|
|
4
|
+
*
|
|
5
|
+
* The receiver passes the request query params; the driver
|
|
6
|
+
* returns the challenge string to echo back when the verify
|
|
7
|
+
* token matches, or `null` to reject.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function verifyMetaChallenge(
|
|
11
|
+
params: Record<string, string | undefined>,
|
|
12
|
+
verifyToken: string,
|
|
13
|
+
): string | null {
|
|
14
|
+
const mode = params['hub.mode']
|
|
15
|
+
const token = params['hub.verify_token']
|
|
16
|
+
const challenge = params['hub.challenge']
|
|
17
|
+
if (mode === 'subscribe' && token === verifyToken && challenge) return challenge
|
|
18
|
+
return null
|
|
19
|
+
}
|
package/src/message.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `OutgoingMessage` — the lowest-common-denominator wire shape the
|
|
3
|
+
* `InstantManager` exposes for portable sends. Apps that target
|
|
4
|
+
* multiple providers stick to these fields; apps that need
|
|
5
|
+
* provider-specific richness (LINE Flex, WhatsApp templates,
|
|
6
|
+
* Messenger generic templates) drop down to `raw` or call into the
|
|
7
|
+
* subpath driver directly.
|
|
8
|
+
*
|
|
9
|
+
* Every field is optional so the same shape can carry "just text",
|
|
10
|
+
* "text + image", "quick replies only", etc. Drivers throw
|
|
11
|
+
* `ProviderUnsupportedError` for fields they can't fulfil — apps
|
|
12
|
+
* gate on `driver.capabilities` to branch ahead of the call.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface OutgoingMessage {
|
|
16
|
+
/** Plain-text body. Required for `send.text` calls. */
|
|
17
|
+
text?: string
|
|
18
|
+
/** Media + structured content attached to the message. */
|
|
19
|
+
attachments?: Attachment[]
|
|
20
|
+
/** Buttons rendered alongside the message (LINE quick reply, WhatsApp buttons, Messenger quick replies). */
|
|
21
|
+
quickReplies?: QuickReply[]
|
|
22
|
+
/**
|
|
23
|
+
* Provider-native message object. When set, the driver
|
|
24
|
+
* forwards it verbatim and ignores the LCD fields above.
|
|
25
|
+
* Use this when reaching for provider-specific richness
|
|
26
|
+
* (e.g. a `FlexBubble` from `@strav/instant/line`).
|
|
27
|
+
*/
|
|
28
|
+
raw?: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type Attachment =
|
|
32
|
+
| { type: 'image'; url: string; previewUrl?: string }
|
|
33
|
+
| { type: 'video'; url: string; previewUrl?: string; durationMs?: number }
|
|
34
|
+
| { type: 'audio'; url: string; durationMs?: number }
|
|
35
|
+
| { type: 'file'; url: string; fileName?: string; sizeBytes?: number }
|
|
36
|
+
| { type: 'location'; latitude: number; longitude: number; title?: string; address?: string }
|
|
37
|
+
| { type: 'sticker'; packageId: string; stickerId: string }
|
|
38
|
+
|
|
39
|
+
export interface QuickReply {
|
|
40
|
+
label: string
|
|
41
|
+
/** Action fired when the reply is tapped. */
|
|
42
|
+
action:
|
|
43
|
+
| { type: 'message'; text: string }
|
|
44
|
+
| { type: 'postback'; data: string; displayText?: string }
|
|
45
|
+
| { type: 'uri'; uri: string }
|
|
46
|
+
/** Icon shown next to the label, where the provider supports it. */
|
|
47
|
+
iconUrl?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `SendResult` — what every send call returns. Drivers populate
|
|
52
|
+
* `messageId` when the provider hands one back (LINE's
|
|
53
|
+
* `x-line-request-id`, WhatsApp's `messages[0].id`); some only
|
|
54
|
+
* confirm acceptance and leave it undefined.
|
|
55
|
+
*/
|
|
56
|
+
export interface SendResult {
|
|
57
|
+
/** Driver name (`'line'`, `'whatsapp'`, …). */
|
|
58
|
+
provider: string
|
|
59
|
+
/** Whether the upstream API accepted the send. */
|
|
60
|
+
accepted: boolean
|
|
61
|
+
/** Provider-issued reference, when one exists. */
|
|
62
|
+
messageId?: string
|
|
63
|
+
/** Raw provider response for advanced inspection. */
|
|
64
|
+
raw?: unknown
|
|
65
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@strav/instant` — runtime config shapes.
|
|
3
|
+
*
|
|
4
|
+
* `InstantConfig` is the `config.instant` shape apps declare in
|
|
5
|
+
* `config/instant.ts`. Multi-provider by default (apps could run
|
|
6
|
+
* one LINE bot for support, another for marketing). `providers`
|
|
7
|
+
* is keyed by app-chosen instance name; each entry carries a
|
|
8
|
+
* `driver` discriminator the framework resolves to a concrete
|
|
9
|
+
* `InstantDriver`. Apps with a single provider just register one
|
|
10
|
+
* entry.
|
|
11
|
+
*
|
|
12
|
+
* `ProviderConfig` is free-form so each adapter owns its own
|
|
13
|
+
* config shape (`LineProviderConfig` etc.) without forcing the
|
|
14
|
+
* core to know every vendor field.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface InstantConfig {
|
|
18
|
+
/** Key into `providers`. The default routing target for unqualified `instant.*` calls. */
|
|
19
|
+
default: string
|
|
20
|
+
providers: Record<string, ProviderConfig>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProviderConfig {
|
|
24
|
+
/**
|
|
25
|
+
* Driver identifier — must match a name registered via
|
|
26
|
+
* `manager.extend(name, factory)` (typically by an adapter
|
|
27
|
+
* ServiceProvider).
|
|
28
|
+
*/
|
|
29
|
+
driver: string
|
|
30
|
+
/** Driver-specific fields — see each adapter's `*Config`. */
|
|
31
|
+
[key: string]: unknown
|
|
32
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WebhookEvent` — the normalized inbound event union the manager
|
|
3
|
+
* exposes to apps. Drivers convert their provider-native event
|
|
4
|
+
* shape (LINE event, WhatsApp change, Messenger entry) into one
|
|
5
|
+
* of these variants. Events the driver can't normalize map to
|
|
6
|
+
* `unknown` and surface via `raw` so apps that opt into raw
|
|
7
|
+
* handling don't lose anything.
|
|
8
|
+
*
|
|
9
|
+
* Every variant carries `provider`, `userId` (the opaque
|
|
10
|
+
* recipient identifier — LINE `userId`, WhatsApp phone, Messenger
|
|
11
|
+
* PSID), `timestamp`, and `raw` so apps can drop down when the
|
|
12
|
+
* normalized shape isn't enough.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface WebhookEventBase {
|
|
16
|
+
/** Driver name. */
|
|
17
|
+
provider: string
|
|
18
|
+
/** Opaque user identifier from the provider. */
|
|
19
|
+
userId: string
|
|
20
|
+
/** Provider-issued event timestamp (Date). */
|
|
21
|
+
timestamp: Date
|
|
22
|
+
/** Conversation source — direct chat (`user`), group (`group`), room (`room`), etc. */
|
|
23
|
+
source: 'user' | 'group' | 'room' | 'unknown'
|
|
24
|
+
/** Group / room id when `source !== 'user'`. */
|
|
25
|
+
sourceId?: string
|
|
26
|
+
/** Reply token (LINE) or message context for short-lived reply windows. */
|
|
27
|
+
replyToken?: string
|
|
28
|
+
/** Provider-native event payload. */
|
|
29
|
+
raw: unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TextMessageEvent extends WebhookEventBase {
|
|
33
|
+
type: 'message.text'
|
|
34
|
+
text: string
|
|
35
|
+
/** Provider-issued message id. */
|
|
36
|
+
messageId: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MediaMessageEvent extends WebhookEventBase {
|
|
40
|
+
type: 'message.image' | 'message.video' | 'message.audio' | 'message.file'
|
|
41
|
+
messageId: string
|
|
42
|
+
/** When the provider hands back content directly (vs. needing a fetch). */
|
|
43
|
+
contentUrl?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LocationMessageEvent extends WebhookEventBase {
|
|
47
|
+
type: 'message.location'
|
|
48
|
+
messageId: string
|
|
49
|
+
latitude: number
|
|
50
|
+
longitude: number
|
|
51
|
+
title?: string
|
|
52
|
+
address?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StickerMessageEvent extends WebhookEventBase {
|
|
56
|
+
type: 'message.sticker'
|
|
57
|
+
messageId: string
|
|
58
|
+
packageId?: string
|
|
59
|
+
stickerId?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PostbackEvent extends WebhookEventBase {
|
|
63
|
+
type: 'postback'
|
|
64
|
+
data: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface FollowEvent extends WebhookEventBase {
|
|
68
|
+
type: 'follow' | 'unfollow'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface JoinEvent extends WebhookEventBase {
|
|
72
|
+
type: 'join' | 'leave'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface BeaconEvent extends WebhookEventBase {
|
|
76
|
+
type: 'beacon'
|
|
77
|
+
beacon: { hwid: string; kind: 'enter' | 'banner' | 'stay'; dm?: string }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface UnknownEvent extends WebhookEventBase {
|
|
81
|
+
type: 'unknown'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type WebhookEvent =
|
|
85
|
+
| TextMessageEvent
|
|
86
|
+
| MediaMessageEvent
|
|
87
|
+
| LocationMessageEvent
|
|
88
|
+
| StickerMessageEvent
|
|
89
|
+
| PostbackEvent
|
|
90
|
+
| FollowEvent
|
|
91
|
+
| JoinEvent
|
|
92
|
+
| BeaconEvent
|
|
93
|
+
| UnknownEvent
|