@vantran-se/openchamber-web 2.0.3 → 2.0.5
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/README.md +20 -28
- package/bin/cli.js +3 -1
- package/bin/cli.test.js +13 -4
- package/bin/lib/DOCUMENTATION.md +4 -1
- package/bin/lib/commands-serve.js +9 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CWhdh0wf.js → CodeMirrorCommentWidgets-DCAQTLMM.js} +1 -1
- package/dist/assets/DiffView-uzNpHBlV.js +4 -0
- package/dist/assets/FilesView-Bkq0sjNx.js +17 -0
- package/dist/assets/{GitView-DKIjpnu4.js → GitView-BmibZC7n.js} +4 -4
- package/dist/assets/{GuestAttachDialog-BIa9jCfL.js → GuestAttachDialog-D7TJG1rt.js} +1 -1
- package/dist/assets/GuestSurfacePane-PAcYaFzP.js +1 -0
- package/dist/assets/{JsonTreeView-CtI1Mqwo.js → JsonTreeView-BNT2yUVu.js} +1 -1
- package/dist/assets/{LinearIssuesView-BD2zLu-r.js → LinearIssuesView-D3Azksju.js} +1 -1
- package/dist/assets/MarkdownImageGallery-BGY6tuiK.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DJmhQ37f.js +12 -0
- package/dist/assets/{OnboardingScreen-B8TxloXZ.js → OnboardingScreen-D_41H2qC.js} +1 -1
- package/dist/assets/{PatchDiff--Civs1xY.js → PatchDiff-C0RSqa9W.js} +1 -1
- package/dist/assets/{PierreDiffViewer-BlDmw3-A.js → PierreDiffViewer-BBE7tTY2.js} +3 -3
- package/dist/assets/{PlanView-DyH2SyCP.js → PlanView-Bqc-xuDe.js} +4 -4
- package/dist/assets/PluginPane-DQ0k41Q-.js +3 -0
- package/dist/assets/SettingsView-BAiJU5hJ.js +30 -0
- package/dist/assets/{SettingsWindow-y9WpZqK6.js → SettingsWindow-DuC4ecLw.js} +1 -1
- package/dist/assets/{SubmoduleDiffSummary-DeFmhPFZ.js → SubmoduleDiffSummary-0HgmcMXz.js} +1 -1
- package/dist/assets/{ToolOutputDialog-8DmF7_PZ.js → ToolOutputDialog-DdHkQJEE.js} +6 -6
- package/dist/assets/{ToolPartDiffPreview-Ca15DKE1.js → ToolPartDiffPreview-DWdN0r5K.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-BfEBad09.js → VirtualizedFileDiff-ClDezf2d.js} +1 -1
- package/dist/assets/{WalkthroughView-Cj3K8Xz6.js → WalkthroughView-BJicp0RC.js} +2 -2
- package/dist/assets/{appThemeRegistry-DAdawJZn.js → appThemeRegistry-CiuyzfSW.js} +1 -1
- package/dist/assets/changesTree-CePHbFcj.js +1 -0
- package/dist/assets/commitAndPush-DTKb6IwT.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-RF-KjfUH.js → computeVirtualFileMetrics-kM_zJNMt.js} +2 -2
- package/dist/assets/de-BMnfXm-G.js +10 -0
- package/dist/assets/es-DKEvCCL6.js +16 -0
- package/dist/assets/fr-D9fgE2xE.js +14 -0
- package/dist/assets/index-DUke1nme.css +1 -0
- package/dist/assets/{index-CL7ZQ94-.js → index-DWB8bR_g.js} +1 -1
- package/dist/assets/index-Di6zUe5h.js +41 -0
- package/dist/assets/{isDiffPlainText-D2yJ9fO6.js → isDiffPlainText-DOoIsGqs.js} +1 -1
- package/dist/assets/{isFilePlainText-BwMEq1Ey.js → isFilePlainText-bt5VHvcd.js} +1 -1
- package/dist/assets/ja-BEXad-KW.js +16 -0
- package/dist/assets/ko-DGrQ0lEe.js +16 -0
- package/dist/assets/{main-quLSA7jc.js → main-BpLQbZC2.js} +2 -2
- package/dist/assets/main-CI9DtHJv.js +896 -0
- package/dist/assets/{markdown-shiki.worker-CahLvHIq.js → markdown-shiki.worker-Bg8SoAJJ.js} +8 -5
- package/dist/assets/markdownCore-DK2KK5RW.js +7 -0
- package/dist/assets/{miniChat-D-lJJibr.js → miniChat-CIWaR8wG.js} +2 -2
- package/dist/assets/{mobile-VTtphbW6.js → mobile-CnWKEYIP.js} +2 -2
- package/dist/assets/patchFileDiff-Cqn0C0az.js +13 -0
- package/dist/assets/pl-BlhkmR5b.js +16 -0
- package/dist/assets/pt-BR-Bs96Ew9I.js +16 -0
- package/dist/assets/{renderElectronMiniChatApp-DF7ch5eM.js → renderElectronMiniChatApp-D9t0-9nV.js} +2 -2
- package/dist/assets/renderMobileApp-CW7fVs_s.js +25 -0
- package/dist/assets/runtimeEndpointReset-D0PcCb3K.js +27 -0
- package/dist/assets/scheduledTasksApi-Dahv3ws0.js +1 -0
- package/dist/assets/{shikiHighlight-_HeCCHx1.js → shikiHighlight-l-MbaNS4.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-C95XdFGj.js → textMateThemeFromAppTheme-C-kGcOpD.js} +1 -1
- package/dist/assets/tr-CehUK1M9.js +7 -0
- package/dist/assets/uk-Bno5WGNz.js +16 -0
- package/dist/assets/{useAppFontEffects-ZM9ij19-.js → useAppFontEffects-DDjuwVqi.js} +160 -156
- package/dist/assets/{useInlineCommentController-CkfxMJPy.js → useInlineCommentController-Clg8EO3A.js} +2 -2
- package/dist/assets/{useRootScrollLock-BtiAC1fj.js → useRootScrollLock-nDcRrgJ1.js} +2 -2
- package/dist/assets/{vendor-codemirror-language-data-U-lXzXNK.js → vendor-codemirror-language-data-oKF0MAwd.js} +2 -2
- package/dist/assets/vendor-opencode-client-DSZl7jSi.js +12 -0
- package/dist/assets/vendor-zod-BSoePkpP.js +39 -0
- package/dist/assets/{web-components-C583Pmfm.js → web-components-Bc1WPd4i.js} +1 -1
- package/dist/assets/zh-CN-DnAweIWo.js +16 -0
- package/dist/assets/zh-TW-BbBBK25p.js +16 -0
- package/dist/index.html +5 -5
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +3 -3
- package/server/index.d.ts +2 -0
- package/server/index.js +196 -52
- package/server/lib/agent-tool/DOCUMENTATION.md +5 -0
- package/server/lib/agent-tool/runtime.js +89 -51
- package/server/lib/agent-tool/runtime.test.js +17 -0
- package/server/lib/browser-control/DOCUMENTATION.md +14 -2
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/global-hub.js +45 -7
- package/server/lib/event-stream/global-hub.test.js +128 -2
- package/server/lib/event-stream/global-ws-bridge.js +3 -1
- package/server/lib/event-stream/rebind.test.js +1 -0
- package/server/lib/event-stream/runtime.js +1 -2
- package/server/lib/event-stream/upstream-reader.js +3 -2
- package/server/lib/git/DOCUMENTATION.md +1 -1
- package/server/lib/git/service.js +22 -0
- package/server/lib/git/service.test.js +8 -0
- package/server/lib/guests/DOCUMENTATION.md +4 -3
- package/server/lib/guests/catalog.js +19 -1
- package/server/lib/guests/catalog.test.js +22 -0
- package/server/lib/guests/html-styles.js +5 -3
- package/server/lib/guests/html-styles.test.js +3 -1
- package/server/lib/guests/routes.js +11 -3
- package/server/lib/guests/service.js +5 -0
- package/server/lib/guests/surface.js +55 -9
- package/server/lib/guests/surface.test.js +56 -6
- package/server/lib/notifications/DOCUMENTATION.md +10 -0
- package/server/lib/notifications/emit-route.js +118 -0
- package/server/lib/notifications/emit-route.test.js +112 -0
- package/server/lib/openchamber-control/actions.js +16 -2
- package/server/lib/openchamber-control/service.js +23 -0
- package/server/lib/openchamber-control/service.test.js +31 -0
- package/server/lib/openchamber-sessions/fork-inheritance.js +103 -0
- package/server/lib/openchamber-sessions/fork-inheritance.test.js +69 -0
- package/server/lib/openchamber-sessions/routes.js +11 -0
- package/server/lib/openchamber-sessions/routes.test.js +18 -0
- package/server/lib/opencode/DOCUMENTATION.md +23 -23
- package/server/lib/opencode/bootstrap-runtime.js +14 -1
- package/server/lib/opencode/compatibility.js +6 -3
- package/server/lib/opencode/compatibility.test.js +8 -0
- package/server/lib/opencode/config-entity-v2.test.js +27 -0
- package/server/lib/opencode/config-paths.test.js +12 -0
- package/server/lib/opencode/config-v2.d.ts +2 -0
- package/server/lib/opencode/config-v2.js +42 -0
- package/server/lib/opencode/config-v2.test.js +17 -0
- package/server/lib/opencode/core-routes.js +20 -4
- package/server/lib/opencode/core-routes.test.js +41 -0
- package/server/lib/opencode/env-runtime.js +1 -8
- package/server/lib/opencode/feature-routes-runtime.js +2 -1
- package/server/lib/opencode/lifecycle.js +156 -2
- package/server/lib/opencode/lifecycle.test.js +255 -1
- package/server/lib/opencode/managed-config-file.js +18 -6
- package/server/lib/opencode/managed-config-file.test.js +23 -7
- package/server/lib/opencode/mcp.js +7 -2
- package/server/lib/opencode/network-runtime.js +16 -7
- package/server/lib/opencode/network-runtime.test.js +29 -1
- package/server/lib/opencode/npm-registry-config.d.ts +1 -0
- package/server/lib/opencode/npm-registry-config.js +123 -0
- package/server/lib/opencode/npm-registry-config.test.js +167 -0
- package/server/lib/opencode/npm-registry.js +5 -6
- package/server/lib/opencode/npm-registry.test.js +30 -0
- package/server/lib/opencode/openchamber-routes.js +29 -11
- package/server/lib/opencode/openchamber-routes.test.js +18 -0
- package/server/lib/opencode/project-directory-runtime.js +8 -0
- package/server/lib/opencode/project-directory-runtime.test.js +13 -0
- package/server/lib/opencode/provider-source-route.test.js +59 -0
- package/server/lib/opencode/providers.js +3 -0
- package/server/lib/opencode/providers.test.js +62 -0
- package/server/lib/opencode/proxy.js +53 -16
- package/server/lib/opencode/routes-upgrade.test.js +1 -1
- package/server/lib/opencode/routes.js +36 -1
- package/server/lib/opencode/server-startup-runtime.js +25 -0
- package/server/lib/opencode/server-startup-runtime.test.js +67 -1
- package/server/lib/opencode/server-utils-runtime.js +5 -0
- package/server/lib/opencode/settings-helpers.js +10 -0
- package/server/lib/opencode/settings-helpers.test.js +2 -2
- package/server/lib/opencode/settings-registry.json +17 -0
- package/server/lib/opencode/settings-runtime.js +1 -0
- package/server/lib/opencode/shared-service-runtime.js +155 -0
- package/server/lib/opencode/shared-service-runtime.test.js +180 -0
- package/server/lib/opencode/shared.js +49 -4
- package/server/lib/opencode/shared.test.js +78 -1
- package/server/lib/opencode/shutdown-runtime.js +14 -5
- package/server/lib/opencode/shutdown-runtime.test.js +77 -2
- package/server/lib/opencode/skill-routes.js +28 -12
- package/server/lib/opencode/skill-routes.test.js +121 -1
- package/server/lib/opencode/startup-pipeline-runtime.js +5 -4
- package/server/lib/opencode/startup-pipeline-runtime.test.js +18 -7
- package/server/lib/opencode/upgrade-capability.js +0 -14
- package/server/lib/opencode/v1-migration-topup.js +21 -15
- package/server/lib/opencode/v1-migration-topup.test.js +14 -6
- package/server/lib/opencode/v2-install.d.ts +1 -0
- package/server/lib/opencode/v2-install.js +8 -7
- package/server/lib/opencode/v2-install.test.js +13 -4
- package/server/lib/opencode/watcher.js +3 -1
- package/server/lib/opencode/watcher.test.js +54 -0
- package/server/lib/opencode/websearch-config.js +13 -1
- package/server/lib/quota/providers/kimi.js +1 -1
- package/server/lib/realtime-proxy.js +5 -1
- package/server/lib/realtime-proxy.test.js +29 -0
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +11 -0
- package/server/lib/session-assist/DOCUMENTATION.md +17 -5
- package/server/lib/session-assist/runtime.js +19 -2
- package/server/lib/session-assist/runtime.test.js +8 -0
- package/server/lib/session-goal/DOCUMENTATION.md +11 -0
- package/server/lib/small-model/DOCUMENTATION.md +11 -10
- package/server/lib/small-model/index.js +27 -32
- package/server/lib/small-model/index.test.js +28 -16
- package/server/lib/spaces/DOCUMENTATION.md +404 -32
- package/server/lib/spaces/code-in-bait.js +145 -6
- package/server/lib/spaces/code-in.js +10 -150
- package/server/lib/spaces/code-in.test.js +7 -83
- package/server/lib/spaces/code-out.js +1819 -0
- package/server/lib/spaces/code-out.test.js +3945 -0
- package/server/lib/spaces/code-transfer.js +173 -0
- package/server/lib/spaces/command-stream.test.js +181 -0
- package/server/lib/spaces/dispatcher.js +670 -0
- package/server/lib/spaces/dispatcher.test.js +582 -0
- package/server/lib/spaces/gatekeeper-channel.js +61 -7
- package/server/lib/spaces/gatekeeper-channel.test.js +43 -7
- package/server/lib/spaces/gatekeeper-program.cjs +68 -14
- package/server/lib/spaces/gatekeeper-program.test.js +118 -3
- package/server/lib/spaces/hardening.js +20 -4
- package/server/lib/spaces/hardening.test.js +18 -3
- package/server/lib/spaces/host-git.js +7 -4
- package/server/lib/spaces/host-git.test.js +6 -2
- package/server/lib/spaces/host.js +310 -0
- package/server/lib/spaces/host.test.js +273 -0
- package/server/lib/spaces/journey.js +552 -0
- package/server/lib/spaces/journey.test.js +589 -0
- package/server/lib/spaces/layout.js +42 -8
- package/server/lib/spaces/manager.js +4 -3
- package/server/lib/spaces/manager.test.js +6 -0
- package/server/lib/spaces/places/code-out.docker.live.test.js +412 -0
- package/server/lib/spaces/places/contract-suite.js +36 -0
- package/server/lib/spaces/places/dispatcher.docker.live.test.js +296 -0
- package/server/lib/spaces/places/docker-live-support.js +27 -4
- package/server/lib/spaces/places/docker.js +33 -5
- package/server/lib/spaces/places/docker.test.js +42 -3
- package/server/lib/spaces/places/escape-suite.js +118 -14
- package/server/lib/spaces/places/fake-docker.js +3 -1
- package/server/lib/spaces/places/grants.docker.live.test.js +197 -0
- package/server/lib/spaces/places/memory-place.js +24 -1
- package/server/lib/spaces/places/registry.js +1 -1
- package/server/lib/spaces/places/server.docker.live.test.js +69 -33
- package/server/lib/spaces/places/sessions.docker.live.test.js +222 -0
- package/server/lib/spaces/routes.js +216 -0
- package/server/lib/spaces/routes.test.js +213 -0
- package/server/lib/spaces/run-command.js +261 -8
- package/server/lib/spaces/run-command.test.js +193 -3
- package/server/lib/spaces/space-events.js +153 -0
- package/server/lib/spaces/space-events.test.js +160 -0
- package/server/lib/spaces/space-opencode.js +52 -0
- package/server/lib/spaces/space-opencode.test.js +41 -0
- package/server/lib/spaces/space-records.js +150 -0
- package/server/lib/spaces/space-records.test.js +113 -0
- package/server/lib/spaces/space-sessions.js +210 -0
- package/server/lib/spaces/space-sessions.test.js +161 -0
- package/server/lib/spaces/websocket.js +329 -0
- package/server/lib/spaces/websocket.test.js +345 -0
- package/server/lib/ui-auth/ui-auth.js +8 -2
- package/server/lib/ui-auth/ui-auth.test.js +29 -0
- package/server/opencode-proxy.test.js +271 -0
- package/server/proxy-headers.js +8 -2
- package/server/proxy-headers.test.js +20 -0
- package/dist/assets/DiffView-CX3wAX2S.js +0 -4
- package/dist/assets/FilesView-OL3CON3r.js +0 -17
- package/dist/assets/GuestSurfacePane-B_cqX5I5.js +0 -1
- package/dist/assets/MarkdownImageGallery-DFiTFZgj.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DZm5sf1W.js +0 -12
- package/dist/assets/PluginPane-BibJit86.js +0 -3
- package/dist/assets/SettingsView-h-VpVO47.js +0 -29
- package/dist/assets/commitAndPush-BIPfOMXY.js +0 -1
- package/dist/assets/de-BqCNGROu.js +0 -10
- package/dist/assets/es-BTPpTRgN.js +0 -16
- package/dist/assets/fr-hxbzMdAg.js +0 -14
- package/dist/assets/index-CLcM8HKx.css +0 -1
- package/dist/assets/index-CvLgnuNI.js +0 -41
- package/dist/assets/ja-_zLwwYiy.js +0 -16
- package/dist/assets/ko-CFn3Dzhc.js +0 -16
- package/dist/assets/main-BLJaug9I.js +0 -896
- package/dist/assets/markdownCore-DS9FdwrI.js +0 -5
- package/dist/assets/patchFileDiff-D9iBcTEi.js +0 -13
- package/dist/assets/pl-DFHE7xac.js +0 -16
- package/dist/assets/pt-BR-CUjZF179.js +0 -16
- package/dist/assets/renderMobileApp-CNQhdXsi.js +0 -25
- package/dist/assets/runtimeEndpointReset-DeaERc0M.js +0 -27
- package/dist/assets/scheduledTasksApi-BD6Vx9pu.js +0 -1
- package/dist/assets/tr-BB6sA_zH.js +0 -7
- package/dist/assets/uk-CB4o8g-o.js +0 -16
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +0 -12
- package/dist/assets/vendor-zod-Gea9uBlY.js +0 -39
- package/dist/assets/zh-CN-BRyDuWhc.js +0 -16
- package/dist/assets/zh-TW-BDGbWA1Q.js +0 -16
|
@@ -2,10 +2,18 @@ import { readOpenCodeInfo, isSupportedOpenCodeVersion } from './compatibility.js
|
|
|
2
2
|
export const createOpenCodeNetworkRuntime = (deps) => {
|
|
3
3
|
const {
|
|
4
4
|
state,
|
|
5
|
-
|
|
5
|
+
getOpenCodeBaseUrl,
|
|
6
|
+
getOpenCodeAuthHeaders: readOpenCodeAuthHeaders,
|
|
6
7
|
configuredOpenCodeHostname = '127.0.0.1',
|
|
7
8
|
} = deps;
|
|
8
9
|
|
|
10
|
+
const getOpenCodeAuthHeaders = () => Object.fromEntries(
|
|
11
|
+
Object.entries(readOpenCodeAuthHeaders?.() ?? {}).map(([key, value]) => [
|
|
12
|
+
key.toLowerCase() === 'authorization' ? 'Authorization' : key,
|
|
13
|
+
value,
|
|
14
|
+
]),
|
|
15
|
+
);
|
|
16
|
+
|
|
9
17
|
const resolveConnectHostname = () => {
|
|
10
18
|
const raw = typeof configuredOpenCodeHostname === 'string' ? configuredOpenCodeHostname.trim() : '';
|
|
11
19
|
const hostname = raw || '127.0.0.1';
|
|
@@ -83,14 +91,14 @@ export const createOpenCodeNetworkRuntime = (deps) => {
|
|
|
83
91
|
};
|
|
84
92
|
|
|
85
93
|
const buildOpenCodeUrl = (path, prefixOverride) => {
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
const base = getOpenCodeBaseUrl?.() ?? state.openCodeBaseUrl;
|
|
95
|
+
if (!base && !state.openCodePort) {
|
|
96
|
+
throw new Error('OpenCode endpoint is not available');
|
|
88
97
|
}
|
|
89
98
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
90
|
-
const prefix = normalizeApiPrefix(prefixOverride
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
return `${base}${fullPath}`;
|
|
99
|
+
const prefix = normalizeApiPrefix(prefixOverride ?? '');
|
|
100
|
+
const fallback = `http://${resolveConnectHostname()}:${state.openCodePort}`;
|
|
101
|
+
return `${String(base ?? fallback).replace(/\/+$/, '')}${prefix}${normalizedPath}`;
|
|
94
102
|
};
|
|
95
103
|
|
|
96
104
|
const detectOpenCodeApiPrefix = () => {
|
|
@@ -107,6 +115,7 @@ export const createOpenCodeNetworkRuntime = (deps) => {
|
|
|
107
115
|
|
|
108
116
|
return {
|
|
109
117
|
waitForReady,
|
|
118
|
+
getOpenCodeAuthHeaders,
|
|
110
119
|
normalizeApiPrefix,
|
|
111
120
|
setDetectedOpenCodeApiPrefix,
|
|
112
121
|
buildOpenCodeUrl,
|
|
@@ -13,7 +13,8 @@ const createRuntime = (overrides = {}) => createOpenCodeNetworkRuntime({
|
|
|
13
13
|
openCodeApiDetectionTimer: null,
|
|
14
14
|
...overrides.state,
|
|
15
15
|
},
|
|
16
|
-
|
|
16
|
+
getOpenCodeBaseUrl: overrides.getOpenCodeBaseUrl,
|
|
17
|
+
getOpenCodeAuthHeaders: overrides.getOpenCodeAuthHeaders ?? (() => ({})),
|
|
17
18
|
configuredOpenCodeHostname: overrides.configuredOpenCodeHostname,
|
|
18
19
|
});
|
|
19
20
|
|
|
@@ -48,6 +49,33 @@ describe('OpenCode network runtime', () => {
|
|
|
48
49
|
expect(runtime.buildOpenCodeUrl('/provider')).toBe('http://remote.example:4096/provider');
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
it('builds each request from the current shared-service base URL', () => {
|
|
53
|
+
let baseUrl = 'http://127.0.0.1:4096';
|
|
54
|
+
const runtime = createRuntime({
|
|
55
|
+
state: { openCodePort: null },
|
|
56
|
+
getOpenCodeBaseUrl: () => baseUrl,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(runtime.buildOpenCodeUrl('/api/session')).toBe('http://127.0.0.1:4096/api/session');
|
|
60
|
+
baseUrl = 'http://127.0.0.1:5096/';
|
|
61
|
+
expect(runtime.buildOpenCodeUrl('/api/session')).toBe('http://127.0.0.1:5096/api/session');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('normalizes current service auth without dropping additional headers', () => {
|
|
65
|
+
let headers = { authorization: 'Basic first', 'x-service-header': 'one' };
|
|
66
|
+
const runtime = createRuntime({ getOpenCodeAuthHeaders: () => headers });
|
|
67
|
+
|
|
68
|
+
expect(runtime.getOpenCodeAuthHeaders()).toEqual({
|
|
69
|
+
Authorization: 'Basic first',
|
|
70
|
+
'x-service-header': 'one',
|
|
71
|
+
});
|
|
72
|
+
headers = { authorization: 'Basic replacement', 'x-service-header': 'two' };
|
|
73
|
+
expect(runtime.getOpenCodeAuthHeaders()).toEqual({
|
|
74
|
+
Authorization: 'Basic replacement',
|
|
75
|
+
'x-service-header': 'two',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
51
79
|
it('normalizes wildcard and IPv6 OpenCode bind hosts for local connects', () => {
|
|
52
80
|
expect(createRuntime({ configuredOpenCodeHostname: '0.0.0.0' }).buildOpenCodeUrl('/provider'))
|
|
53
81
|
.toBe('http://127.0.0.1:4096/provider');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function resolveNpmRegistryRequest(packageName: string): { url: string; headers: Record<string, string> };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
6
|
+
|
|
7
|
+
function configPath() {
|
|
8
|
+
const configured = [process.env.npm_config_userconfig, process.env.NPM_CONFIG_USERCONFIG]
|
|
9
|
+
.map((value) => value?.trim())
|
|
10
|
+
.find(Boolean);
|
|
11
|
+
return configured || path.join(os.homedir(), '.npmrc');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// npm's ini parser accepts `key = "value"`; the quotes are not part of the value.
|
|
15
|
+
function unquote(value) {
|
|
16
|
+
const quoted = value.length >= 2 && (value[0] === '"' || value[0] === "'") && value.at(-1) === value[0];
|
|
17
|
+
return quoted ? value.slice(1, -1) : value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function configValues() {
|
|
21
|
+
try {
|
|
22
|
+
const values = new Map();
|
|
23
|
+
for (const rawLine of fs.readFileSync(configPath(), 'utf8').split(/\r?\n/)) {
|
|
24
|
+
const line = rawLine.trim();
|
|
25
|
+
if (line === '' || line.startsWith('#') || line.startsWith(';')) continue;
|
|
26
|
+
|
|
27
|
+
const separator = line.indexOf('=');
|
|
28
|
+
if (separator < 1) continue;
|
|
29
|
+
values.set(line.slice(0, separator).trim(), unquote(line.slice(separator + 1).trim()));
|
|
30
|
+
}
|
|
31
|
+
return values;
|
|
32
|
+
} catch {
|
|
33
|
+
return new Map();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function configuredValue(values, key) {
|
|
38
|
+
const configured = values.get(key);
|
|
39
|
+
if (!configured) return configured;
|
|
40
|
+
|
|
41
|
+
return configured.replace(/\$\{([^}]+)\}/g, (_match, variable) => {
|
|
42
|
+
const value = process.env[variable];
|
|
43
|
+
if (value === undefined) throw new Error('Missing npm configuration environment variable');
|
|
44
|
+
return value;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function registryFor(packageName, values) {
|
|
49
|
+
const scope = packageName.startsWith('@') ? packageName.split('/', 1)[0] : null;
|
|
50
|
+
const scopedEnvironmentRegistry = scope && [
|
|
51
|
+
process.env[`npm_config_${scope}:registry`],
|
|
52
|
+
process.env[`NPM_CONFIG_${scope}:REGISTRY`],
|
|
53
|
+
].map((value) => value?.trim()).find(Boolean);
|
|
54
|
+
if (scopedEnvironmentRegistry) return scopedEnvironmentRegistry;
|
|
55
|
+
|
|
56
|
+
const scopedRegistry = scope && configuredValue(values, `${scope}:registry`);
|
|
57
|
+
if (scopedRegistry) return scopedRegistry;
|
|
58
|
+
|
|
59
|
+
const environmentRegistry = [process.env.npm_config_registry, process.env.NPM_CONFIG_REGISTRY]
|
|
60
|
+
.map((value) => value?.trim())
|
|
61
|
+
.find(Boolean);
|
|
62
|
+
if (environmentRegistry) return environmentRegistry;
|
|
63
|
+
|
|
64
|
+
return configuredValue(values, 'registry') || DEFAULT_NPM_REGISTRY;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function authFor(registry, values) {
|
|
68
|
+
const registryScope = `//${registry.host}${registry.pathname.replace(/\/+$/, '')}/`;
|
|
69
|
+
let bestMatch = null;
|
|
70
|
+
|
|
71
|
+
for (const key of values.keys()) {
|
|
72
|
+
const match = /^(\/\/.*\/):(?:_authToken|_auth|username|_password)$/.exec(key);
|
|
73
|
+
if (match && registryScope.startsWith(match[1]) && (!bestMatch || match[1].length > bestMatch.length)) {
|
|
74
|
+
bestMatch = match[1];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!bestMatch) return {};
|
|
79
|
+
|
|
80
|
+
const token = configuredValue(values, `${bestMatch}:_authToken`);
|
|
81
|
+
if (token) return { Authorization: `Bearer ${token}` };
|
|
82
|
+
|
|
83
|
+
const authorization = configuredValue(values, `${bestMatch}:_auth`);
|
|
84
|
+
if (authorization) return { Authorization: `Basic ${authorization}` };
|
|
85
|
+
|
|
86
|
+
const username = configuredValue(values, `${bestMatch}:username`);
|
|
87
|
+
const password = configuredValue(values, `${bestMatch}:_password`);
|
|
88
|
+
if (username && password) {
|
|
89
|
+
const decodedPassword = Buffer.from(password, 'base64').toString('utf8');
|
|
90
|
+
return { Authorization: `Basic ${Buffer.from(`${username}:${decodedPassword}`).toString('base64')}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build a credential-safe package metadata request using npm's standard user configuration.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} packageName
|
|
100
|
+
* @returns {{ url: string, headers: Record<string, string> }}
|
|
101
|
+
*/
|
|
102
|
+
export function resolveNpmRegistryRequest(packageName) {
|
|
103
|
+
const values = configValues();
|
|
104
|
+
let registry;
|
|
105
|
+
try {
|
|
106
|
+
registry = new URL(registryFor(packageName, values));
|
|
107
|
+
if (!['http:', 'https:'].includes(registry.protocol) || registry.search || registry.hash) {
|
|
108
|
+
throw new Error('invalid registry');
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error('Invalid npm registry URL');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const headers = authFor(registry, values);
|
|
115
|
+
registry.username = '';
|
|
116
|
+
registry.password = '';
|
|
117
|
+
registry.pathname = `${registry.pathname.replace(/\/+$/, '')}/`;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
url: `${registry.toString()}${encodeURIComponent(packageName).replace(/^%40/i, '@')}`,
|
|
121
|
+
headers,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
5
|
+
|
|
6
|
+
import { resolveNpmRegistryRequest } from './npm-registry-config.js';
|
|
7
|
+
|
|
8
|
+
const originalLowerRegistry = process.env.npm_config_registry;
|
|
9
|
+
const originalUpperRegistry = process.env.NPM_CONFIG_REGISTRY;
|
|
10
|
+
const originalUserConfig = process.env.NPM_CONFIG_USERCONFIG;
|
|
11
|
+
const originalLowerUserConfig = process.env.npm_config_userconfig;
|
|
12
|
+
const originalPrivateRegistry = process.env.PRIVATE_NPM_REGISTRY;
|
|
13
|
+
const originalPrivateToken = process.env.PRIVATE_NPM_TOKEN;
|
|
14
|
+
const originalScopedRegistry = process.env['npm_config_@scope:registry'];
|
|
15
|
+
let userConfigPath;
|
|
16
|
+
|
|
17
|
+
describe('npm registry configuration', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
userConfigPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-npmrc-')), '.npmrc');
|
|
20
|
+
process.env.NPM_CONFIG_USERCONFIG = userConfigPath;
|
|
21
|
+
delete process.env.npm_config_userconfig;
|
|
22
|
+
delete process.env.npm_config_registry;
|
|
23
|
+
delete process.env.NPM_CONFIG_REGISTRY;
|
|
24
|
+
delete process.env.PRIVATE_NPM_REGISTRY;
|
|
25
|
+
delete process.env.PRIVATE_NPM_TOKEN;
|
|
26
|
+
delete process.env['npm_config_@scope:registry'];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
fs.rmSync(path.dirname(userConfigPath), { recursive: true, force: true });
|
|
31
|
+
if (originalLowerRegistry === undefined) delete process.env.npm_config_registry;
|
|
32
|
+
else process.env.npm_config_registry = originalLowerRegistry;
|
|
33
|
+
if (originalUpperRegistry === undefined) delete process.env.NPM_CONFIG_REGISTRY;
|
|
34
|
+
else process.env.NPM_CONFIG_REGISTRY = originalUpperRegistry;
|
|
35
|
+
if (originalUserConfig === undefined) delete process.env.NPM_CONFIG_USERCONFIG;
|
|
36
|
+
else process.env.NPM_CONFIG_USERCONFIG = originalUserConfig;
|
|
37
|
+
if (originalLowerUserConfig === undefined) delete process.env.npm_config_userconfig;
|
|
38
|
+
else process.env.npm_config_userconfig = originalLowerUserConfig;
|
|
39
|
+
if (originalPrivateRegistry === undefined) delete process.env.PRIVATE_NPM_REGISTRY;
|
|
40
|
+
else process.env.PRIVATE_NPM_REGISTRY = originalPrivateRegistry;
|
|
41
|
+
if (originalPrivateToken === undefined) delete process.env.PRIVATE_NPM_TOKEN;
|
|
42
|
+
else process.env.PRIVATE_NPM_TOKEN = originalPrivateToken;
|
|
43
|
+
if (originalScopedRegistry === undefined) delete process.env['npm_config_@scope:registry'];
|
|
44
|
+
else process.env['npm_config_@scope:registry'] = originalScopedRegistry;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('uses the default public registry without npm configuration', () => {
|
|
48
|
+
expect(resolveNpmRegistryRequest('@scope/pkg')).toEqual({
|
|
49
|
+
url: 'https://registry.npmjs.org/@scope%2Fpkg',
|
|
50
|
+
headers: {},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('uses the user npm registry for an unscoped package', () => {
|
|
55
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/custom/npm/\n');
|
|
56
|
+
|
|
57
|
+
expect(resolveNpmRegistryRequest('private-plugin')).toEqual({
|
|
58
|
+
url: 'https://mirror.example.com/custom/npm/private-plugin',
|
|
59
|
+
headers: {},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('prefers a scoped user registry', () => {
|
|
64
|
+
fs.writeFileSync(userConfigPath, [
|
|
65
|
+
'registry=https://mirror.example.com/npm/',
|
|
66
|
+
'@scope:registry=https://packages.example.com/npm/',
|
|
67
|
+
].join('\n'));
|
|
68
|
+
|
|
69
|
+
expect(resolveNpmRegistryRequest('@scope/plugin').url).toBe('https://packages.example.com/npm/@scope%2Fplugin');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('prefers a scoped user registry over an inherited general registry', () => {
|
|
73
|
+
fs.writeFileSync(userConfigPath, '@scope:registry=https://packages.example.com/npm/\n');
|
|
74
|
+
process.env.NPM_CONFIG_REGISTRY = 'https://inherited.example.com/npm/';
|
|
75
|
+
|
|
76
|
+
expect(resolveNpmRegistryRequest('@scope/plugin').url).toBe('https://packages.example.com/npm/@scope%2Fplugin');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('prefers a scoped inherited registry over user npm configuration', () => {
|
|
80
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/npm/\n');
|
|
81
|
+
process.env['npm_config_@scope:registry'] = 'https://packages.example.com/npm/';
|
|
82
|
+
|
|
83
|
+
expect(resolveNpmRegistryRequest('@scope/plugin').url).toBe('https://packages.example.com/npm/@scope%2Fplugin');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('prefers an inherited npm registry over user configuration', () => {
|
|
87
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/npm/\n');
|
|
88
|
+
process.env.NPM_CONFIG_REGISTRY = 'https://inherited.example.com/npm/';
|
|
89
|
+
|
|
90
|
+
expect(resolveNpmRegistryRequest('plugin').url).toBe('https://inherited.example.com/npm/plugin');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('uses a matching npm token without exposing it in the request URL', () => {
|
|
94
|
+
fs.writeFileSync(userConfigPath, [
|
|
95
|
+
'registry=https://packages.example.com/npm/',
|
|
96
|
+
'//packages.example.com/npm/:_authToken=private-token',
|
|
97
|
+
].join('\n'));
|
|
98
|
+
|
|
99
|
+
const request = resolveNpmRegistryRequest('private-plugin');
|
|
100
|
+
|
|
101
|
+
expect(request.url).toBe('https://packages.example.com/npm/private-plugin');
|
|
102
|
+
expect(request.url).not.toContain('private-token');
|
|
103
|
+
expect(request.headers).toEqual({ Authorization: 'Bearer private-token' });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('uses npm basic authentication with its base64-encoded password', () => {
|
|
107
|
+
fs.writeFileSync(userConfigPath, [
|
|
108
|
+
'registry=https://packages.example.com/npm/',
|
|
109
|
+
'//packages.example.com/npm/:username=registry-user',
|
|
110
|
+
'//packages.example.com/npm/:_password=c2VjcmV0',
|
|
111
|
+
].join('\n'));
|
|
112
|
+
|
|
113
|
+
expect(resolveNpmRegistryRequest('private-plugin').headers).toEqual({
|
|
114
|
+
Authorization: 'Basic cmVnaXN0cnktdXNlcjpzZWNyZXQ=',
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('expands environment variables in user npm configuration', () => {
|
|
119
|
+
process.env.PRIVATE_NPM_TOKEN = 'expanded-token';
|
|
120
|
+
fs.writeFileSync(userConfigPath, [
|
|
121
|
+
'registry=${PRIVATE_NPM_REGISTRY}',
|
|
122
|
+
'//packages.example.com/npm/:_authToken=${PRIVATE_NPM_TOKEN}',
|
|
123
|
+
].join('\n'));
|
|
124
|
+
process.env.PRIVATE_NPM_REGISTRY = 'https://packages.example.com/npm/';
|
|
125
|
+
|
|
126
|
+
expect(resolveNpmRegistryRequest('private-plugin')).toEqual({
|
|
127
|
+
url: 'https://packages.example.com/npm/private-plugin',
|
|
128
|
+
headers: { Authorization: 'Bearer expanded-token' },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('reads quoted values the way npm does', () => {
|
|
133
|
+
fs.writeFileSync(userConfigPath, [
|
|
134
|
+
'@scope:registry = "https://packages.example.com/npm/"',
|
|
135
|
+
"//packages.example.com/npm/:_authToken='quoted-token'",
|
|
136
|
+
].join('\n'));
|
|
137
|
+
|
|
138
|
+
expect(resolveNpmRegistryRequest('@scope/plugin')).toEqual({
|
|
139
|
+
url: 'https://packages.example.com/npm/@scope%2Fplugin',
|
|
140
|
+
headers: { Authorization: 'Bearer quoted-token' },
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('honors the lowercase userconfig variable npm sets for child processes', () => {
|
|
145
|
+
delete process.env.NPM_CONFIG_USERCONFIG;
|
|
146
|
+
process.env.npm_config_userconfig = userConfigPath;
|
|
147
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/npm/\n');
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
expect(resolveNpmRegistryRequest('plugin').url).toBe('https://mirror.example.com/npm/plugin');
|
|
151
|
+
} finally {
|
|
152
|
+
delete process.env.npm_config_userconfig;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('rejects unresolved npm configuration environment variables', () => {
|
|
157
|
+
fs.writeFileSync(userConfigPath, 'registry=${MISSING_NPM_REGISTRY}\n');
|
|
158
|
+
|
|
159
|
+
expect(() => resolveNpmRegistryRequest('private-plugin')).toThrow('Invalid npm registry URL');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('rejects malformed registry values without exposing their contents', () => {
|
|
163
|
+
fs.writeFileSync(userConfigPath, 'registry=not-a-url-private-token\n');
|
|
164
|
+
|
|
165
|
+
expect(() => resolveNpmRegistryRequest('plugin')).toThrow('Invalid npm registry URL');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -2,9 +2,10 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
|
|
5
|
+
import { resolveNpmRegistryRequest } from './npm-registry-config.js';
|
|
6
|
+
|
|
5
7
|
const NPM_CACHE_TTL_MS = 3_600_000;
|
|
6
8
|
const NPM_FETCH_TIMEOUT_MS = 5_000;
|
|
7
|
-
const NPM_REGISTRY_BASE = 'https://registry.npmjs.org';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {Object} NpmPackagePayload
|
|
@@ -50,10 +51,6 @@ function _getUserAgent() {
|
|
|
50
51
|
return _userAgent;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function encodeName(name) {
|
|
54
|
-
return encodeURIComponent(name).replace(/^%40/, '@');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
54
|
function parseDistTags(value) {
|
|
58
55
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
59
56
|
return {};
|
|
@@ -87,10 +84,12 @@ function cacheResult(name, payload) {
|
|
|
87
84
|
*/
|
|
88
85
|
export async function lookupNpmPackage(name) {
|
|
89
86
|
try {
|
|
90
|
-
const
|
|
87
|
+
const request = resolveNpmRegistryRequest(name);
|
|
88
|
+
const response = await fetch(request.url, {
|
|
91
89
|
headers: {
|
|
92
90
|
'User-Agent': _getUserAgent(),
|
|
93
91
|
Accept: 'application/json',
|
|
92
|
+
...request.headers,
|
|
94
93
|
},
|
|
95
94
|
signal: AbortSignal.timeout(NPM_FETCH_TIMEOUT_MS),
|
|
96
95
|
});
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
2
5
|
import * as npm from './npm-registry.js';
|
|
3
6
|
|
|
4
7
|
const originalFetch = globalThis.fetch;
|
|
5
8
|
const originalDateNow = Date.now;
|
|
9
|
+
const originalUserConfig = process.env.NPM_CONFIG_USERCONFIG;
|
|
10
|
+
const originalLowerUserConfig = process.env.npm_config_userconfig;
|
|
11
|
+
const originalLowerRegistry = process.env.npm_config_registry;
|
|
12
|
+
const originalUpperRegistry = process.env.NPM_CONFIG_REGISTRY;
|
|
6
13
|
|
|
7
14
|
let fetchMock;
|
|
15
|
+
let userConfigPath;
|
|
8
16
|
|
|
9
17
|
function jsonResponse(body, status = 200) {
|
|
10
18
|
return Promise.resolve(new Response(JSON.stringify(body), {
|
|
@@ -17,6 +25,11 @@ describe('npm registry client', () => {
|
|
|
17
25
|
beforeEach(() => {
|
|
18
26
|
npm.clearCache();
|
|
19
27
|
Date.now = originalDateNow;
|
|
28
|
+
userConfigPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-npmrc-')), '.npmrc');
|
|
29
|
+
process.env.NPM_CONFIG_USERCONFIG = userConfigPath;
|
|
30
|
+
delete process.env.npm_config_userconfig;
|
|
31
|
+
delete process.env.npm_config_registry;
|
|
32
|
+
delete process.env.NPM_CONFIG_REGISTRY;
|
|
20
33
|
fetchMock = mock(() => jsonResponse({}));
|
|
21
34
|
globalThis.fetch = fetchMock;
|
|
22
35
|
});
|
|
@@ -25,6 +38,15 @@ describe('npm registry client', () => {
|
|
|
25
38
|
npm.clearCache();
|
|
26
39
|
globalThis.fetch = originalFetch;
|
|
27
40
|
Date.now = originalDateNow;
|
|
41
|
+
fs.rmSync(path.dirname(userConfigPath), { recursive: true, force: true });
|
|
42
|
+
if (originalUserConfig === undefined) delete process.env.NPM_CONFIG_USERCONFIG;
|
|
43
|
+
else process.env.NPM_CONFIG_USERCONFIG = originalUserConfig;
|
|
44
|
+
if (originalLowerUserConfig === undefined) delete process.env.npm_config_userconfig;
|
|
45
|
+
else process.env.npm_config_userconfig = originalLowerUserConfig;
|
|
46
|
+
if (originalLowerRegistry === undefined) delete process.env.npm_config_registry;
|
|
47
|
+
else process.env.npm_config_registry = originalLowerRegistry;
|
|
48
|
+
if (originalUpperRegistry === undefined) delete process.env.NPM_CONFIG_REGISTRY;
|
|
49
|
+
else process.env.NPM_CONFIG_REGISTRY = originalUpperRegistry;
|
|
28
50
|
});
|
|
29
51
|
|
|
30
52
|
test('200 success returns latest versions and dist tags', async () => {
|
|
@@ -171,6 +193,14 @@ describe('npm registry client', () => {
|
|
|
171
193
|
expect(fetchMock.mock.calls[0][0]).toBe('https://registry.npmjs.org/@scope%2Fpkg');
|
|
172
194
|
});
|
|
173
195
|
|
|
196
|
+
test('user npm configuration changes the package metadata registry', async () => {
|
|
197
|
+
fs.writeFileSync(userConfigPath, 'registry=https://mirror.example.com/npm/\n');
|
|
198
|
+
|
|
199
|
+
await npm.getNpmInfo('private-plugin');
|
|
200
|
+
|
|
201
|
+
expect(fetchMock.mock.calls[0][0]).toBe('https://mirror.example.com/npm/private-plugin');
|
|
202
|
+
});
|
|
203
|
+
|
|
174
204
|
test('user-agent header is present', async () => {
|
|
175
205
|
await npm.getNpmInfo('foo');
|
|
176
206
|
|
|
@@ -71,6 +71,26 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
71
71
|
|
|
72
72
|
let desktopRestartError = null;
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* How this server was launched, read from its instance file. A foreground
|
|
76
|
+
* server outside systemd has no process that can restart it after an
|
|
77
|
+
* install, so it cannot update itself.
|
|
78
|
+
*/
|
|
79
|
+
const readLaunchState = async () => {
|
|
80
|
+
const currentPort = server.address()?.port || 3000;
|
|
81
|
+
const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
|
|
82
|
+
let storedOptions = { port: currentPort, daemon: true };
|
|
83
|
+
try {
|
|
84
|
+
const content = await fs.promises.readFile(instanceFilePath, 'utf8');
|
|
85
|
+
storedOptions = JSON.parse(content);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
|
|
89
|
+
const isForegroundService = launchMode === 'foreground';
|
|
90
|
+
const systemdServiceUnit = isForegroundService ? resolveSystemdServiceUnit(process.env) : null;
|
|
91
|
+
return { storedOptions, launchMode, isForegroundService, systemdServiceUnit };
|
|
92
|
+
};
|
|
93
|
+
|
|
74
94
|
app.get('/api/openchamber/update-check', async (req, res) => {
|
|
75
95
|
try {
|
|
76
96
|
const parseString = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
|
@@ -121,6 +141,14 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
121
141
|
} else {
|
|
122
142
|
const { checkForUpdates } = await import('../package-manager.js');
|
|
123
143
|
updateInfo = await checkForUpdates(updateRequest);
|
|
144
|
+
// Tell clients up front that the install route will refuse, so they
|
|
145
|
+
// show the manual command instead of an Update button that fails.
|
|
146
|
+
if (updateInfo?.available && updateRequest.appType === 'web') {
|
|
147
|
+
const { isForegroundService, systemdServiceUnit } = await readLaunchState();
|
|
148
|
+
if (isForegroundService && !systemdServiceUnit) {
|
|
149
|
+
updateInfo = { ...updateInfo, installBlocked: 'service-manager' };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
124
152
|
}
|
|
125
153
|
res.json(updateInfo);
|
|
126
154
|
} catch (error) {
|
|
@@ -215,17 +243,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
|
|
215
243
|
return;
|
|
216
244
|
}
|
|
217
245
|
|
|
218
|
-
const
|
|
219
|
-
const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
|
|
220
|
-
let storedOptions = { port: currentPort, daemon: true };
|
|
221
|
-
try {
|
|
222
|
-
const content = await fs.promises.readFile(instanceFilePath, 'utf8');
|
|
223
|
-
storedOptions = JSON.parse(content);
|
|
224
|
-
} catch {
|
|
225
|
-
}
|
|
226
|
-
const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
|
|
227
|
-
const isForegroundService = launchMode === 'foreground';
|
|
228
|
-
const systemdServiceUnit = isForegroundService ? resolveSystemdServiceUnit(process.env) : null;
|
|
246
|
+
const { storedOptions, launchMode, isForegroundService, systemdServiceUnit } = await readLaunchState();
|
|
229
247
|
|
|
230
248
|
if (isForegroundService) {
|
|
231
249
|
if (!systemdServiceUnit) {
|
|
@@ -203,6 +203,24 @@ describe('OpenChamber desktop host update route', () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
describe('OpenChamber foreground update route', () => {
|
|
206
|
+
it('marks an available update as blocked when the foreground server has no service manager', async () => {
|
|
207
|
+
const { app } = createApp();
|
|
208
|
+
|
|
209
|
+
const response = await request(app).get('/api/openchamber/update-check?appType=web').expect(200);
|
|
210
|
+
|
|
211
|
+
expect(response.body).toMatchObject({ available: true, installBlocked: 'service-manager' });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('leaves the update installable for a systemd-owned or daemon server', async () => {
|
|
215
|
+
const systemd = createApp({ environment: { INVOCATION_ID: 'systemd-invocation' } });
|
|
216
|
+
const daemon = createApp({ storedOptions: { launchMode: 'daemon' } });
|
|
217
|
+
|
|
218
|
+
for (const { app } of [systemd, daemon]) {
|
|
219
|
+
const response = await request(app).get('/api/openchamber/update-check?appType=web').expect(200);
|
|
220
|
+
expect(response.body.installBlocked).toBeUndefined();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
206
224
|
it('rejects a foreground update when the server is not owned by systemd', async () => {
|
|
207
225
|
const { app } = createApp();
|
|
208
226
|
|
|
@@ -16,6 +16,10 @@ export const createProjectDirectoryRuntime = (dependencies) => {
|
|
|
16
16
|
readSettingsFromDiskMigrated,
|
|
17
17
|
getReadSettingsFromDiskMigrated,
|
|
18
18
|
sanitizeProjects,
|
|
19
|
+
// `refuseDirectory(candidate)` answers the reason a directory may not be used on this host,
|
|
20
|
+
// or null. It runs before the path is looked at, so a refused directory is never touched and
|
|
21
|
+
// never falls back to another one. The isolated-spaces host refuses `/spaces/...` with it.
|
|
22
|
+
refuseDirectory = () => null,
|
|
19
23
|
} = dependencies;
|
|
20
24
|
const realpathCache = createRealpathCache({
|
|
21
25
|
realpath: fsPromises.realpath.bind(fsPromises),
|
|
@@ -38,6 +42,10 @@ export const createProjectDirectoryRuntime = (dependencies) => {
|
|
|
38
42
|
if (!resolved) {
|
|
39
43
|
return { ok: false, error: 'Directory parameter is required' };
|
|
40
44
|
}
|
|
45
|
+
const refusal = refuseDirectory(resolved);
|
|
46
|
+
if (typeof refusal === 'string' && refusal.length > 0) {
|
|
47
|
+
return { ok: false, error: refusal };
|
|
48
|
+
}
|
|
41
49
|
try {
|
|
42
50
|
const stats = await fsPromises.stat(resolved);
|
|
43
51
|
if (!stats.isDirectory()) {
|
|
@@ -42,6 +42,19 @@ describe('project directory runtime', () => {
|
|
|
42
42
|
expect(result).toEqual({ ok: true, directory: '/real/path/to/project', requestedDirectory: '/symlink/path/to/project' });
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it('refuses a directory the host says no to, before it looks at the disk', async () => {
|
|
46
|
+
let statCalls = 0;
|
|
47
|
+
const runtime = createTestRuntime({
|
|
48
|
+
fsPromises: { stat: async () => { statCalls += 1; return { isDirectory: () => true }; }, realpath: async (p) => p },
|
|
49
|
+
refuseDirectory: (candidate) => (candidate.startsWith('/spaces/') ? 'A directory under /spaces/ belongs to an isolated space' : null),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(await runtime.validateDirectoryPath('/spaces/a1b2c3d4e5f6/repo')).toEqual({ ok: false, error: 'A directory under /spaces/ belongs to an isolated space' });
|
|
53
|
+
expect(statCalls).toBe(0);
|
|
54
|
+
expect(await runtime.validateDirectoryPath('/home/user/project')).toMatchObject({ ok: true });
|
|
55
|
+
expect(statCalls).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
45
58
|
it('returns error when candidate is empty', async () => {
|
|
46
59
|
const runtime = createTestRuntime();
|
|
47
60
|
const result = await runtime.validateDirectoryPath('');
|