channelkit 1.0.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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +829 -0
  3. package/config.example.yaml +37 -0
  4. package/dist/api/middleware/auth.d.ts +14 -0
  5. package/dist/api/middleware/auth.d.ts.map +1 -0
  6. package/dist/api/middleware/auth.js +130 -0
  7. package/dist/api/middleware/auth.js.map +1 -0
  8. package/dist/api/routes/config.d.ts +4 -0
  9. package/dist/api/routes/config.d.ts.map +1 -0
  10. package/dist/api/routes/config.js +794 -0
  11. package/dist/api/routes/config.js.map +1 -0
  12. package/dist/api/routes/dashboard.d.ts +4 -0
  13. package/dist/api/routes/dashboard.d.ts.map +1 -0
  14. package/dist/api/routes/dashboard.js +89 -0
  15. package/dist/api/routes/dashboard.js.map +1 -0
  16. package/dist/api/routes/inbound.d.ts +4 -0
  17. package/dist/api/routes/inbound.d.ts.map +1 -0
  18. package/dist/api/routes/inbound.js +293 -0
  19. package/dist/api/routes/inbound.js.map +1 -0
  20. package/dist/api/routes/logs.d.ts +4 -0
  21. package/dist/api/routes/logs.d.ts.map +1 -0
  22. package/dist/api/routes/logs.js +49 -0
  23. package/dist/api/routes/logs.js.map +1 -0
  24. package/dist/api/routes/mcp.d.ts +4 -0
  25. package/dist/api/routes/mcp.d.ts.map +1 -0
  26. package/dist/api/routes/mcp.js +100 -0
  27. package/dist/api/routes/mcp.js.map +1 -0
  28. package/dist/api/routes/restart.d.ts +4 -0
  29. package/dist/api/routes/restart.d.ts.map +1 -0
  30. package/dist/api/routes/restart.js +11 -0
  31. package/dist/api/routes/restart.js.map +1 -0
  32. package/dist/api/routes/send.d.ts +4 -0
  33. package/dist/api/routes/send.d.ts.map +1 -0
  34. package/dist/api/routes/send.js +66 -0
  35. package/dist/api/routes/send.js.map +1 -0
  36. package/dist/api/routes/settings.d.ts +4 -0
  37. package/dist/api/routes/settings.d.ts.map +1 -0
  38. package/dist/api/routes/settings.js +133 -0
  39. package/dist/api/routes/settings.js.map +1 -0
  40. package/dist/api/routes/tunnel.d.ts +4 -0
  41. package/dist/api/routes/tunnel.d.ts.map +1 -0
  42. package/dist/api/routes/tunnel.js +209 -0
  43. package/dist/api/routes/tunnel.js.map +1 -0
  44. package/dist/api/routes/twilio.d.ts +4 -0
  45. package/dist/api/routes/twilio.d.ts.map +1 -0
  46. package/dist/api/routes/twilio.js +138 -0
  47. package/dist/api/routes/twilio.js.map +1 -0
  48. package/dist/api/routes/update.d.ts +4 -0
  49. package/dist/api/routes/update.d.ts.map +1 -0
  50. package/dist/api/routes/update.js +42 -0
  51. package/dist/api/routes/update.js.map +1 -0
  52. package/dist/api/server.d.ts +52 -0
  53. package/dist/api/server.d.ts.map +1 -0
  54. package/dist/api/server.js +415 -0
  55. package/dist/api/server.js.map +1 -0
  56. package/dist/api/types.d.ts +61 -0
  57. package/dist/api/types.d.ts.map +1 -0
  58. package/dist/api/types.js +3 -0
  59. package/dist/api/types.js.map +1 -0
  60. package/dist/channels/base.d.ts +15 -0
  61. package/dist/channels/base.d.ts.map +1 -0
  62. package/dist/channels/base.js +20 -0
  63. package/dist/channels/base.js.map +1 -0
  64. package/dist/channels/email/gmail.d.ts +36 -0
  65. package/dist/channels/email/gmail.d.ts.map +1 -0
  66. package/dist/channels/email/gmail.js +351 -0
  67. package/dist/channels/email/gmail.js.map +1 -0
  68. package/dist/channels/email/index.d.ts +3 -0
  69. package/dist/channels/email/index.d.ts.map +1 -0
  70. package/dist/channels/email/index.js +8 -0
  71. package/dist/channels/email/index.js.map +1 -0
  72. package/dist/channels/email/resend.d.ts +29 -0
  73. package/dist/channels/email/resend.d.ts.map +1 -0
  74. package/dist/channels/email/resend.js +155 -0
  75. package/dist/channels/email/resend.js.map +1 -0
  76. package/dist/channels/endpoint/index.d.ts +21 -0
  77. package/dist/channels/endpoint/index.d.ts.map +1 -0
  78. package/dist/channels/endpoint/index.js +80 -0
  79. package/dist/channels/endpoint/index.js.map +1 -0
  80. package/dist/channels/sms/index.d.ts +37 -0
  81. package/dist/channels/sms/index.d.ts.map +1 -0
  82. package/dist/channels/sms/index.js +163 -0
  83. package/dist/channels/sms/index.js.map +1 -0
  84. package/dist/channels/telegram/index.d.ts +24 -0
  85. package/dist/channels/telegram/index.d.ts.map +1 -0
  86. package/dist/channels/telegram/index.js +231 -0
  87. package/dist/channels/telegram/index.js.map +1 -0
  88. package/dist/channels/voice/index.d.ts +62 -0
  89. package/dist/channels/voice/index.d.ts.map +1 -0
  90. package/dist/channels/voice/index.js +286 -0
  91. package/dist/channels/voice/index.js.map +1 -0
  92. package/dist/channels/whatsapp/index.d.ts +31 -0
  93. package/dist/channels/whatsapp/index.d.ts.map +1 -0
  94. package/dist/channels/whatsapp/index.js +383 -0
  95. package/dist/channels/whatsapp/index.js.map +1 -0
  96. package/dist/cli/commands/demo.d.ts +4 -0
  97. package/dist/cli/commands/demo.d.ts.map +1 -0
  98. package/dist/cli/commands/demo.js +55 -0
  99. package/dist/cli/commands/demo.js.map +1 -0
  100. package/dist/cli/commands/init.d.ts +2 -0
  101. package/dist/cli/commands/init.d.ts.map +1 -0
  102. package/dist/cli/commands/init.js +254 -0
  103. package/dist/cli/commands/init.js.map +1 -0
  104. package/dist/cli/commands/install-skill.d.ts +4 -0
  105. package/dist/cli/commands/install-skill.d.ts.map +1 -0
  106. package/dist/cli/commands/install-skill.js +60 -0
  107. package/dist/cli/commands/install-skill.js.map +1 -0
  108. package/dist/cli/commands/send.d.ts +5 -0
  109. package/dist/cli/commands/send.d.ts.map +1 -0
  110. package/dist/cli/commands/send.js +46 -0
  111. package/dist/cli/commands/send.js.map +1 -0
  112. package/dist/cli/commands/start.d.ts +5 -0
  113. package/dist/cli/commands/start.d.ts.map +1 -0
  114. package/dist/cli/commands/start.js +129 -0
  115. package/dist/cli/commands/start.js.map +1 -0
  116. package/dist/cli/helpers.d.ts +26 -0
  117. package/dist/cli/helpers.d.ts.map +1 -0
  118. package/dist/cli/helpers.js +120 -0
  119. package/dist/cli/helpers.js.map +1 -0
  120. package/dist/cli/index.d.ts +3 -0
  121. package/dist/cli/index.d.ts.map +1 -0
  122. package/dist/cli/index.js +282 -0
  123. package/dist/cli/index.js.map +1 -0
  124. package/dist/cli/wizards/channel.d.ts +4 -0
  125. package/dist/cli/wizards/channel.d.ts.map +1 -0
  126. package/dist/cli/wizards/channel.js +285 -0
  127. package/dist/cli/wizards/channel.js.map +1 -0
  128. package/dist/cli/wizards/provision.d.ts +4 -0
  129. package/dist/cli/wizards/provision.d.ts.map +1 -0
  130. package/dist/cli/wizards/provision.js +213 -0
  131. package/dist/cli/wizards/provision.js.map +1 -0
  132. package/dist/cli/wizards/service.d.ts +5 -0
  133. package/dist/cli/wizards/service.d.ts.map +1 -0
  134. package/dist/cli/wizards/service.js +212 -0
  135. package/dist/cli/wizards/service.js.map +1 -0
  136. package/dist/cli.d.ts +3 -0
  137. package/dist/cli.d.ts.map +1 -0
  138. package/dist/cli.js +6 -0
  139. package/dist/cli.js.map +1 -0
  140. package/dist/config/parser.d.ts +6 -0
  141. package/dist/config/parser.d.ts.map +1 -0
  142. package/dist/config/parser.js +37 -0
  143. package/dist/config/parser.js.map +1 -0
  144. package/dist/config/types.d.ts +170 -0
  145. package/dist/config/types.d.ts.map +1 -0
  146. package/dist/config/types.js +3 -0
  147. package/dist/config/types.js.map +1 -0
  148. package/dist/core/apiServer.d.ts +2 -0
  149. package/dist/core/apiServer.d.ts.map +1 -0
  150. package/dist/core/apiServer.js +7 -0
  151. package/dist/core/apiServer.js.map +1 -0
  152. package/dist/core/groupStore.d.ts +19 -0
  153. package/dist/core/groupStore.d.ts.map +1 -0
  154. package/dist/core/groupStore.js +48 -0
  155. package/dist/core/groupStore.js.map +1 -0
  156. package/dist/core/logger.d.ts +42 -0
  157. package/dist/core/logger.d.ts.map +1 -0
  158. package/dist/core/logger.js +142 -0
  159. package/dist/core/logger.js.map +1 -0
  160. package/dist/core/messageHandler.d.ts +15 -0
  161. package/dist/core/messageHandler.d.ts.map +1 -0
  162. package/dist/core/messageHandler.js +309 -0
  163. package/dist/core/messageHandler.js.map +1 -0
  164. package/dist/core/restart.d.ts +3 -0
  165. package/dist/core/restart.d.ts.map +1 -0
  166. package/dist/core/restart.js +35 -0
  167. package/dist/core/restart.js.map +1 -0
  168. package/dist/core/router.d.ts +56 -0
  169. package/dist/core/router.d.ts.map +1 -0
  170. package/dist/core/router.js +168 -0
  171. package/dist/core/router.js.map +1 -0
  172. package/dist/core/tunnel.d.ts +16 -0
  173. package/dist/core/tunnel.d.ts.map +1 -0
  174. package/dist/core/tunnel.js +99 -0
  175. package/dist/core/tunnel.js.map +1 -0
  176. package/dist/core/types.d.ts +54 -0
  177. package/dist/core/types.d.ts.map +1 -0
  178. package/dist/core/types.js +3 -0
  179. package/dist/core/types.js.map +1 -0
  180. package/dist/core/updater.d.ts +44 -0
  181. package/dist/core/updater.d.ts.map +1 -0
  182. package/dist/core/updater.js +264 -0
  183. package/dist/core/updater.js.map +1 -0
  184. package/dist/core/webhook.d.ts +26 -0
  185. package/dist/core/webhook.d.ts.map +1 -0
  186. package/dist/core/webhook.js +224 -0
  187. package/dist/core/webhook.js.map +1 -0
  188. package/dist/dashboard/assets/browser-D_-rzKir.js +8 -0
  189. package/dist/dashboard/assets/index-CNa084vI.js +88 -0
  190. package/dist/dashboard/assets/index-CRvIEyjF.css +1 -0
  191. package/dist/dashboard/index.html +17 -0
  192. package/dist/index.d.ts +22 -0
  193. package/dist/index.d.ts.map +1 -0
  194. package/dist/index.js +551 -0
  195. package/dist/index.js.map +1 -0
  196. package/dist/mcp/index.d.ts +3 -0
  197. package/dist/mcp/index.d.ts.map +1 -0
  198. package/dist/mcp/index.js +6 -0
  199. package/dist/mcp/index.js.map +1 -0
  200. package/dist/mcp/server.d.ts +45 -0
  201. package/dist/mcp/server.d.ts.map +1 -0
  202. package/dist/mcp/server.js +197 -0
  203. package/dist/mcp/server.js.map +1 -0
  204. package/dist/mcp/tools.d.ts +16 -0
  205. package/dist/mcp/tools.d.ts.map +1 -0
  206. package/dist/mcp/tools.js +502 -0
  207. package/dist/mcp/tools.js.map +1 -0
  208. package/dist/media/formatter.d.ts +12 -0
  209. package/dist/media/formatter.d.ts.map +1 -0
  210. package/dist/media/formatter.js +147 -0
  211. package/dist/media/formatter.js.map +1 -0
  212. package/dist/media/processor.d.ts +33 -0
  213. package/dist/media/processor.d.ts.map +1 -0
  214. package/dist/media/processor.js +145 -0
  215. package/dist/media/processor.js.map +1 -0
  216. package/dist/media/stt.d.ts +16 -0
  217. package/dist/media/stt.d.ts.map +1 -0
  218. package/dist/media/stt.js +298 -0
  219. package/dist/media/stt.js.map +1 -0
  220. package/dist/media/tts.d.ts +19 -0
  221. package/dist/media/tts.d.ts.map +1 -0
  222. package/dist/media/tts.js +135 -0
  223. package/dist/media/tts.js.map +1 -0
  224. package/dist/onboarding/index.d.ts +28 -0
  225. package/dist/onboarding/index.d.ts.map +1 -0
  226. package/dist/onboarding/index.js +144 -0
  227. package/dist/onboarding/index.js.map +1 -0
  228. package/dist/paths.d.ts +9 -0
  229. package/dist/paths.d.ts.map +1 -0
  230. package/dist/paths.js +14 -0
  231. package/dist/paths.js.map +1 -0
  232. package/dist/provisioning/twilio.d.ts +51 -0
  233. package/dist/provisioning/twilio.d.ts.map +1 -0
  234. package/dist/provisioning/twilio.js +175 -0
  235. package/dist/provisioning/twilio.js.map +1 -0
  236. package/echo-server.js +163 -0
  237. package/package.json +79 -0
