@diegoaltoworks/chatter 0.47.0 → 0.49.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 (118) hide show
  1. package/README.md +7 -6
  2. package/dist/auth/apikeys.d.ts.map +1 -1
  3. package/dist/auth/secretStrength.d.ts +8 -0
  4. package/dist/auth/secretStrength.d.ts.map +1 -0
  5. package/dist/bin/create-apikey.js +22 -3
  6. package/dist/bin/wa-pair.js +59 -11
  7. package/dist/channels/gates.d.ts +9 -0
  8. package/dist/channels/gates.d.ts.map +1 -1
  9. package/dist/channels/index.d.ts +1 -0
  10. package/dist/channels/index.d.ts.map +1 -1
  11. package/dist/channels/index.js +252 -8
  12. package/dist/channels/index.mjs +250 -8
  13. package/dist/channels/pipeline.d.ts +106 -0
  14. package/dist/channels/pipeline.d.ts.map +1 -0
  15. package/dist/channels/senders.d.ts +2 -1
  16. package/dist/channels/senders.d.ts.map +1 -1
  17. package/dist/channels/whatsapp/channel.d.ts +4 -0
  18. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  19. package/dist/channels/whatsapp/crypto.d.ts +17 -3
  20. package/dist/channels/whatsapp/crypto.d.ts.map +1 -1
  21. package/dist/channels/whatsapp/inbound.d.ts +34 -4
  22. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  23. package/dist/channels/whatsapp/index.d.ts +2 -1
  24. package/dist/channels/whatsapp/index.d.ts.map +1 -1
  25. package/dist/channels/whatsapp/index.js +395 -159
  26. package/dist/channels/whatsapp/index.mjs +394 -160
  27. package/dist/channels/whatsapp/router.d.ts +107 -0
  28. package/dist/channels/whatsapp/router.d.ts.map +1 -0
  29. package/dist/core/answer.d.ts +5 -2
  30. package/dist/core/answer.d.ts.map +1 -1
  31. package/dist/core/logger.d.ts +23 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/messages.d.ts +8 -0
  34. package/dist/core/messages.d.ts.map +1 -1
  35. package/dist/core/pipeline.d.ts +2 -0
  36. package/dist/core/pipeline.d.ts.map +1 -1
  37. package/dist/core/retrieval.d.ts +4 -0
  38. package/dist/core/retrieval.d.ts.map +1 -1
  39. package/dist/flows/engine.d.ts +3 -0
  40. package/dist/flows/engine.d.ts.map +1 -1
  41. package/dist/flows/index.d.ts +1 -0
  42. package/dist/flows/index.d.ts.map +1 -1
  43. package/dist/flows/index.js +91 -25
  44. package/dist/flows/index.mjs +91 -26
  45. package/dist/flows/intent.d.ts +2 -1
  46. package/dist/flows/intent.d.ts.map +1 -1
  47. package/dist/flows/loader.d.ts +5 -3
  48. package/dist/flows/loader.d.ts.map +1 -1
  49. package/dist/flows/types.d.ts +9 -0
  50. package/dist/flows/types.d.ts.map +1 -1
  51. package/dist/history/index.d.ts +17 -0
  52. package/dist/history/index.d.ts.map +1 -0
  53. package/dist/history/index.js +100 -0
  54. package/dist/history/index.mjs +73 -0
  55. package/dist/history/tursoStore.d.ts +26 -0
  56. package/dist/history/tursoStore.d.ts.map +1 -0
  57. package/dist/history/types.d.ts +18 -0
  58. package/dist/history/types.d.ts.map +1 -0
  59. package/dist/index.d.ts +5 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1146 -32444
  62. package/dist/index.mjs +1082 -32410
  63. package/dist/mcp-server/cost-tracker.d.ts +11 -6
  64. package/dist/mcp-server/cost-tracker.d.ts.map +1 -1
  65. package/dist/mcp-server/logger.d.ts +5 -2
  66. package/dist/mcp-server/logger.d.ts.map +1 -1
  67. package/dist/mcp-server/sdk.d.ts +23 -0
  68. package/dist/mcp-server/sdk.d.ts.map +1 -0
  69. package/dist/mcp-server/tool-input.d.ts +12 -0
  70. package/dist/mcp-server/tool-input.d.ts.map +1 -0
  71. package/dist/mcp-server/types.d.ts +10 -3
  72. package/dist/mcp-server/types.d.ts.map +1 -1
  73. package/dist/mcp-server.d.ts +1 -2
  74. package/dist/mcp-server.d.ts.map +1 -1
  75. package/dist/mcp-server.js +192 -111
  76. package/dist/mcp-server.mjs +179 -98
  77. package/dist/middleware/auth.d.ts.map +1 -1
  78. package/dist/middleware/bodyLimit.d.ts +11 -0
  79. package/dist/middleware/bodyLimit.d.ts.map +1 -0
  80. package/dist/middleware/clientKey.d.ts +15 -0
  81. package/dist/middleware/clientKey.d.ts.map +1 -0
  82. package/dist/middleware/cors.d.ts.map +1 -1
  83. package/dist/middleware/jwt.d.ts +0 -3
  84. package/dist/middleware/jwt.d.ts.map +1 -1
  85. package/dist/middleware/originMatch.d.ts +27 -0
  86. package/dist/middleware/originMatch.d.ts.map +1 -0
  87. package/dist/middleware/ratelimit.d.ts +0 -6
  88. package/dist/middleware/ratelimit.d.ts.map +1 -1
  89. package/dist/middleware/referrer.d.ts +5 -1
  90. package/dist/middleware/referrer.d.ts.map +1 -1
  91. package/dist/middleware/securityHeaders.d.ts +20 -0
  92. package/dist/middleware/securityHeaders.d.ts.map +1 -0
  93. package/dist/middleware/session.d.ts +2 -1
  94. package/dist/middleware/session.d.ts.map +1 -1
  95. package/dist/middleware/windowBucket.d.ts +19 -0
  96. package/dist/middleware/windowBucket.d.ts.map +1 -0
  97. package/dist/personas/index.js +20 -1
  98. package/dist/personas/index.mjs +20 -1
  99. package/dist/personas/resolver.d.ts +3 -0
  100. package/dist/personas/resolver.d.ts.map +1 -1
  101. package/dist/routes/demo.d.ts.map +1 -1
  102. package/dist/routes/openai.d.ts.map +1 -1
  103. package/dist/routes/private.d.ts.map +1 -1
  104. package/dist/routes/public.d.ts.map +1 -1
  105. package/dist/scheduler/compose.d.ts +2 -1
  106. package/dist/scheduler/compose.d.ts.map +1 -1
  107. package/dist/scheduler/index.js +29 -9
  108. package/dist/scheduler/index.mjs +29 -9
  109. package/dist/scheduler/scheduler.d.ts +3 -0
  110. package/dist/scheduler/scheduler.d.ts.map +1 -1
  111. package/dist/scheduler/tick.d.ts +2 -0
  112. package/dist/scheduler/tick.d.ts.map +1 -1
  113. package/dist/server.d.ts.map +1 -1
  114. package/dist/server.js +678 -528
  115. package/dist/server.mjs +678 -528
  116. package/dist/types.d.ts +87 -0
  117. package/dist/types.d.ts.map +1 -1
  118. package/package.json +23 -8
