@marcusrbrown/infra 0.13.1 → 0.13.3

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 +11965 -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,308 +0,0 @@
1
- import type {goke} from 'goke'
2
-
3
- import {z} from 'zod'
4
-
5
- import {buildKnownHostsArgs} from '../../lib/known-hosts'
6
- import {validateGatewayHost} from './host'
7
-
8
- declare const process: {
9
- env: Record<string, string | undefined>
10
- exitCode?: number
11
- }
12
-
13
- const MITMPROXY_CERTS_VOLUME = 'mitmproxy-certs'
14
- const CA_CERT_FILE = 'mitmproxy-ca-cert.pem'
15
- const CA_KEY_FILE = 'mitmproxy-ca.pem'
16
- const COMPOSE_PROJECT_DIR = '/opt/gateway/deploy'
17
- const EXPECTED_ARCHIVE_FILES = new Set([CA_CERT_FILE, CA_KEY_FILE])
18
-
19
- // ─── Types ────────────────────────────────────────────────────────────────────
20
-
21
- export type RestoreSpawnFn = (
22
- cmd: string[],
23
- opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
24
- ) => {
25
- stdout: ReadableStream<Uint8Array>
26
- stderr: ReadableStream<Uint8Array>
27
- exited: Promise<number>
28
- }
29
-
30
- export interface RestoreOpts {
31
- host: string
32
- input: string
33
- includeCa: boolean
34
- }
35
-
36
- export type RestoreResult = {ok: true; confirmed: true} | {ok: false; error: string}
37
-
38
- // ─── Helpers ──────────────────────────────────────────────────────────────────
39
-
40
- /** Returns the argv array for an SSH command to the given host. */
41
- function sshCommand(host: string, remote: string): string[] {
42
- return [
43
- 'ssh',
44
- '-o',
45
- 'BatchMode=yes',
46
- '-o',
47
- 'StrictHostKeyChecking=yes',
48
- ...buildKnownHostsArgs(),
49
- `root@${host}`,
50
- remote,
51
- ]
52
- }
53
-
54
- async function spawnText(
55
- spawn: RestoreSpawnFn,
56
- cmd: string[],
57
- env: Record<string, string>,
58
- ): Promise<{stdout: string; stderr: string; exitCode: number}> {
59
- const child = spawn(cmd, {env, stdout: 'pipe', stderr: 'pipe'})
60
- const [stdout, stderr, exitCode] = await Promise.all([
61
- new Response(child.stdout).text(),
62
- new Response(child.stderr).text(),
63
- child.exited,
64
- ])
65
- return {stdout, stderr, exitCode}
66
- }
67
-
68
- /**
69
- * Validates that the local tarball contains exactly the two expected CA files
70
- * and nothing else. Uses `tar -tf` to list contents without extracting.
71
- * Returns an error string if invalid, or undefined if valid.
72
- */
73
- export function validateBackupArchive(
74
- inputPath: string,
75
- spawn: RestoreSpawnFn,
76
- env: Record<string, string>,
77
- ): Promise<string | undefined> {
78
- return spawnText(spawn, ['tar', '-tf', inputPath], env).then(result => {
79
- if (result.exitCode !== 0) {
80
- return `Cannot read archive (tar -tf exit ${result.exitCode}): ${result.stderr.trim() || 'unknown error'}`
81
- }
82
- const listed = result.stdout
83
- .split('\n')
84
- .map(f => f.trim())
85
- .filter(Boolean)
86
- const listedSet = new Set(listed)
87
- const missing = [...EXPECTED_ARCHIVE_FILES].filter(f => !listedSet.has(f))
88
- const extra = listed.filter(f => !EXPECTED_ARCHIVE_FILES.has(f))
89
- if (missing.length > 0 || extra.length > 0) {
90
- const parts: string[] = []
91
- if (missing.length > 0) parts.push(`missing: ${missing.join(', ')}`)
92
- if (extra.length > 0) parts.push(`unexpected: ${extra.join(', ')}`)
93
- return `Backup archive is malformed — ${parts.join('; ')}`
94
- }
95
- return undefined
96
- })
97
- }
98
-
99
- // ─── Core logic ───────────────────────────────────────────────────────────────
100
-
101
- function defaultSpawn(
102
- cmd: string[],
103
- opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
104
- ): ReturnType<RestoreSpawnFn> {
105
- return Bun.spawn(cmd, opts) as ReturnType<RestoreSpawnFn>
106
- }
107
-
108
- export async function restoreGatewayCa(
109
- opts: RestoreOpts,
110
- spawn: RestoreSpawnFn = defaultSpawn,
111
- ): Promise<RestoreResult> {
112
- if (!opts.includeCa) {
113
- return {ok: false, error: 'Only CA restore is currently supported; --no-include-ca is not accepted'}
114
- }
115
-
116
- // Validate input file is non-empty before SSHing
117
- const inputFile = Bun.file(opts.input)
118
- const inputSize = inputFile.size
119
-
120
- if (inputSize === 0) {
121
- return {ok: false, error: `Input file is empty: ${opts.input}`}
122
- }
123
-
124
- // Validate host before any SSH invocation
125
- validateGatewayHost(opts.host)
126
-
127
- const env: Record<string, string> = {
128
- PATH: process.env.PATH ?? '/usr/bin:/bin',
129
- HOME: process.env.HOME ?? '/tmp',
130
- ...(process.env.SSH_AUTH_SOCK ? {SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK} : {}),
131
- }
132
-
133
- // COR1: Validate archive contents before touching the remote — fail fast before SCP
134
- const archiveError = await validateBackupArchive(opts.input, spawn, env)
135
- if (archiveError !== undefined) {
136
- return {ok: false, error: archiveError}
137
- }
138
-
139
- // SEC2: Generate an unguessable remote tmp path via mktemp on the droplet
140
- const mktempResult = await spawnText(spawn, sshCommand(opts.host, 'mktemp -t gateway-ca-restore-XXXXXX.tar'), env)
141
- if (mktempResult.exitCode !== 0) {
142
- return {
143
- ok: false,
144
- error: `mktemp failed on remote (exit ${mktempResult.exitCode}): ${mktempResult.stderr.trim() || 'unknown error'}`,
145
- }
146
- }
147
- const tmpRemote = mktempResult.stdout.trim()
148
-
149
- // SCP the tarball to the unguessable remote path
150
- const scpCmd = [
151
- 'scp',
152
- '-o',
153
- 'BatchMode=yes',
154
- '-o',
155
- 'StrictHostKeyChecking=yes',
156
- ...buildKnownHostsArgs(),
157
- opts.input,
158
- `root@${opts.host}:${tmpRemote}`,
159
- ]
160
-
161
- let primaryError: RestoreResult | undefined
162
-
163
- try {
164
- const scpResult = await spawnText(spawn, scpCmd, env)
165
- if (scpResult.exitCode !== 0) {
166
- primaryError = {
167
- ok: false,
168
- error: `SCP failed (exit ${scpResult.exitCode}): ${scpResult.stderr.trim() || 'unknown error'}`,
169
- }
170
- return primaryError
171
- }
172
-
173
- // Defense-in-depth: chmod 600 the SCP'd file on the remote before docker run
174
- const chmodResult = await spawnText(spawn, sshCommand(opts.host, `chmod 600 ${tmpRemote}`), env)
175
- if (chmodResult.exitCode !== 0) {
176
- primaryError = {
177
- ok: false,
178
- error: `chmod 600 on remote failed (exit ${chmodResult.exitCode}): ${chmodResult.stderr.trim() || 'unknown error'}`,
179
- }
180
- return primaryError
181
- }
182
-
183
- // Extract the tarball into the mitmproxy-certs volume
184
- const extractResult = await spawnText(
185
- spawn,
186
- sshCommand(
187
- opts.host,
188
- `docker run --rm -v ${MITMPROXY_CERTS_VOLUME}:/dst -v ${tmpRemote}:/src.tar:ro alpine sh -c 'tar -xf /src.tar -C /dst'`,
189
- ),
190
- env,
191
- )
192
- if (extractResult.exitCode !== 0) {
193
- primaryError = {
194
- ok: false,
195
- error: `docker run extract failed (exit ${extractResult.exitCode}): ${extractResult.stderr.trim() || 'unknown error'}`,
196
- }
197
- return primaryError
198
- }
199
-
200
- // Restart mitmproxy and gateway services
201
- const restartResult = await spawnText(
202
- spawn,
203
- sshCommand(opts.host, `docker compose --project-directory ${COMPOSE_PROJECT_DIR} restart mitmproxy gateway`),
204
- env,
205
- )
206
- if (restartResult.exitCode !== 0) {
207
- primaryError = {
208
- ok: false,
209
- error: `docker compose restart failed (exit ${restartResult.exitCode}): ${restartResult.stderr.trim() || 'unknown error'}`,
210
- }
211
- return primaryError
212
- }
213
-
214
- // Byte-equal confirmation: read cert and key from inside the volume
215
- const [certResult, keyResult] = await Promise.all([
216
- spawnText(
217
- spawn,
218
- sshCommand(opts.host, `docker run --rm -v ${MITMPROXY_CERTS_VOLUME}:/src:ro alpine cat /src/${CA_CERT_FILE}`),
219
- env,
220
- ),
221
- spawnText(
222
- spawn,
223
- sshCommand(opts.host, `docker run --rm -v ${MITMPROXY_CERTS_VOLUME}:/src:ro alpine cat /src/${CA_KEY_FILE}`),
224
- env,
225
- ),
226
- ])
227
-
228
- if (certResult.exitCode !== 0 || keyResult.exitCode !== 0) {
229
- primaryError = {
230
- ok: false,
231
- error: `Confirmation read failed — cert exit ${certResult.exitCode}, key exit ${keyResult.exitCode}`,
232
- }
233
- return primaryError
234
- }
235
-
236
- // Extract cert and key from the local tarball for comparison
237
- const localCertProc = Bun.spawnSync(['tar', '-xOf', opts.input, CA_CERT_FILE])
238
- const localKeyProc = Bun.spawnSync(['tar', '-xOf', opts.input, CA_KEY_FILE])
239
-
240
- const localCert = new TextDecoder().decode(localCertProc.stdout)
241
- const localKey = new TextDecoder().decode(localKeyProc.stdout)
242
-
243
- if (certResult.stdout !== localCert || keyResult.stdout !== localKey) {
244
- primaryError = {
245
- ok: false,
246
- error: `Confirmation mismatch: restored CA content does not match input tarball. The volume may be in an inconsistent state.`,
247
- }
248
- return primaryError
249
- }
250
-
251
- return {ok: true, confirmed: true}
252
- } finally {
253
- // Always clean up the tmp file from the droplet; never let cleanup mask the primary failure
254
- try {
255
- await spawnText(spawn, sshCommand(opts.host, `rm -f ${tmpRemote}`), env)
256
- } catch (error) {
257
- console.error(`Cleanup also failed: ${error instanceof Error ? error.message : String(error)}`)
258
- }
259
- }
260
- }
261
-
262
- // ─── Command registration ─────────────────────────────────────────────────────
263
-
264
- export function registerGatewayRestore(cli: ReturnType<typeof goke>): void {
265
- cli
266
- .command(
267
- 'gateway restore',
268
- 'Restore the mitmproxy CA certificate and private key from a local tarball into the gateway Docker volume. Restarts mitmproxy and gateway services after restore and confirms byte-equal content before reporting success.',
269
- )
270
- .option(
271
- '--input <file>',
272
- z.string().describe('Path to the tarball produced by `gateway backup --include-ca`. Required.'),
273
- )
274
- .option(
275
- '--include-ca',
276
- z
277
- .boolean()
278
- .default(true)
279
- .describe('Restore the mitmproxy CA certificate and private key. Currently the only supported restore target.'),
280
- )
281
- .example('# Restore CA from a backup tarball')
282
- .example('infra gateway restore --input apps/gateway/.local/mitmproxy-ca.tar')
283
- .example('# Restore CA from a custom path')
284
- .example('infra gateway restore --input /secure/backup/mitmproxy-ca.tar --include-ca')
285
- .action(async options => {
286
- const hostEnvKey = 'GATEWAY_HOST'
287
- const host = process.env[hostEnvKey]
288
-
289
- if (!host) {
290
- console.error(`Gateway host not set. Export ${hostEnvKey} before running restore.`)
291
- process.exitCode = 1
292
- return
293
- }
294
-
295
- const input = options.input
296
- const includeCa = options.includeCa !== false
297
-
298
- const result = await restoreGatewayCa({host, input, includeCa})
299
-
300
- if (!result.ok) {
301
- console.error(`Restore failed: ${result.error}`)
302
- process.exitCode = 1
303
- return
304
- }
305
-
306
- console.log('CA restore complete. Byte-equal confirmation passed.')
307
- })
308
- }