@diegoaltoworks/chatter 0.35.0 → 0.37.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.
@@ -150,6 +150,126 @@ function loadBaileys() {
150
150
  return cached;
151
151
  }
152
152
 
153
+ // src/channels/whatsapp/pairing.ts
154
+ var RESTART_REQUIRED_STATUS = 515;
155
+ var MAX_PAIRING_RECONNECTS = 12;
156
+ var PAIRING_RECONNECT_DELAY_MS = 2e3;
157
+ var PAIRING_RECONNECT_MAX_DELAY_MS = 8e3;
158
+ var RESTART_REQUIRED_DELAY_MS = 500;
159
+ function pairingReconnectDelayMs(statusCode, attempt) {
160
+ if (statusCode === RESTART_REQUIRED_STATUS) return RESTART_REQUIRED_DELAY_MS;
161
+ return Math.min(
162
+ PAIRING_RECONNECT_DELAY_MS * 2 ** Math.max(0, attempt - 1),
163
+ PAIRING_RECONNECT_MAX_DELAY_MS
164
+ );
165
+ }
166
+ function decidePairingClose(input) {
167
+ if (input.statusCode === input.loggedOutCode) return { kind: "fatal", reason: "loggedOut" };
168
+ if (input.attempts >= input.maxAttempts) return { kind: "fatal", reason: "exhausted" };
169
+ const attempt = input.attempts + 1;
170
+ return {
171
+ kind: "reconnect",
172
+ attempt,
173
+ delayMs: pairingReconnectDelayMs(input.statusCode, attempt)
174
+ };
175
+ }
176
+ function describeError(error) {
177
+ return error instanceof Error ? error.message : String(error);
178
+ }
179
+ function runPairing(deps) {
180
+ const maxAttempts = deps.maxAttempts ?? MAX_PAIRING_RECONNECTS;
181
+ const schedule = deps.schedule ?? ((fn, ms) => {
182
+ setTimeout(fn, ms);
183
+ });
184
+ return new Promise((resolve) => {
185
+ let attempts = 0;
186
+ let generation = 0;
187
+ let codeRequested = false;
188
+ let settled = false;
189
+ const settle = (result) => {
190
+ if (settled) return;
191
+ settled = true;
192
+ resolve(result);
193
+ };
194
+ const status = (message) => deps.onStatus?.(message);
195
+ const requestCode = async (sock) => {
196
+ if (settled || codeRequested || !deps.phoneNumber) return;
197
+ try {
198
+ const code = await sock.requestPairingCode(deps.phoneNumber);
199
+ codeRequested = true;
200
+ deps.onPairingCode?.(code);
201
+ } catch (error) {
202
+ settle({
203
+ ok: false,
204
+ reason: "error",
205
+ message: `Failed to request pairing code: ${describeError(error)}`
206
+ });
207
+ }
208
+ };
209
+ const handleUpdate = async (update, sock) => {
210
+ const { connection, lastDisconnect, qr } = update;
211
+ if (qr && !deps.phoneNumber && deps.onQr) {
212
+ try {
213
+ await deps.onQr(qr);
214
+ } catch (error) {
215
+ status(`QR render failed: ${describeError(error)}`);
216
+ }
217
+ }
218
+ if (connection === "open") {
219
+ settle({ ok: true, userId: sock.user?.id });
220
+ return;
221
+ }
222
+ if (connection === "close") {
223
+ const statusCode = lastDisconnect?.error?.output?.statusCode;
224
+ const action = decidePairingClose({
225
+ statusCode,
226
+ loggedOutCode: deps.loggedOutCode,
227
+ attempts,
228
+ maxAttempts
229
+ });
230
+ if (action.kind === "fatal") {
231
+ settle({
232
+ ok: false,
233
+ reason: action.reason,
234
+ message: action.reason === "loggedOut" ? "Pairing rejected (logged out). Run wa-pair again for a fresh code." : `Gave up after ${maxAttempts} reconnect attempts (last close: ${statusCode ?? "?"}). Run wa-pair again.`
235
+ });
236
+ return;
237
+ }
238
+ attempts = action.attempt;
239
+ generation += 1;
240
+ sock.end?.();
241
+ status(
242
+ statusCode === RESTART_REQUIRED_STATUS ? `Registered \u2014 WhatsApp asked for a restart (515). Reconnecting to finish the link (${action.attempt}/${maxAttempts})...` : `Connection closed (${statusCode ?? "?"}) \u2014 reconnecting with the stored session (${action.attempt}/${maxAttempts})...`
243
+ );
244
+ schedule(() => void attach(), action.delayMs);
245
+ }
246
+ };
247
+ const attach = async () => {
248
+ if (settled) return;
249
+ const mine = generation;
250
+ const attempt = attempts;
251
+ let sock;
252
+ try {
253
+ sock = await deps.connect(attempt);
254
+ } catch (error) {
255
+ settle({ ok: false, reason: "error", message: describeError(error) });
256
+ return;
257
+ }
258
+ sock.ev.on("connection.update", (update) => {
259
+ if (mine !== generation || settled) return;
260
+ void handleUpdate(update, sock);
261
+ });
262
+ if (deps.phoneNumber && !codeRequested) {
263
+ schedule(() => {
264
+ if (mine !== generation) return;
265
+ void requestCode(sock);
266
+ }, deps.requestCodeDelayMs ?? 3e3);
267
+ }
268
+ };
269
+ void attach();
270
+ });
271
+ }
272
+
153
273
  // bin/wa-pair.ts