package/README.md CHANGED
@@ -64,8 +64,10 @@ and CI loads the built bundles under each). Two things differ:
64
64
  `serve({ port: 8181, fetch: app.fetch })` from [`@hono/node-server`](https://github.com/honojs/node-server) on Node.
65
65
  - **Static assets** — serving the widget files (`/chatter.js`, `/chatter.css`)
66
66
  and a `publicDir` needs a runtime adapter, picked automatically. On Node that
67
- adapter is `@hono/node-server`, an optional peer dependency: install it, or
68
- run with `features: { headless: true }` if the server serves no files.
67
+ adapter is `@hono/node-server`, an optional peer dependency: without it,
68
+ `/chatter.js` and `/chatter.css` 404 and the server logs an actionable
69
+ error naming the missing package. Install it, or run with
70
+ `features: { headless: true }` if the server should serve no files.
69
71
 
70
72
  Everything else — the chat pipeline, RAG, channels, the OpenAI-compatible API —
71
73
  is runtime-neutral. Development and the quality gates run on Bun.
@@ -90,7 +92,6 @@ Complete guides for setup, deployment, and integration:
90
92
  - **[Usage Metering](./docs/usage.md)** - Daily spend caps for paid features
91
93
  - **[Deployment](./docs/deployment.md)** - Google Cloud Run, Fly.io, Railway, VPS
92
94
  - **[Testing](./docs/testing.md)** - Comprehensive testing guide
93
- - **[Code Quality](./docs/CODE_QUALITY.md)** - Security assessment and architecture review
94
95
  - **[Sprint Review](./docs/sprint-review.md)** - Cross-module & security-invariant audit
95
96
  - **[FAQs](./docs/faqs.md)** - Troubleshooting and common questions
96
97
 
@@ -137,7 +138,7 @@ See [Client Setup Guide](./docs/client.md) for detailed integration examples, th
137
138
 
138
139
  ## MCP Server Integration
139
140
 
140
- Use Chatter as a Model Context Protocol (MCP) server to expose your chatbot to Claude Desktop, VS Code extensions, and other MCP-compatible tools:
141
+ Use Chatter as a Model Context Protocol (MCP) server to expose your chatbot to Claude Desktop, VS Code extensions, and other MCP-compatible tools. `@modelcontextprotocol/sdk` and `zod` are optional peer dependencies scoped to this subpath — install them to use it: `bun add @modelcontextprotocol/sdk zod`.
141
142
 
142
143
  **Basic Setup:**
143
144
  ```typescript
@@ -237,7 +238,7 @@ Add to your `claude_desktop_config.json`:
237
238
  Both tools:
238
239
  - Support single messages or full conversation history
239
240
  - Track conversation IDs across sessions for continuity
240
- - Return token usage and cost estimates in response metadata
241
+ - Return token usage in response metadata, plus an estimated USD cost when `openai.pricing` is configured
241
242
  - Use RAG-powered context retrieval from your knowledge base
242
243
  - Can be customized with different names and descriptions
243
244
  - Can be individually enabled/disabled
@@ -245,7 +246,7 @@ Both tools:
245
246
 
246
247
  **Features:**
247
248
  - **Conversation ID Tracking**: Pass `conversationId` parameter to maintain session continuity across tool calls
248
- - **Cost Tracking**: Every response includes token usage (prompt/completion/total) and estimated USD cost
249
+ - **Cost Tracking**: Every response includes token usage (prompt/completion/total). Add `openai: { pricing: { promptPer1M, completionPer1M } }` to also get an estimated USD cost — without it, `estimatedCost` is `null` rather than guessing at a price for the wrong model
249
250
  - **Rate Limiting**: Optional per-tool rate limiting (requests per minute) to control API usage
250
251
  - **Observability**: Comprehensive logging with conversation tracking and cost data
251
252
 
@@ -1 +1 @@
1
- {"version":3,"file":"apikeys.d.ts","sourceRoot":"","sources":["../../src/auth/apikeys.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,IAAI,EAAE,SAAS,CAAC;IAChB,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,+BAA+B;IAC/B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,WAAW,CAAa;IAEhC,YAAY,MAAM,EAAE,MAAM,EAKzB;IAED;;;;;OAKG;IACG,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBzD;IAED;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAsBjD;IAED;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA6BvB;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAW1C;CACF"}
1
+ {"version":3,"file":"apikeys.d.ts","sourceRoot":"","sources":["../../src/auth/apikeys.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,IAAI,EAAE,SAAS,CAAC;IAChB,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,+BAA+B;IAC/B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,WAAW,CAAa;IAEhC,YAAY,MAAM,EAAE,MAAM,EAGzB;IAED;;;;;OAKG;IACG,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsBzD;IAED;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAsBjD;IAED;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA6BvB;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAW1C;CACF"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared strength check for caller-supplied secrets (JWT signing key, WhatsApp
3
+ * session-encryption passphrase). Both guard high-value material — API key
4
+ * forgery or WhatsApp account takeover — so a short or empty value fails
5
+ * fast with an actionable error instead of quietly producing a weak key.
6
+ */
7
+ export declare function assertStrongSecret(secret: string | undefined, label: string): void;
8
+ //# sourceMappingURL=secretStrength.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secretStrength.d.ts","sourceRoot":"","sources":["../../src/auth/secretStrength.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAUlF"}
@@ -4,12 +4,25 @@
4
4
  // src/auth/apikeys.ts
5
5
  var import_node_crypto = require("node:crypto");
6
6
  var import_jose = require("jose");
7
+
8
+ // src/auth/secretStrength.ts
9
+ var MIN_SECRET_LENGTH = 16;
10
+ function assertStrongSecret(secret, label) {
11
+ if (!secret) {
12
+ throw new Error(`${label} is required and cannot be empty.`);
13
+ }
14
+ if (secret.length < MIN_SECRET_LENGTH) {
15
+ throw new Error(
16
+ `${label} is too weak: must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length}). Generate one with \`openssl rand -base64 32\`.`
17
+ );
18
+ }
19
+ }
20
+
21
+ // src/auth/apikeys.ts
7
22
  var ApiKeyManager = class {
8
23
  secretBytes;
9
24
  constructor(secret) {
10
- if (!secret) {
11
- throw new Error("API key secret is required");
12
- }
25
+ assertStrongSecret(secret, "API key secret (CHATTER_SECRET / config.auth.secret)");
13
26
  this.secretBytes = new TextEncoder().encode(secret);
14
27
  }
15
28
  /**
@@ -155,6 +168,12 @@ Examples:
155
168
  console.error(" CHATTER_SECRET=your-secret npx chatter create-apikey\n");
156
169
  process.exit(1);
157
170
  }
171
+ try {
172
+ assertStrongSecret(secret, "CHATTER_SECRET");
173
+ } catch (error) {
174
+ console.error(`\u274C ${error.message}`);
175
+ process.exit(1);
176
+ }
158
177
  const manager = new ApiKeyManager(secret);
159
178
  const keyName = options.name || "api-key";
160
179
  const expiresIn = options.expiresIn || "365d";
@@ -26,28 +26,74 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // bin/wa-pair.ts
27
27
  var import_client = require("@libsql/client");
28
28
 
29
+ // src/auth/secretStrength.ts
30
+ var MIN_SECRET_LENGTH = 16;
31
+ function assertStrongSecret(secret, label) {
32
+ if (!secret) {
33
+ throw new Error(`${label} is required and cannot be empty.`);
34
+ }
35
+ if (secret.length < MIN_SECRET_LENGTH) {
36
+ throw new Error(
37
+ `${label} is too weak: must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length}). Generate one with \`openssl rand -base64 32\`.`
38
+ );
39
+ }
40
+ }
41
+
29
42
  // src/channels/whatsapp/crypto.ts
30
43
  var import_node_crypto = require("node:crypto");
44
+ var import_node_util = require("node:util");
45
+ var scryptAsync = (0, import_node_util.promisify)(import_node_crypto.scrypt);
31
46
  var IV_LENGTH = 12;
32
47
  var TAG_LENGTH = 16;
33
- function deriveKey(secret) {
48
+ var SALT_LENGTH = 16;
49
+ var KEY_LENGTH = 32;
50
+ var VERSION = 1;
51
+ function deriveLegacyKey(secret) {
34
52
  return (0, import_node_crypto.createHash)("sha256").update(secret, "utf-8").digest();
35
53
  }
36
- function encrypt(plaintext, secret) {
54
+ async function deriveKey(secret, salt) {
55
+ return await scryptAsync(secret, salt, KEY_LENGTH);
56
+ }
57
+ async function encrypt(plaintext, secret) {
58
+ const salt = (0, import_node_crypto.randomBytes)(SALT_LENGTH);
37
59
  const iv = (0, import_node_crypto.randomBytes)(IV_LENGTH);
38
- const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", deriveKey(secret), iv);
60
+ const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", await deriveKey(secret, salt), iv);
39
61
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
40
- return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]).toString("base64");
62
+ return Buffer.concat([
63
+ Buffer.from([VERSION]),
64
+ salt,
65
+ iv,
66
+ cipher.getAuthTag(),
67
+ ciphertext
68
+ ]).toString("base64");
41
69
  }
42
- function decrypt(payload, secret) {
43
- const raw = Buffer.from(payload, "base64");
70
+ async function decryptVersioned(raw, secret) {
71
+ const salt = raw.subarray(1, 1 + SALT_LENGTH);
72
+ const iv = raw.subarray(1 + SALT_LENGTH, 1 + SALT_LENGTH + IV_LENGTH);
73
+ const tag = raw.subarray(1 + SALT_LENGTH + IV_LENGTH, 1 + SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
74
+ const ciphertext = raw.subarray(1 + SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
75
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", await deriveKey(secret, salt), iv);
76
+ decipher.setAuthTag(tag);
77
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
78
+ }
79
+ function decryptLegacy(raw, secret) {
44
80
  const iv = raw.subarray(0, IV_LENGTH);
45
81
  const tag = raw.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
46
82
  const ciphertext = raw.subarray(IV_LENGTH + TAG_LENGTH);
47
- const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", deriveKey(secret), iv);
83
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", deriveLegacyKey(secret), iv);
48
84
  decipher.setAuthTag(tag);
49
85
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf-8");
50
86
  }
87
+ async function decrypt(payload, secret) {
88
+ const raw = Buffer.from(payload, "base64");
89
+ if (raw[0] === VERSION) {
90
+ try {
91
+ return await decryptVersioned(raw, secret);
92
+ } catch {
93
+ }
94
+ }
95
+ return decryptLegacy(raw, secret);
96
+ }
51
97
 
52
98
  // src/channels/whatsapp/authState.ts
53
99
  var schemaReady = /* @__PURE__ */ new WeakMap();
@@ -72,10 +118,10 @@ async function useTursoAuthState(db2, secret, sessionId2, runtime) {
72
118
  });
