@marcusrbrown/infra 0.13.0 → 0.13.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.
Files changed (108) hide show
  1. package/dist/cli.js +4977 -0
  2. package/package.json +7 -3
  3. package/src/__snapshots__/cli.test.ts.snap +0 -219
  4. package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
  5. package/src/__test__/mcp-ctx-fixture.ts +0 -104
  6. package/src/cli.test.ts +0 -66
  7. package/src/cli.ts +0 -43
  8. package/src/commands/cliproxy/config.test.ts +0 -475
  9. package/src/commands/cliproxy/config.ts +0 -238
  10. package/src/commands/cliproxy/deploy.test.ts +0 -181
  11. package/src/commands/cliproxy/deploy.ts +0 -157
  12. package/src/commands/cliproxy/host.test.ts +0 -73
  13. package/src/commands/cliproxy/host.ts +0 -31
  14. package/src/commands/cliproxy/index.ts +0 -21
  15. package/src/commands/cliproxy/keys.test.ts +0 -326
  16. package/src/commands/cliproxy/keys.ts +0 -201
  17. package/src/commands/cliproxy/login.test.ts +0 -415
  18. package/src/commands/cliproxy/login.ts +0 -131
  19. package/src/commands/cliproxy/models.test.ts +0 -592
  20. package/src/commands/cliproxy/models.ts +0 -237
  21. package/src/commands/cliproxy/open.test.ts +0 -132
  22. package/src/commands/cliproxy/open.ts +0 -78
  23. package/src/commands/cliproxy/setup/gh.test.ts +0 -218
  24. package/src/commands/cliproxy/setup/gh.ts +0 -250
  25. package/src/commands/cliproxy/setup/preview.test.ts +0 -159
  26. package/src/commands/cliproxy/setup/preview.ts +0 -41
  27. package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
  28. package/src/commands/cliproxy/setup/prompts.ts +0 -99
  29. package/src/commands/cliproxy/setup/providers.test.ts +0 -245
  30. package/src/commands/cliproxy/setup/providers.ts +0 -136
  31. package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
  32. package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
  33. package/src/commands/cliproxy/setup/templates.test.ts +0 -357
  34. package/src/commands/cliproxy/setup/templates.ts +0 -145
  35. package/src/commands/cliproxy/setup/validation.test.ts +0 -567
  36. package/src/commands/cliproxy/setup/validation.ts +0 -209
  37. package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
  38. package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
  39. package/src/commands/cliproxy/setup.test.ts +0 -2366
  40. package/src/commands/cliproxy/setup.ts +0 -809
  41. package/src/commands/cliproxy/shared.test.ts +0 -118
  42. package/src/commands/cliproxy/shared.ts +0 -84
  43. package/src/commands/cliproxy/status.test.ts +0 -1157
  44. package/src/commands/cliproxy/status.ts +0 -597
  45. package/src/commands/dashboard/deploy.test.ts +0 -215
  46. package/src/commands/dashboard/deploy.ts +0 -135
  47. package/src/commands/dashboard/host.test.ts +0 -79
  48. package/src/commands/dashboard/host.ts +0 -33
  49. package/src/commands/dashboard/index.ts +0 -13
  50. package/src/commands/dashboard/logs.test.ts +0 -317
  51. package/src/commands/dashboard/logs.ts +0 -180
  52. package/src/commands/dashboard/status.test.ts +0 -708
  53. package/src/commands/dashboard/status.ts +0 -293
  54. package/src/commands/gateway/backup.test.ts +0 -426
  55. package/src/commands/gateway/backup.ts +0 -205
  56. package/src/commands/gateway/deploy.test.ts +0 -491
  57. package/src/commands/gateway/deploy.ts +0 -173
  58. package/src/commands/gateway/host.test.ts +0 -89
  59. package/src/commands/gateway/host.ts +0 -33
  60. package/src/commands/gateway/index.ts +0 -17
  61. package/src/commands/gateway/logs.test.ts +0 -263
  62. package/src/commands/gateway/logs.ts +0 -160
  63. package/src/commands/gateway/restore.test.ts +0 -650
  64. package/src/commands/gateway/restore.ts +0 -308
  65. package/src/commands/gateway/status.test.ts +0 -755
  66. package/src/commands/gateway/status.ts +0 -255
  67. package/src/commands/keeweb/deploy.test.ts +0 -177
  68. package/src/commands/keeweb/deploy.ts +0 -191
  69. package/src/commands/keeweb/index.ts +0 -11
  70. package/src/commands/keeweb/open.test.ts +0 -54
  71. package/src/commands/keeweb/open.ts +0 -30
  72. package/src/commands/keeweb/status.test.ts +0 -509
  73. package/src/commands/keeweb/status.ts +0 -359
  74. package/src/commands/mcp.test.ts +0 -260
  75. package/src/commands/mcp.ts +0 -58
  76. package/src/commands/status.test.ts +0 -222
  77. package/src/commands/status.ts +0 -161
  78. package/src/commands/umami/deploy.test.ts +0 -202
  79. package/src/commands/umami/deploy.ts +0 -132
  80. package/src/commands/umami/host.test.ts +0 -79
  81. package/src/commands/umami/host.ts +0 -33
  82. package/src/commands/umami/index.ts +0 -13
  83. package/src/commands/umami/logs.test.ts +0 -191
  84. package/src/commands/umami/logs.ts +0 -163
  85. package/src/commands/umami/status.test.ts +0 -633
  86. package/src/commands/umami/status.ts +0 -285
  87. package/src/commands/vpn/client.test.ts +0 -949
  88. package/src/commands/vpn/client.ts +0 -467
  89. package/src/commands/vpn/deploy.test.ts +0 -208
  90. package/src/commands/vpn/deploy.ts +0 -143
  91. package/src/commands/vpn/host.test.ts +0 -89
  92. package/src/commands/vpn/host.ts +0 -36
  93. package/src/commands/vpn/index.ts +0 -15
  94. package/src/commands/vpn/logs.test.ts +0 -234
  95. package/src/commands/vpn/logs.ts +0 -136
  96. package/src/commands/vpn/peers.test.ts +0 -478
  97. package/src/commands/vpn/shared.test.ts +0 -454
  98. package/src/commands/vpn/shared.ts +0 -170
  99. package/src/commands/vpn/status.test.ts +0 -142
  100. package/src/commands/vpn/status.ts +0 -78
  101. package/src/conventions.test.ts +0 -1180
  102. package/src/lib/action-ctx.ts +0 -25
  103. package/src/lib/known-hosts.test.ts +0 -192
  104. package/src/lib/known-hosts.ts +0 -76
  105. package/src/lib/redact.ts +0 -21
  106. package/src/lib/ssh-identity.test.ts +0 -134
  107. package/src/lib/ssh-identity.ts +0 -76
  108. /package/{src/resources/known_hosts → dist/known_hosts-tk724cp0.} +0 -0
