@intentface/latch-channels 0.9.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.
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/ask.d.ts +69 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +94 -0
- package/dist/ask.js.map +1 -0
- package/dist/commands.d.ts +41 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +54 -0
- package/dist/commands.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel.d.ts +207 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +883 -0
- package/dist/kernel.js.map +1 -0
- package/dist/render.d.ts +43 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +192 -0
- package/dist/render.js.map +1 -0
- package/dist/slack/api.d.ts +207 -0
- package/dist/slack/api.d.ts.map +1 -0
- package/dist/slack/api.js +249 -0
- package/dist/slack/api.js.map +1 -0
- package/dist/slack/blocks.d.ts +36 -0
- package/dist/slack/blocks.d.ts.map +1 -0
- package/dist/slack/blocks.js +161 -0
- package/dist/slack/blocks.js.map +1 -0
- package/dist/slack/ids.d.ts +40 -0
- package/dist/slack/ids.d.ts.map +1 -0
- package/dist/slack/ids.js +47 -0
- package/dist/slack/ids.js.map +1 -0
- package/dist/slack/index.d.ts +24 -0
- package/dist/slack/index.d.ts.map +1 -0
- package/dist/slack/index.js +18 -0
- package/dist/slack/index.js.map +1 -0
- package/dist/slack/manifest.d.ts +23 -0
- package/dist/slack/manifest.d.ts.map +1 -0
- package/dist/slack/manifest.js +102 -0
- package/dist/slack/manifest.js.map +1 -0
- package/dist/slack/normalize.d.ts +35 -0
- package/dist/slack/normalize.d.ts.map +1 -0
- package/dist/slack/normalize.js +63 -0
- package/dist/slack/normalize.js.map +1 -0
- package/dist/slack/payloads.d.ts +36 -0
- package/dist/slack/payloads.d.ts.map +1 -0
- package/dist/slack/payloads.js +80 -0
- package/dist/slack/payloads.js.map +1 -0
- package/dist/slack/slackChannel.d.ts +152 -0
- package/dist/slack/slackChannel.d.ts.map +1 -0
- package/dist/slack/slackChannel.js +650 -0
- package/dist/slack/slackChannel.js.map +1 -0
- package/dist/slack/stream.d.ts +88 -0
- package/dist/slack/stream.d.ts.map +1 -0
- package/dist/slack/stream.js +418 -0
- package/dist/slack/stream.js.map +1 -0
- package/dist/slack/verify.d.ts +20 -0
- package/dist/slack/verify.d.ts.map +1 -0
- package/dist/slack/verify.js +32 -0
- package/dist/slack/verify.js.map +1 -0
- package/dist/telegram/api.d.ts +110 -0
- package/dist/telegram/api.d.ts.map +1 -0
- package/dist/telegram/api.js +170 -0
- package/dist/telegram/api.js.map +1 -0
- package/dist/telegram/format.d.ts +26 -0
- package/dist/telegram/format.d.ts.map +1 -0
- package/dist/telegram/format.js +56 -0
- package/dist/telegram/format.js.map +1 -0
- package/dist/telegram/html.d.ts +11 -0
- package/dist/telegram/html.d.ts.map +1 -0
- package/dist/telegram/html.js +35 -0
- package/dist/telegram/html.js.map +1 -0
- package/dist/telegram/index.d.ts +15 -0
- package/dist/telegram/index.d.ts.map +1 -0
- package/dist/telegram/index.js +15 -0
- package/dist/telegram/index.js.map +1 -0
- package/dist/telegram/stream.d.ts +59 -0
- package/dist/telegram/stream.d.ts.map +1 -0
- package/dist/telegram/stream.js +185 -0
- package/dist/telegram/stream.js.map +1 -0
- package/dist/telegram/telegramChannel.d.ts +136 -0
- package/dist/telegram/telegramChannel.d.ts.map +1 -0
- package/dist/telegram/telegramChannel.js +277 -0
- package/dist/telegram/telegramChannel.js.map +1 -0
- package/dist/telegram/verify.d.ts +7 -0
- package/dist/telegram/verify.d.ts.map +1 -0
- package/dist/telegram/verify.js +14 -0
- package/dist/telegram/verify.js.map +1 -0
- package/dist/types.d.ts +240 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/whatsapp/api.d.ts +59 -0
- package/dist/whatsapp/api.d.ts.map +1 -0
- package/dist/whatsapp/api.js +154 -0
- package/dist/whatsapp/api.js.map +1 -0
- package/dist/whatsapp/index.d.ts +13 -0
- package/dist/whatsapp/index.d.ts.map +1 -0
- package/dist/whatsapp/index.js +13 -0
- package/dist/whatsapp/index.js.map +1 -0
- package/dist/whatsapp/verify.d.ts +17 -0
- package/dist/whatsapp/verify.d.ts.map +1 -0
- package/dist/whatsapp/verify.js +32 -0
- package/dist/whatsapp/verify.js.map +1 -0
- package/dist/whatsapp/whatsappChannel.d.ts +104 -0
- package/dist/whatsapp/whatsappChannel.d.ts.map +1 -0
- package/dist/whatsapp/whatsappChannel.js +217 -0
- package/dist/whatsapp/whatsappChannel.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Verify Telegram's webhook secret: the `x-telegram-bot-api-secret-token`
|
|
4
|
+
* header must equal the `secret_token` we registered with `setWebhook`.
|
|
5
|
+
* Constant-time compare (length check first — `timingSafeEqual` requires it).
|
|
6
|
+
*/
|
|
7
|
+
export function verifyTelegramSecretToken(header, expected) {
|
|
8
|
+
if (!header || !expected)
|
|
9
|
+
return false;
|
|
10
|
+
const a = Buffer.from(header);
|
|
11
|
+
const b = Buffer.from(expected);
|
|
12
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/telegram/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CACvC,MAAqB,EACrB,QAAgB;IAEhB,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACvC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic vocabulary for bringing a Latch run to a chat surface.
|
|
3
|
+
*
|
|
4
|
+
* A channel adapter (Slack, Teams, …) only ever sees `RenderEvent`s — the
|
|
5
|
+
* normalized, surface-independent shape of what an agent is doing — and maps
|
|
6
|
+
* them to its platform's primitives (a streamed message, a status line, an
|
|
7
|
+
* approval card). The mapping from a Latch run's raw UIMessage part-stream to
|
|
8
|
+
* these events lives once in `render.ts`, so a new platform is "translate
|
|
9
|
+
* RenderEvents", never "re-parse the AI stream".
|
|
10
|
+
*/
|
|
11
|
+
import type { AppMessage } from "@intentface/latch-core";
|
|
12
|
+
/** A single thing worth showing on a chat surface as a run progresses. */
|
|
13
|
+
export type RenderEvent =
|
|
14
|
+
/** Coarse run lifecycle, for typing/status indicators. */
|
|
15
|
+
{
|
|
16
|
+
type: "status";
|
|
17
|
+
status: RunStatus;
|
|
18
|
+
}
|
|
19
|
+
/** A chunk of the assistant's visible answer. */
|
|
20
|
+
| {
|
|
21
|
+
type: "text-delta";
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
/** A chunk of reasoning/thinking (render collapsed or throttled). */
|
|
25
|
+
| {
|
|
26
|
+
type: "reasoning-delta";
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
/** A tool call started (we know its name + input). */
|
|
30
|
+
| {
|
|
31
|
+
type: "tool-start";
|
|
32
|
+
toolCallId: string;
|
|
33
|
+
toolName: string;
|
|
34
|
+
input?: unknown;
|
|
35
|
+
}
|
|
36
|
+
/** A tool call settled with a result or an error. */
|
|
37
|
+
| {
|
|
38
|
+
type: "tool-end";
|
|
39
|
+
toolCallId: string;
|
|
40
|
+
toolName: string;
|
|
41
|
+
output?: unknown;
|
|
42
|
+
error?: string;
|
|
43
|
+
}
|
|
44
|
+
/** The run failed; `message` is the error text to surface to the user. */
|
|
45
|
+
| {
|
|
46
|
+
type: "error";
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
/** The streamed turn ended (may still be paused awaiting approval — see status). */
|
|
50
|
+
| {
|
|
51
|
+
type: "finish";
|
|
52
|
+
};
|
|
53
|
+
export type RunStatus = "thinking" | "working" | "awaiting_input" | "done" | "error";
|
|
54
|
+
/**
|
|
55
|
+
* A pending human-in-the-loop decision surfaced after a turn pauses. Detected
|
|
56
|
+
* from persisted history (not the live chunk stream), because approvals are
|
|
57
|
+
* message state, not stream chunks.
|
|
58
|
+
*/
|
|
59
|
+
export interface PendingApproval {
|
|
60
|
+
/** The opaque approval id to pass back to `runtime.applyApproval`. */
|
|
61
|
+
approvalId: string;
|
|
62
|
+
/** The tool awaiting approval (bare name, e.g. `web_search`). */
|
|
63
|
+
toolName: string;
|
|
64
|
+
/** The tool input the model proposed, for display. */
|
|
65
|
+
input?: unknown;
|
|
66
|
+
/**
|
|
67
|
+
* OAuth "connect a connection" approvals (`connect_<name>`) are special: they
|
|
68
|
+
* resolve by sending the user through the platform's authorize URL, not by an
|
|
69
|
+
* approve/deny button. `connectionName` is the `<name>` part.
|
|
70
|
+
*/
|
|
71
|
+
isConnect: boolean;
|
|
72
|
+
connectionName?: string;
|
|
73
|
+
}
|
|
74
|
+
/** One choice on an `askUser` question. `label` goes on the button. */
|
|
75
|
+
export interface QuestionOption {
|
|
76
|
+
label: string;
|
|
77
|
+
description: string;
|
|
78
|
+
}
|
|
79
|
+
/** One question from an `askUser` call (mirrors `AskUserQuestion` in the platform UI). */
|
|
80
|
+
export interface Question {
|
|
81
|
+
question: string;
|
|
82
|
+
header?: string;
|
|
83
|
+
options: QuestionOption[];
|
|
84
|
+
multiSelect?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A questionnaire a paused turn is waiting on — an `askUser` tool call parked in
|
|
88
|
+
* `input-available`. Unlike an approval it resolves through
|
|
89
|
+
* `runtime.applyToolResult`, and ALL questions resolve in one call, so a channel
|
|
90
|
+
* must collect every answer before submitting.
|
|
91
|
+
*/
|
|
92
|
+
export interface PendingQuestion {
|
|
93
|
+
/** The tool call to resolve via `runtime.applyToolResult`. */
|
|
94
|
+
toolCallId: string;
|
|
95
|
+
questions: Question[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A questionnaire in progress on a chat surface — at most one per chat.
|
|
99
|
+
*
|
|
100
|
+
* Chat surfaces answer one question at a time, but `applyToolResult` closes the
|
|
101
|
+
* whole tool call in one shot, so the answers collected so far have to live
|
|
102
|
+
* somewhere between taps. Serverless channels have no memory between webhook
|
|
103
|
+
* deliveries, hence a host-backed store.
|
|
104
|
+
*/
|
|
105
|
+
export interface AskSession {
|
|
106
|
+
/** The parked `askUser` call these answers will resolve. */
|
|
107
|
+
toolCallId: string;
|
|
108
|
+
/** Freshness token carried in button payloads so stale buttons are rejected. */
|
|
109
|
+
nonce: string;
|
|
110
|
+
/** The questions as posted — avoids a history read on every tap. */
|
|
111
|
+
questions: Question[];
|
|
112
|
+
/** Index of the question currently on screen. */
|
|
113
|
+
cursor: number;
|
|
114
|
+
/** question text → committed answer, for questions already answered. */
|
|
115
|
+
answers: Record<string, string>;
|
|
116
|
+
/** Option indices ticked so far on the current `multiSelect` question. */
|
|
117
|
+
selected: number[];
|
|
118
|
+
/** Platform chat to post questions into (the Telegram/WhatsApp chat id). */
|
|
119
|
+
promptChatId: string;
|
|
120
|
+
/** Platform id of the visible prompt message (cosmetic — used to edit it). */
|
|
121
|
+
promptRef?: string;
|
|
122
|
+
/** Fence for optimistic concurrency; every successful write bumps it. */
|
|
123
|
+
rev: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Host-backed storage for in-flight questionnaires, injected like `seen` and
|
|
127
|
+
* `claimTurn`. Grouped into one object because these are a single transactional
|
|
128
|
+
* aggregate over one row — partial wiring is a bug, not a degradation, so a
|
|
129
|
+
* channel checks for the whole store or renders questions as plain text.
|
|
130
|
+
*/
|
|
131
|
+
export interface AskStateStore {
|
|
132
|
+
/** The live session for this chat, or null (expired rows count as absent). */
|
|
133
|
+
get(chatId: string): Promise<AskSession | null>;
|
|
134
|
+
/**
|
|
135
|
+
* Claim the right to prompt. Replaces a session for a DIFFERENT tool call,
|
|
136
|
+
* but leaves one for the SAME tool call untouched and answers `"already"` —
|
|
137
|
+
* so re-rendering a still-pending questionnaire never double-prompts.
|
|
138
|
+
*/
|
|
139
|
+
begin(chatId: string, session: Omit<AskSession, "rev">): Promise<"started" | "already">;
|
|
140
|
+
/** Fenced patch. Returns the new rev, or null when `rev` was stale. */
|
|
141
|
+
update(chatId: string, toolCallId: string, rev: number, patch: Partial<Pick<AskSession, "cursor" | "answers" | "selected" | "promptRef">>): Promise<number | null>;
|
|
142
|
+
/** Fenced delete — true only if THIS caller removed it. Gates the submit. */
|
|
143
|
+
finish(chatId: string, toolCallId: string, rev: number): Promise<boolean>;
|
|
144
|
+
/** Unconditional drop, returning whatever was removed. */
|
|
145
|
+
clear(chatId: string): Promise<AskSession | null>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Host-backed "which thread am I in" pointer for surfaces that have exactly one
|
|
149
|
+
* conversation per person (a Telegram DM, a WhatsApp thread) and therefore no
|
|
150
|
+
* native way to start a new one. `/new` bumps the epoch; the channel derives the
|
|
151
|
+
* chat id from it, so epoch 0 keeps the original id and existing conversations
|
|
152
|
+
* never move.
|
|
153
|
+
*/
|
|
154
|
+
export interface ChannelThreadStore {
|
|
155
|
+
/** Current epoch for a surface conversation; 0 when it has never rotated. */
|
|
156
|
+
current(baseChatId: string): Promise<number>;
|
|
157
|
+
/** Start a fresh thread; returns the new (higher) epoch. */
|
|
158
|
+
rotate(baseChatId: string): Promise<number>;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The identity gate's verdict for one inbound platform user. `principal` is the
|
|
162
|
+
* principal the turn runs as (required when `allowed`); `linkUrl` is the signed
|
|
163
|
+
* account-link URL to send on refusal; `silent` suppresses the refusal notice
|
|
164
|
+
* (a disabled binding shouldn't reply at all).
|
|
165
|
+
*/
|
|
166
|
+
export interface AuthResult<P> {
|
|
167
|
+
allowed: boolean;
|
|
168
|
+
principal?: P;
|
|
169
|
+
linkUrl?: string;
|
|
170
|
+
/** Override the refusal message. */
|
|
171
|
+
message?: string;
|
|
172
|
+
silent?: boolean;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The subset of the Latch runtime a channel drives. Structural so the package
|
|
176
|
+
* stays decoupled from the concrete app (and could even target a remote Latch).
|
|
177
|
+
*/
|
|
178
|
+
export interface ChannelRunOps<P> {
|
|
179
|
+
handleChat(input: {
|
|
180
|
+
agent: string;
|
|
181
|
+
chatId?: string;
|
|
182
|
+
principal: P;
|
|
183
|
+
message: AppMessage;
|
|
184
|
+
}): Promise<Response>;
|
|
185
|
+
applyApproval(input: {
|
|
186
|
+
chatId: string;
|
|
187
|
+
principal: P;
|
|
188
|
+
decisions: {
|
|
189
|
+
approvalId: string;
|
|
190
|
+
approved: boolean;
|
|
191
|
+
}[];
|
|
192
|
+
}): Promise<Response>;
|
|
193
|
+
/**
|
|
194
|
+
* Resume a turn paused on a client-handled tool (`askUser`). `output` is the
|
|
195
|
+
* answer map — one call closes the whole tool call. Optional so existing hosts
|
|
196
|
+
* keep type-checking; a channel without it can't render questionnaires.
|
|
197
|
+
*/
|
|
198
|
+
applyToolResult?(input: {
|
|
199
|
+
chatId: string;
|
|
200
|
+
principal: P;
|
|
201
|
+
toolCallId: string;
|
|
202
|
+
output: unknown;
|
|
203
|
+
}): Promise<Response>;
|
|
204
|
+
/**
|
|
205
|
+
* Summarize the conversation and continue from the summary (`/compact`). Null
|
|
206
|
+
* when there's too little to compact. Optional so existing hosts keep
|
|
207
|
+
* type-checking; without it the command reports that it isn't available.
|
|
208
|
+
*/
|
|
209
|
+
compactChat?(input: {
|
|
210
|
+
chatId: string;
|
|
211
|
+
principal: P;
|
|
212
|
+
focus?: string;
|
|
213
|
+
}): Promise<{
|
|
214
|
+
summary: string;
|
|
215
|
+
compacted: number;
|
|
216
|
+
} | null>;
|
|
217
|
+
loadHistory(input: {
|
|
218
|
+
chatId: string;
|
|
219
|
+
principal: P;
|
|
220
|
+
}): Promise<AppMessage[]>;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Host-provided speech-to-text for inbound voice/audio messages. Channels call
|
|
224
|
+
* it with the downloaded audio and put the returned transcript in the user
|
|
225
|
+
* message as text (the audio itself is never sent to the model — chat models
|
|
226
|
+
* generally don't accept audio input). Throws on failure.
|
|
227
|
+
*/
|
|
228
|
+
export type TranscribeAudio = (audio: {
|
|
229
|
+
bytes: Uint8Array;
|
|
230
|
+
mediaType: string;
|
|
231
|
+
filename: string;
|
|
232
|
+
}) => Promise<string>;
|
|
233
|
+
/** A tool-name → approval policy view, for rendering an agent's capabilities. */
|
|
234
|
+
export interface ToolCapability {
|
|
235
|
+
name: string;
|
|
236
|
+
description?: string;
|
|
237
|
+
/** Whether calling this tool pauses for user approval. */
|
|
238
|
+
requiresApproval: boolean;
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic vocabulary for bringing a Latch run to a chat surface.
|
|
3
|
+
*
|
|
4
|
+
* A channel adapter (Slack, Teams, …) only ever sees `RenderEvent`s — the
|
|
5
|
+
* normalized, surface-independent shape of what an agent is doing — and maps
|
|
6
|
+
* them to its platform's primitives (a streamed message, a status line, an
|
|
7
|
+
* approval card). The mapping from a Latch run's raw UIMessage part-stream to
|
|
8
|
+
* these events lives once in `render.ts`, so a new platform is "translate
|
|
9
|
+
* RenderEvents", never "re-parse the AI stream".
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal WhatsApp Business Cloud API client (Meta Graph API) — just what a
|
|
3
|
+
* channel needs: send text / interactive-button messages and download inbound
|
|
4
|
+
* media. JSON POSTs to `graph.facebook.com/<version>/<phoneNumberId>/messages`
|
|
5
|
+
* with a bearer access token. No SDK.
|
|
6
|
+
*/
|
|
7
|
+
export type LazyString = string | (() => string | Promise<string>);
|
|
8
|
+
export declare class WhatsappApiError extends Error {
|
|
9
|
+
readonly status: number;
|
|
10
|
+
constructor(op: string, status: number, detail?: string);
|
|
11
|
+
}
|
|
12
|
+
export interface WhatsappApiOptions {
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
apiVersion?: string;
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
/** An interactive reply button (Meta caps: 3 buttons, 20-char titles). */
|
|
19
|
+
export interface WhatsappReplyButton {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
}
|
|
23
|
+
/** Meta's caps on interactive messages — they decide which affordance fits. */
|
|
24
|
+
export declare const MAX_REPLY_BUTTONS = 3;
|
|
25
|
+
export declare const MAX_LIST_ROWS = 10;
|
|
26
|
+
export declare function whatsappApi(creds: {
|
|
27
|
+
accessToken: LazyString;
|
|
28
|
+
phoneNumberId: LazyString;
|
|
29
|
+
}, opts?: WhatsappApiOptions): {
|
|
30
|
+
/** Send a plain text message (link previews off). */
|
|
31
|
+
sendText(to: string, body: string): Promise<void>;
|
|
32
|
+
/** Send a body with up to 3 quick-reply buttons (approval prompts). */
|
|
33
|
+
sendReplyButtons(to: string, body: string, buttons: WhatsappReplyButton[]): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Send a body with a tappable list — up to 10 rows, so it covers questions
|
|
36
|
+
* that have more options than the 3-button limit allows. Row titles cap at
|
|
37
|
+
* 24 chars; the option's description rides along in `description`.
|
|
38
|
+
*/
|
|
39
|
+
sendList(to: string, body: string, buttonLabel: string, rows: {
|
|
40
|
+
id: string;
|
|
41
|
+
title: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
}[]): Promise<void>;
|
|
44
|
+
/** Resolve an inbound media id to its (short-lived) download URL + metadata. */
|
|
45
|
+
getMediaInfo(mediaId: string): Promise<{
|
|
46
|
+
url?: string;
|
|
47
|
+
mimeType?: string;
|
|
48
|
+
fileSize?: number;
|
|
49
|
+
}>;
|
|
50
|
+
/** Download media from the URL `getMediaInfo` returned (token-authenticated). */
|
|
51
|
+
downloadMedia(url: string): Promise<Uint8Array>;
|
|
52
|
+
/** Validate credentials: fetch the phone number object (admin "save" check). */
|
|
53
|
+
checkCredentials(): Promise<{
|
|
54
|
+
displayPhoneNumber?: string;
|
|
55
|
+
verifiedName?: string;
|
|
56
|
+
}>;
|
|
57
|
+
};
|
|
58
|
+
export type WhatsappClient = ReturnType<typeof whatsappApi>;
|
|
59
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/whatsapp/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAMnE,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAKxD;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,+EAA+E;AAC/E,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC,wBAAgB,WAAW,CACzB,KAAK,EAAE;IAAE,WAAW,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,UAAU,CAAA;CAAE,EAC7D,IAAI,GAAE,kBAAuB;IAwC3B,qDAAqD;iBAClC,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQvD,uEAAuE;yBAEjE,MAAM,QACJ,MAAM,WACH,mBAAmB,EAAE,GAC7B,OAAO,CAAC,IAAI,CAAC;IAiBhB;;;;OAIG;iBAEG,MAAM,QACJ,MAAM,eACC,MAAM,QACb;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,GAC1D,OAAO,CAAC,IAAI,CAAC;IAwBhB,gFAAgF;0BAErE,MAAM,GACd,OAAO,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAUlE,iFAAiF;uBACxD,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAerD,gFAAgF;wBACtD,OAAO,CAAC;QAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;EAW5F;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal WhatsApp Business Cloud API client (Meta Graph API) — just what a
|
|
3
|
+
* channel needs: send text / interactive-button messages and download inbound
|
|
4
|
+
* media. JSON POSTs to `graph.facebook.com/<version>/<phoneNumberId>/messages`
|
|
5
|
+
* with a bearer access token. No SDK.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_GRAPH_BASE_URL = "https://graph.facebook.com";
|
|
8
|
+
const DEFAULT_API_VERSION = "v25.0";
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
10
|
+
export class WhatsappApiError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(op, status, detail) {
|
|
13
|
+
super(`whatsapp:${op} failed: ${detail ?? `HTTP ${status}`}`);
|
|
14
|
+
this.name = "WhatsappApiError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function resolve(value) {
|
|
19
|
+
return typeof value === "function" ? value() : value;
|
|
20
|
+
}
|
|
21
|
+
/** Meta's caps on interactive messages — they decide which affordance fits. */
|
|
22
|
+
export const MAX_REPLY_BUTTONS = 3;
|
|
23
|
+
export const MAX_LIST_ROWS = 10;
|
|
24
|
+
export function whatsappApi(creds, opts = {}) {
|
|
25
|
+
const base = opts.baseUrl ?? DEFAULT_GRAPH_BASE_URL;
|
|
26
|
+
const version = opts.apiVersion ?? DEFAULT_API_VERSION;
|
|
27
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
28
|
+
async function graphFetch(op, path, init) {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetchImpl(`${base}/${version}/${path}`, {
|
|
33
|
+
...init,
|
|
34
|
+
headers: {
|
|
35
|
+
authorization: `Bearer ${await resolve(creds.accessToken)}`,
|
|
36
|
+
...(init.headers ?? {}),
|
|
37
|
+
},
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const detail = await res
|
|
42
|
+
.json()
|
|
43
|
+
.then((j) => j?.error?.message)
|
|
44
|
+
.catch(() => undefined);
|
|
45
|
+
throw new WhatsappApiError(op, res.status, detail);
|
|
46
|
+
}
|
|
47
|
+
return res;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function sendPayload(op, payload) {
|
|
54
|
+
await graphFetch(op, `${await resolve(creds.phoneNumberId)}/messages`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "content-type": "application/json" },
|
|
57
|
+
body: JSON.stringify({ messaging_product: "whatsapp", ...payload }),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
/** Send a plain text message (link previews off). */
|
|
62
|
+
async sendText(to, body) {
|
|
63
|
+
await sendPayload("sendText", {
|
|
64
|
+
to,
|
|
65
|
+
type: "text",
|
|
66
|
+
text: { body, preview_url: false },
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
/** Send a body with up to 3 quick-reply buttons (approval prompts). */
|
|
70
|
+
async sendReplyButtons(to, body, buttons) {
|
|
71
|
+
await sendPayload("sendReplyButtons", {
|
|
72
|
+
to,
|
|
73
|
+
type: "interactive",
|
|
74
|
+
interactive: {
|
|
75
|
+
type: "button",
|
|
76
|
+
body: { text: body.slice(0, 1024) },
|
|
77
|
+
action: {
|
|
78
|
+
buttons: buttons.slice(0, 3).map((b) => ({
|
|
79
|
+
type: "reply",
|
|
80
|
+
reply: { id: b.id, title: b.title.slice(0, 20) },
|
|
81
|
+
})),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Send a body with a tappable list — up to 10 rows, so it covers questions
|
|
88
|
+
* that have more options than the 3-button limit allows. Row titles cap at
|
|
89
|
+
* 24 chars; the option's description rides along in `description`.
|
|
90
|
+
*/
|
|
91
|
+
async sendList(to, body, buttonLabel, rows) {
|
|
92
|
+
await sendPayload("sendList", {
|
|
93
|
+
to,
|
|
94
|
+
type: "interactive",
|
|
95
|
+
interactive: {
|
|
96
|
+
type: "list",
|
|
97
|
+
body: { text: body.slice(0, 1024) },
|
|
98
|
+
action: {
|
|
99
|
+
button: buttonLabel.slice(0, 20),
|
|
100
|
+
sections: [
|
|
101
|
+
{
|
|
102
|
+
title: "Options",
|
|
103
|
+
rows: rows.slice(0, MAX_LIST_ROWS).map((r) => ({
|
|
104
|
+
id: r.id,
|
|
105
|
+
title: r.title.slice(0, 24),
|
|
106
|
+
...(r.description ? { description: r.description.slice(0, 72) } : {}),
|
|
107
|
+
})),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
/** Resolve an inbound media id to its (short-lived) download URL + metadata. */
|
|
115
|
+
async getMediaInfo(mediaId) {
|
|
116
|
+
const res = await graphFetch("getMediaInfo", mediaId, { method: "GET" });
|
|
117
|
+
const j = (await res.json());
|
|
118
|
+
return {
|
|
119
|
+
...(j.url ? { url: j.url } : {}),
|
|
120
|
+
...(j.mime_type ? { mimeType: j.mime_type } : {}),
|
|
121
|
+
...(typeof j.file_size === "number" ? { fileSize: j.file_size } : {}),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
/** Download media from the URL `getMediaInfo` returned (token-authenticated). */
|
|
125
|
+
async downloadMedia(url) {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetchImpl(url, {
|
|
130
|
+
headers: { authorization: `Bearer ${await resolve(creds.accessToken)}` },
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok)
|
|
134
|
+
throw new WhatsappApiError("downloadMedia", res.status);
|
|
135
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
/** Validate credentials: fetch the phone number object (admin "save" check). */
|
|
142
|
+
async checkCredentials() {
|
|
143
|
+
const res = await graphFetch("checkCredentials", await resolve(creds.phoneNumberId), {
|
|
144
|
+
method: "GET",
|
|
145
|
+
});
|
|
146
|
+
const j = (await res.json());
|
|
147
|
+
return {
|
|
148
|
+
...(j.display_phone_number ? { displayPhoneNumber: j.display_phone_number } : {}),
|
|
149
|
+
...(j.verified_name ? { verifiedName: j.verified_name } : {}),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/whatsapp/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @intentface/latch-channels/whatsapp
|
|
3
|
+
*
|
|
4
|
+
* Bring a Latch agent to WhatsApp via the Meta Business Cloud API: per-bot
|
|
5
|
+
* webhook handling (hub.challenge handshake + X-Hub-Signature-256), one reply
|
|
6
|
+
* per turn (split past 4096 chars — WhatsApp cannot edit messages), reply-
|
|
7
|
+
* button approvals, and media → multimodal file parts. A Surface over the
|
|
8
|
+
* turn kernel — identity, credentials and dedupe are injected by the host.
|
|
9
|
+
*/
|
|
10
|
+
export { whatsappApi, WhatsappApiError, type LazyString, type WhatsappApiOptions, type WhatsappClient, type WhatsappReplyButton, } from "./api.js";
|
|
11
|
+
export { verifyWhatsappSignature, whatsappVerifyChallenge } from "./verify.js";
|
|
12
|
+
export { whatsappChannel, whatsappSurface, whatsappChatId, parseWhatsappChatId, encodeWhatsappApprovalId, decodeWhatsappApprovalId, splitWhatsappText, type WhatsappChannelConfig, type WhatsappUser, type WaMediaRef, } from "./whatsappChannel.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/whatsapp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EACL,eAAe,EACf,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,UAAU,GAChB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @intentface/latch-channels/whatsapp
|
|
3
|
+
*
|
|
4
|
+
* Bring a Latch agent to WhatsApp via the Meta Business Cloud API: per-bot
|
|
5
|
+
* webhook handling (hub.challenge handshake + X-Hub-Signature-256), one reply
|
|
6
|
+
* per turn (split past 4096 chars — WhatsApp cannot edit messages), reply-
|
|
7
|
+
* button approvals, and media → multimodal file parts. A Surface over the
|
|
8
|
+
* turn kernel — identity, credentials and dedupe are injected by the host.
|
|
9
|
+
*/
|
|
10
|
+
export { whatsappApi, WhatsappApiError, } from "./api.js";
|
|
11
|
+
export { verifyWhatsappSignature, whatsappVerifyChallenge } from "./verify.js";
|
|
12
|
+
export { whatsappChannel, whatsappSurface, whatsappChatId, parseWhatsappChatId, encodeWhatsappApprovalId, decodeWhatsappApprovalId, splitWhatsappText, } from "./whatsappChannel.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/whatsapp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,WAAW,EACX,gBAAgB,GAKjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EACL,eAAe,EACf,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,iBAAiB,GAIlB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta webhook verification, both halves:
|
|
3
|
+
* - GET handshake: Meta calls with `hub.mode=subscribe&hub.verify_token=…&
|
|
4
|
+
* hub.challenge=…` when the callback URL is saved; echo the challenge iff
|
|
5
|
+
* the verify token matches.
|
|
6
|
+
* - POST signature: `X-Hub-Signature-256: sha256=<hex HMAC of the raw body
|
|
7
|
+
* keyed on the app secret>`, constant-time compared.
|
|
8
|
+
*/
|
|
9
|
+
/** Returns the challenge to echo (HTTP 200 text/plain), or null → 403. */
|
|
10
|
+
export declare function whatsappVerifyChallenge(url: URL, verifyToken: string): string | null;
|
|
11
|
+
/** Verify a webhook POST's `X-Hub-Signature-256` header over the raw body. */
|
|
12
|
+
export declare function verifyWhatsappSignature(opts: {
|
|
13
|
+
appSecret: string;
|
|
14
|
+
rawBody: string;
|
|
15
|
+
signature: string | null;
|
|
16
|
+
}): boolean;
|
|
17
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/whatsapp/verify.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASpF;AAED,8EAA8E;AAC9E,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GAAG,OAAO,CAMV"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Meta webhook verification, both halves:
|
|
4
|
+
* - GET handshake: Meta calls with `hub.mode=subscribe&hub.verify_token=…&
|
|
5
|
+
* hub.challenge=…` when the callback URL is saved; echo the challenge iff
|
|
6
|
+
* the verify token matches.
|
|
7
|
+
* - POST signature: `X-Hub-Signature-256: sha256=<hex HMAC of the raw body
|
|
8
|
+
* keyed on the app secret>`, constant-time compared.
|
|
9
|
+
*/
|
|
10
|
+
/** Returns the challenge to echo (HTTP 200 text/plain), or null → 403. */
|
|
11
|
+
export function whatsappVerifyChallenge(url, verifyToken) {
|
|
12
|
+
const mode = url.searchParams.get("hub.mode");
|
|
13
|
+
const token = url.searchParams.get("hub.verify_token");
|
|
14
|
+
const challenge = url.searchParams.get("hub.challenge");
|
|
15
|
+
if (mode !== "subscribe" || !token || !challenge || !verifyToken)
|
|
16
|
+
return null;
|
|
17
|
+
const a = Buffer.from(token);
|
|
18
|
+
const b = Buffer.from(verifyToken);
|
|
19
|
+
if (a.length !== b.length || !timingSafeEqual(a, b))
|
|
20
|
+
return null;
|
|
21
|
+
return challenge;
|
|
22
|
+
}
|
|
23
|
+
/** Verify a webhook POST's `X-Hub-Signature-256` header over the raw body. */
|
|
24
|
+
export function verifyWhatsappSignature(opts) {
|
|
25
|
+
if (!opts.signature || !opts.appSecret)
|
|
26
|
+
return false;
|
|
27
|
+
const expected = `sha256=${createHmac("sha256", opts.appSecret).update(opts.rawBody).digest("hex")}`;
|
|
28
|
+
const a = Buffer.from(expected);
|
|
29
|
+
const b = Buffer.from(opts.signature);
|
|
30
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/whatsapp/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,MAAM,UAAU,uBAAuB,CAAC,GAAQ,EAAE,WAAmB;IACnE,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACxD,IAAI,IAAI,KAAK,WAAW,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9E,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACnC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACjE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,uBAAuB,CAAC,IAIvC;IACC,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IACrD,MAAM,QAAQ,GAAG,UAAU,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IACrG,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC"}
|