73
119
  const row = result.rows[0];
74
120
  if (!row) return null;
75
- return JSON.parse(decrypt(row.value, secret), runtime.bufferJSON.reviver);
121
+ return JSON.parse(await decrypt(row.value, secret), runtime.bufferJSON.reviver);
76
122
  }
77
123
  async function write(id, value) {
78
- const payload = encrypt(JSON.stringify(value, runtime.bufferJSON.replacer), secret);
124
+ const payload = await encrypt(JSON.stringify(value, runtime.bufferJSON.replacer), secret);
79
125
  await db2.execute({
80
126
  sql: "INSERT OR REPLACE INTO wa_auth (id, value) VALUES (?, ?)",
81
127
  args: [rowId(id), payload]
@@ -386,8 +432,10 @@ if (args.includes("--code") && !flagValue("--code")) {
386
432
  var phoneNumber = flagValue("--code")?.replace(/[^0-9]/g, "");
387
433
  var reset = args.includes("--reset");
388
434
  var sessionSecret = process.env.WA_SESSION_SECRET;
389
- if (!sessionSecret) {
390
- console.error("\u274C WA_SESSION_SECRET is required (any strong passphrase; keep it stable).");
435
+ try {
436
+ assertStrongSecret(sessionSecret, "WA_SESSION_SECRET");
437
+ } catch (error) {
438
+ console.error(`\u274C ${error.message}`);
391
439
  process.exit(1);
392
440
  }
393
441
  if (!process.env.TURSO_URL) {
@@ -37,6 +37,15 @@ export interface ChannelGateConfig {
37
37
  muteRegex?: RegExp;
38
38
  unmuteRegex?: RegExp;
39
39
  }
40
+ /**
41
+ * True when `msg` is dropped because its chat isn't on `allowedChats`. The
42
+ * allowlist is the first group-only check {@link decideChannelAction} makes,
43
+ * before mute/unmute and addressing, so whenever this is true the allowlist
44
+ * really is the attributable reason for the "ignore" — a transport can log
45
+ * just this case without misattributing a drop that happened for a different
46
+ * cause (from the bot itself, blank text, muted, unaddressed).
47
+ */
48
+ export declare function isBlockedByAllowlist(msg: ChannelMessage, config: Pick<ChannelGateConfig, "allowedChats">): boolean;
40
49
  /**
41
50
  * DMs always reply. In groups, only an allowlisted, unmuted chat gets a
42
51
  * reply, and only when addressed (mentioned or replying to the bot) —
@@ -1 +1 @@
1
- {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/channels/gates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,eAAe,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,wFAAwF;IACxF,YAAY,EAAE,OAAO,CAAC;IACtB,sHAAsH;IACtH,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kIAAkI;IAClI,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,aAAa,CAYjG;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB,SAGD,MAAM,KAAG,OAAO,CAY5C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,iBAAiB,CAAC,EACvD,QAAQ,EAAE;IAAE,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GAChF,OAAO,CAET;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,uBAAuB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAE5D,qHAAqH;AACrH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,GAAG,SAAS,EACtB,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAMT;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,UAAU,CAAC,EACjD,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAET"}
1
+ {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/channels/gates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,eAAe,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,wFAAwF;IACxF,YAAY,EAAE,OAAO,CAAC;IACtB,sHAAsH;IACtH,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kIAAkI;IAClI,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,GAC9C,OAAO,CAQT;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,aAAa,CAUjG;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB,SAGD,MAAM,KAAG,OAAO,CAY5C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,iBAAiB,CAAC,EACvD,QAAQ,EAAE;IAAE,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GAChF,OAAO,CAET;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,uBAAuB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAE5D,qHAAqH;AACrH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,GAAG,SAAS,EACtB,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAMT;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,UAAU,CAAC,EACjD,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAET"}
@@ -42,6 +42,7 @@ export interface Channel {
42
42
  stop?(): void | Promise<void>;
43
43
  }
44
44
  export * from "./gates";
45
+ export { createInboundPipeline, type InboundPipelineConfig, type InboundPipelineDeps, type InboundPipelineOutcome, type InboundReplySender, type InboundTurn, } from "./pipeline";
45
46
  export type { ChannelSender, ChannelSenderRegistry } from "./senders";
46
47
  export { createSenderRegistry } from "./senders";
47
48
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/channels/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,MAAM,WAAW,OAAO;IACtB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,cAAc,SAAS,CAAC;AACxB,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/channels/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,MAAM,WAAW,OAAO;IACtB,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,cAAc,SAAS,CAAC;AACxB,OAAO,EACL,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC"}
@@ -20,9 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/channels/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ createInboundPipeline: () => createInboundPipeline,
23
24
  createSenderRegistry: () => createSenderRegistry,
24
25
  createSlidingWindowRateLimiter: () => createSlidingWindowRateLimiter,
25
26
  decideChannelAction: () => decideChannelAction,
27
+ isBlockedByAllowlist: () => isBlockedByAllowlist,
26
28
  isEffectivelyFromSelf: () => isEffectivelyFromSelf,
27
29
  isFromAnySession: () => isFromAnySession,
28
30
  underReplyRateLimit: () => underReplyRateLimit
@@ -33,12 +35,13 @@ module.exports = __toCommonJS(index_exports);
33
35
  function matches(re, text) {
34
36
  return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
35
37
  }
38
+ function isBlockedByAllowlist(msg, config) {
39
+ return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
40
+ }
36
41
  function decideChannelAction(msg, config) {
37
42
  if (msg.fromBot || !msg.text.trim()) return "ignore";
38
43
  if (!msg.isDirectMessage) {
39
- if (config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId)) {
40
- return "ignore";
41
- }
44
+ if (isBlockedByAllowlist(msg, config)) return "ignore";
42
45
  if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
43
46
  if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
44
47
  if (config.mutedChats.has(msg.chatId)) return "ignore";
@@ -75,13 +78,252 @@ function isEffectivelyFromSelf(msg, registry) {
75
78
  return msg.fromBot || isFromAnySession(msg.senderId, registry);
76
79
  }
77
80
 
81
+ // src/core/guardrails.ts
82
+ function detectLeakage(text) {
83
+ const flags = [
84
+ /system prompt/i,
85
+ /hidden instruction/i,
86
+ /here (are|is) (my|the) rules/i,
87
+ /BEGIN SYSTEM PROMPT/i,
88
+ /tool instructions/i
89
+ ];
90
+ return flags.some((rx) => rx.test(text));
91
+ }
92
+ function scrubOutput(text) {
93
+ return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
94
+ }
95
+ function guardOutput(text) {
96
+ if (detectLeakage(text)) {
97
+ return "Sorry, I can't share internal instructions. How else can I help?";
98
+ }
99
+ return scrubOutput(text);
100
+ }
101
+
102
+ // src/core/llm.ts
103
+ var DEFAULT_MODEL = "gpt-4o";
104
+ async function completeOnce({
105
+ client,
106
+ system,
107
+ messages,
108
+ temperature = 0.2,
109
+ model = DEFAULT_MODEL
110
+ }) {
111
+ const res = await client.chat.completions.create({
112
+ model,
113
+ temperature,
114
+ messages: [{ role: "system", content: system }, ...messages]
115
+ });
116
+ const text = res.choices[0]?.message?.content ?? "";
117
+ return {
118
+ content: guardOutput(text),
119
+ usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
120
+ };
121
+ }
122
+
123
+ // src/core/answer.ts
124
+ var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
125
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
126
+ const result = await answerFn({ system, messages, mode, sender, conversationId });
127
+ const raw = typeof result === "string" ? result : result?.content;
128
+ const usage = typeof result === "string" ? void 0 : result?.usage;
129
+ return {
130
+ content: guardOutput(typeof raw === "string" ? raw : ""),
131
+ usage: usage ?? noUsage()
132
+ };
133
+ }
134
+ async function answerOnce({
135
+ answerFn,
136
+ client,
137
+ system,
138
+ messages,
139
+ mode,
140
+ sender,
141
+ conversationId,
142
+ temperature,
143
+ model
144
+ }) {
145
+ if (answerFn) {
146
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
147
+ }
148
+ return completeOnce({ client, system, messages, temperature, model });
149
+ }
150
+
151
+ // src/core/buckets.ts
152
+ function defaultBuckets(mode) {
153
+ return ["base", mode];
154
+ }
155
+ function isIdentified(sender) {
156
+ return typeof sender === "string" && sender.trim().length > 0;
157
+ }
158
+ function normalize(buckets) {
159
+ const out = [];
160
+ for (const raw of buckets) {
161
+ if (typeof raw !== "string") continue;
162
+ const bucket = raw.trim();
163
+ if (bucket && !out.includes(bucket)) out.push(bucket);
164
+ }
165
+ return out;
166
+ }
167
+ async function resolveBuckets({
168
+ mode,
169
+ sender,
170
+ bucketsFor
171
+ }) {
172
+ if (!bucketsFor) return void 0;
173
+ const identified = isIdentified(sender);
174
+ const requested = await bucketsFor(identified ? { mode, sender } : { mode });
175
+ if (!Array.isArray(requested)) return void 0;
176
+ const cleaned = normalize(requested);
177
+ if (identified) return cleaned;
178
+ const ceiling = new Set(defaultBuckets(mode));
179
+ return cleaned.filter((bucket) => ceiling.has(bucket));
180
+ }
181
+
182
+ // src/core/messages.ts
183
+ function lastUserMessage(messages) {
184
+ return [...messages].reverse().find((m) => m.role === "user");
185
+ }
186
+
187
+ // src/core/pipeline.ts
188
+ var MODE_SETTINGS = {
189
+ public: { topK: 6, label: "Context" },
190
+ private: { topK: 8, label: "Internal Context" }
191
+ };
192
+ async function prepareChat({
193
+ store,
194
+ prompts,
195
+ mode,
196
+ messages,
197
+ personaLayer,
198
+ channelHint,
199
+ buckets
200
+ }) {
201
+ const lastUserMsg = lastUserMessage(messages);
202
+ if (!lastUserMsg) {
203
+ throw new Error("no user message found in conversation");
204
+ }
205
+ const { topK, label } = MODE_SETTINGS[mode];
206
+ const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
207
+ const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
208
+ const hint = channelHint?.trim();
209
+ const system = [
210
+ prompts.baseSystemRules,
211
+ persona,
212
+ ...hint ? [hint] : [],
213
+ `${label}:
214
+ ${ctx.join("\n\n")}`
215
+ ].join("\n\n");
216
+ return { system, messages, context: ctx };
217
+ }
218
+
219
+ // src/channels/pipeline.ts
220
+ var DEFAULT_HISTORY_LIMIT = 20;
221
+ var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
222
+ var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
223
+ async function resolvePersonaLayer(config, ctx) {
224
+ if (!config.personaResolver) return void 0;
225
+ try {
226
+ return await config.personaResolver(ctx);
227
+ } catch {
228
+ return void 0;
229
+ }
230
+ }
231
+ function createInboundPipeline(deps, config) {
232
+ const mutedChats = /* @__PURE__ */ new Set();
233
+ const now = config.now ?? Date.now;
234
+ const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
235
+ const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
236
+ const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
237
+ const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
238
+ const mode = "public";
239
+ return async function handleInboundMessage(msg, turn) {
240
+ const allowedChats = config.allowedChats ?? [];
241
+ const action = decideChannelAction(msg, {
242
+ allowedChats,
243
+ mutedChats,
244
+ muteRegex: config.muteRegex,
245
+ unmuteRegex: config.unmuteRegex
246
+ });
247
+ if (action === "mute") {
248
+ mutedChats.add(msg.chatId);
249
+ if (config.muteReply) await turn.reply.sendGateReply(msg.chatId, config.muteReply);
250
+ return { action: "mute" };
251
+ }
252
+ if (action === "unmute") {
253
+ mutedChats.delete(msg.chatId);
254
+ if (config.unmuteReply) await turn.reply.sendGateReply(msg.chatId, config.unmuteReply);
255
+ return { action: "unmute" };
256
+ }
257
+ if (action === "ignore") {
258
+ return { action: "ignore" };
259
+ }
260
+ if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
261
+ return { action: "rate-limited" };
262
+ }
263
+ const sender = typeof turn.sender === "function" ? await turn.sender() : turn.sender ?? msg.senderId;
264
+ const conversationId = turn.conversationId ?? msg.chatId;
265
+ if (turn.intercept && await turn.intercept(sender)) {
266
+ return { action: "intercepted" };
267
+ }
268
+ const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
269
+ const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
270
+ const priorTurns = config.history ? await config.history.store.load(
271
+ conversationId,
272
+ config.history.limit ?? DEFAULT_HISTORY_LIMIT
273
+ ) : [];
274
+ const messages = [...priorTurns, { role: "user", content: msg.text }];
275
+ await config.history?.store.append(conversationId, { role: "user", content: msg.text });
276
+ const { system } = await prepareChat({
277
+ store: deps.store,
278
+ prompts: deps.prompts,
279
+ mode,
280
+ messages,
281
+ personaLayer,
282
+ channelHint: config.channelHint,
283
+ buckets
284
+ });
285
+ const { content } = await answerOnce({
286
+ answerFn: config.answerFn,
287
+ client: deps.client,
288
+ system,
289
+ messages,
290
+ mode,
291
+ sender,
292
+ conversationId,
293
+ model: config.model
294
+ });
295
+ if (!content) {
296
+ return { action: "ignore" };
297
+ }
298
+ await config.history?.store.append(conversationId, { role: "assistant", content });
299
+ await turn.reply.sendAnswer(msg.chatId, content);
300
+ return { action: "reply", content };
301
+ };
302
+ }
303
+
304
+ // src/core/logger.ts
305
+ var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
306
+ function createConsoleLogger(level = "info") {
307
+ const threshold = LEVEL_ORDER[level];
308
+ const write = (at) => (...args) => {
309
+ if (LEVEL_ORDER[at] < threshold) return;
310
+ console.error(`[${at}]`, ...args);
311
+ };
312
+ return {
313
+ debug: write("debug"),
314
+ info: write("info"),
315
+ warn: write("warn"),
316
+ error: write("error")
317
+ };
318
+ }
319
+
78
320
  // src/channels/senders.ts
79
- function createSenderRegistry() {
321
+ function createSenderRegistry(logger = createConsoleLogger()) {
80
322
  const senders = /* @__PURE__ */ new Map();
81
323
  return {
82
324
  register(name, sender) {
83
325
  if (senders.has(name)) {
84
- console.warn(`Channel sender "${name}" is already registered; overwriting.`);
326
+ logger.warn(`Channel sender "${name}" is already registered; overwriting.`);
85
327
  }
86
328
  senders.set(name, sender);
87
329
  },
@@ -98,7 +340,7 @@ function createSenderRegistry() {
98
340
  await sender.sendText(chatId, text);
99
341
  return true;
100
342
  } catch (error) {
101
- console.warn(`Channel sender "${name}" sendText failed:`, error);
343
+ logger.warn(`Channel sender "${name}" sendText failed:`, error);
102
344
  return false;
103
345
  }
104
346
  },
@@ -109,7 +351,7 @@ function createSenderRegistry() {
109
351
  await sender.sendVoice(chatId, payload);
110
352
  return true;
111
353
  } catch (error) {
112
- console.warn(`Channel sender "${name}" sendVoice failed:`, error);
354
+ logger.warn(`Channel sender "${name}" sendVoice failed:`, error);
113
355
  return false;
114
356
  }
115
357
  },
@@ -120,7 +362,7 @@ function createSenderRegistry() {
120
362
  await sender.sendMedia(chatId, payload);
121
363
  return true;
122
364
  } catch (error) {
123
- console.warn(`Channel sender "${name}" sendMedia failed:`, error);
365
+ logger.warn(`Channel sender "${name}" sendMedia failed:`, error);
124
366
  return false;
125
367
  }
126
368
  }
@@ -128,9 +370,11 @@ function createSenderRegistry() {
128
370
  }
129
371
  // Annotate the CommonJS export names for ESM import in node:
130
372
  0 && (module.exports = {
373
+ createInboundPipeline,
131
374
  createSenderRegistry,
132
375
  createSlidingWindowRateLimiter,
133
376
  decideChannelAction,
377
+ isBlockedByAllowlist,
134
378
  isEffectivelyFromSelf,
135
379
  isFromAnySession,
136
380
  underReplyRateLimit