@openchambery/web 1.17.1-beta.7 → 1.17.1-beta.8
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 +3 -39
- package/bin/cli-output.js +0 -16
- package/bin/cli.js +3 -66
- package/bin/cli.test.js +4 -81
- package/bin/lib/DOCUMENTATION.md +5 -19
- package/bin/lib/cli-args.js +3 -327
- package/bin/lib/cli-http.js +0 -16
- package/bin/lib/cli-lifecycle.js +2 -111
- package/bin/lib/cli-paths.js +0 -57
- package/dist/assets/{AgentAvatar-htFrIvQx.js → AgentAvatar-C6Hxu5_o.js} +1 -1
- package/dist/assets/{AssistantView-BZiJsNuI.js → AssistantView-ZlXc0xFT.js} +1 -1
- package/dist/assets/{ChatContainer-CdmcMnL3.js → ChatContainer-BXFy3Uu2.js} +5 -5
- package/dist/assets/{DiagramView-B7sq74V-.js → DiagramView-CgotrQyK.js} +1 -1
- package/dist/assets/{FadeInOnReveal-8fY7XL23.js → FadeInOnReveal-CvNA_aGD.js} +1 -1
- package/dist/assets/{Icon-B7eLbnyj.js → Icon-BPJ_quEe.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-DICju9ve.js → MarkdownRendererImpl-Cj4y0P0S.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-CuxWU1yf.js → MobileDetailNavigation-5tAL5d_M.js} +1 -1
- package/dist/assets/{MobileShareBridge-CPg9Eyro.js → MobileShareBridge-BL7Ehxhd.js} +1 -1
- package/dist/assets/{MobileSurface-D7sDH_tx.js → MobileSurface-ByMnQ5rD.js} +1 -1
- package/dist/assets/{MultiRunWindow-MSO0Equt.js → MultiRunWindow-Che7L3TX.js} +1 -1
- package/dist/assets/{OnboardingScreen-C87u_MLg.js → OnboardingScreen-CdvaEBtt.js} +1 -1
- package/dist/assets/{OverlayScrollbar-Kmmybw-z.js → OverlayScrollbar-C2pEDFq-.js} +1 -1
- package/dist/assets/SettingsView-D0p-e8Px.js +25 -0
- package/dist/assets/{SettingsWindow-Bl25cfXX.js → SettingsWindow-KYhsFUrb.js} +1 -1
- package/dist/assets/{TerminalView-jR5jP1b0.js → TerminalView-Da5aLqHR.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DCEZb2DZ.js → ToolOutputDialog-DZI4uxx3.js} +1 -1
- package/dist/assets/{appThemeRegistry-aiRF-s3c.js → appThemeRegistry-DAgBeFsZ.js} +2 -2
- package/dist/assets/{button-bYWkDtDI.js → button-BmNJBT13.js} +1 -1
- package/dist/assets/{clipboard-CzpzN4Um.js → clipboard-CXWbHCAk.js} +1 -1
- package/dist/assets/{collapsible-A1PFvMRp.js → collapsible-68IqWBM1.js} +1 -1
- package/dist/assets/{context-menu-CkpHn-Bm.js → context-menu-BXH0AcOc.js} +1 -1
- package/dist/assets/desktopHosts-BpjUSDz4.js +1 -0
- package/dist/assets/{desktopRelayRestore-C5vYnSTd.js → desktopRelayRestore-BzUEk1i7.js} +1 -1
- package/dist/assets/{diagram-BkwKWaTO.js → diagram-YN0GZ1Q0.js} +1 -1
- package/dist/assets/{dialog-DW7-_iqo.js → dialog-BV1ENlLf.js} +1 -1
- package/dist/assets/es-EXZUX1sB.js +15 -0
- package/dist/assets/{fileQueries-P9SX4YwC.js → fileQueries-i4oziMnC.js} +1 -1
- package/dist/assets/fr-jWNPO91J.js +13 -0
- package/dist/assets/{gitApi-CMsZ2v08.js → gitApi-BW5AFgD1.js} +1 -1
- package/dist/assets/{gitApiHttp-DvIIcDAO.js → gitApiHttp-C1DaEMKR.js} +1 -1
- package/dist/assets/{input-B9aNttqV.js → input-BLVhPvhN.js} +1 -1
- package/dist/assets/{insertionBoundaries-Di3rCXgN.js → insertionBoundaries-D4KbXFFj.js} +1 -1
- package/dist/assets/ja-Dcb0VtDE.js +15 -0
- package/dist/assets/ko-CX71xNDX.js +15 -0
- package/dist/assets/{main-DY27kMV5.js → main-CcyyPAib.js} +2 -2
- package/dist/assets/{main-Chtf300b.js → main-DzxdsF_k.js} +5 -5
- package/dist/assets/{markdown-worker-BoB1_DPo.js → markdown-worker-Cq3Twp1A.js} +1 -1
- package/dist/assets/{markdownTheme-om5XcPqq.js → markdownTheme-CCnvI6L0.js} +1 -1
- package/dist/assets/{miniChat-BWbpl5SN.js → miniChat-BsNf9y48.js} +2 -2
- package/dist/assets/mobile-CsJg2IAq.js +2 -0
- package/dist/assets/{multirun-BfPcVktg.js → multirun-qs5hZkwM.js} +1 -1
- package/dist/assets/persistence-C_KCiMcY.js +3 -0
- package/dist/assets/{pl-BSjLn4dm.js → pl-mR_3Sqr8.js} +2 -2
- package/dist/assets/{projectMeta-CiAR7MRG.js → projectMeta-1SSTNg8w.js} +1 -1
- package/dist/assets/pt-BR-D1RpVOvC.js +15 -0
- package/dist/assets/radio-DBJG9Dd1.js +1 -0
- package/dist/assets/{renderElectronMiniChatApp-CiEXAVSt.js → renderElectronMiniChatApp-DOWE0jSA.js} +2 -2
- package/dist/assets/{renderMobileApp-BHvgSVPk.js → renderMobileApp-bklJJSZi.js} +3 -3
- package/dist/assets/{runtimeConfig-49YbZaqI.js → runtimeConfig-JSaYFA5x.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-CdhtjuPe.js → runtimeEndpointReset-ix1NxqYw.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-XeYica3j.js → sortable-tabs-strip-DncWjNx4.js} +1 -1
- package/dist/assets/src-B8mzksNL.css +2 -0
- package/dist/assets/store-oojlrF4d.js +15 -0
- package/dist/assets/{terminalApi-DAu7xKli.js → terminalApi-BYTA045F.js} +1 -1
- package/dist/assets/{tooltip-BSlxaNrR.js → tooltip-BYGBBTc2.js} +1 -1
- package/dist/assets/uk-UvnVqWv-.js +15 -0
- package/dist/assets/{url-DjkTDrad.js → url-CGHw6D-E.js} +1 -1
- package/dist/assets/useAppFontEffects-B1EhZNex.js +7 -0
- package/dist/assets/{useDesktopWindowControlsLayout-BwYnhrEy.js → useDesktopWindowControlsLayout-Dc69l-pm.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-CEmsrrEs.js → useEffectiveDirectory-dCGvvYRh.js} +1 -1
- package/dist/assets/{useFontPreferences-BxMT_kty.js → useFontPreferences-Bu4ME5hk.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DryW-QdG.js → useMobileNavigationStore-DhkIZBJw.js} +4 -4
- package/dist/assets/useSessionAutoCleanup-D-r9oDye.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CFW2AoPL.js → useWorkerHighlightedLines-05dLFgTI.js} +1 -1
- package/dist/assets/{utils-RUACCsOs.js → utils-DyUyiLje.js} +1 -1
- package/dist/assets/zh-CN-9v-RC966.js +15 -0
- package/dist/assets/zh-TW-BZ_wZdC-.js +15 -0
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/index.d.ts +0 -6
- package/server/index.js +5 -153
- package/server/lib/opencode/DOCUMENTATION.md +6 -10
- package/server/lib/opencode/cli-entry-runtime.js +0 -10
- package/server/lib/opencode/cli-options.js +0 -65
- package/server/lib/opencode/core-routes.js +2 -4
- package/server/lib/opencode/routes.test.js +0 -1
- package/server/lib/opencode/server-startup-runtime.js +0 -55
- package/server/lib/opencode/settings-helpers.js +0 -55
- package/server/lib/opencode/settings-helpers.test.js +0 -20
- package/server/lib/opencode/settings-normalization-runtime.js +0 -87
- package/server/lib/opencode/settings-normalization-runtime.test.js +0 -3
- package/server/lib/opencode/settings-runtime.js +9 -102
- package/server/lib/opencode/settings-runtime.test.js +0 -10
- package/server/lib/opencode/shutdown-runtime.js +1 -9
- package/server/lib/opencode/shutdown-runtime.test.js +0 -2
- package/server/lib/opencode/startup-pipeline-runtime.js +0 -20
- package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -3
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -80
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- package/bin/lib/cli-tunnel-capabilities.js +0 -9
- package/bin/lib/cli-tunnel-profiles.js +0 -366
- package/bin/lib/cli-tunnel-utils.js +0 -300
- package/bin/lib/commands-tunnel.js +0 -1761
- package/dist/assets/SettingsView-5YNu54KI.js +0 -25
- package/dist/assets/desktopHosts-D5m_ggY1.js +0 -1
- package/dist/assets/es-Cei_75Jt.js +0 -15
- package/dist/assets/fr-Dn7WSHh2.js +0 -13
- package/dist/assets/ja-DFARVqVj.js +0 -15
- package/dist/assets/ko-pyLnIyDY.js +0 -15
- package/dist/assets/mobile-DEQeGskq.js +0 -2
- package/dist/assets/persistence-CHA81Od7.js +0 -3
- package/dist/assets/pt-BR-DaWvr_0A.js +0 -15
- package/dist/assets/radio-BnF4XhCC.js +0 -1
- package/dist/assets/src-BtkWqMFu.css +0 -2
- package/dist/assets/store-DIpqFVjs.js +0 -15
- package/dist/assets/uk-CHCRqRKf.js +0 -15
- package/dist/assets/useAppFontEffects-BRNICZS2.js +0 -7
- package/dist/assets/useSessionAutoCleanup-DMJ1LiW_.js +0 -1
- package/dist/assets/zh-CN-BWEQZKXk.js +0 -15
- package/dist/assets/zh-TW-BbO-gJq3.js +0 -15
- package/server/lib/cloudflare-tunnel.js +0 -618
- package/server/lib/ngrok-tunnel.js +0 -343
- package/server/lib/ngrok-tunnel.test.js +0 -50
- package/server/lib/tunnels/DOCUMENTATION.md +0 -21
- package/server/lib/tunnels/executable-search.js +0 -140
- package/server/lib/tunnels/executable-search.test.js +0 -96
- package/server/lib/tunnels/index.js +0 -179
- package/server/lib/tunnels/index.test.js +0 -93
- package/server/lib/tunnels/install-help.js +0 -56
- package/server/lib/tunnels/install-help.test.js +0 -31
- package/server/lib/tunnels/managed-config.js +0 -201
- package/server/lib/tunnels/providers/cloudflare.js +0 -266
- package/server/lib/tunnels/providers/ngrok.js +0 -123
- package/server/lib/tunnels/registry.js +0 -51
- package/server/lib/tunnels/routes.js +0 -610
- package/server/lib/tunnels/types.js +0 -245
- package/server/lib/tunnels/types.test.js +0 -34
|
@@ -1,86 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { createTunnelRoutesRuntime } from '../tunnels/routes.js';
|
|
4
|
-
|
|
5
|
-
export const createTunnelWiringRuntime = (dependencies) => {
|
|
6
|
-
const {
|
|
7
|
-
crypto,
|
|
8
|
-
URL,
|
|
9
|
-
tunnelProviderRegistry,
|
|
10
|
-
tunnelAuthController,
|
|
11
|
-
readSettingsFromDiskMigrated,
|
|
12
|
-
readManagedRemoteTunnelConfigFromDisk,
|
|
13
|
-
normalizeTunnelProvider,
|
|
14
|
-
normalizeTunnelMode,
|
|
15
|
-
normalizeOptionalPath,
|
|
16
|
-
normalizeManagedRemoteTunnelHostname,
|
|
17
|
-
normalizeTunnelBootstrapTtlMs,
|
|
18
|
-
normalizeTunnelSessionTtlMs,
|
|
19
|
-
isSupportedTunnelMode,
|
|
20
|
-
upsertManagedRemoteTunnelToken,
|
|
21
|
-
resolveManagedRemoteTunnelToken,
|
|
22
|
-
TUNNEL_MODE_QUICK,
|
|
23
|
-
TUNNEL_MODE_MANAGED_LOCAL,
|
|
24
|
-
TUNNEL_MODE_MANAGED_REMOTE,
|
|
25
|
-
TUNNEL_PROVIDER_CLOUDFLARE,
|
|
26
|
-
TunnelServiceError,
|
|
27
|
-
getActiveTunnelController,
|
|
28
|
-
setActiveTunnelController,
|
|
29
|
-
getRuntimeManagedRemoteTunnelHostname,
|
|
30
|
-
setRuntimeManagedRemoteTunnelHostname,
|
|
31
|
-
getRuntimeManagedRemoteTunnelToken,
|
|
32
|
-
setRuntimeManagedRemoteTunnelToken,
|
|
33
|
-
} = dependencies;
|
|
34
|
-
|
|
35
|
-
const initialize = (app, initialPort) => {
|
|
1
|
+
export const createTunnelWiringRuntime = () => {
|
|
2
|
+
const initialize = (_app, initialPort) => {
|
|
36
3
|
let activePort = initialPort;
|
|
37
4
|
|
|
38
|
-
const tunnelService = createTunnelService({
|
|
39
|
-
registry: tunnelProviderRegistry,
|
|
40
|
-
getController: getActiveTunnelController,
|
|
41
|
-
setController: setActiveTunnelController,
|
|
42
|
-
getActivePort: () => activePort,
|
|
43
|
-
onQuickTunnelWarning: () => {
|
|
44
|
-
printTunnelWarning();
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const tunnelRoutesRuntime = createTunnelRoutesRuntime({
|
|
49
|
-
crypto,
|
|
50
|
-
URL,
|
|
51
|
-
tunnelService,
|
|
52
|
-
tunnelProviderRegistry,
|
|
53
|
-
tunnelAuthController,
|
|
54
|
-
readSettingsFromDiskMigrated,
|
|
55
|
-
readManagedRemoteTunnelConfigFromDisk,
|
|
56
|
-
normalizeTunnelProvider,
|
|
57
|
-
normalizeTunnelMode,
|
|
58
|
-
normalizeOptionalPath,
|
|
59
|
-
normalizeManagedRemoteTunnelHostname,
|
|
60
|
-
normalizeTunnelBootstrapTtlMs,
|
|
61
|
-
normalizeTunnelSessionTtlMs,
|
|
62
|
-
isSupportedTunnelMode,
|
|
63
|
-
upsertManagedRemoteTunnelToken,
|
|
64
|
-
resolveManagedRemoteTunnelToken,
|
|
65
|
-
TUNNEL_MODE_QUICK,
|
|
66
|
-
TUNNEL_MODE_MANAGED_LOCAL,
|
|
67
|
-
TUNNEL_MODE_MANAGED_REMOTE,
|
|
68
|
-
TUNNEL_PROVIDER_CLOUDFLARE,
|
|
69
|
-
TunnelServiceError,
|
|
70
|
-
getActivePort: () => activePort,
|
|
71
|
-
getRuntimeManagedRemoteTunnelHostname,
|
|
72
|
-
setRuntimeManagedRemoteTunnelHostname,
|
|
73
|
-
getRuntimeManagedRemoteTunnelToken,
|
|
74
|
-
setRuntimeManagedRemoteTunnelToken,
|
|
75
|
-
getActiveTunnelController,
|
|
76
|
-
setActiveTunnelController,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
tunnelRoutesRuntime.registerRoutes(app);
|
|
80
|
-
|
|
81
5
|
return {
|
|
82
|
-
tunnelService,
|
|
83
|
-
startTunnelWithNormalizedRequest: (...args) => tunnelRoutesRuntime.startTunnelWithNormalizedRequest(...args),
|
|
84
6
|
getActivePort: () => activePort,
|
|
85
7
|
setActivePort: (value) => {
|
|
86
8
|
activePort = value;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
The private relay lets an OpenChamber client (mobile app, browser, or another desktop) reach a user's OpenChamber instance through OpenChamber-hosted infrastructure when the instance is not directly reachable (behind NAT
|
|
5
|
+
The private relay lets an OpenChamber client (mobile app, browser, or another desktop) reach a user's OpenChamber instance through OpenChamber-hosted infrastructure when the instance is not directly reachable (behind NAT or without a public URL). The instance dials **outbound** to the relay; nothing needs to be exposed inbound.
|
|
6
6
|
|
|
7
7
|
Traffic is **end-to-end encrypted between the two endpoints** (client and host instance). The relay infrastructure forwards opaque ciphertext and cannot read application traffic — it is an untrusted transport, not a trusted middlebox.
|
|
8
8
|
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { cloudflareTunnelProviderCapabilities } from '../../server/lib/tunnels/providers/cloudflare.js';
|
|
2
|
-
import { ngrokTunnelProviderCapabilities } from '../../server/lib/tunnels/providers/ngrok.js';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_TUNNEL_PROVIDER_CAPABILITIES = [
|
|
5
|
-
cloudflareTunnelProviderCapabilities,
|
|
6
|
-
ngrokTunnelProviderCapabilities,
|
|
7
|
-
];
|
|
8
|
-
|
|
9
|
-
export { DEFAULT_TUNNEL_PROVIDER_CAPABILITIES };
|
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import {
|
|
5
|
-
getTunnelProfilesFilePath,
|
|
6
|
-
getLegacyCloudflareManagedRemoteFilePath,
|
|
7
|
-
} from './cli-paths.js';
|
|
8
|
-
|
|
9
|
-
const TUNNEL_PROFILES_VERSION = 1;
|
|
10
|
-
const MAX_TOKEN_FILE_BYTES = 8 * 1024;
|
|
11
|
-
|
|
12
|
-
function normalizeProfileProvider(value) {
|
|
13
|
-
if (typeof value !== 'string') return '';
|
|
14
|
-
return value.trim().toLowerCase();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeProfileMode(value) {
|
|
18
|
-
if (typeof value !== 'string') return '';
|
|
19
|
-
return value.trim().toLowerCase();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function normalizeProfileName(value) {
|
|
23
|
-
if (typeof value !== 'string') return '';
|
|
24
|
-
return value.trim();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeProfileHostname(value) {
|
|
28
|
-
if (typeof value !== 'string') return '';
|
|
29
|
-
return value.trim();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeProfileToken(value) {
|
|
33
|
-
if (typeof value !== 'string') return '';
|
|
34
|
-
return value.trim();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function suggestProfileNameFromHostname(hostname) {
|
|
38
|
-
const normalizedHost = normalizeProfileHostname(hostname);
|
|
39
|
-
if (!normalizedHost) return 'prod-main';
|
|
40
|
-
const firstLabel = normalizedHost.split('.')[0] || normalizedHost;
|
|
41
|
-
const sanitized = firstLabel.replace(/[^a-zA-Z0-9-_]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
42
|
-
return sanitized || 'prod-main';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function maskToken(token) {
|
|
46
|
-
if (typeof token !== 'string' || token.length === 0) {
|
|
47
|
-
return '***';
|
|
48
|
-
}
|
|
49
|
-
if (token.length <= 4) {
|
|
50
|
-
return '*'.repeat(token.length);
|
|
51
|
-
}
|
|
52
|
-
return `${'*'.repeat(Math.max(4, token.length - 4))}${token.slice(-4)}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function readTokenFromFileSafely(tokenFilePath) {
|
|
56
|
-
const absolutePath = path.resolve(tokenFilePath);
|
|
57
|
-
let realPath;
|
|
58
|
-
try {
|
|
59
|
-
realPath = fs.realpathSync(absolutePath);
|
|
60
|
-
} catch (error) {
|
|
61
|
-
if (error?.code === 'ENOENT') {
|
|
62
|
-
throw new Error(`Token file '${absolutePath}' not found.`);
|
|
63
|
-
}
|
|
64
|
-
if (error?.code === 'EACCES') {
|
|
65
|
-
throw new Error(`Token file '${absolutePath}' is not readable. Check file permissions.`);
|
|
66
|
-
}
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let stats;
|
|
71
|
-
try {
|
|
72
|
-
stats = fs.statSync(realPath);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
if (error?.code === 'EACCES') {
|
|
75
|
-
throw new Error(`Token file '${absolutePath}' is not readable. Check file permissions.`);
|
|
76
|
-
}
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!stats.isFile()) {
|
|
81
|
-
throw new Error(`Token file '${absolutePath}' must be a regular file.`);
|
|
82
|
-
}
|
|
83
|
-
if (stats.size <= 0) {
|
|
84
|
-
throw new Error(`Token file '${absolutePath}' is empty.`);
|
|
85
|
-
}
|
|
86
|
-
if (stats.size > MAX_TOKEN_FILE_BYTES) {
|
|
87
|
-
throw new Error(`Token file '${absolutePath}' is too large (max ${MAX_TOKEN_FILE_BYTES} bytes).`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const raw = fs.readFileSync(realPath, 'utf8');
|
|
91
|
-
if (raw.includes('\u0000')) {
|
|
92
|
-
throw new Error(`Token file '${absolutePath}' appears to be binary. Use a plain text token file.`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const value = raw.trim();
|
|
96
|
-
if (!value) {
|
|
97
|
-
throw new Error(`Token file '${absolutePath}' is empty.`);
|
|
98
|
-
}
|
|
99
|
-
return value;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function resolveToken(options) {
|
|
103
|
-
const sources = [
|
|
104
|
-
options.tokenStdin ? 'stdin' : null,
|
|
105
|
-
options.tokenFile ? 'file' : null,
|
|
106
|
-
options.token ? 'flag' : null,
|
|
107
|
-
].filter(Boolean);
|
|
108
|
-
|
|
109
|
-
if (sources.length > 1) {
|
|
110
|
-
throw new Error(`Multiple token sources specified (${sources.join(', ')}). Use only one of --token, --token-file, or --token-stdin.`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (options.tokenStdin) {
|
|
114
|
-
const fd = fs.openSync('/dev/stdin', 'r');
|
|
115
|
-
try {
|
|
116
|
-
const buf = Buffer.alloc(65536);
|
|
117
|
-
const bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
|
|
118
|
-
const value = buf.slice(0, bytesRead).toString('utf8').trim();
|
|
119
|
-
if (!value) {
|
|
120
|
-
throw new Error('No token received from stdin.');
|
|
121
|
-
}
|
|
122
|
-
return value;
|
|
123
|
-
} finally {
|
|
124
|
-
fs.closeSync(fd);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (options.tokenFile) {
|
|
129
|
-
return readTokenFromFileSafely(options.tokenFile);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return typeof options.token === 'string' ? options.token.trim() : undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function redactProfileForOutput(profile, showSecrets = false) {
|
|
136
|
-
if (!profile || typeof profile !== 'object') {
|
|
137
|
-
return profile;
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
...profile,
|
|
141
|
-
token: showSecrets ? profile.token : maskToken(profile.token),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function redactProfilesForOutput(profiles, showSecrets = false) {
|
|
146
|
-
if (!Array.isArray(profiles)) {
|
|
147
|
-
return profiles;
|
|
148
|
-
}
|
|
149
|
-
return profiles.map((entry) => redactProfileForOutput(entry, showSecrets));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function formatProfileTokenStatus(profile, showSecrets = false) {
|
|
153
|
-
const token = typeof profile?.token === 'string' ? profile.token.trim() : '';
|
|
154
|
-
if (!token) {
|
|
155
|
-
return 'token:missing';
|
|
156
|
-
}
|
|
157
|
-
if (showSecrets) {
|
|
158
|
-
return `token:${token}`;
|
|
159
|
-
}
|
|
160
|
-
return 'token:present';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function sanitizeTunnelProfilesData(data) {
|
|
164
|
-
const parsed = data && typeof data === 'object' ? data : {};
|
|
165
|
-
const list = Array.isArray(parsed.profiles) ? parsed.profiles : [];
|
|
166
|
-
const seen = new Set();
|
|
167
|
-
const profiles = [];
|
|
168
|
-
for (const entry of list) {
|
|
169
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
170
|
-
const id = typeof entry.id === 'string' && entry.id.trim().length > 0 ? entry.id.trim() : crypto.randomUUID();
|
|
171
|
-
const provider = normalizeProfileProvider(entry.provider);
|
|
172
|
-
const mode = normalizeProfileMode(entry.mode);
|
|
173
|
-
const name = normalizeProfileName(entry.name);
|
|
174
|
-
const hostname = normalizeProfileHostname(entry.hostname);
|
|
175
|
-
const token = normalizeProfileToken(entry.token);
|
|
176
|
-
if (!provider || !mode || !name || !hostname || !token) continue;
|
|
177
|
-
const key = `${provider}::${name.toLowerCase()}`;
|
|
178
|
-
if (seen.has(key)) continue;
|
|
179
|
-
seen.add(key);
|
|
180
|
-
profiles.push({
|
|
181
|
-
id,
|
|
182
|
-
name,
|
|
183
|
-
provider,
|
|
184
|
-
mode,
|
|
185
|
-
hostname,
|
|
186
|
-
token,
|
|
187
|
-
createdAt: Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
|
|
188
|
-
updatedAt: Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
return { version: TUNNEL_PROFILES_VERSION, profiles };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function warnIfUnsafeFilePermissions(filePath, { shouldWarn = true } = {}) {
|
|
195
|
-
if (process.platform === 'win32') {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (!shouldWarn) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
try {
|
|
202
|
-
const stats = fs.statSync(filePath);
|
|
203
|
-
const perms = stats.mode & 0o777;
|
|
204
|
-
if (perms & 0o077) {
|
|
205
|
-
const octal = perms.toString(8).padStart(3, '0');
|
|
206
|
-
console.warn(
|
|
207
|
-
`Warning: Profile file '${filePath}' has permissions ${octal} (should be 600). ` +
|
|
208
|
-
`Other users may be able to read tunnel tokens. Fix with: chmod 600 '${filePath}'`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
// File may not exist yet — not an error
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function readTunnelProfilesFromDisk(options = {}) {
|
|
217
|
-
const filePath = getTunnelProfilesFilePath();
|
|
218
|
-
try {
|
|
219
|
-
warnIfUnsafeFilePermissions(filePath, options);
|
|
220
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
221
|
-
return sanitizeTunnelProfilesData(JSON.parse(raw));
|
|
222
|
-
} catch {
|
|
223
|
-
return { version: TUNNEL_PROFILES_VERSION, profiles: [] };
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function writeTunnelProfilesToDisk(data) {
|
|
228
|
-
const filePath = getTunnelProfilesFilePath();
|
|
229
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
230
|
-
fs.writeFileSync(filePath, JSON.stringify(sanitizeTunnelProfilesData(data), null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function writeManagedRemotePairsToDiskFromProfiles(profilesData) {
|
|
234
|
-
const profiles = sanitizeTunnelProfilesData(profilesData).profiles;
|
|
235
|
-
const cloudflareManagedRemote = profiles.filter(
|
|
236
|
-
(entry) => entry.provider === 'cloudflare' && entry.mode === 'managed-remote'
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
const tunnels = cloudflareManagedRemote.map((entry) => ({
|
|
240
|
-
id: entry.id,
|
|
241
|
-
name: entry.name,
|
|
242
|
-
hostname: entry.hostname,
|
|
243
|
-
token: entry.token,
|
|
244
|
-
updatedAt: Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
|
|
245
|
-
}));
|
|
246
|
-
|
|
247
|
-
const filePath = getLegacyCloudflareManagedRemoteFilePath();
|
|
248
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
249
|
-
fs.writeFileSync(filePath, JSON.stringify({ version: 1, tunnels }, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function readLegacyManagedRemoteEntries() {
|
|
253
|
-
try {
|
|
254
|
-
const raw = fs.readFileSync(getLegacyCloudflareManagedRemoteFilePath(), 'utf8');
|
|
255
|
-
const parsed = JSON.parse(raw);
|
|
256
|
-
const tunnels = Array.isArray(parsed?.tunnels) ? parsed.tunnels : [];
|
|
257
|
-
return tunnels
|
|
258
|
-
.map((entry) => {
|
|
259
|
-
if (!entry || typeof entry !== 'object') return null;
|
|
260
|
-
const id = typeof entry.id === 'string' && entry.id.trim().length > 0 ? entry.id.trim() : crypto.randomUUID();
|
|
261
|
-
const name = normalizeProfileName(entry.name);
|
|
262
|
-
const hostname = normalizeProfileHostname(entry.hostname);
|
|
263
|
-
const token = normalizeProfileToken(entry.token);
|
|
264
|
-
if (!name || !hostname || !token) return null;
|
|
265
|
-
return {
|
|
266
|
-
id,
|
|
267
|
-
name,
|
|
268
|
-
provider: 'cloudflare',
|
|
269
|
-
mode: 'managed-remote',
|
|
270
|
-
hostname,
|
|
271
|
-
token,
|
|
272
|
-
createdAt: Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
|
|
273
|
-
updatedAt: Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
|
|
274
|
-
};
|
|
275
|
-
})
|
|
276
|
-
.filter(Boolean);
|
|
277
|
-
} catch {
|
|
278
|
-
return [];
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function makeUniqueProfileName(provider, desiredName, existingProfiles) {
|
|
283
|
-
const normalizedDesired = normalizeProfileName(desiredName);
|
|
284
|
-
if (!normalizedDesired) {
|
|
285
|
-
return '';
|
|
286
|
-
}
|
|
287
|
-
const existingNames = new Set(
|
|
288
|
-
existingProfiles
|
|
289
|
-
.filter((entry) => entry.provider === provider)
|
|
290
|
-
.map((entry) => entry.name.toLowerCase())
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
if (!existingNames.has(normalizedDesired.toLowerCase())) {
|
|
294
|
-
return normalizedDesired;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
let index = 2;
|
|
298
|
-
while (true) {
|
|
299
|
-
const candidate = `${normalizedDesired}-${index}`;
|
|
300
|
-
if (!existingNames.has(candidate.toLowerCase())) {
|
|
301
|
-
return candidate;
|
|
302
|
-
}
|
|
303
|
-
index += 1;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function ensureTunnelProfilesMigrated(options = {}) {
|
|
308
|
-
const current = readTunnelProfilesFromDisk(options);
|
|
309
|
-
if (current.profiles.length > 0) {
|
|
310
|
-
return current;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const legacyEntries = readLegacyManagedRemoteEntries();
|
|
314
|
-
if (legacyEntries.length === 0) {
|
|
315
|
-
return current;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const migratedProfiles = [];
|
|
319
|
-
for (const entry of legacyEntries) {
|
|
320
|
-
const name = makeUniqueProfileName(entry.provider, entry.name, migratedProfiles);
|
|
321
|
-
migratedProfiles.push({ ...entry, name });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const migrated = sanitizeTunnelProfilesData({ version: TUNNEL_PROFILES_VERSION, profiles: migratedProfiles });
|
|
325
|
-
writeTunnelProfilesToDisk(migrated);
|
|
326
|
-
writeManagedRemotePairsToDiskFromProfiles(migrated);
|
|
327
|
-
return migrated;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function resolveProfileByName(profiles, profileName, provider) {
|
|
331
|
-
const normalizedName = normalizeProfileName(profileName).toLowerCase();
|
|
332
|
-
const normalizedProvider = normalizeProfileProvider(provider);
|
|
333
|
-
const matches = profiles.filter((entry) => {
|
|
334
|
-
if (entry.name.toLowerCase() !== normalizedName) return false;
|
|
335
|
-
if (!normalizedProvider) return true;
|
|
336
|
-
return entry.provider === normalizedProvider;
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
if (matches.length === 0) {
|
|
340
|
-
return { profile: null, error: `No tunnel profile found for name '${profileName}'. Run 'openchamber tunnel profile list'.` };
|
|
341
|
-
}
|
|
342
|
-
if (matches.length > 1) {
|
|
343
|
-
return { profile: null, error: `Profile name '${profileName}' exists for multiple providers. Use --provider <id>.` };
|
|
344
|
-
}
|
|
345
|
-
return { profile: matches[0], error: null };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
export {
|
|
350
|
-
normalizeProfileProvider,
|
|
351
|
-
normalizeProfileMode,
|
|
352
|
-
normalizeProfileName,
|
|
353
|
-
normalizeProfileHostname,
|
|
354
|
-
normalizeProfileToken,
|
|
355
|
-
suggestProfileNameFromHostname,
|
|
356
|
-
maskToken,
|
|
357
|
-
resolveToken,
|
|
358
|
-
redactProfileForOutput,
|
|
359
|
-
redactProfilesForOutput,
|
|
360
|
-
formatProfileTokenStatus,
|
|
361
|
-
warnIfUnsafeFilePermissions,
|
|
362
|
-
writeTunnelProfilesToDisk,
|
|
363
|
-
writeManagedRemotePairsToDiskFromProfiles,
|
|
364
|
-
ensureTunnelProfilesMigrated,
|
|
365
|
-
resolveProfileByName,
|
|
366
|
-
};
|