@diegoaltoworks/chatter 0.27.0 → 0.29.0

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 (42) hide show
  1. package/dist/bin/wa-pair.js +320 -0
  2. package/dist/channels/index.d.ts +47 -0
  3. package/dist/channels/index.d.ts.map +1 -0
  4. package/dist/channels/index.js +137 -0
  5. package/dist/channels/index.mjs +105 -0
  6. package/dist/channels/senders.d.ts +30 -0
  7. package/dist/channels/senders.d.ts.map +1 -0
  8. package/dist/channels/senders.test.d.ts +2 -0
  9. package/dist/channels/senders.test.d.ts.map +1 -0
  10. package/dist/channels/whatsapp/authState.d.ts +39 -0
  11. package/dist/channels/whatsapp/authState.d.ts.map +1 -0
  12. package/dist/channels/whatsapp/authState.test.d.ts +2 -0
  13. package/dist/channels/whatsapp/authState.test.d.ts.map +1 -0
  14. package/dist/channels/whatsapp/baileys.d.ts +21 -0
  15. package/dist/channels/whatsapp/baileys.d.ts.map +1 -0
  16. package/dist/channels/whatsapp/baileys.test.d.ts +2 -0
  17. package/dist/channels/whatsapp/baileys.test.d.ts.map +1 -0
  18. package/dist/channels/whatsapp/channel.d.ts +95 -0
  19. package/dist/channels/whatsapp/channel.d.ts.map +1 -0
  20. package/dist/channels/whatsapp/channel.test.d.ts +2 -0
  21. package/dist/channels/whatsapp/channel.test.d.ts.map +1 -0
  22. package/dist/channels/whatsapp/crypto.d.ts +10 -0
  23. package/dist/channels/whatsapp/crypto.d.ts.map +1 -0
  24. package/dist/channels/whatsapp/crypto.test.d.ts +2 -0
  25. package/dist/channels/whatsapp/crypto.test.d.ts.map +1 -0
  26. package/dist/channels/whatsapp/index.d.ts +33 -0
  27. package/dist/channels/whatsapp/index.d.ts.map +1 -0
  28. package/dist/channels/whatsapp/index.js +478 -0
  29. package/dist/channels/whatsapp/index.mjs +428 -0
  30. package/dist/channels/whatsapp/lease.d.ts +42 -0
  31. package/dist/channels/whatsapp/lease.d.ts.map +1 -0
  32. package/dist/channels/whatsapp/lease.test.d.ts +2 -0
  33. package/dist/channels/whatsapp/lease.test.d.ts.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +83 -2
  37. package/dist/index.mjs +83 -2
  38. package/dist/server.d.ts +21 -2
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/types.d.ts +17 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +29 -4
@@ -0,0 +1,428 @@
1
+ // src/channels/whatsapp/crypto.ts
2
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
3
+ var IV_LENGTH = 12;
4
+ var TAG_LENGTH = 16;
5
+ function deriveKey(secret) {
6
+ return createHash("sha256").update(secret, "utf-8").digest();
7
+ }
8
+ function encrypt(plaintext, secret) {
9
+ const iv = randomBytes(IV_LENGTH);
10
+ const cipher = createCipheriv("aes-256-gcm", deriveKey(secret), iv);
11
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
12
+ return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]).toString("base64");
13
+ }
14
+ function decrypt(payload, secret) {
15
+ const raw = Buffer.from(payload, "base64");
16
+ const iv = raw.subarray(0, IV_LENGTH);
17
+ const tag = raw.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
18
+ const ciphertext = raw.subarray(IV_LENGTH + TAG_LENGTH);
19
+ const decipher = createDecipheriv("aes-256-gcm", deriveKey(secret), iv);
20
+ decipher.setAuthTag(tag);
21
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
22
+ }
23
+
24
+ // src/channels/whatsapp/authState.ts
25
+ var schemaReady = /* @__PURE__ */ new WeakMap();
26
+ function ensureAuthSchema(client) {
27
+ let ready = schemaReady.get(client);
28
+ if (!ready) {
29
+ ready = client.execute("CREATE TABLE IF NOT EXISTS wa_auth (id TEXT PRIMARY KEY, value TEXT NOT NULL)").catch((error) => {
30
+ schemaReady.delete(client);
31
+ throw error;
32
+ });
33
+ schemaReady.set(client, ready);
34
+ }
35
+ return ready;
36
+ }
37
+ async function useTursoAuthState(db, secret, sessionId, runtime) {
38
+ await ensureAuthSchema(db);
39
+ const rowId = (id) => sessionId === "default" ? id : `s:${sessionId}/${id}`;
40
+ async function read(id) {
41
+ const result = await db.execute({
42
+ sql: "SELECT value FROM wa_auth WHERE id = ?",
43
+ args: [rowId(id)]
44
+ });
45
+ const row = result.rows[0];
46
+ if (!row) return null;
47
+ return JSON.parse(decrypt(row.value, secret), runtime.bufferJSON.reviver);
48
+ }
49
+ async function write(id, value) {
50
+ const payload = encrypt(JSON.stringify(value, runtime.bufferJSON.replacer), secret);
51
+ await db.execute({
52
+ sql: "INSERT OR REPLACE INTO wa_auth (id, value) VALUES (?, ?)",
53
+ args: [rowId(id), payload]
54
+ });
55
+ }
56
+ async function remove(id) {
57
+ await db.execute({ sql: "DELETE FROM wa_auth WHERE id = ?", args: [rowId(id)] });
58
+ }
59
+ const creds = await read("creds") ?? runtime.initAuthCreds();
60
+ return {
61
+ state: {
62
+ creds,
63
+ keys: {
64
+ get: async (type, ids) => {
65
+ const data = {};
66
+ await Promise.all(
67
+ ids.map(async (id) => {
68
+ let value = await read(`${type}-${id}`);
69
+ if (type === "app-state-sync-key" && value) {
70
+ value = runtime.appStateSyncKeyFromObject(
71
+ value
72
+ );
73
+ }
74
+ if (value) data[id] = value;
75
+ })
76
+ );
77
+ return data;
78
+ },
79
+ set: async (data) => {
80
+ const jobs = [];
81
+ for (const category of Object.keys(data)) {
82
+ const entries = data[category];
83
+ if (!entries) continue;
84
+ for (const id of Object.keys(entries)) {
85
+ const value = entries[id];
86
+ jobs.push(value ? write(`${category}-${id}`, value) : remove(`${category}-${id}`));
87
+ }
88
+ }
89
+ await Promise.all(jobs);
90
+ }
91
+ }
92
+ },
93
+ saveCreds: () => write("creds", creds),
94
+ clear: async () => {
95
+ if (sessionId === "default") {
96
+ await db.execute("DELETE FROM wa_auth WHERE id NOT LIKE 's:%'");
97
+ } else {
98
+ await db.execute({
99
+ sql: "DELETE FROM wa_auth WHERE id LIKE ?",
100
+ args: [`s:${sessionId}/%`]
101
+ });
102
+ }
103
+ }
104
+ };
105
+ }
106
+
107
+ // src/channels/whatsapp/baileys.ts
108
+ function wrapMissingBaileysError(cause) {
109
+ return new Error(
110
+ "The WhatsApp channel ('@diegoaltoworks/chatter/whatsapp') requires the optional peer dependency '@whiskeysockets/baileys', which is not installed. Install it with `bun add @whiskeysockets/baileys` (or npm/pnpm/yarn) to use this channel.",
111
+ { cause }
112
+ );
113
+ }
114
+ var cached;
115
+ function loadBaileys() {
116
+ if (!cached) {
117
+ cached = import("@whiskeysockets/baileys").catch((error) => {
118
+ cached = void 0;
119
+ throw wrapMissingBaileysError(error);
120
+ });
121
+ }
122
+ return cached;
123
+ }
124
+ function silentLogger() {
125
+ const noop = () => void 0;
126
+ const logger = {
127
+ level: "silent",
128
+ trace: noop,
129
+ debug: noop,
130
+ info: noop,
131
+ warn: noop,
132
+ error: noop,
133
+ fatal: noop
134
+ };
135
+ logger.child = () => logger;
136
+ return logger;
137
+ }
138
+
139
+ // src/channels/whatsapp/lease.ts
140
+ var LEASE_STALE_MS = 9e4;
141
+ var LEASE_HEARTBEAT_MS = 3e4;
142
+ var LEASE_WAIT_MS = 3e4;
143
+ function canAcquireLease(existing, instanceId, now, staleMs = LEASE_STALE_MS) {
144
+ if (!existing) return true;
145
+ if (existing.instanceId === instanceId) return true;
146
+ return now - existing.heartbeatAt >= staleMs;
147
+ }
148
+ var schemaReady2 = /* @__PURE__ */ new WeakMap();
149
+ function ensureWaLeaseSchema(client) {
150
+ let ready = schemaReady2.get(client);
151
+ if (!ready) {
152
+ ready = client.execute(
153
+ `CREATE TABLE IF NOT EXISTS wa_lease (
154
+ session_id TEXT PRIMARY KEY,
155
+ instance_id TEXT NOT NULL,
156
+ heartbeat_at INTEGER NOT NULL
157
+ )`
158
+ ).catch((error) => {
159
+ schemaReady2.delete(client);
160
+ throw error;
161
+ });
162
+ schemaReady2.set(client, ready);
163
+ }
164
+ return ready;
165
+ }
166
+ function createTursoWaLeaseStore(client) {
167
+ return {
168
+ async tryAcquire(sessionId, instanceId, now, staleMs) {
169
+ await ensureWaLeaseSchema(client);
170
+ const result = await client.execute({
171
+ sql: `INSERT INTO wa_lease (session_id, instance_id, heartbeat_at) VALUES (?, ?, ?)
172
+ ON CONFLICT(session_id) DO UPDATE SET
173
+ instance_id = excluded.instance_id,
174
+ heartbeat_at = excluded.heartbeat_at
175
+ WHERE wa_lease.instance_id = excluded.instance_id
176
+ OR wa_lease.heartbeat_at <= excluded.heartbeat_at - ?
177
+ RETURNING instance_id`,
178
+ args: [sessionId, instanceId, now, staleMs]
179
+ });
180
+ const row = result.rows[0];
181
+ return Boolean(row) && row.instance_id === instanceId;
182
+ },
183
+ async release(sessionId, instanceId) {
184
+ await ensureWaLeaseSchema(client);
185
+ await client.execute({
186
+ sql: "DELETE FROM wa_lease WHERE session_id = ? AND instance_id = ?",
187
+ args: [sessionId, instanceId]
188
+ });
189
+ }
190
+ };
191
+ }
192
+
193
+ // src/channels/whatsapp/channel.ts
194
+ var RECONNECT_DELAY_MS = 5e3;
195
+ var RECONNECT_MAX_DELAY_MS = 10 * 60 * 1e3;
196
+ function reconnectDelayMs(failures) {
197
+ return Math.min(RECONNECT_DELAY_MS * 2 ** Math.min(failures, 20), RECONNECT_MAX_DELAY_MS);
198
+ }
199
+ async function shutdownWaSessions(sessions) {
200
+ await Promise.all(
201
+ [...sessions.values()].map(async (handle) => {
202
+ handle.stopHeartbeat();
203
+ handle.sock.end(void 0);
204
+ await handle.release().catch(() => void 0);
205
+ })
206
+ );
207
+ }
208
+ async function loseSession(sessionId, sessions) {
209
+ const handle = sessions.get(sessionId);
210
+ if (!handle) return;
211
+ sessions.delete(sessionId);
212
+ await shutdownWaSessions(/* @__PURE__ */ new Map([[sessionId, handle]]));
213
+ }
214
+ async function acquireSessionLease(sessionId, deps) {
215
+ const { leaseStore, instanceId, sessions, isStopped, connect } = deps;
216
+ const now = deps.now ?? Date.now;
217
+ const staleMs = deps.staleMs ?? LEASE_STALE_MS;
218
+ const heartbeatMs = deps.heartbeatMs ?? LEASE_HEARTBEAT_MS;
219
+ const waitMs = deps.waitMs ?? LEASE_WAIT_MS;
220
+ const schedule = deps.schedule ?? ((fn, ms) => void setTimeout(fn, ms));
221
+ if (isStopped()) return;
222
+ const retry = () => {
223
+ schedule(
224
+ () => void acquireSessionLease(sessionId, deps).catch(
225
+ (error) => console.error(`WhatsApp[${sessionId}]: lease re-check failed:`, error)
226
+ ),
227
+ waitMs
228
+ );
229
+ };
230
+ let acquired;
231
+ try {
232
+ acquired = await leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs);
233
+ } catch (error) {
234
+ console.warn(`WhatsApp[${sessionId}]: lease acquire failed, retrying:`, error);
235
+ retry();
236
+ return;
237
+ }
238
+ if (!acquired) {
239
+ console.warn(`WhatsApp[${sessionId}]: lease held by another instance, re-checking later`);
240
+ retry();
241
+ return;
242
+ }
243
+ const heartbeat = setInterval(() => {
244
+ void leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs).then((stillHeld) => {
245
+ if (stillHeld || isStopped()) return;
246
+ console.error(`WhatsApp[${sessionId}]: lost the lease to another instance \u2014 disconnecting`);
247
+ void loseSession(sessionId, sessions).then(retry);
248
+ }).catch((error) => console.warn(`WhatsApp[${sessionId}]: lease heartbeat failed:`, error));
249
+ }, heartbeatMs);
250
+ if (typeof heartbeat === "object" && "unref" in heartbeat) heartbeat.unref();
251
+ sessions.set(sessionId, {
252
+ sock: { end: () => void 0 },
253
+ stopHeartbeat: () => clearInterval(heartbeat),
254
+ release: () => leaseStore.release(sessionId, instanceId)
255
+ });
256
+ try {
257
+ await connect(sessionId);
258
+ } catch (error) {
259
+ console.error(`WhatsApp[${sessionId}]: connect failed right after acquiring the lease:`, error);
260
+ await loseSession(sessionId, sessions);
261
+ retry();
262
+ }
263
+ }
264
+ function senderNameFor(channelName, sessionId) {
265
+ return sessionId === "default" ? channelName : `${channelName}:${sessionId}`;
266
+ }
267
+ function createWhatsAppChannel(config) {
268
+ const channelName = "whatsapp";
269
+ const sessionIds = config.sessionIds && config.sessionIds.length > 0 ? config.sessionIds : ["default"];
270
+ const now = config.now ?? Date.now;
271
+ const schedule = config.schedule ?? ((fn, ms) => void setTimeout(fn, ms));
272
+ const staleMs = config.staleMs ?? LEASE_STALE_MS;
273
+ const heartbeatMs = config.heartbeatMs ?? LEASE_HEARTBEAT_MS;
274
+ const waitMs = config.waitMs ?? LEASE_WAIT_MS;
275
+ const loadBaileys2 = config.loadBaileys ?? loadBaileys;
276
+ let stopped = false;
277
+ const instanceId = crypto.randomUUID();
278
+ const sessions = /* @__PURE__ */ new Map();
279
+ const failureCounts = /* @__PURE__ */ new Map();
280
+ const registeredSenderNames = /* @__PURE__ */ new Map();
281
+ let senders;
282
+ return {
283
+ name: channelName,
284
+ async start(deps) {
285
+ const db = deps.db;
286
+ const leaseStore = createTursoWaLeaseStore(db);
287
+ senders = deps.senders;
288
+ await loadBaileys2();
289
+ async function connect(sessionId) {
290
+ if (stopped || !sessions.has(sessionId)) return;
291
+ const baileys = await loadBaileys2();
292
+ const runtime = {
293
+ bufferJSON: baileys.BufferJSON,
294
+ initAuthCreds: baileys.initAuthCreds,
295
+ appStateSyncKeyFromObject: (value) => baileys.proto.Message.AppStateSyncKeyData.fromObject(value)
296
+ };
297
+ const { state, saveCreds } = await useTursoAuthState(
298
+ db,
299
+ config.sessionSecret,
300
+ sessionId,
301
+ runtime
302
+ );
303
+ if (!state.creds.registered) {
304
+ console.warn(
305
+ `WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
306
+ );
307
+ schedule(() => void connect(sessionId).catch(() => void 0), 6e4);
308
+ return;
309
+ }
310
+ const { version } = await baileys.fetchLatestBaileysVersion();
311
+ const logger = silentLogger();
312
+ const sock = baileys.makeWASocket({
313
+ version,
314
+ auth: {
315
+ creds: state.creds,
316
+ // biome-ignore lint/suspicious/noExplicitAny: silentLogger is structurally pino-compatible; Baileys' logger type is not exported
317
+ keys: baileys.makeCacheableSignalKeyStore(state.keys, logger)
318
+ },
319
+ // biome-ignore lint/suspicious/noExplicitAny: silentLogger is structurally pino-compatible; Baileys' logger type is not exported
320
+ logger,
321
+ markOnlineOnConnect: false,
322
+ syncFullHistory: false
323
+ });
324
+ const leaseHandle = sessions.get(sessionId);
325
+ if (leaseHandle) leaseHandle.sock = sock;
326
+ sock.ev.on(
327
+ "creds.update",
328
+ () => void saveCreds().catch(
329
+ (error) => console.warn(`WhatsApp[${sessionId}]: creds save failed:`, error)
330
+ )
331
+ );
332
+ sock.ev.on("connection.update", (update) => {
333
+ const { connection, lastDisconnect } = update;
334
+ if (connection === "open") {
335
+ failureCounts.set(sessionId, 0);
336
+ console.log(`WhatsApp[${sessionId}]: connected as ${sock.user?.id ?? "unknown"}`);
337
+ const senderName = senderNameFor(channelName, sessionId);
338
+ const sender = {
339
+ sendText: async (chatId, text) => {
340
+ await sock.sendMessage(chatId, { text });
341
+ }
342
+ };
343
+ deps.senders.register(senderName, sender);
344
+ registeredSenderNames.set(sessionId, senderName);
345
+ }
346
+ if (connection === "close") {
347
+ const senderName = registeredSenderNames.get(sessionId);
348
+ if (senderName) {
349
+ deps.senders.unregister(senderName);
350
+ registeredSenderNames.delete(sessionId);
351
+ }
352
+ const statusCode = lastDisconnect?.error?.output?.statusCode;
353
+ if (statusCode === baileys.DisconnectReason.loggedOut) {
354
+ console.error(
355
+ `WhatsApp[${sessionId}]: session logged out \u2014 re-pair to reconnect (see docs/channels.md).`
356
+ );
357
+ void loseSession(sessionId, sessions);
358
+ return;
359
+ }
360
+ if (!stopped && sessions.has(sessionId)) {
361
+ const delay = reconnectDelayMs(failureCounts.get(sessionId) ?? 0);
362
+ failureCounts.set(sessionId, (failureCounts.get(sessionId) ?? 0) + 1);
363
+ console.warn(
364
+ `WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
365
+ );
366
+ schedule(
367
+ () => void connect(sessionId).catch(
368
+ (error) => console.error(`WhatsApp[${sessionId}]: reconnect failed:`, error)
369
+ ),
370
+ delay
371
+ );
372
+ }
373
+ }
374
+ });
375
+ if (config.onMessage) {
376
+ sock.ev.on("messages.upsert", async ({ messages, type }) => {
377
+ if (type !== "notify") return;
378
+ for (const message of messages) {
379
+ try {
380
+ await config.onMessage?.({ sessionId, sock, message });
381
+ } catch (error) {
382
+ console.warn(`WhatsApp[${sessionId}]: onMessage handler failed:`, error);
383
+ }
384
+ }
385
+ });
386
+ }
387
+ }
388
+ for (const sessionId of sessionIds) {
389
+ void acquireSessionLease(sessionId, {
390
+ leaseStore,
391
+ instanceId,
392
+ sessions,
393
+ isStopped: () => stopped,
394
+ connect,
395
+ now,
396
+ staleMs,
397
+ heartbeatMs,
398
+ waitMs,
399
+ schedule
400
+ }).catch((error) => console.error(`WhatsApp[${sessionId}]: failed to start:`, error));
401
+ }
402
+ },
403
+ async stop() {
404
+ stopped = true;
405
+ for (const senderName of registeredSenderNames.values()) {
406
+ senders?.unregister(senderName);
407
+ }
408
+ registeredSenderNames.clear();
409
+ await shutdownWaSessions(sessions);
410
+ }
411
+ };
412
+ }
413
+ export {
414
+ LEASE_HEARTBEAT_MS,
415
+ LEASE_STALE_MS,
416
+ LEASE_WAIT_MS,
417
+ acquireSessionLease,
418
+ canAcquireLease,
419
+ createTursoWaLeaseStore,
420
+ createWhatsAppChannel,
421
+ decrypt,
422
+ encrypt,
423
+ loadBaileys,
424
+ reconnectDelayMs,
425
+ senderNameFor,
426
+ shutdownWaSessions,
427
+ useTursoAuthState
428
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Deploy-overlap protection: at most one running instance may hold a given
3
+ * WhatsApp session's Baileys connection at a time.
4
+ *
5
+ * Rolling deploys can briefly run the outgoing and incoming revision side by
6
+ * side, and Baileys/WhatsApp treats a second concurrent connection as a
7
+ * takeover — it logs the first one out. The lease lives in Turso (`wa_lease`)
8
+ * so it's visible to every instance. Acquiring/renewing is a single atomic
9
+ * upsert — see `createTursoWaLeaseStore` — so two instances racing the same
10
+ * row can never both believe they hold it.
11
+ */
12
+ import type { Client } from "@libsql/client";
13
+ /** Stale after this long without a heartbeat — a crashed holder's lease frees up. */
14
+ export declare const LEASE_STALE_MS = 90000;
15
+ /** How often a holder renews its lease while connected. */
16
+ export declare const LEASE_HEARTBEAT_MS = 30000;
17
+ /** How long a non-holder waits before re-checking whether the lease freed up. */
18
+ export declare const LEASE_WAIT_MS = 30000;
19
+ export interface WaLeaseRow {
20
+ instanceId: string;
21
+ heartbeatAt: number;
22
+ }
23
+ /**
24
+ * Pure: may `instanceId` take (or keep) the lease right now, given the
25
+ * current row (null = never held)? Mirrors the SQL WHERE clause in
26
+ * `createTursoWaLeaseStore` — keep the two in sync.
27
+ */
28
+ export declare function canAcquireLease(existing: WaLeaseRow | null, instanceId: string, now: number, staleMs?: number): boolean;
29
+ export interface WaLeaseStore {
30
+ /** Atomically acquires or renews the lease; resolves true iff `instanceId` now holds it. */
31
+ tryAcquire(sessionId: string, instanceId: string, now: number, staleMs: number): Promise<boolean>;
32
+ /** Releases the lease iff still held by `instanceId` (no-op otherwise — a late release must never undo a takeover). */
33
+ release(sessionId: string, instanceId: string): Promise<void>;
34
+ }
35
+ /**
36
+ * A Turso-backed `WaLeaseStore`. The WHERE clause on DO UPDATE encodes
37
+ * `canAcquireLease`: SQLite skips the update (and RETURNING yields no row)
38
+ * when it's false, so a losing racer sees zero rows back — no separate
39
+ * read-then-write, no race window.
40
+ */
41
+ export declare function createTursoWaLeaseStore(client: Client): WaLeaseStore;
42
+ //# sourceMappingURL=lease.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/lease.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,qFAAqF;AACrF,eAAO,MAAM,cAAc,QAAS,CAAC;AACrC,2DAA2D;AAC3D,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,iFAAiF;AACjF,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,UAAU,GAAG,IAAI,EAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,MAAuB,GAC/B,OAAO,CAIT;AAED,MAAM,WAAW,YAAY;IAC3B,4FAA4F;IAC5F,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClG,uHAAuH;IACvH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAuBD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAyBpE"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lease.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.test.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/lease.test.ts"],"names":[],"mappings":""}
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  */
13
13
  export type { ApiKeyOptions, ApiKeyPayload, VerifyResult } from "./auth";
14
14
  export { ApiKeyManager } from "./auth";
15
+ export type { Channel } from "./channels";
15
16
  export type { ChatBotConfig, ChatButtonConfig, ChatConfig, ChatMessage, ChatMode, StreamCallbacks, } from "./client";
16
17
  export { Chat, ChatBot, ChatButton } from "./client";
17
18
  export type { AnswerFn, AnswerFnInput, AnswerFnResult, AnswerOptions, AnswerUsage, } from "./core/answer";
@@ -39,6 +40,7 @@ export { demoRoutes } from "./routes/demo";
39
40
  export { openaiRoutes } from "./routes/openai";
40
41
  export { privateRoutes } from "./routes/private";
41
42
  export { publicRoutes } from "./routes/public";
43
+ export type { ChatterApp } from "./server";
42
44
  export { createServer } from "./server";
43
45
  export type { BotBranding, BotChatConfig, BotIdentity, ChatterConfig, CustomRoutes, EmbeddingChunk, KnowledgeDocument, ServerDependencies, } from "./types";
44
46
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,eAAe,GAChB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EACV,QAAQ,EACR,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,YAAY,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,aAAa,EACb,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAIvC,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,eAAe,GAChB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EACV,QAAQ,EACR,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,YAAY,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,aAAa,EACb,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -33171,6 +33171,58 @@ var import_hono5 = require("hono");
33171
33171
  var import_bun = require("hono/bun");
33172
33172
  var import_openai2 = __toESM(require("openai"));
33173
33173
 
33174
+ // src/channels/senders.ts
33175
+ function createSenderRegistry() {
33176
+ const senders = /* @__PURE__ */ new Map();
33177
+ return {
33178
+ register(name, sender) {
33179
+ if (senders.has(name)) {
33180
+ console.warn(`Channel sender "${name}" is already registered; overwriting.`);
33181
+ }
33182
+ senders.set(name, sender);
33183
+ },
33184
+ unregister(name) {
33185
+ senders.delete(name);
33186
+ },
33187
+ available(name) {
33188
+ return senders.has(name);
33189
+ },
33190
+ async sendText(name, chatId, text) {
33191
+ const sender = senders.get(name);
33192
+ if (!sender) return false;
33193
+ try {
33194
+ await sender.sendText(chatId, text);
33195
+ return true;
33196
+ } catch (error51) {
33197
+ console.warn(`Channel sender "${name}" sendText failed:`, error51);
33198
+ return false;
33199
+ }
33200
+ },
33201
+ async sendVoice(name, chatId, payload) {
33202
+ const sender = senders.get(name);
33203
+ if (!sender?.sendVoice) return false;
33204
+ try {
33205
+ await sender.sendVoice(chatId, payload);
33206
+ return true;
33207
+ } catch (error51) {
33208
+ console.warn(`Channel sender "${name}" sendVoice failed:`, error51);
33209
+ return false;
33210
+ }
33211
+ },
33212
+ async sendMedia(name, chatId, payload) {
33213
+ const sender = senders.get(name);
33214
+ if (!sender?.sendMedia) return false;
33215
+ try {
33216
+ await sender.sendMedia(chatId, payload);
33217
+ return true;
33218
+ } catch (error51) {
33219
+ console.warn(`Channel sender "${name}" sendMedia failed:`, error51);
33220
+ return false;
33221
+ }
33222
+ }
33223
+ };
33224
+ }
33225
+
33174
33226
  // src/core/widgets.ts
33175
33227
  var import_node_fs3 = require("node:fs");
33176
33228
  var import_node_path3 = require("node:path");
@@ -33296,7 +33348,8 @@ async function createServer(config2) {
33296
33348
  app.get("/demo/chatbot", (0, import_bun.serveStatic)({ path: `${publicDir}/demo-chatbot.html` }));
33297
33349
  app.get("/demo/react", (0, import_bun.serveStatic)({ path: `${publicDir}/react-demo.html` }));
33298
33350
  }
33299
- const deps = { client, store, db, config: config2, prompts, apiKeyManager };
33351
+ const senders = createSenderRegistry();
33352
+ const deps = { client, store, db, config: config2, prompts, apiKeyManager, senders };
33300
33353
  if (enablePublic) {
33301
33354
  app.route("/", publicRoutes(deps));
33302
33355
  }
@@ -33312,8 +33365,36 @@ async function createServer(config2) {
33312
33365
  if (config2.customRoutes) {
33313
33366
  await config2.customRoutes(app, deps);
33314
33367
  }
33368
+ const started = [];
33369
+ if (config2.channels && config2.channels.length > 0) {
33370
+ for (const channel of config2.channels) {
33371
+ try {
33372
+ await channel.start(deps);
33373
+ started.push(channel);
33374
+ console.log(`\u2705 Channel "${channel.name}" started`);
33375
+ } catch (error51) {
33376
+ console.error(`\u274C Channel "${channel.name}" failed to start:`, error51);
33377
+ }
33378
+ }
33379
+ }
33315
33380
  console.log(`\u2705 ${config2.bot.name} server ready`);
33316
- return app;
33381
+ return Object.assign(app, {
33382
+ // Scoped to this call's started channels, not global — two servers in
33383
+ // one process each stop only their own. A channel's `stop()` throwing
33384
+ // (sync or async) is caught per-channel so one misbehaving channel can't
33385
+ // stop the rest from cleaning up.
33386
+ stopChannels: async () => {
33387
+ await Promise.allSettled(
33388
+ started.map(async (channel) => {
33389
+ try {
33390
+ await channel.stop?.();
33391
+ } catch (error51) {
33392
+ console.error(`Channel "${channel.name}" failed to stop:`, error51);
33393
+ }
33394
+ })
33395
+ );
33396
+ }
33397
+ });
33317
33398
  }
33318
33399
  // Annotate the CommonJS export names for ESM import in node:
33319
33400
  0 && (module.exports = {