@@ -0,0 +1,794 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerConfigRoutes = registerConfigRoutes;
37
+ const parser_1 = require("../../config/parser");
38
+ const whatsapp_1 = require("../../channels/whatsapp");
39
+ const paths_1 = require("../../paths");
40
+ /** Per-channel QR state for polling. */
41
+ const pairState = new Map();
42
+ /** Keys in channel configs that contain secrets and should be masked in API responses. */
43
+ const SENSITIVE_CHANNEL_KEYS = ['api_key', 'bot_token', 'auth_token', 'client_secret', 'webhook_secret', 'secret'];
44
+ function maskValue(val) {
45
+ if (val.length <= 4)
46
+ return '••••';
47
+ return '•'.repeat(val.length - 4) + val.slice(-4);
48
+ }
49
+ function maskChannelSecrets(channels) {
50
+ const masked = {};
51
+ for (const [name, ch] of Object.entries(channels)) {
52
+ const copy = { ...ch };
53
+ for (const key of SENSITIVE_CHANNEL_KEYS) {
54
+ if (typeof copy[key] === 'string' && copy[key].length > 0) {
55
+ copy[key] = maskValue(copy[key]);
56
+ }
57
+ }
58
+ masked[name] = copy;
59
+ }
60
+ return masked;
61
+ }
62
+ /** Validate a channel/service name: only alphanumeric, hyphens, and underscores allowed. */
63
+ function isValidName(name) {
64
+ return /^[a-zA-Z0-9_-]+$/.test(name);
65
+ }
66
+ function registerConfigRoutes(app, ctx) {
67
+ // GET /api/config — return channels and services from config file (secrets masked)
68
+ app.get('/api/config', (_req, res) => {
69
+ if (!ctx.configPath) {
70
+ res.status(503).json({ error: 'Config path not set' });
71
+ return;
72
+ }
73
+ try {
74
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
75
+ // Attach connection status from runtime channel instances
76
+ const channels = maskChannelSecrets(config.channels);
77
+ for (const [name, ch] of Object.entries(channels)) {
78
+ const runtime = ctx.channels.get(name);
79
+ ch.connected = runtime ? runtime.connected : false;
80
+ ch.statusMessage = runtime ? runtime.statusMessage || null : null;
81
+ }
82
+ res.json({ channels, services: config.services || {}, baileysAvailable: (0, whatsapp_1.isBaileysAvailable)() });
83
+ }
84
+ catch (err) {
85
+ res.status(500).json({ error: 'Failed to load config' });
86
+ }
87
+ });
88
+ // POST /api/config/services — add a new service
89
+ app.post('/api/config/services', (req, res) => {
90
+ if (!ctx.configPath) {
91
+ res.status(503).json({ error: 'Config path not set' });
92
+ return;
93
+ }
94
+ const { name, channel, webhook, code, command, allow_list, method, auth } = req.body;
95
+ if (!name || !channel || !webhook) {
96
+ res.status(400).json({ error: 'name, channel, and webhook are required' });
97
+ return;
98
+ }
99
+ if (!isValidName(name)) {
100
+ res.status(400).json({ error: 'Name must contain only letters, numbers, hyphens, and underscores' });
101
+ return;
102
+ }
103
+ const validMethods = ['POST', 'GET', 'PUT', 'PATCH'];
104
+ if (method && !validMethods.includes(method.toUpperCase())) {
105
+ res.status(400).json({ error: `Invalid method. Must be one of: ${validMethods.join(', ')}` });
106
+ return;
107
+ }
108
+ if (auth && !['bearer', 'header'].includes(auth.type)) {
109
+ res.status(400).json({ error: 'Invalid auth type. Must be "bearer" or "header"' });
110
+ return;
111
+ }
112
+ try {
113
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
114
+ if (!config.services)
115
+ config.services = {};
116
+ if (config.services[name]) {
117
+ res.status(409).json({ error: `Service "${name}" already exists` });
118
+ return;
119
+ }
120
+ if (!config.channels[channel]) {
121
+ res.status(400).json({ error: `Channel "${channel}" does not exist` });
122
+ return;
123
+ }
124
+ const methodUpper = method ? method.toUpperCase() : undefined;
125
+ config.services[name] = {
126
+ channel, webhook,
127
+ ...(methodUpper && methodUpper !== 'POST' && { method: methodUpper }),
128
+ ...(auth?.type && { auth }),
129
+ ...(code && { code }),
130
+ ...(command && { command }),
131
+ ...(Array.isArray(allow_list) && allow_list.length > 0 && { allow_list }),
132
+ };
133
+ (0, parser_1.saveConfig)(ctx.configPath, config);
134
+ ctx.reloadRouter?.();
135
+ ctx.broadcast({ type: 'configChanged' });
136
+ res.json({ ok: true });
137
+ }
138
+ catch (err) {
139
+ console.error('[config]', err);
140
+ res.status(500).json({ error: 'Internal server error' });
141
+ }
142
+ });
143
+ // PUT /api/config/services/:name — update service fields
144
+ app.put('/api/config/services/:name', (req, res) => {
145
+ if (!ctx.configPath) {
146
+ res.status(503).json({ error: 'Config path not set' });
147
+ return;
148
+ }
149
+ const { name } = req.params;
150
+ const { webhook, code, command, stt, tts, voice, format, allow_list, method, auth } = req.body;
151
+ if (!webhook) {
152
+ res.status(400).json({ error: 'webhook is required' });
153
+ return;
154
+ }
155
+ const validMethods = ['POST', 'GET', 'PUT', 'PATCH'];
156
+ if (method && !validMethods.includes(method.toUpperCase())) {
157
+ res.status(400).json({ error: `Invalid method. Must be one of: ${validMethods.join(', ')}` });
158
+ return;
159
+ }
160
+ if (auth && !['bearer', 'header'].includes(auth.type)) {
161
+ res.status(400).json({ error: 'Invalid auth type. Must be "bearer" or "header"' });
162
+ return;
163
+ }
164
+ const validSttProviders = ['google', 'whisper', 'deepgram'];
165
+ const validTtsProviders = ['google', 'elevenlabs', 'openai'];
166
+ const validFormatProviders = ['openai', 'anthropic', 'google'];
167
+ if (stt && !validSttProviders.includes(stt.provider)) {
168
+ res.status(400).json({ error: `Invalid STT provider. Must be one of: ${validSttProviders.join(', ')}` });
169
+ return;
170
+ }
171
+ if (tts && !validTtsProviders.includes(tts.provider)) {
172
+ res.status(400).json({ error: `Invalid TTS provider. Must be one of: ${validTtsProviders.join(', ')}` });
173
+ return;
174
+ }
175
+ if (format && format.provider && !validFormatProviders.includes(format.provider)) {
176
+ res.status(400).json({ error: `Invalid format provider. Must be one of: ${validFormatProviders.join(', ')}` });
177
+ return;
178
+ }
179
+ try {
180
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
181
+ if (!config.services?.[name]) {
182
+ res.status(404).json({ error: `Service "${name}" not found` });
183
+ return;
184
+ }
185
+ config.services[name].webhook = webhook;
186
+ const methodUpper = method ? method.toUpperCase() : undefined;
187
+ if (methodUpper && methodUpper !== 'POST') {
188
+ config.services[name].method = methodUpper;
189
+ }
190
+ else {
191
+ delete config.services[name].method;
192
+ }
193
+ if (auth?.type) {
194
+ config.services[name].auth = auth;
195
+ }
196
+ else {
197
+ delete config.services[name].auth;
198
+ }
199
+ if (code) {
200
+ config.services[name].code = code;
201
+ }
202
+ else {
203
+ delete config.services[name].code;
204
+ }
205
+ if (command) {
206
+ config.services[name].command = command;
207
+ }
208
+ else {
209
+ delete config.services[name].command;
210
+ }
211
+ if ('stt' in req.body) {
212
+ if (stt && stt.provider) {
213
+ config.services[name].stt = { provider: stt.provider };
214
+ if (stt.language)
215
+ config.services[name].stt.language = stt.language;
216
+ if (stt.alternative_languages?.length)
217
+ config.services[name].stt.alternative_languages = stt.alternative_languages;
218
+ }
219
+ else {
220
+ delete config.services[name].stt;
221
+ }
222
+ }
223
+ if ('tts' in req.body) {
224
+ if (tts && tts.provider) {
225
+ config.services[name].tts = { provider: tts.provider };
226
+ if (tts.language)
227
+ config.services[name].tts.language = tts.language;
228
+ if (tts.voice)
229
+ config.services[name].tts.voice = tts.voice;
230
+ }
231
+ else {
232
+ delete config.services[name].tts;
233
+ }
234
+ }
235
+ if ('voice' in req.body) {
236
+ if (voice && Object.keys(voice).some(k => voice[k])) {
237
+ const v = {};
238
+ if (voice.greeting)
239
+ v.greeting = voice.greeting;
240
+ if (voice.hold_message)
241
+ v.hold_message = voice.hold_message;
242
+ if (voice.hold_music)
243
+ v.hold_music = voice.hold_music;
244
+ if (voice.language)
245
+ v.language = voice.language;
246
+ if (voice.voice_name)
247
+ v.voice_name = voice.voice_name;
248
+ if (voice.max_record_seconds)
249
+ v.max_record_seconds = Number(voice.max_record_seconds);
250
+ if (voice.conversational !== undefined)
251
+ v.conversational = !!voice.conversational;
252
+ if (Object.keys(v).length > 0) {
253
+ config.services[name].voice = v;
254
+ }
255
+ else {
256
+ delete config.services[name].voice;
257
+ }
258
+ }
259
+ else {
260
+ delete config.services[name].voice;
261
+ }
262
+ }
263
+ if ('format' in req.body) {
264
+ if (format && format.provider) {
265
+ config.services[name].format = { provider: format.provider, prompt: format.prompt || '' };
266
+ if (format.model)
267
+ config.services[name].format.model = format.model;
268
+ }
269
+ else {
270
+ delete config.services[name].format;
271
+ }
272
+ }
273
+ if (Array.isArray(allow_list) && allow_list.length > 0) {
274
+ config.services[name].allow_list = allow_list;
275
+ }
276
+ else {
277
+ delete config.services[name].allow_list;
278
+ }
279
+ (0, parser_1.saveConfig)(ctx.configPath, config);
280
+ ctx.reloadRouter?.();
281
+ ctx.broadcast({ type: 'configChanged' });
282
+ res.json({ ok: true });
283
+ }
284
+ catch (err) {
285
+ console.error('[config]', err);
286
+ res.status(500).json({ error: 'Internal server error' });
287
+ }
288
+ });
289
+ // DELETE /api/config/services/:name — remove a service
290
+ app.delete('/api/config/services/:name', (req, res) => {
291
+ if (!ctx.configPath) {
292
+ res.status(503).json({ error: 'Config path not set' });
293
+ return;
294
+ }
295
+ const { name } = req.params;
296
+ try {
297
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
298
+ if (!config.services?.[name]) {
299
+ res.status(404).json({ error: `Service "${name}" not found` });
300
+ return;
301
+ }
302
+ delete config.services[name];
303
+ (0, parser_1.saveConfig)(ctx.configPath, config);
304
+ ctx.reloadRouter?.();
305
+ ctx.broadcast({ type: 'configChanged' });
306
+ res.json({ ok: true });
307
+ }
308
+ catch (err) {
309
+ console.error('[config]', err);
310
+ res.status(500).json({ error: 'Internal server error' });
311
+ }
312
+ });
313
+ // POST /api/config/channels — add a new channel
314
+ app.post('/api/config/channels', (req, res) => {
315
+ if (!ctx.configPath) {
316
+ res.status(503).json({ error: 'Config path not set' });
317
+ return;
318
+ }
319
+ const { name, allow_list, ...fields } = req.body;
320
+ if (!name || !fields.type) {
321
+ res.status(400).json({ error: 'name and type are required' });
322
+ return;
323
+ }
324
+ if (!isValidName(name)) {
325
+ res.status(400).json({ error: 'Name must contain only letters, numbers, hyphens, and underscores' });
326
+ return;
327
+ }
328
+ try {
329
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
330
+ if (config.channels[name]) {
331
+ res.status(409).json({ error: `Channel "${name}" already exists` });
332
+ return;
333
+ }
334
+ config.channels[name] = {
335
+ ...fields,
336
+ ...(Array.isArray(allow_list) && allow_list.length > 0 && { allow_list }),
337
+ };
338
+ (0, parser_1.saveConfig)(ctx.configPath, config);
339
+ ctx.broadcast({ type: 'configChanged' });
340
+ res.json({ ok: true });
341
+ }
342
+ catch (err) {
343
+ console.error('[config]', err);
344
+ res.status(500).json({ error: 'Internal server error' });
345
+ }
346
+ });
347
+ // PUT /api/config/channels/:name — update channel settings
348
+ app.put('/api/config/channels/:name', (req, res) => {
349
+ if (!ctx.configPath) {
350
+ res.status(503).json({ error: 'Config path not set' });
351
+ return;
352
+ }
353
+ const { name } = req.params;
354
+ const { unmatched, allow_list, mode } = req.body;
355
+ try {
356
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
357
+ if (!config.channels[name]) {
358
+ res.status(404).json({ error: `Channel "${name}" not found` });
359
+ return;
360
+ }
361
+ if (mode !== undefined) {
362
+ if (!['service', 'groups'].includes(mode)) {
363
+ res.status(400).json({ error: 'Mode must be "service" or "groups"' });
364
+ return;
365
+ }
366
+ config.channels[name].mode = mode;
367
+ }
368
+ if (unmatched) {
369
+ config.channels[name].unmatched = unmatched;
370
+ }
371
+ else {
372
+ delete config.channels[name].unmatched;
373
+ }
374
+ if (allow_list !== undefined) {
375
+ if (Array.isArray(allow_list) && allow_list.length > 0) {
376
+ config.channels[name].allow_list = allow_list;
377
+ }
378
+ else {
379
+ delete config.channels[name].allow_list;
380
+ }
381
+ }
382
+ (0, parser_1.saveConfig)(ctx.configPath, config);
383
+ ctx.broadcast({ type: 'configChanged' });
384
+ res.json({ ok: true });
385
+ }
386
+ catch (err) {
387
+ console.error('[config]', err);
388
+ res.status(500).json({ error: 'Internal server error' });
389
+ }
390
+ });
391
+ // PUT /api/config/channels/:name/sms-settings
392
+ app.put('/api/config/channels/:name/sms-settings', async (req, res) => {
393
+ if (!ctx.configPath) {
394
+ res.status(503).json({ error: 'Config path not set' });
395
+ return;
396
+ }
397
+ const { name } = req.params;
398
+ const { inbound_mode, poll_interval } = req.body;
399
+ if (!inbound_mode || !['polling', 'webhook'].includes(inbound_mode)) {
400
+ res.status(400).json({ error: 'inbound_mode must be "polling" or "webhook"' });
401
+ return;
402
+ }
403
+ try {
404
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
405
+ const ch = config.channels[name];
406
+ if (!ch) {
407
+ res.status(404).json({ error: `Channel "${name}" not found` });
408
+ return;
409
+ }
410
+ if (ch.type !== 'sms') {
411
+ res.status(400).json({ error: 'Not an SMS channel' });
412
+ return;
413
+ }
414
+ if (inbound_mode === 'webhook' && !ctx.publicUrl) {
415
+ res.status(400).json({ error: 'Service is not externalized. Please externalize first.' });
416
+ return;
417
+ }
418
+ if (inbound_mode === 'polling') {
419
+ ch.poll_interval = parseInt(poll_interval) || 60;
420
+ }
421
+ else {
422
+ delete ch.poll_interval;
423
+ }
424
+ (0, parser_1.saveConfig)(ctx.configPath, config);
425
+ try {
426
+ const Twilio = (await Promise.resolve().then(() => __importStar(require('twilio')))).default;
427
+ const client = Twilio(ch.account_sid, ch.auth_token);
428
+ const numbers = await client.incomingPhoneNumbers.list({ phoneNumber: ch.number, limit: 1 });
429
+ if (numbers.length > 0) {
430
+ const numberSid = numbers[0].sid;
431
+ if (inbound_mode === 'webhook') {
432
+ const webhookUrl = `${ctx.publicUrl}/inbound/twilio/${name}`;
433
+ await client.incomingPhoneNumbers(numberSid).update({ smsUrl: webhookUrl, smsMethod: 'POST' });
434
+ console.log(`📱 Updated Twilio SMS webhook to ${webhookUrl}`);
435
+ }
436
+ else {
437
+ await client.incomingPhoneNumbers(numberSid).update({ smsUrl: 'https://api.vapi.ai/twilio/sms', smsMethod: 'POST' });
438
+ console.log(`📱 Reverted Twilio SMS webhook to default`);
439
+ }
440
+ }
441
+ else {
442
+ console.warn(`⚠️ Twilio number ${ch.number} not found — webhook not updated`);
443
+ }
444
+ }
445
+ catch (twilioErr) {
446
+ console.error(`[sms-settings] Twilio webhook update failed: ${twilioErr.message}`);
447
+ }
448
+ ctx.broadcast({ type: 'configChanged' });
449
+ res.json({ ok: true });
450
+ }
451
+ catch (err) {
452
+ console.error('[config]', err);
453
+ res.status(500).json({ error: 'Internal server error' });
454
+ }
455
+ });
456
+ // PUT /api/config/channels/:name/email-settings
457
+ app.put('/api/config/channels/:name/email-settings', async (req, res) => {
458
+ if (!ctx.configPath) {
459
+ res.status(503).json({ error: 'Config path not set' });
460
+ return;
461
+ }
462
+ const { name } = req.params;
463
+ const { inbound_mode, poll_interval, from_email } = req.body;
464
+ if (!inbound_mode || !['polling', 'webhook'].includes(inbound_mode)) {
465
+ res.status(400).json({ error: 'inbound_mode must be "polling" or "webhook"' });
466
+ return;
467
+ }
468
+ try {
469
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
470
+ const ch = config.channels[name];
471
+ if (!ch) {
472
+ res.status(404).json({ error: `Channel "${name}" not found` });
473
+ return;
474
+ }
475
+ if (ch.type !== 'email' || ch.provider !== 'resend') {
476
+ res.status(400).json({ error: 'Not a Resend email channel' });
477
+ return;
478
+ }
479
+ if (inbound_mode === 'webhook' && !ctx.publicUrl) {
480
+ res.status(400).json({ error: 'Service is not externalized. Please externalize first.' });
481
+ return;
482
+ }
483
+ const apiKey = ch.api_key;
484
+ if (inbound_mode === 'webhook') {
485
+ if (ch.webhook_id) {
486
+ try {
487
+ await fetch(`https://api.resend.com/webhooks/${ch.webhook_id}`, {
488
+ method: 'DELETE',
489
+ headers: { Authorization: `Bearer ${apiKey}` },
490
+ });
491
+ }
492
+ catch (_) { }
493
+ }
494
+ const webhookUrl = `${ctx.publicUrl}/inbound/resend/${name}`;
495
+ const createRes = await fetch('https://api.resend.com/webhooks', {
496
+ method: 'POST',
497
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
498
+ body: JSON.stringify({ endpoint: webhookUrl, events: ['email.received'] }),
499
+ });
500
+ if (!createRes.ok) {
501
+ const err = await createRes.text();
502
+ res.status(502).json({ error: `Resend webhook creation failed: ${err}` });
503
+ return;
504
+ }
505
+ const result = await createRes.json();
506
+ ch.webhook_id = result.id;
507
+ if (result.signing_secret)
508
+ ch.webhook_secret = result.signing_secret;
509
+ delete ch.poll_interval;
510
+ console.log(`📬 Registered Resend inbound webhook for "${name}" → ${webhookUrl}`);
511
+ }
512
+ else {
513
+ if (ch.webhook_id) {
514
+ try {
515
+ await fetch(`https://api.resend.com/webhooks/${ch.webhook_id}`, {
516
+ method: 'DELETE',
517
+ headers: { Authorization: `Bearer ${apiKey}` },
518
+ });
519
+ console.log(`📬 Removed Resend webhook for "${name}"`);
520
+ }
521
+ catch (e) {
522
+ console.error(`[email-settings] Failed to delete Resend webhook: ${e.message}`);
523
+ }
524
+ delete ch.webhook_id;
525
+ delete ch.webhook_secret;
526
+ }
527
+ ch.poll_interval = parseInt(poll_interval) || 30;
528
+ }
529
+ if (from_email && typeof from_email === 'string') {
530
+ ch.from_email = from_email.trim();
531
+ }
532
+ (0, parser_1.saveConfig)(ctx.configPath, config);
533
+ ctx.broadcast({ type: 'configChanged' });
534
+ res.json({ ok: true });
535
+ }
536
+ catch (err) {
537
+ console.error('[config]', err);
538
+ res.status(500).json({ error: 'Internal server error' });
539
+ }
540
+ });
541
+ // GET /api/config/channels/:name/secret — return the raw secret for clipboard copy
542
+ app.get('/api/config/channels/:name/secret', (req, res) => {
543
+ if (!ctx.configPath) {
544
+ res.status(503).json({ error: 'Config path not set' });
545
+ return;
546
+ }
547
+ try {
548
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
549
+ const ch = config.channels[req.params.name];
550
+ if (!ch) {
551
+ res.status(404).json({ error: 'Channel not found' });
552
+ return;
553
+ }
554
+ res.json({ secret: ch.secret || '' });
555
+ }
556
+ catch (err) {
557
+ console.error('[config]', err);
558
+ res.status(500).json({ error: 'Internal server error' });
559
+ }
560
+ });
561
+ // DELETE /api/config/channels/:name — remove a channel and its dependent services
562
+ app.delete('/api/config/channels/:name', (req, res) => {
563
+ if (!ctx.configPath) {
564
+ res.status(503).json({ error: 'Config path not set' });
565
+ return;
566
+ }
567
+ const { name } = req.params;
568
+ try {
569
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
570
+ if (!config.channels[name]) {
571
+ res.status(404).json({ error: `Channel "${name}" not found` });
572
+ return;
573
+ }
574
+ delete config.channels[name];
575
+ if (config.services) {
576
+ for (const [svcName, svc] of Object.entries(config.services)) {
577
+ if (svc.channel === name)
578
+ delete config.services[svcName];
579
+ }
580
+ }
581
+ (0, parser_1.saveConfig)(ctx.configPath, config);
582
+ ctx.broadcast({ type: 'configChanged' });
583
+ res.json({ ok: true });
584
+ }
585
+ catch (err) {
586
+ console.error('[config]', err);
587
+ res.status(500).json({ error: 'Internal server error' });
588
+ }
589
+ });
590
+ // POST /api/config/channels/:name/reconnect — reconnect a WhatsApp channel using existing auth
591
+ app.post('/api/config/channels/:name/reconnect', async (req, res) => {
592
+ const { name } = req.params;
593
+ try {
594
+ const existing = ctx.channels.get(name);
595
+ if (!existing) {
596
+ res.status(404).json({ error: `Channel "${name}" not found or not running` });
597
+ return;
598
+ }
599
+ // Disconnect first to stop any reconnect loop
600
+ await existing.disconnect().catch(() => { });
601
+ res.json({ ok: true, message: 'Reconnecting...' });
602
+ // Re-connect with existing auth state
603
+ existing.connect().catch((err) => {
604
+ console.error(`[config] Reconnect failed for ${name}:`, err.message);
605
+ });
606
+ }
607
+ catch (err) {
608
+ console.error('[config]', err);
609
+ res.status(500).json({ error: 'Internal server error' });
610
+ }
611
+ });
612
+ // POST /api/config/channels/:name/pair — trigger WhatsApp QR pairing
613
+ app.post('/api/config/channels/:name/pair', async (req, res) => {
614
+ if (!ctx.configPath) {
615
+ res.status(503).json({ error: 'Config path not set' });
616
+ return;
617
+ }
618
+ const { name } = req.params;
619
+ try {
620
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
621
+ const ch = config.channels[name];
622
+ if (!ch) {
623
+ res.status(404).json({ error: `Channel "${name}" not found` });
624
+ return;
625
+ }
626
+ if (ch.type !== 'whatsapp') {
627
+ res.status(400).json({ error: 'Only WhatsApp channels support QR pairing' });
628
+ return;
629
+ }
630
+ // Initialize pair state for polling
631
+ pairState.set(name, { qr: null, status: 'waiting' });
632
+ const existing = ctx.channels.get(name);
633
+ if (existing) {
634
+ await existing.disconnect().catch(() => { });
635
+ }
636
+ const { rm } = await Promise.resolve().then(() => __importStar(require('fs/promises')));
637
+ const { join } = await Promise.resolve().then(() => __importStar(require('path')));
638
+ const authDir = join(paths_1.DEFAULT_AUTH_DIR, `whatsapp-${name}`);
639
+ await rm(authDir, { recursive: true, force: true });
640
+ const channel = existing || new whatsapp_1.WhatsAppChannel(name, ch);
641
+ const onQR = (qr) => {
642
+ pairState.set(name, { qr, status: 'waiting' });
643
+ };
644
+ const onConnected = () => {
645
+ pairState.set(name, { qr: null, status: 'paired' });
646
+ channel.removeListener('qr', onQR);
647
+ channel.removeListener('connected', onConnected);
648
+ if (!existing)
649
+ channel.disconnect().catch(() => { });
650
+ };
651
+ channel.on('qr', onQR);
652
+ channel.on('connected', onConnected);
653
+ channel.connect().catch((err) => {
654
+ pairState.set(name, { qr: null, status: 'error', error: err.message });
655
+ });
656
+ if (!existing) {
657
+ setTimeout(() => { channel.disconnect().catch(() => { }); }, 65000);
658
+ }
659
+ res.json({ ok: true, message: 'Pairing started.' });
660
+ }
661
+ catch (err) {
662
+ console.error('[config]', err);
663
+ res.status(500).json({ error: 'Internal server error' });
664
+ }
665
+ });
666
+ // GET /api/config/channels/:name/pair-status — poll for QR code during pairing
667
+ app.get('/api/config/channels/:name/pair-status', (_req, res) => {
668
+ const { name } = _req.params;
669
+ const state = pairState.get(name);
670
+ if (!state) {
671
+ res.json({ status: 'idle' });
672
+ return;
673
+ }
674
+ res.json(state);
675
+ });
676
+ // POST /api/config/channels/:name/gmail-auth — trigger Gmail OAuth flow
677
+ app.post('/api/config/channels/:name/gmail-auth', async (req, res) => {
678
+ if (!ctx.configPath) {
679
+ res.status(503).json({ error: 'Config path not set' });
680
+ return;
681
+ }
682
+ const { name } = req.params;
683
+ try {
684
+ const config = (0, parser_1.loadConfig)(ctx.configPath, { validate: false });
685
+ const ch = config.channels[name];
686
+ if (!ch) {
687
+ res.status(404).json({ error: `Channel "${name}" not found` });
688
+ return;
689
+ }
690
+ if (ch.type !== 'email' || ch.provider !== 'gmail') {
691
+ res.status(400).json({ error: 'Only Gmail channels support OAuth' });
692
+ return;
693
+ }
694
+ const emailConfig = ch;
695
+ const http = await Promise.resolve().then(() => __importStar(require('http')));
696
+ const { join, dirname } = await Promise.resolve().then(() => __importStar(require('path')));
697
+ const { existsSync, mkdirSync, writeFileSync } = await Promise.resolve().then(() => __importStar(require('fs')));
698
+ // Check if already authenticated
699
+ const tokenPath = join(paths_1.DEFAULT_AUTH_DIR, `gmail-${name}.json`);
700
+ if (existsSync(tokenPath)) {
701
+ try {
702
+ const tokens = JSON.parse(require('fs').readFileSync(tokenPath, 'utf-8'));
703
+ if (tokens?.refresh_token) {
704
+ res.json({ ok: true, already_authenticated: true });
705
+ ctx.broadcast({ type: 'gmail-auth-success', channel: name });
706
+ return;
707
+ }
708
+ }
709
+ catch { }
710
+ }
711
+ // Start a temporary HTTP server for the OAuth callback
712
+ const server = http.createServer(async (cbReq, cbRes) => {
713
+ const url = new URL(cbReq.url || '/', `http://localhost`);
714
+ const authCode = url.searchParams.get('code');
715
+ const error = url.searchParams.get('error');
716
+ if (error) {
717
+ cbRes.writeHead(200, { 'Content-Type': 'text/html' });
718
+ cbRes.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Authorization failed</h2><p>You can close this tab and return to the dashboard.</p></body></html>');
719
+ server.close();
720
+ ctx.broadcast({ type: 'gmail-auth-error', channel: name, error: `OAuth error: ${error}` });
721
+ return;
722
+ }
723
+ if (!authCode) {
724
+ cbRes.writeHead(400, { 'Content-Type': 'text/plain' });
725
+ cbRes.end('Missing code parameter');
726
+ return;
727
+ }
728
+ // Exchange code for tokens
729
+ try {
730
+ const addr = server.address();
731
+ const redirectUri = `http://localhost:${addr.port}`;
732
+ const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
733
+ method: 'POST',
734
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
735
+ body: new URLSearchParams({
736
+ code: authCode,
737
+ client_id: emailConfig.client_id,
738
+ client_secret: emailConfig.client_secret,
739
+ redirect_uri: redirectUri,
740
+ grant_type: 'authorization_code',
741
+ }),
742
+ });
743
+ if (!tokenRes.ok) {
744
+ const err = await tokenRes.text();
745
+ throw new Error(`Token exchange failed: ${err}`);
746
+ }
747
+ const data = await tokenRes.json();
748
+ const tokens = {
749
+ access_token: data.access_token,
750
+ refresh_token: data.refresh_token,
751
+ expiry: Date.now() + (data.expires_in * 1000),
752
+ };
753
+ // Save tokens
754
+ const dir = dirname(tokenPath);
755
+ if (!existsSync(dir))
756
+ mkdirSync(dir, { recursive: true });
757
+ writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
758
+ cbRes.writeHead(200, { 'Content-Type': 'text/html' });
759
+ cbRes.end('<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Gmail authenticated successfully!</h2><p>You can close this tab and return to the dashboard.</p></body></html>');
760
+ server.close();
761
+ ctx.broadcast({ type: 'gmail-auth-success', channel: name });
762
+ }
763
+ catch (err) {
764
+ cbRes.writeHead(200, { 'Content-Type': 'text/html' });
765
+ cbRes.end(`<html><body style="font-family:system-ui;text-align:center;padding:60px"><h2>Authentication failed</h2><p>${err.message}</p></body></html>`);
766
+ server.close();
767
+ ctx.broadcast({ type: 'gmail-auth-error', channel: name, error: err.message });
768
+ }
769
+ });
770
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
771
+ const addr = server.address();
772
+ const redirectUri = `http://localhost:${addr.port}`;
773
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
774
+ `client_id=${encodeURIComponent(emailConfig.client_id)}` +
775
+ `&redirect_uri=${encodeURIComponent(redirectUri)}` +
776
+ `&response_type=code` +
777
+ `&scope=${encodeURIComponent('https://www.googleapis.com/auth/gmail.modify')}` +
778
+ `&access_type=offline` +
779
+ `&prompt=consent`;
780
+ // Timeout after 2 minutes
781
+ setTimeout(() => {
782
+ server.close();
783
+ ctx.broadcast({ type: 'gmail-auth-error', channel: name, error: 'OAuth timeout — no callback received within 2 minutes' });
784
+ }, 120000);
785
+ res.json({ ok: true, auth_url: authUrl });
786
+ ctx.broadcast({ type: 'gmail-auth-url', channel: name, authUrl });
787
+ }
788
+ catch (err) {
789
+ console.error('[config]', err);
790
+ res.status(500).json({ error: 'Internal server error' });
791
+ }
792
+ });
793
+ }
794
+ //# sourceMappingURL=config.js.map