@chatpanel/bridge 0.10.42 → 0.11.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.
package/src/server.js CHANGED
@@ -67,7 +67,7 @@ import {
67
67
  // Hardcoded (not read from package.json) so it survives Bun's single-file
68
68
  // --compile, where package.json isn't on a readable FS. CI fails the publish if
69
69
  // this drifts from package.json, so the two can't silently diverge.
70
- const VERSION = '0.10.42';
70
+ const VERSION = '0.11.0';
71
71
  const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
72
72
  const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
73
73
 
@@ -244,6 +244,57 @@ function ensureToken() {
244
244
  log('error', `could not initialise bridge token: ${e?.message || e}`);
245
245
  }
246
246
  }
247
+ // ---------------------------------------------------------------------------
248
+ // CHANNELS — a messaging surface (Telegram today) driving a local agent.
249
+ //
250
+ // Hosted HERE because a channel has to be running when nobody is looking: the point is to
251
+ // reach your machine from a phone with the browser closed, and the bridge is the only
252
+ // always-on local process a ChatPanel user already has. The alternative — a second daemon, or
253
+ // an npm install, or a service worker Chrome suspends — is another thing a non-technical
254
+ // person has to install and keep alive, which is the same as not shipping it.
255
+ //
256
+ // The bridge owns transport and auth; @chatpanel/channels (vendored to src/channels) owns the
257
+ // contract: verify a bot, hold its token 0600, enroll a phone by one-time code, cap it with
258
+ // `reach`, redact both ways. Loaded lazily so the redaction engine never touches a boot where
259
+ // no channel is configured.
260
+ let channelsSvc = null;
261
+ async function channelService() {
262
+ if (channelsSvc) return channelsSvc;
263
+ const { createChannelService } = await import('./channels/service.js');
264
+ channelsSvc = createChannelService({
265
+ home: join(os.homedir(), '.chatpanel'),
266
+ dataDir: join(os.homedir(), '.chatpanel', 'channels'),
267
+ // It talks to THIS bridge as a privileged local client — same port, same token it just
268
+ // read. No second address to configure and get wrong.
269
+ bridge: { baseUrl: `http://127.0.0.1:${PORT}`, token: AUTH_TOKEN },
270
+ logger: { log: (m) => log('info', m), warn: (m) => log('info', m), error: (m) => log('error', m) },
271
+ });
272
+ return channelsSvc;
273
+ }
274
+
275
+ async function handleChannels(req, res, action) {
276
+ try {
277
+ const svc = await channelService();
278
+ const body = req.method === 'POST' ? await readBody(req) : {};
279
+ if (action === 'status') return json(res, 200, await svc.status());
280
+ if (action === 'connect') {
281
+ // The token is verified with Telegram before it is written, so a typo fails HERE, in the
282
+ // settings screen, with a reason — not later as a silent poll loop nobody reads the logs of.
283
+ const r = await svc.connect(body);
284
+ return json(res, 200, { ok: true, ...r });
285
+ }
286
+ if (action === 'pair') return json(res, 200, await svc.pair());
287
+ if (action === 'unpair') return json(res, 200, await svc.unpair(String(body.actorId || '')));
288
+ if (action === 'settings') return json(res, 200, await svc.update(body));
289
+ if (action === 'disconnect') return json(res, 200, await svc.stop({ forget: !!body.forget }));
290
+ return json(res, 404, { error: 'unknown channel action' });
291
+ } catch (e) {
292
+ // A readable reason, because every one of these is something a person can fix: a bad token,
293
+ // a bot not created yet, no network.
294
+ return json(res, 400, { error: e?.message || String(e) });
295
+ }
296
+ }
297
+
247
298
  function tokenOk(req) {
248
299
  if (!AUTH_TOKEN) return false;
249
300
  const h = String(req.headers['authorization'] || '');
@@ -278,8 +329,16 @@ const PRIVILEGED_POST = new Set([
278
329
  // endpoint that touches a run is already guarded. An unauthenticated hole next to nine
279
330
  // guarded neighbours is a hole regardless of how little it grants.
280
331
  '/cancel',
332
+ // A channel is a way into this machine from the internet. Everything that configures one —
333
+ // and the code that enrolls a phone — is as privileged as /chat itself.
334
+ '/channels/connect',
335
+ '/channels/pair',
336
+ '/channels/unpair',
337
+ '/channels/settings',
338
+ '/channels/disconnect',
281
339
  ]);
282
- const PRIVILEGED_GET = new Set(['/debug']);
340
+ // /channels lists which phones may drive this machine. That is not a public reading.
341
+ const PRIVILEGED_GET = new Set(['/debug', '/channels']);
283
342
 
284
343
  // /skills* is NOT privileged, and that is a considered position rather than a
285
344
  // convenience. `privileged` adds exactly one thing over the origin allowlist: it requires
@@ -1123,6 +1182,10 @@ const server = createServer(async (req, res) => {
1123
1182
  if (req.method === 'GET') { res.writeHead(405); return res.end(); } // no server-initiated stream
1124
1183
  if (req.method === 'DELETE') { deleteSession(sid); res.writeHead(204); return res.end(); }
1125
1184
  }
1185
+ if (req.method === 'GET' && url.pathname === '/channels') return handleChannels(req, res, 'status');
1186
+ if (req.method === 'POST' && url.pathname.startsWith('/channels/')) {
1187
+ return handleChannels(req, res, url.pathname.slice('/channels/'.length));
1188
+ }
1126
1189
  if (req.method === 'POST' && url.pathname === '/cancel') return handleCancel(req, res);
1127
1190
  if (req.method === 'POST' && url.pathname === '/tool-result') return handleToolResult(req, res);
1128
1191
  if (req.method === 'POST' && url.pathname === '/mcp-local') return handleMcpLocal(req, res);
@@ -1237,6 +1300,17 @@ function startServer() {
1237
1300
  log('info', ` ${a.ok ? '✓' : '✕'} ${label}${a.ok ? '' : ' — ' + (a.reason || 'unavailable')}`);
1238
1301
  }
1239
1302
  log('info', 'Open the ChatPanel side panel; installed agents (Claude Code, Codex, Antigravity) appear automatically.');
1303
+ // A channel someone connected last week must come back by itself after a reboot — nobody
1304
+ // is at the keyboard to press start, which is the entire premise of driving this from a
1305
+ // phone. Nothing loads and nothing runs until a bot has actually been connected.
1306
+ channelService()
1307
+ .then((svc) => svc.startIfConfigured())
1308
+ .then((r) => {
1309
+ if (r?.skipped) return;
1310
+ if (r?.ok) log('info', 'channels: telegram connected — polling for messages');
1311
+ else log('error', `channels: telegram not started — ${r?.error || 'unknown error'}`);
1312
+ })
1313
+ .catch((e) => log('error', `channels: ${e?.message || e}`));
1240
1314
  });
1241
1315
  }
1242
1316