@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.
- package/dist/cli.js +11965 -0
- package/package.json +7 -3
- package/src/__snapshots__/cli.test.ts.snap +0 -219
- package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
- package/src/__test__/mcp-ctx-fixture.ts +0 -104
- package/src/cli.test.ts +0 -66
- package/src/cli.ts +0 -43
- package/src/commands/cliproxy/config.test.ts +0 -475
- package/src/commands/cliproxy/config.ts +0 -238
- package/src/commands/cliproxy/deploy.test.ts +0 -181
- package/src/commands/cliproxy/deploy.ts +0 -157
- package/src/commands/cliproxy/host.test.ts +0 -73
- package/src/commands/cliproxy/host.ts +0 -31
- package/src/commands/cliproxy/index.ts +0 -21
- package/src/commands/cliproxy/keys.test.ts +0 -326
- package/src/commands/cliproxy/keys.ts +0 -201
- package/src/commands/cliproxy/login.test.ts +0 -415
- package/src/commands/cliproxy/login.ts +0 -131
- package/src/commands/cliproxy/models.test.ts +0 -592
- package/src/commands/cliproxy/models.ts +0 -237
- package/src/commands/cliproxy/open.test.ts +0 -132
- package/src/commands/cliproxy/open.ts +0 -78
- package/src/commands/cliproxy/setup/gh.test.ts +0 -218
- package/src/commands/cliproxy/setup/gh.ts +0 -250
- package/src/commands/cliproxy/setup/preview.test.ts +0 -159
- package/src/commands/cliproxy/setup/preview.ts +0 -41
- package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
- package/src/commands/cliproxy/setup/prompts.ts +0 -99
- package/src/commands/cliproxy/setup/providers.test.ts +0 -245
- package/src/commands/cliproxy/setup/providers.ts +0 -136
- package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
- package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
- package/src/commands/cliproxy/setup/templates.test.ts +0 -357
- package/src/commands/cliproxy/setup/templates.ts +0 -145
- package/src/commands/cliproxy/setup/validation.test.ts +0 -567
- package/src/commands/cliproxy/setup/validation.ts +0 -209
- package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
- package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
- package/src/commands/cliproxy/setup.test.ts +0 -2366
- package/src/commands/cliproxy/setup.ts +0 -809
- package/src/commands/cliproxy/shared.test.ts +0 -118
- package/src/commands/cliproxy/shared.ts +0 -84
- package/src/commands/cliproxy/status.test.ts +0 -1157
- package/src/commands/cliproxy/status.ts +0 -597
- package/src/commands/dashboard/deploy.test.ts +0 -215
- package/src/commands/dashboard/deploy.ts +0 -135
- package/src/commands/dashboard/host.test.ts +0 -79
- package/src/commands/dashboard/host.ts +0 -33
- package/src/commands/dashboard/index.ts +0 -13
- package/src/commands/dashboard/logs.test.ts +0 -317
- package/src/commands/dashboard/logs.ts +0 -180
- package/src/commands/dashboard/status.test.ts +0 -708
- package/src/commands/dashboard/status.ts +0 -293
- package/src/commands/gateway/backup.test.ts +0 -426
- package/src/commands/gateway/backup.ts +0 -205
- package/src/commands/gateway/deploy.test.ts +0 -491
- package/src/commands/gateway/deploy.ts +0 -173
- package/src/commands/gateway/host.test.ts +0 -89
- package/src/commands/gateway/host.ts +0 -33
- package/src/commands/gateway/index.ts +0 -17
- package/src/commands/gateway/logs.test.ts +0 -263
- package/src/commands/gateway/logs.ts +0 -160
- package/src/commands/gateway/restore.test.ts +0 -650
- package/src/commands/gateway/restore.ts +0 -308
- package/src/commands/gateway/status.test.ts +0 -755
- package/src/commands/gateway/status.ts +0 -255
- package/src/commands/keeweb/deploy.test.ts +0 -177
- package/src/commands/keeweb/deploy.ts +0 -191
- package/src/commands/keeweb/index.ts +0 -11
- package/src/commands/keeweb/open.test.ts +0 -54
- package/src/commands/keeweb/open.ts +0 -30
- package/src/commands/keeweb/status.test.ts +0 -509
- package/src/commands/keeweb/status.ts +0 -359
- package/src/commands/mcp.test.ts +0 -260
- package/src/commands/mcp.ts +0 -58
- package/src/commands/status.test.ts +0 -222
- package/src/commands/status.ts +0 -161
- package/src/commands/umami/deploy.test.ts +0 -202
- package/src/commands/umami/deploy.ts +0 -132
- package/src/commands/umami/host.test.ts +0 -79
- package/src/commands/umami/host.ts +0 -33
- package/src/commands/umami/index.ts +0 -13
- package/src/commands/umami/logs.test.ts +0 -191
- package/src/commands/umami/logs.ts +0 -163
- package/src/commands/umami/status.test.ts +0 -633
- package/src/commands/umami/status.ts +0 -285
- package/src/commands/vpn/client.test.ts +0 -949
- package/src/commands/vpn/client.ts +0 -467
- package/src/commands/vpn/deploy.test.ts +0 -208
- package/src/commands/vpn/deploy.ts +0 -143
- package/src/commands/vpn/host.test.ts +0 -89
- package/src/commands/vpn/host.ts +0 -36
- package/src/commands/vpn/index.ts +0 -15
- package/src/commands/vpn/logs.test.ts +0 -234
- package/src/commands/vpn/logs.ts +0 -136
- package/src/commands/vpn/peers.test.ts +0 -478
- package/src/commands/vpn/shared.test.ts +0 -454
- package/src/commands/vpn/shared.ts +0 -170
- package/src/commands/vpn/status.test.ts +0 -142
- package/src/commands/vpn/status.ts +0 -78
- package/src/conventions.test.ts +0 -1180
- package/src/lib/action-ctx.ts +0 -25
- package/src/lib/known-hosts.test.ts +0 -192
- package/src/lib/known-hosts.ts +0 -76
- package/src/lib/redact.ts +0 -21
- package/src/lib/ssh-identity.test.ts +0 -134
- package/src/lib/ssh-identity.ts +0 -76
- /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
|
-
}
|