@vantran-se/openchamber-web 2.0.2 → 2.0.4

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 (93) hide show
  1. package/README.md +20 -28
  2. package/bin/cli.js +3 -1
  3. package/bin/cli.test.js +13 -4
  4. package/bin/lib/DOCUMENTATION.md +4 -1
  5. package/bin/lib/commands-serve.js +9 -0
  6. package/dist/assets/{CodeMirrorCommentWidgets-B82Oye3V.js → CodeMirrorCommentWidgets-DZF8ntaq.js} +1 -1
  7. package/dist/assets/{DiffView-DJLmWn6Z.js → DiffView-D2p4Ar0H.js} +1 -1
  8. package/dist/assets/{FilesView-DjksmZRo.js → FilesView-BeTtbsip.js} +1 -1
  9. package/dist/assets/{GitView-DiUsUIgr.js → GitView-DwQwqDvu.js} +1 -1
  10. package/dist/assets/{GuestAttachDialog-CYwWYLr3.js → GuestAttachDialog-CoXM0gh_.js} +1 -1
  11. package/dist/assets/{GuestSurfacePane-CmctyRI4.js → GuestSurfacePane-DIJvkkM-.js} +1 -1
  12. package/dist/assets/{JsonTreeView-DOHHcbgJ.js → JsonTreeView-Cq_27v0y.js} +1 -1
  13. package/dist/assets/{LinearIssuesView-DaqnylcV.js → LinearIssuesView-CsSSRMhn.js} +1 -1
  14. package/dist/assets/{MarkdownImageGallery-HJNvWt7L.js → MarkdownImageGallery-CY0dWTaE.js} +1 -1
  15. package/dist/assets/{MarkdownRendererImpl-nDZRGg2I.js → MarkdownRendererImpl-ILdXzLrt.js} +1 -1
  16. package/dist/assets/{OnboardingScreen-C2B5Bsof.js → OnboardingScreen-ClatMQAX.js} +1 -1
  17. package/dist/assets/{PatchDiff-C4FLBjpt.js → PatchDiff-DOwlrIK9.js} +1 -1
  18. package/dist/assets/{PierreDiffViewer-C_1S9c9n.js → PierreDiffViewer-CrcGqqD_.js} +1 -1
  19. package/dist/assets/{PlanView-_eZvb-G5.js → PlanView-BGLsMgQk.js} +1 -1
  20. package/dist/assets/{PluginPane-sM8fcZIV.js → PluginPane-B39JEUL8.js} +1 -1
  21. package/dist/assets/{SettingsView-BRfr46hb.js → SettingsView-13B2_G-A.js} +1 -1
  22. package/dist/assets/{SettingsWindow-CcUDwQHg.js → SettingsWindow-B0BAy-f9.js} +1 -1
  23. package/dist/assets/{SubmoduleDiffSummary-CrN1ZGwl.js → SubmoduleDiffSummary-fcdDzCSW.js} +1 -1
  24. package/dist/assets/{ToolOutputDialog-Cij4jI09.js → ToolOutputDialog-B7xaUz2H.js} +1 -1
  25. package/dist/assets/{ToolPartDiffPreview-BXzwQ-JY.js → ToolPartDiffPreview-CkWCaUmZ.js} +1 -1
  26. package/dist/assets/{VirtualizedFileDiff-DSWTLJNH.js → VirtualizedFileDiff-DONC4eI2.js} +1 -1
  27. package/dist/assets/{WalkthroughView-CpUUgY8H.js → WalkthroughView-CXDImOXW.js} +1 -1
  28. package/dist/assets/{appThemeRegistry-B5tHTkR8.js → appThemeRegistry-Br2TLSlb.js} +1 -1
  29. package/dist/assets/{commitAndPush-D7D8qV8p.js → commitAndPush-CcI4ol7c.js} +1 -1
  30. package/dist/assets/{computeVirtualFileMetrics-cq9uQ30i.js → computeVirtualFileMetrics-_-20umCs.js} +1 -1
  31. package/dist/assets/{de-CUO1-KqF.js → de-BtYnUgII.js} +1 -1
  32. package/dist/assets/{es-BlbC6sGJ.js → es-crP5qPye.js} +1 -1
  33. package/dist/assets/{fr-D2nwe_0W.js → fr-CLSRuALk.js} +1 -1
  34. package/dist/assets/{index-BQOoYIOu.js → index-OKDYjcMF.js} +1 -1
  35. package/dist/assets/{isDiffPlainText-D0obJ2p8.js → isDiffPlainText-D77M6q1f.js} +1 -1
  36. package/dist/assets/{isFilePlainText-DB3_rjyW.js → isFilePlainText-C54B4Mns.js} +1 -1
  37. package/dist/assets/{ja-CA7vnDsZ.js → ja-SZ1-ucLV.js} +1 -1
  38. package/dist/assets/{ko-Chy6a9l-.js → ko-HthbT_Ve.js} +1 -1
  39. package/dist/assets/{main-B9JFK3vF.js → main-BJklC7Cl.js} +4 -4
  40. package/dist/assets/{main-Bhiqnrhd.js → main-CGEvLMd2.js} +2 -2
  41. package/dist/assets/{markdownCore-tfqOFaBd.js → markdownCore-BzQuETm0.js} +1 -1
  42. package/dist/assets/{miniChat-B0UOL5zA.js → miniChat-BSxkRQLj.js} +2 -2
  43. package/dist/assets/{mobile-BJppFIEo.js → mobile-C5KRTNOB.js} +2 -2
  44. package/dist/assets/{patchFileDiff-Db6KuwCP.js → patchFileDiff-DGw2KryM.js} +1 -1
  45. package/dist/assets/{pl-CTR5A_1P.js → pl-C8eYfjL1.js} +1 -1
  46. package/dist/assets/{pt-BR-BE1O1KEx.js → pt-BR-cYOrZdxS.js} +1 -1
  47. package/dist/assets/{renderElectronMiniChatApp-Yz5sfoFh.js → renderElectronMiniChatApp-CnkAQV6o.js} +2 -2
  48. package/dist/assets/{renderMobileApp-QQ-yB8aY.js → renderMobileApp-CMxjwisj.js} +3 -3
  49. package/dist/assets/{runtimeEndpointReset-DKDdzRzm.js → runtimeEndpointReset-C45jrFq4.js} +3 -3
  50. package/dist/assets/{scheduledTasksApi-C5sCuNqf.js → scheduledTasksApi-CkK1F9s2.js} +1 -1
  51. package/dist/assets/{shikiHighlight-xURoo5t_.js → shikiHighlight-4O0ZM14D.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-BZLGuYTU.js → textMateThemeFromAppTheme-yNq7b0DZ.js} +1 -1
  53. package/dist/assets/{tr-Bh5vK6cW.js → tr-B4IQ5t-T.js} +1 -1
  54. package/dist/assets/{uk-CLDkZFy_.js → uk-DCoXG6ve.js} +1 -1
  55. package/dist/assets/{useAppFontEffects-BxrtCnfR.js → useAppFontEffects-Bzyszv2N.js} +48 -48
  56. package/dist/assets/{useInlineCommentController-CeXuR-SR.js → useInlineCommentController-zOf6ORAq.js} +1 -1
  57. package/dist/assets/{useRootScrollLock-DgWxezc1.js → useRootScrollLock-DCaZ4BWi.js} +2 -2
  58. package/dist/assets/{vendor-codemirror-language-data-DOuaQUZ0.js → vendor-codemirror-language-data-6cEFPNUR.js} +2 -2
  59. package/dist/assets/{web-components-63H1h21j.js → web-components-BEo4-ISu.js} +1 -1
  60. package/dist/assets/{zh-CN-DODgcmYj.js → zh-CN-BXBoxsFk.js} +1 -1
  61. package/dist/assets/{zh-TW-ByNfIwLA.js → zh-TW-C6oelpnL.js} +1 -1
  62. package/dist/index.html +1 -1
  63. package/dist/mini-chat.html +1 -1
  64. package/dist/mobile.html +1 -1
  65. package/package.json +1 -1
  66. package/server/index.d.ts +2 -0
  67. package/server/index.js +114 -52
  68. package/server/lib/agent-tool/DOCUMENTATION.md +47 -127
  69. package/server/lib/agent-tool/runtime.js +66 -47
  70. package/server/lib/agent-tool/runtime.test.js +2 -0
  71. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  72. package/server/lib/event-stream/global-hub.js +22 -6
  73. package/server/lib/event-stream/global-hub.test.js +93 -2
  74. package/server/lib/event-stream/rebind.test.js +1 -0
  75. package/server/lib/event-stream/runtime.js +1 -2
  76. package/server/lib/opencode/DOCUMENTATION.md +23 -23
  77. package/server/lib/opencode/lifecycle.js +126 -0
  78. package/server/lib/opencode/lifecycle.test.js +228 -1
  79. package/server/lib/opencode/network-runtime.js +16 -7
  80. package/server/lib/opencode/network-runtime.test.js +29 -1
  81. package/server/lib/opencode/proxy.js +15 -14
  82. package/server/lib/opencode/server-startup-runtime.js +25 -0
  83. package/server/lib/opencode/server-startup-runtime.test.js +67 -1
  84. package/server/lib/opencode/shared-service-runtime.js +155 -0
  85. package/server/lib/opencode/shared-service-runtime.test.js +180 -0
  86. package/server/lib/opencode/shutdown-runtime.js +11 -5
  87. package/server/lib/opencode/shutdown-runtime.test.js +77 -2
  88. package/server/lib/opencode/startup-pipeline-runtime.js +5 -4
  89. package/server/lib/opencode/startup-pipeline-runtime.test.js +18 -7
  90. package/server/lib/opencode/watcher.test.js +54 -0
  91. package/server/opencode-proxy.test.js +158 -0
  92. package/server/proxy-headers.js +8 -2
  93. package/server/proxy-headers.test.js +20 -0