@@ -1,215 +0,0 @@
1
- import {resolve} from 'node:path'
2
- import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
3
-
4
- import {getDashboardDeployEnv, validateDashboardRemotePreconditions} from './deploy'
5
-
6
- const repoRoot = resolve(import.meta.dir, '../../../../..')
7
-
8
- const envKeys = [
9
- 'HOME',
10
- 'PATH',
11
- 'SSH_AUTH_SOCK',
12
- 'DASHBOARD_DOMAIN',
13
- 'DASHBOARD_SSH_KEY',
14
- 'DASHBOARD_GITHUB_APP_ID',
15
- 'DASHBOARD_GITHUB_APP_KEY',
16
- 'DASHBOARD_OAUTH_CLIENT_ID',
17
- 'DASHBOARD_OAUTH_CLIENT_SECRET',
18
- 'DASHBOARD_OPERATOR_LOGIN',
19
- 'DASHBOARD_COOKIE_KEY',
20
- ] as const
21
-
22
- type ManagedEnvKey = (typeof envKeys)[number]
23
-
24
- let originalEnv: Partial<Record<ManagedEnvKey, string | undefined>>
25
-
26
- function restoreManagedEnv(): void {
27
- for (const key of envKeys) {
28
- const value = originalEnv[key]
29
- if (value === undefined) {
30
- delete process.env[key]
31
- } else {
32
- process.env[key] = value
33
- }
34
- }
35
- }
36
-
37
- function setManagedEnv(overrides: Partial<Record<ManagedEnvKey, string | undefined>>): void {
38
- restoreManagedEnv()
39
- for (const [key, value] of Object.entries(overrides)) {
40
- if (value === undefined) {
41
- delete process.env[key as ManagedEnvKey]
42
- } else {
43
- process.env[key as ManagedEnvKey] = value
44
- }
45
- }
46
- }
47
-
48
- beforeEach(() => {
49
- originalEnv = {}
50
- for (const key of envKeys) {
51
- originalEnv[key] = process.env[key]
52
- }
53
- })
54
-
55
- afterEach(() => {
56
- restoreManagedEnv()
57
- })
58
-
59
- // ─── getDashboardDeployEnv ────────────────────────────────────────────────────
60
-
61
- describe('getDashboardDeployEnv', () => {
62
- it('returns env object with required keys when all are set', () => {
63
- setManagedEnv({
64
- PATH: '/usr/bin:/bin',
65
- HOME: '/home/user',
66
- SSH_AUTH_SOCK: '/tmp/ssh-agent.sock',
67
- DASHBOARD_DOMAIN: 'dashboard.fro.bot',
68
- })
69
-
70
- const env = getDashboardDeployEnv()
71
-
72
- expect(env.PATH).toBe('/usr/bin:/bin')
73
- expect(env.HOME).toBe('/home/user')
74
- expect(env.SSH_AUTH_SOCK).toBe('/tmp/ssh-agent.sock')
75
- expect(env.DASHBOARD_DOMAIN).toBe('dashboard.fro.bot')
76
- })
77
-
78
- it('throws when PATH is missing', () => {
79
- setManagedEnv({PATH: undefined, HOME: '/home/user', SSH_AUTH_SOCK: '/tmp/ssh-agent.sock'})
80
-
81
- expect(() => getDashboardDeployEnv()).toThrow('PATH is required')
82
- })
83
-
84
- it('throws when HOME is missing', () => {
85
- setManagedEnv({PATH: '/usr/bin:/bin', HOME: undefined, SSH_AUTH_SOCK: '/tmp/ssh-agent.sock'})
86
-
87
- expect(() => getDashboardDeployEnv()).toThrow('HOME is required')
88
- })
89
-
90
- it('throws when SSH_AUTH_SOCK is missing and no DASHBOARD_SSH_KEY either', () => {
91
- setManagedEnv({
92
- PATH: '/usr/bin:/bin',
93
- HOME: '/home/user',
94
- SSH_AUTH_SOCK: undefined,
95
- DASHBOARD_SSH_KEY: undefined,
96
- })
97
-
98
- expect(() => getDashboardDeployEnv()).toThrow('Local deploy needs an SSH context')
99
- })
100
-
101
- it('succeeds with only SSH_AUTH_SOCK set (no DASHBOARD_SSH_KEY)', () => {
102
- setManagedEnv({
103
- PATH: '/usr/bin:/bin',
104
- HOME: '/home/user',
105
- SSH_AUTH_SOCK: '/tmp/ssh-agent.sock',
106
- DASHBOARD_SSH_KEY: undefined,
107
- })
108
-
109
- const env = getDashboardDeployEnv()
110
-
111
- expect(env.SSH_AUTH_SOCK).toBe('/tmp/ssh-agent.sock')
112
- expect('DASHBOARD_SSH_KEY' in env).toBe(false)
113
- })
114
-
115
- it('succeeds with only DASHBOARD_SSH_KEY set (no SSH_AUTH_SOCK) and includes the key in env', () => {
116
- setManagedEnv({
117
- PATH: '/usr/bin:/bin',
118
- HOME: '/home/user',
119
- SSH_AUTH_SOCK: undefined,
120
- DASHBOARD_SSH_KEY: 'ssh-ed25519 AAAA...',
121
- })
122
-
123
- const env = getDashboardDeployEnv()
124
-
125
- expect(env.DASHBOARD_SSH_KEY).toBe('ssh-ed25519 AAAA...')
126
- expect('SSH_AUTH_SOCK' in env).toBe(false)
127
- expect(env.DASHBOARD_SSH_KEY).toBe('ssh-ed25519 AAAA...')
128
- expect('SSH_AUTH_SOCK' in env).toBe(false)
129
- })
130
-
131
- it('forwards all DASHBOARD_* vars from process.env (not a selective subset)', () => {
132
- setManagedEnv({
133
- PATH: '/usr/bin:/bin',
134
- HOME: '/home/user',
135
- SSH_AUTH_SOCK: '/tmp/ssh-agent.sock',
136
- DASHBOARD_DOMAIN: 'dashboard.fro.bot',
137
- DASHBOARD_GITHUB_APP_ID: '123456',
138
- DASHBOARD_GITHUB_APP_KEY: '-----BEGIN RSA PRIVATE KEY-----\nfake\n-----END RSA PRIVATE KEY-----',
139
- DASHBOARD_OAUTH_CLIENT_ID: 'Iv1.abc123',
140
- DASHBOARD_OAUTH_CLIENT_SECRET: 'oauthsecret',
141
- DASHBOARD_OPERATOR_LOGIN: 'marcusrbrown',
142
- DASHBOARD_COOKIE_KEY: 'cookiekey',
143
- })
144
-
145
- const env = getDashboardDeployEnv()
146
-
147
- // All DASHBOARD_* vars must be present in the returned env
148
- expect(env.DASHBOARD_DOMAIN).toBe('dashboard.fro.bot')
149
- expect(env.DASHBOARD_GITHUB_APP_ID).toBe('123456')
150
- expect(env.DASHBOARD_OAUTH_CLIENT_ID).toBe('Iv1.abc123')
151
- expect(env.DASHBOARD_OAUTH_CLIENT_SECRET).toBe('oauthsecret')
152
- expect(env.DASHBOARD_OPERATOR_LOGIN).toBe('marcusrbrown')
153
- expect(env.DASHBOARD_COOKIE_KEY).toBe('cookiekey')
154
- })
155
- })
156
-
157
- // ─── validateDashboardRemotePreconditions ─────────────────────────────────────
158
-
159
- describe('validateDashboardRemotePreconditions', () => {
160
- it('throws a clear error when gh is not available', () => {
161
- // We cannot reliably mock Bun.which, so we test the function contract:
162
- // if gh is not installed, it should throw with a helpful message.
163
- // This test verifies the error message shape by calling with a known-missing binary.
164
- // In CI where gh IS installed, we skip this test.
165
- if (Bun.which('gh')) {
166
- // gh is available — just verify the function does not throw
167
- expect(() => validateDashboardRemotePreconditions()).not.toThrow()
168
- return
169
- }
170
-
171
- expect(() => validateDashboardRemotePreconditions()).toThrow('gh CLI is required')
172
- })
173
- })
174
-
175
- // ─── deploy command (subprocess integration via CLI) ─────────────────────────
176
-
177
- describe('deploy command', () => {
178
- it('dry-run remote mode prints planned gh workflow run command without executing', async () => {
179
- const proc = Bun.spawn(['bun', 'run', 'packages/cli/src/cli.ts', 'dashboard', 'deploy', '--dry-run'], {
180
- cwd: repoRoot,
181
- env: {...process.env, NO_COLOR: '1'},
182
- stdout: 'pipe',
183
- stderr: 'pipe',
184
- })
185
-
186
- const [stdout, _stderr, exitCode] = await Promise.all([
187
- new Response(proc.stdout).text(),
188
- new Response(proc.stderr).text(),
189
- proc.exited,
190
- ])
191
-
192
- expect(exitCode).toBe(0)
193
- expect(stdout).toContain('Dry run')
194
- expect(stdout).toContain('Deploy Dashboard')
195
- })
196
-
197
- it('dry-run local mode prints planned bun command without executing', async () => {
198
- const proc = Bun.spawn(['bun', 'run', 'packages/cli/src/cli.ts', 'dashboard', 'deploy', '--local', '--dry-run'], {
199
- cwd: repoRoot,
200
- env: {...process.env, NO_COLOR: '1'},
201
- stdout: 'pipe',
202
- stderr: 'pipe',
203
- })
204
-
205
- const [stdout, _stderr, exitCode] = await Promise.all([
206
- new Response(proc.stdout).text(),
207
- new Response(proc.stderr).text(),
208
- proc.exited,
209
- ])
210
-
211
- expect(exitCode).toBe(0)
212
- expect(stdout).toContain('Dry run')
213
- expect(stdout).toContain('apps/dashboard')
214
- })
215
- })
@@ -1,135 +0,0 @@
1
- import type {goke} from 'goke'
2
-
3
- import {z} from 'zod'
4
-
5
- declare const process: {
6
- env: Record<string, string | undefined>
7
- exitCode?: number
8
- }
9
-
10
- const REPO = 'marcusrbrown/infra'
11
- const WORKFLOW_NAME = 'Deploy Dashboard'
12
- const WORKFLOW_URL = 'https://github.com/marcusrbrown/infra/actions/workflows/deploy-dashboard.yaml'
13
-
14
- type CliInstance = ReturnType<typeof goke>
15
-
16
- // ─── Env helpers ─────────────────────────────────────────────────────────────
17
-
18
- export function getDashboardDeployEnv(): Record<string, string> {
19
- const path = process.env.PATH
20
- const home = process.env.HOME
21
- const sshAuthSock = process.env.SSH_AUTH_SOCK
22
- const sshKey = process.env.DASHBOARD_SSH_KEY
23
-
24
- if (!path) {
25
- throw new Error('PATH is required for local deploy')
26
- }
27
-
28
- if (!home) {
29
- throw new Error('HOME is required for local deploy')
30
- }
31
-
32
- if (!sshAuthSock && !sshKey) {
33
- throw new Error(
34
- 'Local deploy needs an SSH context: set SSH_AUTH_SOCK (ssh-agent) or DASHBOARD_SSH_KEY (key from env).',
35
- )
36
- }
37
-
38
- // Spread all of process.env so DASHBOARD_* vars (GITHUB_APP_ID, OAUTH_CLIENT_ID, etc.)
39
- // are forwarded to the spawned deploy.ts process. validateEnv in deploy.ts requires
40
- // all DASHBOARD_* credential vars to be present — a selective subset would cause it to fail.
41
- // Filter out undefined values (Record<string, string> does not allow undefined).
42
- const fullEnv: Record<string, string> = {}
43
- for (const [k, v] of Object.entries(process.env)) {
44
- if (v !== undefined) {
45
- fullEnv[k] = v
46
- }
47
- }
48
- return fullEnv
49
- }
50
-
51
- export function validateDashboardRemotePreconditions(): void {
52
- if (!Bun.which('gh')) {
53
- throw new Error('gh CLI is required for remote deploy. Install gh and run `gh auth login`.')
54
- }
55
- }
56
-
57
- // ─── Command registration ─────────────────────────────────────────────────────
58
-
59
- export function registerDashboardDeploy(cli: CliInstance): void {
60
- cli
61
- .command(
62
- 'dashboard deploy',
63
- 'Deploy the dashboard. Default mode triggers the GitHub Deploy Dashboard workflow, while --local runs apps/dashboard deploy directly with Bun.',
64
- )
65
- .option(
66
- '--local',
67
- z
68
- .boolean()
69
- .default(false)
70
- .describe('Run local deployment with Bun using apps/dashboard instead of triggering GitHub Actions.'),
71
- )
72
- .option(
73
- '--dry-run',
74
- z
75
- .boolean()
76
- .default(false)
77
- .describe(
78
- 'Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.',
79
- ),
80
- )
81
- .example('# Trigger remote GitHub Actions deploy (default mode)')
82
- .example('infra dashboard deploy')
83
- .example('# Validate local deploy preconditions and planned command')
84
- .example('infra dashboard deploy --local --dry-run')
85
- .example('# Run local deploy with explicit SSH agent context')
86
- .example('infra dashboard deploy --local')
87
- .action(async options => {
88
- if (options.local) {
89
- const command = ['bun', 'run', '--cwd', 'apps/dashboard', 'deploy']
90
-
91
- if (options.dryRun) {
92
- console.log('Dry run: local dashboard deploy')
93
- console.log(`- command: ${command.join(' ')}`)
94
- return
95
- }
96
-
97
- const env = getDashboardDeployEnv()
98
-
99
- const child = Bun.spawn(command, {
100
- env,
101
- stdout: 'inherit',
102
- stderr: 'inherit',
103
- })
104
-
105
- const exitCode = await child.exited
106
- if (exitCode !== 0) {
107
- throw new Error(`Local deploy failed with exit code ${exitCode}`)
108
- }
109
-
110
- return
111
- }
112
-
113
- validateDashboardRemotePreconditions()
114
-
115
- if (options.dryRun) {
116
- console.log('Dry run: remote dashboard deploy')
117
- console.log(`- command: gh workflow run "${WORKFLOW_NAME}" --repo ${REPO}`)
118
- console.log(`- workflow URL: ${WORKFLOW_URL}`)
119
- return
120
- }
121
-
122
- const child = Bun.spawn(['gh', 'workflow', 'run', WORKFLOW_NAME, '--repo', REPO], {
123
- stdout: 'inherit',
124
- stderr: 'inherit',
125
- })
126
-
127
- const exitCode = await child.exited
128
- if (exitCode !== 0) {
129
- throw new Error(`Failed to trigger "${WORKFLOW_NAME}" workflow (exit code ${exitCode})`)
130
- }
131
-
132
- console.log(`Workflow triggered: ${WORKFLOW_URL}`)
133
- console.log('Approve the dashboard environment deployment in GitHub Actions to continue.')
134
- })
135
- }
@@ -1,79 +0,0 @@
1
- import {describe, expect, it} from 'bun:test'
2
-
3
- import {validateDashboardHost} from './host'
4
-
5
- describe('validateDashboardHost', () => {
6
- it('accepts a standard FQDN', () => {
7
- expect(() => validateDashboardHost('dashboard.fro.bot')).not.toThrow()
8
- expect(validateDashboardHost('dashboard.fro.bot')).toBe('dashboard.fro.bot')
9
- })
10
-
11
- it('accepts localhost', () => {
12
- expect(() => validateDashboardHost('localhost')).not.toThrow()
13
- })
14
-
15
- it('accepts an IPv4 address', () => {
16
- expect(() => validateDashboardHost('147.182.133.210')).not.toThrow()
17
- })
18
-
19
- it('accepts a single-character hostname', () => {
20
- expect(() => validateDashboardHost('a')).not.toThrow()
21
- })
22
-
23
- it('accepts a hostname with hyphens', () => {
24
- expect(() => validateDashboardHost('my-dashboard.prod.example.com')).not.toThrow()
25
- })
26
-
27
- it('rejects a leading-hyphen value (ProxyCommand injection vector)', () => {
28
- expect(() => validateDashboardHost('-oProxyCommand=evil')).toThrow('Invalid DASHBOARD_DOMAIN')
29
- })
30
-
31
- it('rejects a value with shell metacharacters (semicolon)', () => {
32
- expect(() => validateDashboardHost('dashboard.fro.bot;rm -rf')).toThrow('Invalid DASHBOARD_DOMAIN')
33
- })
34
-
35
- it('rejects a value with shell metacharacters (backtick)', () => {
36
- expect(() => validateDashboardHost('dashboard.fro.bot`id`')).toThrow('Invalid DASHBOARD_DOMAIN')
37
- })
38
-
39
- it('rejects a value with spaces', () => {
40
- expect(() => validateDashboardHost('dashboard fro.bot')).toThrow('Invalid DASHBOARD_DOMAIN')
41
- })
42
-
43
- it('rejects a value with an at-sign', () => {
44
- expect(() => validateDashboardHost('user@dashboard.fro.bot')).toThrow('Invalid DASHBOARD_DOMAIN')
45
- })
46
-
47
- it('rejects an empty string', () => {
48
- expect(() => validateDashboardHost('')).toThrow('Invalid DASHBOARD_DOMAIN')
49
- })
50
-
51
- it('omits the rejected value entirely from the error message', () => {
52
- const longMalicious = `-oProxyCommand=${'A'.repeat(100)}`
53
- let message = ''
54
- try {
55
- validateDashboardHost(longMalicious)
56
- } catch (error) {
57
- message = error instanceof Error ? error.message : String(error)
58
- }
59
- // The value is never echoed (it may be a misdirected secret) — only the constraint is shown.
60
- expect(message).toContain('Invalid DASHBOARD_DOMAIN')
61
- expect(message).not.toContain('ProxyCommand')
62
- expect(message).not.toContain('AAAA')
63
- })
64
-
65
- // ── Secret value redaction ───────────────────────────────────────────────────
66
-
67
- it('does not echo the rejected value in the error message', () => {
68
- const secretValue = 'AWS_SECRET_KEY=AKIA1234567890EXAMPLE'
69
- let message = ''
70
- try {
71
- validateDashboardHost(secretValue)
72
- } catch (error) {
73
- message = error instanceof Error ? error.message : String(error)
74
- }
75
- expect(message).toContain('Invalid DASHBOARD_DOMAIN')
76
- expect(message).not.toContain('AKIA1234567890EXAMPLE')
77
- expect(message).not.toContain('AWS_SECRET_KEY')
78
- })
79
- })
@@ -1,33 +0,0 @@
1
- // ─── Dashboard host validation ────────────────────────────────────────────────
2
- //
3
- // Validates DASHBOARD_DOMAIN values before they are passed as ssh argv arguments.
4
- // A value starting with `-` would be interpreted by ssh as an option flag,
5
- // enabling ProxyCommand injection and local code execution.
6
-
7
- const VALID_HOST_RE = /^[a-z\d][a-z\d.\-]*$/i
8
-
9
- /**
10
- * Validates a candidate DASHBOARD_DOMAIN value against a strict hostname allowlist.
11
- *
12
- * Accepts: hostnames, FQDNs, IPv4 addresses, `localhost`.
13
- * Rejects: empty strings, values starting with `-`, and anything containing
14
- * characters outside `[A-Za-z0-9.-]`.
15
- *
16
- * @throws {Error} when empty or not a valid hostname. The rejected value is
17
- * never included in the message (it may be a misdirected secret).
18
- * @returns The validated host string (unchanged).
19
- */
20
- export function validateDashboardHost(host: string): string {
21
- if (!host) {
22
- throw new Error('Invalid DASHBOARD_DOMAIN: value is empty')
23
- }
24
-
25
- if (!VALID_HOST_RE.test(host)) {
26
- // Do NOT echo the value — it may be a secret that was redirected via --key
27
- throw new Error(
28
- `Invalid DASHBOARD_DOMAIN: value is not a valid hostname (must match ${String.raw`[A-Za-z0-9][A-Za-z0-9.\-]*`})`,
29
- )
30
- }
31
-
32
- return host
33
- }
@@ -1,13 +0,0 @@
1
- import type {goke} from 'goke'
2
-
3
- import {registerDashboardDeploy} from './deploy'
4
- import {registerDashboardLogs} from './logs'
5
- import {registerDashboardStatus} from './status'
6
-
7
- export {getDashboardStatusSummary} from './status'
8
-
9
- export function registerDashboardCommands(cli: ReturnType<typeof goke>): void {
10
- registerDashboardStatus(cli)
11
- registerDashboardDeploy(cli)
12
- registerDashboardLogs(cli)
13
- }