@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.
- package/dist/cli.js +4977 -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,21 +0,0 @@
|
|
|
1
|
-
import type {goke} from 'goke'
|
|
2
|
-
|
|
3
|
-
import {registerCliproxyConfig} from './config'
|
|
4
|
-
import {registerCliproxyDeploy} from './deploy'
|
|
5
|
-
import {registerCliproxyKeys} from './keys'
|
|
6
|
-
import {registerCliproxyLogin} from './login'
|
|
7
|
-
import {registerCliproxyModels} from './models'
|
|
8
|
-
import {registerCliproxyOpen} from './open'
|
|
9
|
-
import {registerCliproxySetup} from './setup'
|
|
10
|
-
import {registerCliproxyStatus} from './status'
|
|
11
|
-
|
|
12
|
-
export function registerCliproxyCommands(cli: ReturnType<typeof goke>): void {
|
|
13
|
-
registerCliproxyStatus(cli)
|
|
14
|
-
registerCliproxyDeploy(cli)
|
|
15
|
-
registerCliproxyConfig(cli)
|
|
16
|
-
registerCliproxyKeys(cli)
|
|
17
|
-
registerCliproxyLogin(cli)
|
|
18
|
-
registerCliproxyModels(cli)
|
|
19
|
-
registerCliproxyOpen(cli)
|
|
20
|
-
registerCliproxySetup(cli)
|
|
21
|
-
}
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {createCapturedCtx, expectCapturedToInclude} from '../../__test__/mcp-ctx-fixture'
|
|
4
|
-
import {cliproxyKeysAddAction, cliproxyKeysListAction, cliproxyKeysRemoveAction, toStringArray} from './keys'
|
|
5
|
-
|
|
6
|
-
const originalFetch = globalThis.fetch
|
|
7
|
-
|
|
8
|
-
type FetchReplacement = (url: string, init?: RequestInit) => Promise<Response>
|
|
9
|
-
|
|
10
|
-
function createFetchImplementation(handler: FetchReplacement): typeof fetch {
|
|
11
|
-
return Object.assign(
|
|
12
|
-
(input: string | URL | Request, init?: RequestInit) => {
|
|
13
|
-
if (typeof input !== 'string') {
|
|
14
|
-
throw new TypeError(`Unexpected non-string fetch input: ${String(input)}`)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return handler(input, init)
|
|
18
|
-
},
|
|
19
|
-
{preconnect: originalFetch.preconnect},
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('toStringArray', () => {
|
|
24
|
-
it('returns string array as-is', () => {
|
|
25
|
-
expect(toStringArray(['a', 'b', 'c'])).toEqual(['a', 'b', 'c'])
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('filters non-string items from array', () => {
|
|
29
|
-
expect(toStringArray(['a', 1, null, 'b'])).toEqual(['a', 'b'])
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('extracts api-keys from object', () => {
|
|
33
|
-
expect(toStringArray({'api-keys': ['x', 'y']})).toEqual(['x', 'y'])
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('extracts api_keys from object (underscore variant)', () => {
|
|
37
|
-
expect(toStringArray({api_keys: ['x', 'y']})).toEqual(['x', 'y'])
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('returns empty array for null', () => {
|
|
41
|
-
expect(toStringArray(null)).toEqual([])
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('returns empty array for unrecognized shape', () => {
|
|
45
|
-
expect(toStringArray({other: 'stuff'})).toEqual([])
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
describe('cliproxyKeysListAction (Mode C, Tier-2 ctx capture)', () => {
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
globalThis.fetch = createFetchImplementation(async () => {
|
|
52
|
-
throw new Error('Unexpected fetch call')
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
afterEach(() => {
|
|
57
|
-
globalThis.fetch = originalFetch
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('Mode C: captures numbered list to ctx.stdout and returns key array', async () => {
|
|
61
|
-
globalThis.fetch = createFetchImplementation(
|
|
62
|
-
async () =>
|
|
63
|
-
new Response(JSON.stringify(['sk-live-aaa', 'sk-live-bbb']), {
|
|
64
|
-
status: 200,
|
|
65
|
-
headers: {'content-type': 'application/json'},
|
|
66
|
-
}),
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
const {ctx, captured} = createCapturedCtx()
|
|
70
|
-
const result = await cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
71
|
-
|
|
72
|
-
// Tier-2: stdout contains formatted list
|
|
73
|
-
expect(expectCapturedToInclude(captured, 'sk-live-aaa')).toBe(true)
|
|
74
|
-
expect(expectCapturedToInclude(captured, '1.')).toBe(true)
|
|
75
|
-
expect(expectCapturedToInclude(captured, '2.')).toBe(true)
|
|
76
|
-
|
|
77
|
-
// Mode C: action returns the parsed array
|
|
78
|
-
expect(Array.isArray(result)).toBe(true)
|
|
79
|
-
expect(result).toEqual(['sk-live-aaa', 'sk-live-bbb'])
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('Mode C: security warning goes to ctx.stderr', async () => {
|
|
83
|
-
globalThis.fetch = createFetchImplementation(
|
|
84
|
-
async () =>
|
|
85
|
-
new Response(JSON.stringify(['sk-live-aaa']), {
|
|
86
|
-
status: 200,
|
|
87
|
-
headers: {'content-type': 'application/json'},
|
|
88
|
-
}),
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
const {ctx, captured} = createCapturedCtx()
|
|
92
|
-
await cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
93
|
-
|
|
94
|
-
const stderrText = captured.stderr.join('')
|
|
95
|
-
expect(stderrText).toContain('API keys')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('Mode C: returns empty array and prints "No API keys configured" when list is empty', async () => {
|
|
99
|
-
globalThis.fetch = createFetchImplementation(
|
|
100
|
-
async () =>
|
|
101
|
-
new Response(JSON.stringify([]), {
|
|
102
|
-
status: 200,
|
|
103
|
-
headers: {'content-type': 'application/json'},
|
|
104
|
-
}),
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
const {ctx, captured} = createCapturedCtx()
|
|
108
|
-
const result = await cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
109
|
-
|
|
110
|
-
expect(expectCapturedToInclude(captured, 'No API keys configured')).toBe(true)
|
|
111
|
-
expect(result).toEqual([])
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('Mode C: --json flag outputs JSON array to ctx.stdout', async () => {
|
|
115
|
-
globalThis.fetch = createFetchImplementation(
|
|
116
|
-
async () =>
|
|
117
|
-
new Response(JSON.stringify(['sk-live-aaa']), {
|
|
118
|
-
status: 200,
|
|
119
|
-
headers: {'content-type': 'application/json'},
|
|
120
|
-
}),
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
const {ctx, captured} = createCapturedCtx()
|
|
124
|
-
const result = await cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key', json: true}, ctx)
|
|
125
|
-
|
|
126
|
-
const stdoutText = captured.stdout.join('')
|
|
127
|
-
const parsed = JSON.parse(stdoutText)
|
|
128
|
-
expect(parsed).toEqual(['sk-live-aaa'])
|
|
129
|
-
expect(result).toEqual(['sk-live-aaa'])
|
|
130
|
-
})
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
describe('cliproxyKeysAddAction (Mode A, positional arg, Tier-2 ctx capture)', () => {
|
|
134
|
-
afterEach(() => {
|
|
135
|
-
globalThis.fetch = originalFetch
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('Mode A: captures success message to ctx.stdout after adding key', async () => {
|
|
139
|
-
let putCalled = false
|
|
140
|
-
globalThis.fetch = createFetchImplementation(async (_url, init) => {
|
|
141
|
-
if (init?.method === 'PUT') {
|
|
142
|
-
putCalled = true
|
|
143
|
-
return new Response(JSON.stringify(['sk-live-existing', 'sk-live-new']), {
|
|
144
|
-
status: 200,
|
|
145
|
-
headers: {'content-type': 'application/json'},
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// GET current keys
|
|
150
|
-
return new Response(JSON.stringify(['sk-live-existing']), {
|
|
151
|
-
status: 200,
|
|
152
|
-
headers: {'content-type': 'application/json'},
|
|
153
|
-
})
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
const {ctx, captured} = createCapturedCtx()
|
|
157
|
-
await cliproxyKeysAddAction('sk-live-new', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
158
|
-
|
|
159
|
-
expect(putCalled).toBe(true)
|
|
160
|
-
expect(expectCapturedToInclude(captured, 'sk-live-new')).toBe(true)
|
|
161
|
-
expect(expectCapturedToInclude(captured, 'Added key')).toBe(true)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('Mode A: skips PUT when key already present', async () => {
|
|
165
|
-
let putCalled = false
|
|
166
|
-
globalThis.fetch = createFetchImplementation(async (_url, init) => {
|
|
167
|
-
if (init?.method === 'PUT') {
|
|
168
|
-
putCalled = true
|
|
169
|
-
return new Response('{}', {status: 200})
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return new Response(JSON.stringify(['sk-live-existing']), {
|
|
173
|
-
status: 200,
|
|
174
|
-
headers: {'content-type': 'application/json'},
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
const {ctx, captured} = createCapturedCtx()
|
|
179
|
-
await cliproxyKeysAddAction('sk-live-existing', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
180
|
-
|
|
181
|
-
expect(putCalled).toBe(false)
|
|
182
|
-
expect(expectCapturedToInclude(captured, 'already present')).toBe(true)
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
describe('cliproxyKeysRemoveAction (Mode A, positional arg, Tier-2 ctx capture)', () => {
|
|
187
|
-
afterEach(() => {
|
|
188
|
-
globalThis.fetch = originalFetch
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('Mode A: captures DELETE response to ctx.stdout', async () => {
|
|
192
|
-
globalThis.fetch = createFetchImplementation(
|
|
193
|
-
async () =>
|
|
194
|
-
new Response(JSON.stringify({removed: true}), {
|
|
195
|
-
status: 200,
|
|
196
|
-
headers: {'content-type': 'application/json'},
|
|
197
|
-
}),
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
const {ctx, captured} = createCapturedCtx()
|
|
201
|
-
await cliproxyKeysRemoveAction('sk-live-old', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx)
|
|
202
|
-
|
|
203
|
-
expect(expectCapturedToInclude(captured, 'removed')).toBe(true)
|
|
204
|
-
})
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
describe('cliproxyKeysListAction (Tier-2 failure-path parity)', () => {
|
|
208
|
-
const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
|
|
209
|
-
|
|
210
|
-
afterEach(() => {
|
|
211
|
-
globalThis.fetch = originalFetch
|
|
212
|
-
if (originalManagementKey === undefined) {
|
|
213
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
214
|
-
} else {
|
|
215
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('Tier-2: missing CLIPROXY_MANAGEMENT_KEY routes to ctx.console.error + exit(1)', async () => {
|
|
220
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
221
|
-
|
|
222
|
-
const {ctx, captured} = createCapturedCtx()
|
|
223
|
-
await expect(cliproxyKeysListAction({url: 'https://cliproxy.example.com'}, ctx)).rejects.toMatchObject({
|
|
224
|
-
name: 'MockProcessExit',
|
|
225
|
-
code: 1,
|
|
226
|
-
})
|
|
227
|
-
expect(captured.stderr.join('')).toContain('Management API key')
|
|
228
|
-
expect(captured.exit).toEqual({code: 1})
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
it('Tier-2: HTTP 500 response routes to ctx.console.error + exit(1)', async () => {
|
|
232
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('server error', {status: 500}))
|
|
233
|
-
|
|
234
|
-
const {ctx, captured} = createCapturedCtx()
|
|
235
|
-
await expect(
|
|
236
|
-
cliproxyKeysListAction({url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
|
|
237
|
-
).rejects.toMatchObject({
|
|
238
|
-
name: 'MockProcessExit',
|
|
239
|
-
code: 1,
|
|
240
|
-
})
|
|
241
|
-
expect(captured.stderr.join('')).toContain('HTTP 500')
|
|
242
|
-
expect(captured.exit).toEqual({code: 1})
|
|
243
|
-
})
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
describe('cliproxyKeysAddAction (Tier-2 failure-path parity)', () => {
|
|
247
|
-
const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
|
|
248
|
-
|
|
249
|
-
afterEach(() => {
|
|
250
|
-
globalThis.fetch = originalFetch
|
|
251
|
-
if (originalManagementKey === undefined) {
|
|
252
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
253
|
-
} else {
|
|
254
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
|
|
255
|
-
}
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('Tier-2: missing CLIPROXY_MANAGEMENT_KEY routes to ctx.console.error + exit(1)', async () => {
|
|
259
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
260
|
-
|
|
261
|
-
const {ctx, captured} = createCapturedCtx()
|
|
262
|
-
await expect(
|
|
263
|
-
cliproxyKeysAddAction('sk-live-new', {url: 'https://cliproxy.example.com'}, ctx),
|
|
264
|
-
).rejects.toMatchObject({
|
|
265
|
-
name: 'MockProcessExit',
|
|
266
|
-
code: 1,
|
|
267
|
-
})
|
|
268
|
-
expect(captured.stderr.join('')).toContain('Management API key')
|
|
269
|
-
expect(captured.exit).toEqual({code: 1})
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
it('Tier-2: HTTP 500 on GET routes to ctx.console.error + exit(1)', async () => {
|
|
273
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('server error', {status: 500}))
|
|
274
|
-
|
|
275
|
-
const {ctx, captured} = createCapturedCtx()
|
|
276
|
-
await expect(
|
|
277
|
-
cliproxyKeysAddAction('sk-live-new', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
|
|
278
|
-
).rejects.toMatchObject({
|
|
279
|
-
name: 'MockProcessExit',
|
|
280
|
-
code: 1,
|
|
281
|
-
})
|
|
282
|
-
expect(captured.stderr.join('')).toContain('HTTP 500')
|
|
283
|
-
expect(captured.exit).toEqual({code: 1})
|
|
284
|
-
})
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
describe('cliproxyKeysRemoveAction (Tier-2 failure-path parity)', () => {
|
|
288
|
-
const originalManagementKey = process.env.CLIPROXY_MANAGEMENT_KEY
|
|
289
|
-
|
|
290
|
-
afterEach(() => {
|
|
291
|
-
globalThis.fetch = originalFetch
|
|
292
|
-
if (originalManagementKey === undefined) {
|
|
293
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
294
|
-
} else {
|
|
295
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = originalManagementKey
|
|
296
|
-
}
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
it('Tier-2: missing CLIPROXY_MANAGEMENT_KEY routes to ctx.console.error + exit(1)', async () => {
|
|
300
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
301
|
-
|
|
302
|
-
const {ctx, captured} = createCapturedCtx()
|
|
303
|
-
await expect(
|
|
304
|
-
cliproxyKeysRemoveAction('sk-live-old', {url: 'https://cliproxy.example.com'}, ctx),
|
|
305
|
-
).rejects.toMatchObject({
|
|
306
|
-
name: 'MockProcessExit',
|
|
307
|
-
code: 1,
|
|
308
|
-
})
|
|
309
|
-
expect(captured.stderr.join('')).toContain('Management API key')
|
|
310
|
-
expect(captured.exit).toEqual({code: 1})
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
it('Tier-2: HTTP 500 response routes to ctx.console.error + exit(1)', async () => {
|
|
314
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('server error', {status: 500}))
|
|
315
|
-
|
|
316
|
-
const {ctx, captured} = createCapturedCtx()
|
|
317
|
-
await expect(
|
|
318
|
-
cliproxyKeysRemoveAction('sk-live-old', {url: 'https://cliproxy.example.com', key: 'mgmt-key'}, ctx),
|
|
319
|
-
).rejects.toMatchObject({
|
|
320
|
-
name: 'MockProcessExit',
|
|
321
|
-
code: 1,
|
|
322
|
-
})
|
|
323
|
-
expect(captured.stderr.join('')).toContain('HTTP 500')
|
|
324
|
-
expect(captured.exit).toEqual({code: 1})
|
|
325
|
-
})
|
|
326
|
-
})
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import type {goke} from 'goke'
|
|
2
|
-
|
|
3
|
-
import type {ActionCtx} from '../../lib/action-ctx'
|
|
4
|
-
|
|
5
|
-
import {z} from 'zod'
|
|
6
|
-
|
|
7
|
-
import {managementHeaders, parseManagementKeyList, requestJson, toStringArray} from './shared'
|
|
8
|
-
|
|
9
|
-
export {toStringArray} from './shared'
|
|
10
|
-
|
|
11
|
-
/** Minimal ctx surface consumed by cliproxy keys actions. Satisfied by both GokeExecutionContext and CapturedCtx. */
|
|
12
|
-
// ActionCtx imported from lib/action-ctx — single source of truth for action ctx shape
|
|
13
|
-
|
|
14
|
-
const DEFAULT_CLIPROXY_URL = 'https://cliproxy.fro.bot'
|
|
15
|
-
|
|
16
|
-
function stripTrailingSlash(value: string): string {
|
|
17
|
-
return value.endsWith('/') ? value.slice(0, -1) : value
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function resolveBaseUrl(input?: string): string {
|
|
21
|
-
return stripTrailingSlash(input ?? process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function resolveManagementKey(input?: string): string {
|
|
25
|
-
const key = input ?? process.env.CLIPROXY_MANAGEMENT_KEY
|
|
26
|
-
|
|
27
|
-
if (!key) {
|
|
28
|
-
throw new Error('Management API key is required. Pass --key or set CLIPROXY_MANAGEMENT_KEY.')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return key
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface KeysListOptions {
|
|
35
|
-
url?: string
|
|
36
|
-
key?: string
|
|
37
|
-
json?: boolean
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function cliproxyKeysListAction(options: KeysListOptions, ctx: ActionCtx): Promise<string[]> {
|
|
41
|
-
try {
|
|
42
|
-
const baseUrl = resolveBaseUrl(options.url)
|
|
43
|
-
const managementKey = resolveManagementKey(options.key)
|
|
44
|
-
const endpoint = `${baseUrl}/v0/management/api-keys`
|
|
45
|
-
const payload = await requestJson(endpoint, {
|
|
46
|
-
method: 'GET',
|
|
47
|
-
headers: managementHeaders(managementKey),
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const keys = toStringArray(payload)
|
|
51
|
-
ctx.console.error('⚠️ Output contains API keys — avoid logging or storing in shared locations')
|
|
52
|
-
|
|
53
|
-
if (options.json) {
|
|
54
|
-
ctx.console.log(JSON.stringify(keys, null, 2))
|
|
55
|
-
} else if (keys.length === 0) {
|
|
56
|
-
ctx.console.log('No API keys configured')
|
|
57
|
-
} else {
|
|
58
|
-
for (const [index, apiKey] of keys.entries()) {
|
|
59
|
-
ctx.console.log(`${index + 1}. ${apiKey}`)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return keys
|
|
64
|
-
} catch (error) {
|
|
65
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
66
|
-
ctx.console.error(message)
|
|
67
|
-
ctx.process.exit(1)
|
|
68
|
-
return [] // unreachable; satisfies TS that all paths return
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface KeysAddOptions {
|
|
73
|
-
url?: string
|
|
74
|
-
key?: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function cliproxyKeysAddAction(
|
|
78
|
-
apiKeyToAdd: string,
|
|
79
|
-
options: KeysAddOptions,
|
|
80
|
-
ctx: ActionCtx,
|
|
81
|
-
): Promise<void> {
|
|
82
|
-
try {
|
|
83
|
-
const baseUrl = resolveBaseUrl(options.url)
|
|
84
|
-
const managementKey = resolveManagementKey(options.key)
|
|
85
|
-
const endpoint = `${baseUrl}/v0/management/api-keys`
|
|
86
|
-
|
|
87
|
-
const currentPayload = await requestJson(endpoint, {
|
|
88
|
-
method: 'GET',
|
|
89
|
-
headers: managementHeaders(managementKey),
|
|
90
|
-
})
|
|
91
|
-
// Strict parse: a malformed or unexpected GET response must fail closed before the
|
|
92
|
-
// destructive PUT replaces the entire key list. Permissive parsing would collapse
|
|
93
|
-
// unknown shapes to [] and overwrite existing keys.
|
|
94
|
-
const currentKeys = parseManagementKeyList(currentPayload)
|
|
95
|
-
|
|
96
|
-
if (currentKeys.includes(apiKeyToAdd)) {
|
|
97
|
-
ctx.console.log('Key already present; no update required.')
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const nextKeys = [...currentKeys, apiKeyToAdd]
|
|
102
|
-
await requestJson(endpoint, {
|
|
103
|
-
method: 'PUT',
|
|
104
|
-
headers: managementHeaders(managementKey),
|
|
105
|
-
body: JSON.stringify(nextKeys),
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
ctx.console.log(`Added key "${apiKeyToAdd}". Current key count: ${nextKeys.length}.`)
|
|
109
|
-
} catch (error) {
|
|
110
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
111
|
-
ctx.console.error(message)
|
|
112
|
-
ctx.process.exit(1)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface KeysRemoveOptions {
|
|
117
|
-
url?: string
|
|
118
|
-
key?: string
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export async function cliproxyKeysRemoveAction(
|
|
122
|
-
apiKeyToRemove: string,
|
|
123
|
-
options: KeysRemoveOptions,
|
|
124
|
-
ctx: ActionCtx,
|
|
125
|
-
): Promise<void> {
|
|
126
|
-
try {
|
|
127
|
-
const baseUrl = resolveBaseUrl(options.url)
|
|
128
|
-
const managementKey = resolveManagementKey(options.key)
|
|
129
|
-
const params = new URLSearchParams({value: apiKeyToRemove})
|
|
130
|
-
const endpoint = `${baseUrl}/v0/management/api-keys?${params.toString()}`
|
|
131
|
-
|
|
132
|
-
const payload = await requestJson(endpoint, {
|
|
133
|
-
method: 'DELETE',
|
|
134
|
-
headers: managementHeaders(managementKey),
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
ctx.console.log(JSON.stringify(payload, null, 2))
|
|
138
|
-
} catch (error) {
|
|
139
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
140
|
-
ctx.console.error(message)
|
|
141
|
-
ctx.process.exit(1)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function registerCliproxyKeys(cli: ReturnType<typeof goke>): void {
|
|
146
|
-
cli
|
|
147
|
-
.command('cliproxy keys list', 'List CLIProxyAPI API keys from the management API.')
|
|
148
|
-
.option(
|
|
149
|
-
'--url [url]',
|
|
150
|
-
z
|
|
151
|
-
.string()
|
|
152
|
-
.describe(
|
|
153
|
-
'Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.',
|
|
154
|
-
),
|
|
155
|
-
)
|
|
156
|
-
.option(
|
|
157
|
-
'--key [key]',
|
|
158
|
-
z.string().describe('Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.'),
|
|
159
|
-
)
|
|
160
|
-
.option('--json', 'Output raw JSON array instead of a numbered list.')
|
|
161
|
-
.action(cliproxyKeysListAction)
|
|
162
|
-
|
|
163
|
-
cli
|
|
164
|
-
.command(
|
|
165
|
-
'cliproxy keys add <key>',
|
|
166
|
-
'Add an API key by fetching current keys, appending the value, and replacing full key set.',
|
|
167
|
-
)
|
|
168
|
-
.option(
|
|
169
|
-
'--url [url]',
|
|
170
|
-
z
|
|
171
|
-
.string()
|
|
172
|
-
.describe(
|
|
173
|
-
'Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.',
|
|
174
|
-
),
|
|
175
|
-
)
|
|
176
|
-
.option(
|
|
177
|
-
'--key [key]',
|
|
178
|
-
z.string().describe('Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.'),
|
|
179
|
-
)
|
|
180
|
-
.example('# Add a new API key to the current key set')
|
|
181
|
-
.example('infra cliproxy keys add sk-live-123')
|
|
182
|
-
.action(cliproxyKeysAddAction)
|
|
183
|
-
|
|
184
|
-
cli
|
|
185
|
-
.command('cliproxy keys remove <key>', 'Remove an API key via management API endpoint query parameter.')
|
|
186
|
-
.option(
|
|
187
|
-
'--url [url]',
|
|
188
|
-
z
|
|
189
|
-
.string()
|
|
190
|
-
.describe(
|
|
191
|
-
'Base URL for CLIProxyAPI management requests. Falls back to CLIPROXY_URL or https://cliproxy.fro.bot.',
|
|
192
|
-
),
|
|
193
|
-
)
|
|
194
|
-
.option(
|
|
195
|
-
'--key [key]',
|
|
196
|
-
z.string().describe('Management API key. Falls back to CLIPROXY_MANAGEMENT_KEY when omitted.'),
|
|
197
|
-
)
|
|
198
|
-
.example('# Remove an API key from CLIProxyAPI')
|
|
199
|
-
.example('infra cliproxy keys remove sk-live-123')
|
|
200
|
-
.action(cliproxyKeysRemoveAction)
|
|
201
|
-
}
|