@@ -3,6 +3,8 @@ import { EventEmitter } from 'node:events';
3
3
  import express from 'express';
4
4
  import path from 'path';
5
5
 
6
+ import { createOpenCodeConnectionAdapter } from './lib/opencode/lifecycle.js';
7
+ import { createOpenCodeNetworkRuntime } from './lib/opencode/network-runtime.js';
6
8
  import { createSseBoundaryTracker, registerOpenCodeProxy, writeSseChunkWithBackpressure } from './lib/opencode/proxy.js';
7
9
 
8
10
  const listen = (app, host = '127.0.0.1') => new Promise((resolve, reject) => {
@@ -26,6 +28,7 @@ const closeServer = (server) => new Promise((resolve, reject) => {
26
28
 
27
29
  describe('OpenCode proxy SSE forwarding', () => {
28
30
  let upstreamServer;
31
+ let replacementUpstreamServer;
29
32
  let proxyServer;
30
33
  const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
31
34
 
@@ -33,8 +36,10 @@ describe('OpenCode proxy SSE forwarding', () => {
33
36
  Object.defineProperty(process, 'platform', originalPlatform);
34
37
  await closeServer(proxyServer);
35
38
  await closeServer(upstreamServer);
39
+ await closeServer(replacementUpstreamServer);
36
40
  proxyServer = undefined;
37
41
  upstreamServer = undefined;
42
+ replacementUpstreamServer = undefined;
38
43
  });
39
44
 
40
45
  it('forwards event streams with nginx-safe headers', async () => {
@@ -262,6 +267,159 @@ describe('OpenCode proxy SSE forwarding', () => {
262
267
  expect(await response.json()).toEqual({ ok: true, source: 'external-host' });
263
268
  });
264
269
 
270
+ it('does not forward browser authorization when OpenCode has no auth', async () => {
271
+ let seenAuthorization = 'not-requested';
272
+ const upstream = express();
273
+ upstream.get('/api/config/providers', (req, res) => {
274
+ seenAuthorization = req.headers.authorization;
275
+ res.json({ ok: true });
276
+ });
277
+ upstreamServer = await listen(upstream);
278
+ const upstreamPort = upstreamServer.address().port;
279
+
280
+ const app = express();
281
+ registerOpenCodeProxy(app, {
282
+ fs: {},
283
+ os: {},
284
+ path,
285
+ OPEN_CODE_READY_GRACE_MS: 0,
286
+ getRuntime: () => ({
287
+ openCodePort: upstreamPort,
288
+ isOpenCodeReady: true,
289
+ openCodeNotReadySince: 0,
290
+ isRestartingOpenCode: false,
291
+ }),
292
+ getOpenCodeAuthHeaders: () => ({}),
293
+ buildOpenCodeUrl: (requestPath) => `http://127.0.0.1:${upstreamPort}${requestPath}`,
294
+ ensureOpenCodeApiPrefix: () => {},
295
+ });
296
+ proxyServer = await listen(app);
297
+
298
+ const response = await fetch(`http://127.0.0.1:${proxyServer.address().port}/api/config/providers`, {
299
+ headers: { Authorization: 'Bearer browser-secret' },
300
+ });
301
+
302
+ expect(response.status).toBe(200);
303
+ expect(seenAuthorization).toBeUndefined();
304
+ });
305
+
306
+ it('lets mixed-case service headers override browser headers', async () => {
307
+ let seenServiceHeader;
308
+ const upstream = express();
309
+ upstream.get('/api/config/providers', (req, res) => {
310
+ seenServiceHeader = req.headers['x-service-header'];
311
+ res.json({ ok: true });
312
+ });
313
+ upstreamServer = await listen(upstream);
314
+ const upstreamPort = upstreamServer.address().port;
315
+
316
+ const app = express();
317
+ registerOpenCodeProxy(app, {
318
+ fs: {},
319
+ os: {},
320
+ path,
321
+ OPEN_CODE_READY_GRACE_MS: 0,
322
+ getRuntime: () => ({
323
+ openCodePort: upstreamPort,
324
+ isOpenCodeReady: true,
325
+ openCodeNotReadySince: 0,
326
+ isRestartingOpenCode: false,
327
+ }),
328
+ getOpenCodeAuthHeaders: () => ({ 'X-Service-Header': 'trusted-value' }),
329
+ buildOpenCodeUrl: (requestPath) => `http://127.0.0.1:${upstreamPort}${requestPath}`,
330
+ ensureOpenCodeApiPrefix: () => {},
331
+ });
332
+ proxyServer = await listen(app);
333
+
334
+ const response = await fetch(`http://127.0.0.1:${proxyServer.address().port}/api/config/providers`, {
335
+ headers: { 'x-service-header': 'browser-value' },
336
+ });
337
+
338
+ expect(response.status).toBe(200);
339
+ expect(seenServiceHeader).toBe('trusted-value');
340
+ });
341
+
342
+ it('routes JSON and global events through rotated shared-service URL and auth', async () => {
343
+ const credentials = ['Basic first-secret', 'Basic replacement-secret'];
344
+ const received = [[], []];
345
+ const upstreamServers = await Promise.all(credentials.map(async (credential, index) => {
346
+ const upstream = express();
347
+ upstream.get('/api/config/providers', (req, res) => {
348
+ received[index].push({
349
+ path: req.path,
350
+ authorization: req.headers.authorization,
351
+ serviceHeader: req.headers['x-service-header'],
352
+ });
353
+ res.json({ source: index });
354
+ });
355
+ upstream.get('/api/event', (req, res) => {
356
+ received[index].push({
357
+ path: req.path,
358
+ authorization: req.headers.authorization,
359
+ serviceHeader: req.headers['x-service-header'],
360
+ });
361
+ res.type('text/event-stream').send(`data: {"source":${index}}\n\n`);
362
+ });
363
+ return listen(upstream);
364
+ }));
365
+ [upstreamServer, replacementUpstreamServer] = upstreamServers;
366
+ let active = 0;
367
+ const sharedRuntime = {
368
+ getBaseUrl: () => `http://127.0.0.1:${upstreamServers[active].address().port}`,
369
+ getHeaders: () => ({ authorization: credentials[active], 'x-service-header': `service-${active}` }),
370
+ };
371
+ const connection = createOpenCodeConnectionAdapter({ kind: 'shared-local', sharedRuntime });
372
+ const network = createOpenCodeNetworkRuntime({
373
+ state: {},
374
+ getOpenCodeBaseUrl: connection.getOpenCodeBaseUrl,
375
+ getOpenCodeAuthHeaders: connection.getOpenCodeAuthHeaders,
376
+ });
377
+ const app = express();
378
+ registerOpenCodeProxy(app, {
379
+ fs: {},
380
+ os: {},
381
+ path,
382
+ OPEN_CODE_READY_GRACE_MS: 0,
383
+ getRuntime: () => ({
384
+ openCodePort: upstreamServers[active].address().port,
385
+ isOpenCodeReady: true,
386
+ openCodeNotReadySince: 0,
387
+ isRestartingOpenCode: false,
388
+ }),
389
+ getOpenCodeAuthHeaders: network.getOpenCodeAuthHeaders,
390
+ buildOpenCodeUrl: network.buildOpenCodeUrl,
391
+ ensureOpenCodeApiPrefix: () => {},
392
+ });
393
+ proxyServer = await listen(app);
394
+ const proxyBase = `http://127.0.0.1:${proxyServer.address().port}`;
395
+
396
+ const firstJson = await fetch(`${proxyBase}/api/config/providers`);
397
+ const firstBody = await firstJson.text();
398
+ expect(JSON.parse(firstBody)).toEqual({ source: 0 });
399
+ active = 1;
400
+ const secondJson = await fetch(`${proxyBase}/api/config/providers`);
401
+ const secondBody = await secondJson.text();
402
+ const eventResponse = await fetch(`${proxyBase}/api/global/event`);
403
+ const eventBody = await eventResponse.text();
404
+
405
+ expect(JSON.parse(secondBody)).toEqual({ source: 1 });
406
+ expect(eventBody).toBe('data: {"source":1}\n\n');
407
+ expect(received).toEqual([
408
+ [{ path: '/api/config/providers', authorization: credentials[0], serviceHeader: 'service-0' }],
409
+ [
410
+ { path: '/api/config/providers', authorization: credentials[1], serviceHeader: 'service-1' },
411
+ { path: '/api/event', authorization: credentials[1], serviceHeader: 'service-1' },
412
+ ],
413
+ ]);
414
+ expect(firstJson.url).toBe(`${proxyBase}/api/config/providers`);
415
+ expect(secondJson.url).toBe(`${proxyBase}/api/config/providers`);
416
+ expect(eventResponse.url).toBe(`${proxyBase}/api/global/event`);
417
+ for (const browserValue of [firstJson.url, secondJson.url, eventResponse.url, firstBody, secondBody, eventBody]) {
418
+ expect(browserValue).not.toContain('first-secret');
419
+ expect(browserValue).not.toContain('replacement-secret');
420
+ }
421
+ });
422
+
265
423
  it('replays parsed urlencoded bodies to generic API proxy requests', async () => {
266
424
  const upstream = express();
267
425
  upstream.post('/api/form', express.urlencoded({ extended: true }), (req, res) => {
@@ -36,8 +36,14 @@ export const collectForwardProxyHeaders = (requestHeaders, authHeaders = {}) =>
36
36
  headers[normalizedKey] = Array.isArray(value) ? value.join(', ') : String(value);
37
37
  }
38
38
 
39
- if (authHeaders.Authorization) {
40
- headers.Authorization = authHeaders.Authorization;
39
+ for (const [key, value] of Object.entries(authHeaders || {})) {
40
+ if (value === undefined || value === null) continue;
41
+ const lowerKey = key.toLowerCase();
42
+ for (const existingKey of Object.keys(headers)) {
43
+ if (existingKey.toLowerCase() === lowerKey) delete headers[existingKey];
44
+ }
45
+ const normalizedKey = lowerKey === 'authorization' ? 'Authorization' : key;
46
+ headers[normalizedKey] = Array.isArray(value) ? value.join(', ') : String(value);
41
47
  }
42
48
 
43
49
  return headers;
@@ -28,6 +28,17 @@ describe('OpenCode proxy header handling', () => {
28
28
  expect(headers['authorization']).toBeUndefined();
29
29
  });
30
30
 
31
+ it('normalizes lowercase service authorization and preserves service headers', () => {
32
+ const headers = collectForwardProxyHeaders(
33
+ { authorization: 'Bearer oc_client_stale-ui-token' },
34
+ { authorization: 'Basic shared-service-token', 'x-service-header': 'service-value' },
35
+ );
36
+
37
+ expect(headers.Authorization).toBe('Basic shared-service-token');
38
+ expect(headers['authorization']).toBeUndefined();
39
+ expect(headers['x-service-header']).toBe('service-value');
40
+ });
41
+
31
42
  it('drops client authorization when upstream has no managed auth', () => {
32
43
  const headers = collectForwardProxyHeaders({
33
44
  accept: 'application/json',
@@ -39,6 +50,15 @@ describe('OpenCode proxy header handling', () => {
39
50
  expect(headers.accept).toBe('application/json');
40
51
  });
41
52
 
53
+ it('lets trusted service headers replace browser headers case-insensitively', () => {
54
+ const headers = collectForwardProxyHeaders(
55
+ { 'x-service-header': 'browser-value' },
56
+ { 'X-Service-Header': 'trusted-value' },
57
+ );
58
+
59
+ expect(headers).toEqual({ 'X-Service-Header': 'trusted-value' });
60
+ });
61
+
42
62
  it('drops content-encoding from forwarded response headers', () => {
43
63
  expect(shouldForwardProxyResponseHeader('content-encoding')).toBe(false);
44
64
  expect(shouldForwardProxyResponseHeader('Content-Encoding')).toBe(false);