@chatpanel/bridge 0.11.0 → 0.11.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/bridge",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "type": "module",
5
5
  "description": "Local bridge that exposes the AI coding agents installed on your machine \u2014 Claude Code (CLI), Codex (CLI), and Antigravity CLI (formerly Gemini CLI, which remains available for business/enterprise) \u2014 to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
6
6
  "keywords": [
@@ -68,12 +68,15 @@ export function createChannelService({
68
68
  logger = console,
69
69
  fetchImpl = undefined, // injected in tests
70
70
  now = () => Date.now(),
71
+ // The adapter is injectable for the same reason fetchImpl is: the thing worth asserting
72
+ // about the loop is WHAT it is handed, and a real Telegram long-poll cannot be asked.
73
+ startAdapter = startTelegram,
71
74
  } = {}) {
72
75
  const tokenFile = path.join(home, 'telegram-token');
73
76
  const configFile = path.join(dataDir, 'config.json');
74
77
  const pairingFile = path.join(dataDir, 'pairing.json');
75
78
 
76
- let pairing = createPairingStore();
79
+ let pairing = null; // created by load(), then kept — see the note there
77
80
  let settings = { ...DEFAULT_SETTINGS };
78
81
  let appender = null;
79
82
  let bot = null; // { id, username, name } once verified
@@ -83,7 +86,7 @@ export function createChannelService({
83
86
  let attempt = 0;
84
87
  let stopped = true; // deliberate stop — suppresses the restart
85
88
 
86
- const savePairing = () => writeJson(pairingFile, pairing.toJSON());
89
+ const savePairing = () => writeJson(pairingFile, pairing ? pairing.toJSON() : {});
87
90
  const saveSettings = () => writeJson(configFile, settings);
88
91
 
89
92
  async function readToken() {
@@ -100,7 +103,14 @@ export function createChannelService({
100
103
 
101
104
  async function load() {
102
105
  await mkdir(dataDir, { recursive: true });
103
- pairing = createPairingStore(await readJson(pairingFile, {}), { now });
106
+ // Built ONCE and never replaced. spawnLoop() hands this exact object to the adapter, which
107
+ // holds it for the life of a polling loop — so rebuilding it here (as every service call
108
+ // used to) broke pairing in both directions at once: `pair()` minted the code into a fresh
109
+ // store the adapter could not see, so every redeem answered "unknown or expired code" no
110
+ // matter how many codes you generated; and `savePairing()` serialises whichever store this
111
+ // variable currently points at, so a redeem that DID land would have been persisted from
112
+ // the wrong object. Two aliases of one thing is the bug — there is only ever one store.
113
+ if (!pairing) pairing = createPairingStore(await readJson(pairingFile, {}), { now });
104
114
  settings = { ...DEFAULT_SETTINGS, ...(await readJson(configFile, {})) };
105
115
  if (!appender) appender = await createEventLog({ file: path.join(dataDir, 'events.jsonl'), host: 'channel' });
106
116
  }
@@ -110,7 +120,7 @@ export function createChannelService({
110
120
  function spawnLoop(botToken) {
111
121
  controller = new AbortController();
112
122
  running = true;
113
- const done = startTelegram({
123
+ const done = startAdapter({
114
124
  botToken,
115
125
  baseUrl: bridge.baseUrl,
116
126
  token: bridge.token,
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.11.0';
70
+ const VERSION = '0.11.2';
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
 
@@ -337,8 +337,19 @@ const PRIVILEGED_POST = new Set([
337
337
  '/channels/settings',
338
338
  '/channels/disconnect',
339
339
  ]);
340
- // /channels lists which phones may drive this machine. That is not a public reading.
341
- const PRIVILEGED_GET = new Set(['/debug', '/channels']);
340
+ // /channels is NOT here, for the same reason /skills is not, and it was a regression to add
341
+ // it: a privileged GET is unreachable from the extension. The panel holds `<all_urls>`, so
342
+ // its fetches bypass CORS altogether — no preflight fires — and the Fetch spec attaches
343
+ // `Origin` only to requests whose method is not GET or HEAD. That is the whole asymmetry:
344
+ // every privileged POST here works, and a privileged GET can only ever answer the settings
345
+ // page with "forbidden: this endpoint requires the ChatPanel extension or a valid bridge
346
+ // token", which is what shipped in 0.11.0.
347
+ //
348
+ // Nothing is opened up by removing it. `originAllowed` still refuses any web page (it is the
349
+ // check doing the work), so the only caller admitted is a local no-Origin process — which can
350
+ // already read ~/.chatpanel/channels/ off the disk. /debug stays privileged: it exposes
351
+ // configuration a local process cannot otherwise see.
352
+ const PRIVILEGED_GET = new Set(['/debug']);
342
353
 
343
354
  // /skills* is NOT privileged, and that is a considered position rather than a
344
355
  // convenience. `privileged` adds exactly one thing over the origin allowlist: it requires