154
274
  var args = process.argv.slice(2);
155
275
  function flagValue(name) {
@@ -199,10 +319,17 @@ var db = (0, import_client.createClient)({
199
319
  url: process.env.TURSO_URL,
200
320
  authToken: process.env.TURSO_AUTH_TOKEN || ""
201
321
  });
202
- var MAX_RECONNECTS = 12;
203
- var codeRequested = false;
204
- var reconnects = 0;
205
- var didReset = false;
322
+ async function renderQr(qr) {
323
+ const qrcodeTerminal = await import("qrcode-terminal").catch(() => void 0);
324
+ if (!qrcodeTerminal) {
325
+ console.error(
326
+ "\u274C QR mode requires the optional peer dependency 'qrcode-terminal'. Install it with `bun add qrcode-terminal`, or re-run with --code <phoneNumber> for pairing-code mode instead."
327
+ );
328
+ process.exit(1);
329
+ }
330
+ console.log("\nScan this QR code: WhatsApp -> Settings -> Linked Devices -> Link a Device.\n");
331
+ qrcodeTerminal.default.generate(qr, { small: true });
332
+ }
206
333
  async function start() {
207
334
  const baileys = await loadBaileys().catch((error) => {
208
335
  console.error(`\u274C ${error.message}`);
@@ -213,106 +340,73 @@ async function start() {
213
340
  initAuthCreds: baileys.initAuthCreds,
214
341
  appStateSyncKeyFromObject: (value) => baileys.proto.Message.AppStateSyncKeyData.fromObject(value)
215
342
  };
216
- const { state, saveCreds, clear } = await useTursoAuthState(
217
- db,
218
- sessionSecret,
219
- sessionId,
220
- runtime
221
- );
222
- if (reset && !didReset) {
223
- didReset = true;
343
+ if (reset) {
344
+ const { clear } = await useTursoAuthState(db, sessionSecret, sessionId, runtime);
224
345
  await clear();
225
346
  console.log(`Session "${sessionId}" wiped. Pairing fresh...`);
226
- return start();
227
- }
228
- if (state.creds.registered && !codeRequested) {
229
- console.log(
230
- `Already paired. Session "${sessionId}" is connected-ready. Run with --reset to re-pair.`
231
- );
232
- process.exit(0);
233
347
  }
234
348
  const { version } = await baileys.fetchLatestBaileysVersion();
235
- const noop = () => void 0;
236
- const logger = {
237
- level: "silent",
238
- trace: noop,
239
- debug: noop,
240
- info: noop,
241
- warn: noop,
242
- error: noop,
243
- fatal: noop
244
- };
245
- logger.child = () => logger;
246
- const sock = baileys.makeWASocket({
247
- version,
248
- auth: {
249
- creds: state.creds,
250
- // biome-ignore lint/suspicious/noExplicitAny: structural pino-compatible logger
251
- keys: baileys.makeCacheableSignalKeyStore(state.keys, logger)
252
- },
253
- // biome-ignore lint/suspicious/noExplicitAny: structural pino-compatible logger
254
- logger,
255
- markOnlineOnConnect: false
256
- });
257
- sock.ev.on("creds.update", saveCreds);
258
- sock.ev.on("connection.update", async (update) => {
259
- const { connection, lastDisconnect, qr } = update;
260
- if (qr && !phoneNumber) {
261
- const qrcodeTerminal = await import("qrcode-terminal").catch(() => void 0);
262
- if (!qrcodeTerminal) {
263
- console.error(
264
- "\u274C QR mode requires the optional peer dependency 'qrcode-terminal'. Install it with `bun add qrcode-terminal`, or re-run with --code <phoneNumber> for pairing-code mode instead."
265
- );
266
- process.exit(1);
267
- }
268
- console.log(
269
- "\nScan this QR code: WhatsApp -> Settings -> Linked Devices -> Link a Device.\n"
270
- );
271
- qrcodeTerminal.default.generate(qr, { small: true });
272
- }
273
- if (connection === "open") {
349
+ const connect = async (attempt) => {
350
+ const { state, saveCreds } = await useTursoAuthState(
351
+ db,
352
+ sessionSecret,
353
+ sessionId,
354
+ runtime
355
+ );
356
+ if (attempt === 0 && state.creds.registered) {
274
357
  console.log(
275
- `
276
- \u2705 Paired session "${sessionId}" as ${sock.user?.id}. Session saved (encrypted).`
358
+ `Already paired. Session "${sessionId}" is connected-ready. Run with --reset to re-pair.`
277
359
  );
278
- console.log("The running server will pick this session up on its next connect.");
279
360
  process.exit(0);
280
361
  }
281
- if (connection === "close") {
282
- const statusCode = lastDisconnect?.error?.output?.statusCode;
283
- if (codeRequested && reconnects < MAX_RECONNECTS) {
284
- reconnects++;
285
- console.log(
286
- `Connection closed (${statusCode ?? "?"}) - reconnecting (code STILL VALID, keep going on the phone) (${reconnects}/${MAX_RECONNECTS})...`
287
- );
288
- setTimeout(() => void start(), 2e3);
289
- return;
290
- }
291
- if (statusCode === baileys.DisconnectReason.loggedOut) {
292
- console.error("\u274C Pairing rejected (logged out). Run wa-pair again for a fresh code.");
293
- } else {
294
- console.error(`\u274C Connection closed (${statusCode ?? "?"}). Run wa-pair again.`);
295
- }
296
- process.exit(1);
297
- }
362
+ const noop = () => void 0;
363
+ const logger = {
364
+ level: "silent",
365
+ trace: noop,
366
+ debug: noop,
367
+ info: noop,
368
+ warn: noop,
369
+ error: noop,
370
+ fatal: noop
371
+ };
372
+ logger.child = () => logger;
373
+ const sock = baileys.makeWASocket({
374
+ version,
375
+ auth: {
376
+ creds: state.creds,
377
+ // biome-ignore lint/suspicious/noExplicitAny: structural pino-compatible logger
378
+ keys: baileys.makeCacheableSignalKeyStore(state.keys, logger)
379
+ },
380
+ // biome-ignore lint/suspicious/noExplicitAny: structural pino-compatible logger
381
+ logger,
382
+ markOnlineOnConnect: false
383
+ });
384
+ sock.ev.on("creds.update", saveCreds);
385
+ return sock;
386
+ };
387
+ const result = await runPairing({
388
+ connect,
389
+ loggedOutCode: baileys.DisconnectReason.loggedOut,
390
+ phoneNumber,
391
+ onQr: renderQr,
392
+ onPairingCode: (code) => {
393
+ console.log("\n==============================================");
394
+ console.log(` Pairing code for +${phoneNumber}: ${code}`);
395
+ console.log("==============================================");
396
+ console.log("On the phone: WhatsApp -> Settings -> Linked Devices ->");
397
+ console.log("Link a Device -> 'Link with phone number instead' -> enter the code.");
398
+ console.log("Waiting for confirmation (leave this running)...");
399
+ },
400
+ onStatus: (message) => console.log(message)
298
401
  });
299
- if (phoneNumber && !codeRequested && !state.creds.registered) {
300
- setTimeout(async () => {
301
- try {
302
- const code = await sock.requestPairingCode(phoneNumber);
303
- codeRequested = true;
304
- console.log("\n==============================================");
305
- console.log(` Pairing code for +${phoneNumber}: ${code}`);
306
- console.log("==============================================");
307
- console.log("On the phone: WhatsApp -> Settings -> Linked Devices ->");
308
- console.log("Link a Device -> 'Link with phone number instead' -> enter the code.");
309
- console.log("Waiting for confirmation (leave this running)...");
310
- } catch (error) {
311
- console.error("\u274C Failed to request pairing code:", error);
312
- process.exit(1);
313
- }
314
- }, 3e3);
402
+ if (!result.ok) {
403
+ console.error(`\u274C ${result.message}`);
404
+ process.exit(1);
315
405
  }
406
+ console.log(`
407
+ \u2705 Paired session "${sessionId}" as ${result.userId}. Session saved (encrypted).`);
408
+ console.log("The running server will pick this session up on its next connect.");
409
+ process.exit(0);
316
410
  }
317
411
  start().catch((error) => {
318
412
  console.error("\u274C Unexpected error:", error);
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Pairing loop for the `wa-pair` CLI — the transport-independent half, kept
3
+ * out of the bin so it can be unit-tested against a faked socket.
4
+ *
5
+ * Both pairing modes close the socket mid-flow as a matter of course:
6
+ *
7
+ * - QR mode: the moment a scan registers the device, WhatsApp ALWAYS closes
8
+ * with 515 ("restart required"). The link only completes if the client comes
9
+ * straight back with the SAME credentials it just registered — the phone is
10
+ * sitting on "linking" waiting for exactly that. Exiting there (or wiping
11
+ * creds and starting over) fails the pairing every time.
12
+ * - Pairing-code mode: the socket bounces while the code is being typed.
13
+ *
14
+ * So a close during pairing is normal, not fatal: reconnect with stored creds,
15
+ * bounded, until `open` confirms the link. Only an explicit logout or running
16
+ * out of attempts ends the run.
17
+ */
18
+ /** Baileys' "restart required" — emitted immediately after a QR scan registers the device. */
19
+ export declare const RESTART_REQUIRED_STATUS = 515;
20
+ /** Bounded so a genuinely broken pairing terminates instead of looping forever. */
21
+ export declare const MAX_PAIRING_RECONNECTS = 12;
22
+ /**
23
+ * Backoff between pairing attempts. Deliberately tighter than the transport's
24
+ * 5s→10min ladder (`reconnectDelayMs`): that one protects a long-lived
25
+ * connection from hammering WhatsApp during an outage, whereas here a human is
26
+ * holding a phone that gives the link a limited window — a 515 restart has to
27
+ * be answered at once, and other bounces within seconds.
28
+ */
29
+ export declare function pairingReconnectDelayMs(statusCode: number | undefined, attempt: number): number;
30
+ export type PairingCloseAction = {
31
+ kind: "reconnect";
32
+ attempt: number;
33
+ delayMs: number;
34
+ } | {
35
+ kind: "fatal";
36
+ reason: "loggedOut" | "exhausted";
37
+ };
38
+ export interface PairingCloseInput {
39
+ statusCode?: number;
40
+ /** `DisconnectReason.loggedOut` from the loaded Baileys build. */
41
+ loggedOutCode: number;
42
+ /** Reconnects already spent. */
43
+ attempts: number;
44
+ maxAttempts: number;
45
+ }
46
+ /**
47
+ * Pure: what to do about a `connection: "close"` during pairing. A logout is
48
+ * final (the credentials just got revoked — retrying re-revokes them); every
49
+ * other close is transient until the attempt budget runs out.
50
+ */
51
+ export declare function decidePairingClose(input: PairingCloseInput): PairingCloseAction;
52
+ /** The slice of a Baileys socket the pairing loop uses. */
53
+ export interface PairingSocket {
54
+ ev: {
55
+ on: (event: "connection.update", listener: (update: PairingConnectionUpdate) => void) => void;
56
+ };
57
+ user?: {
58
+ id?: string;
59
+ } | null;
60
+ requestPairingCode: (phoneNumber: string) => Promise<string>;
61
+ /** Closes a superseded socket so it stops writing to the session. */
62
+ end?: (error?: Error) => void;
63
+ }
64
+ export interface PairingConnectionUpdate {
65
+ connection?: string;
66
+ qr?: string;
67
+ lastDisconnect?: {
68
+ error?: unknown;
69
+ } | null;
70
+ }
71
+ export type PairingResult = {
72
+ ok: true;
73
+ userId?: string;
74
+ } | {
75
+ ok: false;
76
+ reason: "loggedOut" | "exhausted" | "error";
77
+ message: string;
78
+ };
79
+ export interface PairingRunDeps {
80
+ /**
81
+ * Opens a socket for `attempt` (0 = first). MUST reuse the stored
82
+ * credentials — a reconnect that starts from blank creds shows a fresh QR
83
+ * instead of finishing the link the phone is waiting on.
84
+ */
85
+ connect: (attempt: number) => PairingSocket | Promise<PairingSocket>;
86
+ loggedOutCode: number;
87
+ /** Digits-only number for pairing-code mode; omit for QR mode. */
88
+ phoneNumber?: string;
89
+ maxAttempts?: number;
90
+ /** Delay before asking for a pairing code, giving the socket time to handshake. */
91
+ requestCodeDelayMs?: number;
92
+ /** Overridable for tests; defaults to `setTimeout`. */
93
+ schedule?: (fn: () => void, ms: number) => void;
94
+ /** Renders the QR (QR mode only). A throw is reported, never swallowed. */
95
+ onQr?: (qr: string) => void | Promise<void>;
96
+ onPairingCode?: (code: string) => void;
97
+ /** Progress lines for the operator (reconnect notices, QR render failures). */
98
+ onStatus?: (message: string) => void;
99
+ }
100
+ /**
101
+ * Runs one pairing to a verdict: resolves `{ ok: true }` when the connection
102
+ * reaches `open` (the link is live), or `{ ok: false }` on logout, attempt
103
+ * exhaustion, or a connect/pairing-code failure. Never rejects.
104
+ */
105
+ export declare function runPairing(deps: PairingRunDeps): Promise<PairingResult>;
106
+ //# sourceMappingURL=pairing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/pairing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pairing.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.test.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/pairing.test.ts"],"names":[],"mappings":""}
package/dist/index.d.ts CHANGED
@@ -42,5 +42,7 @@ export { privateRoutes } from "./routes/private";
42
42
  export { publicRoutes } from "./routes/public";
43
43
  export type { ChatterApp } from "./server";
44
44
  export { createServer } from "./server";
45
+ export type { Shortener, ShortenerConfig } from "./shortener";
46
+ export { createShortener } from "./shortener";
45
47
  export type { BotBranding, BotChatConfig, BotIdentity, ChatterConfig, CustomRoutes, EmbeddingChunk, KnowledgeDocument, ServerDependencies, } from "./types";
46
48
  //# 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;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"}
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;AACxC,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,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
@@ -7550,6 +7550,7 @@ __export(index_exports, {
7550
7550
  createServer: () => createServer,
7551
7551
  createSession: () => createSession,
7552
7552
  createSessionMiddleware: () => createSessionMiddleware,
7553
+ createShortener: () => createShortener,
7553
7554
  defaultBuckets: () => defaultBuckets,
7554
7555
  demoRoutes: () => demoRoutes,
7555
7556
  detectLeakage: () => detectLeakage,
@@ -33396,6 +33397,40 @@ async function createServer(config2) {
33396
33397
  }
33397
33398
  });
33398
33399
  }
33400
+
33401
+ // src/shortener.ts
33402
+ var DEFAULT_TIMEOUT_MS = 5e3;
33403
+ function createShortener(config2, fetchImpl = fetch) {
33404
+ const apiUrl = config2.apiUrl.replace(/\/+$/, "");
33405
+ return {
33406
+ async shorten(url2) {
33407
+ const controller = new AbortController();
33408
+ const timeout = setTimeout(() => controller.abort(), config2.timeoutMs ?? DEFAULT_TIMEOUT_MS);
33409
+ try {
33410
+ const response = await fetchImpl(`${apiUrl}/api/v2/links`, {
33411
+ method: "POST",
33412
+ headers: {
33413
+ "Content-Type": "application/json",
33414
+ "X-API-Key": config2.apiKey
33415
+ },
33416
+ body: JSON.stringify({ target: url2, domain: config2.domain }),
33417
+ signal: controller.signal
33418
+ });
33419
+ if (!response.ok) {
33420
+ await response.text().catch(() => "");
33421
+ return url2;
33422
+ }
33423
+ const data = await response.json();
33424
+ if (typeof data.link !== "string") return url2;
33425
+ return data.link.replace(/^http:\/\//, "https://");
33426
+ } catch {
33427
+ return url2;
33428
+ } finally {
33429
+ clearTimeout(timeout);
33430
+ }
33431
+ }
33432
+ };
33433
+ }
33399
33434
  // Annotate the CommonJS export names for ESM import in node:
33400
33435
  0 && (module.exports = {
33401
33436
  ApiKeyManager,
@@ -33418,6 +33453,7 @@ async function createServer(config2) {
33418
33453
  createServer,
33419
33454
  createSession,
33420
33455
  createSessionMiddleware,
33456
+ createShortener,
33421
33457
  defaultBuckets,
33422
33458
  demoRoutes,
33423
33459
  detectLeakage,
package/dist/index.mjs CHANGED
@@ -33358,6 +33358,40 @@ async function createServer(config2) {
33358
33358
  }
33359
33359
  });
33360
33360
  }
33361
+
33362
+ // src/shortener.ts
33363
+ var DEFAULT_TIMEOUT_MS = 5e3;
33364
+ function createShortener(config2, fetchImpl = fetch) {
33365
+ const apiUrl = config2.apiUrl.replace(/\/+$/, "");
33366
+ return {
33367
+ async shorten(url2) {
33368
+ const controller = new AbortController();
33369
+ const timeout = setTimeout(() => controller.abort(), config2.timeoutMs ?? DEFAULT_TIMEOUT_MS);
33370
+ try {
33371
+ const response = await fetchImpl(`${apiUrl}/api/v2/links`, {
33372
+ method: "POST",
33373
+ headers: {
33374
+ "Content-Type": "application/json",
33375
+ "X-API-Key": config2.apiKey
33376
+ },
33377
+ body: JSON.stringify({ target: url2, domain: config2.domain }),
33378
+ signal: controller.signal
33379
+ });
33380
+ if (!response.ok) {
33381
+ await response.text().catch(() => "");
33382
+ return url2;
33383
+ }
33384
+ const data = await response.json();
33385
+ if (typeof data.link !== "string") return url2;
33386
+ return data.link.replace(/^http:\/\//, "https://");
33387
+ } catch {
33388
+ return url2;
33389
+ } finally {
33390
+ clearTimeout(timeout);
33391
+ }
33392
+ }
33393
+ };
33394
+ }
33361
33395
  var export_Chat = import_client3.Chat;
33362
33396
  var export_ChatBot = import_client3.ChatBot;
33363
33397
  var export_ChatButton = import_client3.ChatButton;
@@ -33382,6 +33416,7 @@ export {
33382
33416
  createServer,
33383
33417
  createSession,
33384
33418
  createSessionMiddleware,
33419
+ createShortener,
33385
33420
  defaultBuckets,
33386
33421
  demoRoutes,
33387
33422
  detectLeakage,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * URL shortening against a Kutt-compatible API, config-injected (no
3
+ * `process.env` reads) so hosts can wire credentials however they like.
4
+ *
5
+ * The hard invariant: {@link Shortener.shorten} never throws. Any failure —
6
+ * network error, an aborted request past `timeoutMs`, or a non-2xx/malformed
7
+ * response — resolves to the original URL, since a shortener outage must
8
+ * never break something user-facing that only wanted a shorter link.
9
+ */
10
+ export interface ShortenerConfig {
11
+ /** Base URL of the Kutt (or Kutt-compatible) instance, e.g. "https://kutt.it". */
12
+ apiUrl: string;
13
+ apiKey: string;
14
+ /** Custom short domain configured on the Kutt instance, if any. */
15
+ domain?: string;
16
+ /** Abort the request after this many milliseconds. Defaults to 5000. */
17
+ timeoutMs?: number;
18
+ }
19
+ export interface Shortener {
20
+ /** Resolves to the shortened URL, or the input `url` unchanged on any failure. */
21
+ shorten(url: string): Promise<string>;
22
+ }
23
+ export declare function createShortener(config: ShortenerConfig, fetchImpl?: typeof fetch): Shortener;
24
+ //# sourceMappingURL=shortener.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortener.d.ts","sourceRoot":"","sources":["../src/shortener.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe;IAC9B,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,kFAAkF;IAClF,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvC;AAQD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,SAAS,GAAE,OAAO,KAAa,GAC9B,SAAS,CAqCX"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=shortener.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortener.test.d.ts","sourceRoot":"","sources":["../src/shortener.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegoaltoworks/chatter",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Embeddable AI chatbot framework with RAG, authentication, and customizable widgets",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",