@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,1157 +0,0 @@
|
|
|
1
|
-
import {afterEach, beforeEach, describe, expect, it} from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {createCapturedCtx, expectCapturedToInclude, MockProcessExit} from '../../__test__/mcp-ctx-fixture'
|
|
4
|
-
import {
|
|
5
|
-
checkHttpReachability,
|
|
6
|
-
checkProviderAuth,
|
|
7
|
-
checkUsageStats,
|
|
8
|
-
checkVersion,
|
|
9
|
-
cliproxyStatusAction,
|
|
10
|
-
formatDurationMs,
|
|
11
|
-
formatUsageSummaryLine,
|
|
12
|
-
getCliproxyStatusSummary,
|
|
13
|
-
levelLabel,
|
|
14
|
-
stripTrailingSlash,
|
|
15
|
-
toNumber,
|
|
16
|
-
} from './status'
|
|
17
|
-
|
|
18
|
-
const originalFetch = globalThis.fetch
|
|
19
|
-
|
|
20
|
-
type FetchReplacement = (url: string, init?: RequestInit) => Promise<Response>
|
|
21
|
-
|
|
22
|
-
function createFetchImplementation(handler: FetchReplacement): typeof fetch {
|
|
23
|
-
return Object.assign(
|
|
24
|
-
(input: string | URL | Request, init?: RequestInit) => {
|
|
25
|
-
if (typeof input !== 'string') {
|
|
26
|
-
throw new TypeError(`Unexpected non-string fetch input: ${String(input)}`)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return handler(input, init)
|
|
30
|
-
},
|
|
31
|
-
{preconnect: originalFetch.preconnect},
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('cliproxy status helpers', () => {
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
globalThis.fetch = createFetchImplementation(async () => {
|
|
38
|
-
throw new Error('Unexpected fetch call')
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
globalThis.fetch = originalFetch
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('levelLabel', () => {
|
|
47
|
-
it('returns OK for ok', () => {
|
|
48
|
-
expect(levelLabel('ok')).toBe('OK')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('returns WARN for warning', () => {
|
|
52
|
-
expect(levelLabel('warning')).toBe('WARN')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('returns ERROR for error', () => {
|
|
56
|
-
expect(levelLabel('error')).toBe('ERROR')
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
describe('formatDurationMs', () => {
|
|
61
|
-
it('formats positive durations', () => {
|
|
62
|
-
expect(formatDurationMs(150)).toBe('150ms')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('formats zero duration', () => {
|
|
66
|
-
expect(formatDurationMs(0)).toBe('0ms')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('clamps negative durations to zero', () => {
|
|
70
|
-
expect(formatDurationMs(-5)).toBe('0ms')
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe('stripTrailingSlash', () => {
|
|
75
|
-
it('removes a trailing slash', () => {
|
|
76
|
-
expect(stripTrailingSlash('https://example.com/')).toBe('https://example.com')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('does nothing when there is no trailing slash', () => {
|
|
80
|
-
expect(stripTrailingSlash('https://example.com')).toBe('https://example.com')
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe('toNumber', () => {
|
|
85
|
-
it('returns finite numbers as-is', () => {
|
|
86
|
-
expect(toNumber(42)).toBe(42)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('returns null for non-numbers', () => {
|
|
90
|
-
expect(toNumber('not a number')).toBeNull()
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('returns null for NaN', () => {
|
|
94
|
-
expect(toNumber(Number.NaN)).toBeNull()
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('returns null for infinity', () => {
|
|
98
|
-
expect(toNumber(Number.POSITIVE_INFINITY)).toBeNull()
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
describe('checkHttpReachability', () => {
|
|
103
|
-
it('returns ok for HTTP 200', async () => {
|
|
104
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('ok', {status: 200}))
|
|
105
|
-
|
|
106
|
-
const result = await checkHttpReachability('https://cliproxy.example.com', false)
|
|
107
|
-
|
|
108
|
-
expect(result.level).toBe('ok')
|
|
109
|
-
expect(result.summary).toContain('GET https://cliproxy.example.com → 200')
|
|
110
|
-
expect(result.summary).toContain('ms')
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('returns error for HTTP 500', async () => {
|
|
114
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('error', {status: 500}))
|
|
115
|
-
|
|
116
|
-
const result = await checkHttpReachability('https://cliproxy.example.com', false)
|
|
117
|
-
|
|
118
|
-
expect(result.level).toBe('error')
|
|
119
|
-
expect(result.summary).toContain('GET https://cliproxy.example.com → 500')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('returns error details for network failures', async () => {
|
|
123
|
-
globalThis.fetch = createFetchImplementation(async () => {
|
|
124
|
-
throw new Error('Network timeout')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
const result = await checkHttpReachability('https://cliproxy.example.com', true)
|
|
128
|
-
|
|
129
|
-
expect(result.level).toBe('error')
|
|
130
|
-
expect(result.summary).toContain('Network timeout')
|
|
131
|
-
expect(result.details).toEqual(['URL: https://cliproxy.example.com', 'Timeout: 10000ms'])
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
describe('checkUsageStats', () => {
|
|
136
|
-
it('returns ok for empty array (idle)', async () => {
|
|
137
|
-
globalThis.fetch = createFetchImplementation(
|
|
138
|
-
async () => new Response('[]', {status: 200, headers: {'content-type': 'application/json'}}),
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
142
|
-
|
|
143
|
-
expect(result.level).toBe('ok')
|
|
144
|
-
expect(result.summary).toMatch(/idle|recent: 0/)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('returns ok for all-success queue array', async () => {
|
|
148
|
-
const queue = [{status: 200}, {status: 201}, {status: 200}]
|
|
149
|
-
globalThis.fetch = createFetchImplementation(
|
|
150
|
-
async () => new Response(JSON.stringify(queue), {status: 200, headers: {'content-type': 'application/json'}}),
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
154
|
-
|
|
155
|
-
expect(result.level).toBe('ok')
|
|
156
|
-
expect(result.summary).toContain('recent: 3')
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('returns warning for queue with error-status records', async () => {
|
|
160
|
-
const queue = [{status: 200}, {status: 500}, {status: 200}]
|
|
161
|
-
globalThis.fetch = createFetchImplementation(
|
|
162
|
-
async () => new Response(JSON.stringify(queue), {status: 200, headers: {'content-type': 'application/json'}}),
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
166
|
-
|
|
167
|
-
expect(result.level).toBe('warning')
|
|
168
|
-
expect(result.summary).toContain('recent: 3')
|
|
169
|
-
expect(result.summary).toContain('errors: 1')
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('returns warning when usage-queue returns a non-array object', async () => {
|
|
173
|
-
globalThis.fetch = createFetchImplementation(
|
|
174
|
-
async () =>
|
|
175
|
-
new Response(JSON.stringify({unexpected: 'object'}), {
|
|
176
|
-
status: 200,
|
|
177
|
-
headers: {'content-type': 'application/json'},
|
|
178
|
-
}),
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
182
|
-
|
|
183
|
-
expect(result.level).toBe('warning')
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('returns warning when rate limited', async () => {
|
|
187
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('rate limited', {status: 429}))
|
|
188
|
-
|
|
189
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
190
|
-
|
|
191
|
-
expect(result.level).toBe('warning')
|
|
192
|
-
expect(result.summary).toContain('Rate limited')
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
it('returns error for non-200 responses', async () => {
|
|
196
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('boom', {status: 500}))
|
|
197
|
-
|
|
198
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
199
|
-
|
|
200
|
-
expect(result.level).toBe('error')
|
|
201
|
-
expect(result.summary).toContain('HTTP 500')
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('returns error for network failures', async () => {
|
|
205
|
-
globalThis.fetch = createFetchImplementation(async () => {
|
|
206
|
-
throw new Error('socket hang up')
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
210
|
-
|
|
211
|
-
expect(result.level).toBe('error')
|
|
212
|
-
expect(result.summary).toContain('Unable to read usage stats: socket hang up')
|
|
213
|
-
})
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
describe('checkVersion', () => {
|
|
217
|
-
it('returns ok with latest-version from response', async () => {
|
|
218
|
-
globalThis.fetch = createFetchImplementation(
|
|
219
|
-
async () =>
|
|
220
|
-
new Response(JSON.stringify({'latest-version': 'v6.9.15'}), {
|
|
221
|
-
status: 200,
|
|
222
|
-
headers: {'content-type': 'application/json'},
|
|
223
|
-
}),
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
const result = await checkVersion('https://cliproxy.example.com', 'secret')
|
|
227
|
-
|
|
228
|
-
expect(result.level).toBe('ok')
|
|
229
|
-
expect(result.summary).toBe('v6.9.15')
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('returns warning when latest-version key is missing', async () => {
|
|
233
|
-
globalThis.fetch = createFetchImplementation(
|
|
234
|
-
async () =>
|
|
235
|
-
new Response(JSON.stringify({}), {
|
|
236
|
-
status: 200,
|
|
237
|
-
headers: {'content-type': 'application/json'},
|
|
238
|
-
}),
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
const result = await checkVersion('https://cliproxy.example.com', 'secret')
|
|
242
|
-
|
|
243
|
-
expect(result.level).toBe('warning')
|
|
244
|
-
expect(result.summary).toContain('latest-version')
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
it('returns warning when rate limited', async () => {
|
|
248
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('rate limited', {status: 429}))
|
|
249
|
-
|
|
250
|
-
const result = await checkVersion('https://cliproxy.example.com', 'secret')
|
|
251
|
-
|
|
252
|
-
expect(result.level).toBe('warning')
|
|
253
|
-
expect(result.summary).toContain('Rate limited')
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('returns error for non-200 responses', async () => {
|
|
257
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('boom', {status: 500}))
|
|
258
|
-
|
|
259
|
-
const result = await checkVersion('https://cliproxy.example.com', 'secret')
|
|
260
|
-
|
|
261
|
-
expect(result.level).toBe('error')
|
|
262
|
-
expect(result.summary).toContain('HTTP 500')
|
|
263
|
-
})
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
describe('formatUsageSummaryLine', () => {
|
|
267
|
-
it('formats a recent-activity summary with no errors', () => {
|
|
268
|
-
const result = formatUsageSummaryLine({
|
|
269
|
-
title: 'Usage stats',
|
|
270
|
-
level: 'ok',
|
|
271
|
-
summary: 'recent: 10',
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
expect(result).toBe('Recent requests: 10')
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
it('formats a recent-activity summary with errors appended', () => {
|
|
278
|
-
const result = formatUsageSummaryLine({
|
|
279
|
-
title: 'Usage stats',
|
|
280
|
-
level: 'warning',
|
|
281
|
-
summary: 'recent: 10, errors: 3',
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
expect(result).toBe('Recent requests: 10, 3 errors')
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
it('formats an idle recent summary', () => {
|
|
288
|
-
const result = formatUsageSummaryLine({
|
|
289
|
-
title: 'Usage stats',
|
|
290
|
-
level: 'ok',
|
|
291
|
-
summary: 'recent: 0 (idle)',
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
expect(result).toBe('Recent requests: 0')
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('returns null when summary has no recent-activity field', () => {
|
|
298
|
-
const result = formatUsageSummaryLine({
|
|
299
|
-
title: 'Usage stats',
|
|
300
|
-
level: 'warning',
|
|
301
|
-
summary: 'Rate limited by management API (HTTP 429). Retry in a few moments.',
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
expect(result).toBeNull()
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it('returns null for error summaries without a recent field', () => {
|
|
308
|
-
const result = formatUsageSummaryLine({
|
|
309
|
-
title: 'Usage stats',
|
|
310
|
-
level: 'error',
|
|
311
|
-
summary: 'Unable to read usage stats: socket hang up',
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
expect(result).toBeNull()
|
|
315
|
-
})
|
|
316
|
-
})
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
describe('cliproxyStatusAction (Tier-2 ctx capture, failure-path parity)', () => {
|
|
320
|
-
afterEach(() => {
|
|
321
|
-
globalThis.fetch = originalFetch
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
it('Tier-2: routes unexpected thrown error through ctx.console.error + ctx.process.exit(1)', async () => {
|
|
325
|
-
// Trigger an unexpected error by having ctx.console.log throw on first call
|
|
326
|
-
const {ctx, captured} = createCapturedCtx()
|
|
327
|
-
let callCount = 0
|
|
328
|
-
const originalLog = ctx.console.log
|
|
329
|
-
ctx.console.log = (...args: unknown[]) => {
|
|
330
|
-
callCount++
|
|
331
|
-
if (callCount === 1) {
|
|
332
|
-
throw new Error('Unexpected internal error during status output')
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
originalLog(...args)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('ok', {status: 200}))
|
|
339
|
-
|
|
340
|
-
await expect(cliproxyStatusAction({url: 'https://cliproxy.example.com'}, ctx)).rejects.toMatchObject({
|
|
341
|
-
name: 'MockProcessExit',
|
|
342
|
-
code: 1,
|
|
343
|
-
})
|
|
344
|
-
expect(captured.stderr.join('')).toContain('Unexpected internal error')
|
|
345
|
-
expect(captured.exit).toEqual({code: 1})
|
|
346
|
-
})
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
describe('cliproxyStatusAction (Tier-2 ctx capture)', () => {
|
|
350
|
-
afterEach(() => {
|
|
351
|
-
globalThis.fetch = originalFetch
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
it('Mode A: captures CLIProxyAPI status header to ctx.stdout', async () => {
|
|
355
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('ok', {status: 200}))
|
|
356
|
-
|
|
357
|
-
const {ctx, captured} = createCapturedCtx()
|
|
358
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com'}, ctx)
|
|
359
|
-
|
|
360
|
-
expect(expectCapturedToInclude(captured, 'CLIProxyAPI status')).toBe(true)
|
|
361
|
-
expect(expectCapturedToInclude(captured, 'HTTP reachability')).toBe(true)
|
|
362
|
-
expect(expectCapturedToInclude(captured, 'Summary:')).toBe(true)
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
it('Mode A: calls ctx.process.exit(1) on HTTP error', async () => {
|
|
366
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('error', {status: 500}))
|
|
367
|
-
|
|
368
|
-
const {ctx, captured} = createCapturedCtx()
|
|
369
|
-
let threw: unknown
|
|
370
|
-
try {
|
|
371
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com'}, ctx)
|
|
372
|
-
} catch (error) {
|
|
373
|
-
threw = error
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
expect(threw).toBeInstanceOf(MockProcessExit)
|
|
377
|
-
expect(captured.exit?.code).toBe(1)
|
|
378
|
-
expect(expectCapturedToInclude(captured, 'ERROR')).toBe(true)
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
it('Mode A: shows management key warning when no key provided', async () => {
|
|
382
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('ok', {status: 200}))
|
|
383
|
-
|
|
384
|
-
const savedKey = process.env.CLIPROXY_MANAGEMENT_KEY
|
|
385
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
386
|
-
try {
|
|
387
|
-
const {ctx, captured} = createCapturedCtx()
|
|
388
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com'}, ctx)
|
|
389
|
-
|
|
390
|
-
expect(expectCapturedToInclude(captured, 'CLIPROXY_MANAGEMENT_KEY')).toBe(true)
|
|
391
|
-
} finally {
|
|
392
|
-
if (savedKey !== undefined) process.env.CLIPROXY_MANAGEMENT_KEY = savedKey
|
|
393
|
-
}
|
|
394
|
-
})
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
describe('usage-queue migration', () => {
|
|
398
|
-
afterEach(() => {
|
|
399
|
-
globalThis.fetch = originalFetch
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
it('populated usage-queue returns recent-activity summary with correct total and error count', async () => {
|
|
403
|
-
const queue = [
|
|
404
|
-
{status: 200, model: 'claude-3-5-sonnet'},
|
|
405
|
-
{status: 500, model: 'claude-3-5-sonnet'},
|
|
406
|
-
{status: 200, model: 'claude-3-5-sonnet'},
|
|
407
|
-
]
|
|
408
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
409
|
-
if (url.includes('/v0/management/usage-queue')) {
|
|
410
|
-
return new Response(JSON.stringify(queue), {status: 200, headers: {'content-type': 'application/json'}})
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
417
|
-
|
|
418
|
-
expect(result.level).not.toBe('error')
|
|
419
|
-
expect(result.summary).toContain('recent: 3')
|
|
420
|
-
expect(result.summary).toContain('errors: 1')
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
it('empty usage-queue returns ok/idle result, not an error', async () => {
|
|
424
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
425
|
-
if (url.includes('/v0/management/usage-queue')) {
|
|
426
|
-
return new Response('[]', {status: 200, headers: {'content-type': 'application/json'}})
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
433
|
-
|
|
434
|
-
expect(result.level).toBe('ok')
|
|
435
|
-
expect(result.summary).toMatch(/idle|recent: 0/)
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
it('malformed usage-queue response returns warning, not error, and does not throw', async () => {
|
|
439
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
440
|
-
if (url.includes('/v0/management/usage-queue')) {
|
|
441
|
-
return new Response('{"not":"an-array"}', {status: 200, headers: {'content-type': 'application/json'}})
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
const result = await checkUsageStats('https://cliproxy.example.com', 'secret')
|
|
448
|
-
|
|
449
|
-
expect(result.level).toBe('warning')
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
it('formatUsageSummaryLine returns a human-friendly line for recent-window summary', () => {
|
|
453
|
-
const result = formatUsageSummaryLine({
|
|
454
|
-
title: 'Usage stats',
|
|
455
|
-
level: 'ok',
|
|
456
|
-
summary: 'recent: 5, errors: 1',
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
expect(result).not.toBeNull()
|
|
460
|
-
expect(result).toContain('5')
|
|
461
|
-
})
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
describe('management auth failure surfaces in unified summary (FIX 4)', () => {
|
|
465
|
-
afterEach(() => {
|
|
466
|
-
globalThis.fetch = originalFetch
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
it('getCliproxyStatusSummary with a bad key (401) shows auth failure in version and usageStats, not "— (no key)"', async () => {
|
|
470
|
-
globalThis.fetch = createFetchImplementation(async (url, init) => {
|
|
471
|
-
const hdrs = init?.headers
|
|
472
|
-
const hasKey =
|
|
473
|
-
hdrs instanceof Headers
|
|
474
|
-
? hdrs.has('x-management-key')
|
|
475
|
-
: hdrs !== null && hdrs !== undefined && typeof hdrs === 'object' && 'x-management-key' in hdrs
|
|
476
|
-
if (url.includes('/v0/management/') && hasKey) {
|
|
477
|
-
return new Response('Unauthorized', {status: 401})
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return new Response('ok', {status: 200})
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
const summary = await getCliproxyStatusSummary('https://cliproxy.example.com', 'bad-key', false)
|
|
484
|
-
|
|
485
|
-
// Must NOT show the no-key sentinel — a bad key is distinct from no key
|
|
486
|
-
expect(summary.version).not.toBe('— (no key)')
|
|
487
|
-
expect(summary.usageStats).not.toBe('— (no key)')
|
|
488
|
-
// Must contain some error/auth indicator
|
|
489
|
-
expect(summary.version.toLowerCase()).toMatch(/error|auth|401|management|unauthorized/i)
|
|
490
|
-
expect(summary.usageStats.toLowerCase()).toMatch(/error|auth|401|management|unauthorized/i)
|
|
491
|
-
})
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
describe('ban body word-boundary detection (FIX 5)', () => {
|
|
495
|
-
afterEach(() => {
|
|
496
|
-
globalThis.fetch = originalFetch
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
it('403 with body {detail:"bandwidth exceeded"} is NOT treated as a ban (generic 403)', async () => {
|
|
500
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
501
|
-
if (url.includes('/v0/management/config')) {
|
|
502
|
-
return new Response(JSON.stringify({detail: 'bandwidth exceeded'}), {
|
|
503
|
-
status: 403,
|
|
504
|
-
headers: {'content-type': 'application/json'},
|
|
505
|
-
})
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return new Response('ok', {status: 200})
|
|
509
|
-
})
|
|
510
|
-
|
|
511
|
-
const {ctx, captured} = createCapturedCtx()
|
|
512
|
-
try {
|
|
513
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com', key: 'any-key'}, ctx)
|
|
514
|
-
} catch (error) {
|
|
515
|
-
if (!(error instanceof MockProcessExit)) throw error
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const output = [...captured.stdout, ...captured.stderr].join('\n')
|
|
519
|
-
expect(output.toLowerCase()).not.toMatch(/ip.?ban/)
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
it('403 with body {error:"IP banned"} IS treated as a ban', async () => {
|
|
523
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
524
|
-
if (url.includes('/v0/management/config')) {
|
|
525
|
-
return new Response(JSON.stringify({error: 'IP banned'}), {
|
|
526
|
-
status: 403,
|
|
527
|
-
headers: {'content-type': 'application/json'},
|
|
528
|
-
})
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return new Response('ok', {status: 200})
|
|
532
|
-
})
|
|
533
|
-
|
|
534
|
-
const {ctx, captured} = createCapturedCtx()
|
|
535
|
-
try {
|
|
536
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com', key: 'any-key'}, ctx)
|
|
537
|
-
} catch (error) {
|
|
538
|
-
if (!(error instanceof MockProcessExit)) throw error
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const output = [...captured.stdout, ...captured.stderr].join('\n')
|
|
542
|
-
expect(output.toLowerCase()).toMatch(/ip.?ban/)
|
|
543
|
-
})
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
describe('reachability probe targets /healthz liveness endpoint', () => {
|
|
547
|
-
afterEach(() => {
|
|
548
|
-
globalThis.fetch = originalFetch
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
it('cliproxyStatusAction reports HTTP reachable when /healthz returns 200 and bare base returns 404', async () => {
|
|
552
|
-
const BASE = 'https://cliproxy.example.com'
|
|
553
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
554
|
-
if (url === `${BASE}/healthz`) return new Response('{"status":"ok"}', {status: 200})
|
|
555
|
-
if (url === BASE) return new Response('Not Found', {status: 404})
|
|
556
|
-
return new Response('ok', {status: 200})
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
const {ctx, captured} = createCapturedCtx()
|
|
560
|
-
await cliproxyStatusAction({url: BASE}, ctx)
|
|
561
|
-
|
|
562
|
-
const output = [...captured.stdout, ...captured.stderr].join('\n')
|
|
563
|
-
expect(output).toContain('OK')
|
|
564
|
-
expect(output).not.toMatch(/ERROR.*HTTP reachability|HTTP reachability.*ERROR/)
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
it('getCliproxyStatusSummary reports http ok when /healthz returns 200 and bare base returns 404', async () => {
|
|
568
|
-
const BASE = 'https://cliproxy.example.com'
|
|
569
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
570
|
-
if (url === `${BASE}/healthz`) return new Response('{"status":"ok"}', {status: 200})
|
|
571
|
-
if (url === BASE) return new Response('Not Found', {status: 404})
|
|
572
|
-
return new Response('ok', {status: 200})
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
const summary = await getCliproxyStatusSummary(BASE, '', false)
|
|
576
|
-
|
|
577
|
-
expect(summary.http).toMatch(/^OK/)
|
|
578
|
-
})
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
describe('management auth probe (ban-awareness)', () => {
|
|
582
|
-
afterEach(() => {
|
|
583
|
-
globalThis.fetch = originalFetch
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
it('bad management key causes at most one management fetch and skips version+usage calls', async () => {
|
|
587
|
-
const managementFetchUrls: string[] = []
|
|
588
|
-
|
|
589
|
-
globalThis.fetch = createFetchImplementation(async (url, init) => {
|
|
590
|
-
const hdrs = init?.headers
|
|
591
|
-
const hasManagementKey =
|
|
592
|
-
hdrs instanceof Headers
|
|
593
|
-
? hdrs.has('x-management-key')
|
|
594
|
-
: hdrs !== null && hdrs !== undefined && typeof hdrs === 'object' && 'x-management-key' in hdrs
|
|
595
|
-
if (url.includes('/v0/management/') && hasManagementKey) {
|
|
596
|
-
managementFetchUrls.push(url)
|
|
597
|
-
return new Response('Unauthorized', {status: 401})
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (url.includes('/healthz') || !url.includes('/v0/management/')) {
|
|
601
|
-
return new Response('ok', {status: 200})
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
const {ctx} = createCapturedCtx()
|
|
608
|
-
// Auth failure yields error-level result → exit(1) → MockProcessExit thrown
|
|
609
|
-
try {
|
|
610
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com', key: 'bad-key'}, ctx)
|
|
611
|
-
} catch (error) {
|
|
612
|
-
if (!(error instanceof MockProcessExit)) throw error
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
expect(managementFetchUrls.length).toBe(1)
|
|
616
|
-
expect(managementFetchUrls[0]).toContain('/v0/management/config')
|
|
617
|
-
})
|
|
618
|
-
|
|
619
|
-
it('403 with ban body surfaces a distinct IP-banned message', async () => {
|
|
620
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
621
|
-
if (url.includes('/v0/management/config')) {
|
|
622
|
-
return new Response(JSON.stringify({error: 'IP banned'}), {
|
|
623
|
-
status: 403,
|
|
624
|
-
headers: {'content-type': 'application/json'},
|
|
625
|
-
})
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
return new Response('ok', {status: 200})
|
|
629
|
-
})
|
|
630
|
-
|
|
631
|
-
const {ctx, captured} = createCapturedCtx()
|
|
632
|
-
// 403+ban → error level → exit(1)
|
|
633
|
-
try {
|
|
634
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com', key: 'any-key'}, ctx)
|
|
635
|
-
} catch (error) {
|
|
636
|
-
if (!(error instanceof MockProcessExit)) throw error
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const output = [...captured.stdout, ...captured.stderr].join('\n')
|
|
640
|
-
expect(output.toLowerCase()).toMatch(/ip.?ban/)
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
it('auth error message does not contain the management key value', async () => {
|
|
644
|
-
const secretKey = 'super-secret-mgmt-key-12345'
|
|
645
|
-
|
|
646
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
647
|
-
if (url.includes('/v0/management/config')) {
|
|
648
|
-
return new Response('Unauthorized', {status: 401})
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return new Response('ok', {status: 200})
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
const {ctx, captured} = createCapturedCtx()
|
|
655
|
-
// 401 → error level → exit(1)
|
|
656
|
-
try {
|
|
657
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com', key: secretKey}, ctx)
|
|
658
|
-
} catch (error) {
|
|
659
|
-
if (!(error instanceof MockProcessExit)) throw error
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const allOutput = [...captured.stdout, ...captured.stderr].join('\n')
|
|
663
|
-
expect(allOutput).not.toContain(secretKey)
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
it('successful probe allows version and usage checks to proceed in parallel', async () => {
|
|
667
|
-
const fetchedUrls: string[] = []
|
|
668
|
-
|
|
669
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
670
|
-
fetchedUrls.push(url)
|
|
671
|
-
if (url.includes('/v0/management/config')) {
|
|
672
|
-
return new Response(JSON.stringify({config: 'ok'}), {
|
|
673
|
-
status: 200,
|
|
674
|
-
headers: {'content-type': 'application/json'},
|
|
675
|
-
})
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (url.includes('/v0/management/usage-queue')) {
|
|
679
|
-
return new Response('[]', {status: 200, headers: {'content-type': 'application/json'}})
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (url.includes('/v0/management/latest-version')) {
|
|
683
|
-
return new Response(JSON.stringify({'latest-version': 'v7.1.31'}), {
|
|
684
|
-
status: 200,
|
|
685
|
-
headers: {'content-type': 'application/json'},
|
|
686
|
-
})
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return new Response('ok', {status: 200})
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
const {ctx} = createCapturedCtx()
|
|
693
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com', key: 'valid-key'}, ctx)
|
|
694
|
-
|
|
695
|
-
expect(fetchedUrls.some(u => u.includes('/v0/management/latest-version'))).toBe(true)
|
|
696
|
-
expect(fetchedUrls.some(u => u.includes('/v0/management/usage-queue'))).toBe(true)
|
|
697
|
-
})
|
|
698
|
-
|
|
699
|
-
it('getCliproxyStatusSummary with bad key fires only one management fetch', async () => {
|
|
700
|
-
const managementFetchUrls: string[] = []
|
|
701
|
-
|
|
702
|
-
globalThis.fetch = createFetchImplementation(async (url, init) => {
|
|
703
|
-
const hdrs = init?.headers
|
|
704
|
-
const hasKey =
|
|
705
|
-
hdrs instanceof Headers
|
|
706
|
-
? hdrs.has('x-management-key')
|
|
707
|
-
: hdrs !== null && hdrs !== undefined && typeof hdrs === 'object' && 'x-management-key' in hdrs
|
|
708
|
-
if (url.includes('/v0/management/') && hasKey) {
|
|
709
|
-
managementFetchUrls.push(url)
|
|
710
|
-
return new Response('Unauthorized', {status: 401})
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return new Response('ok', {status: 200})
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
await getCliproxyStatusSummary('https://cliproxy.example.com', 'bad-key', false)
|
|
717
|
-
|
|
718
|
-
expect(managementFetchUrls.length).toBe(1)
|
|
719
|
-
})
|
|
720
|
-
})
|
|
721
|
-
|
|
722
|
-
// ─── Ambient management key trusted-URL binding ───────────────────────────────
|
|
723
|
-
|
|
724
|
-
describe('cliproxyStatusAction — ambient key does not follow agent-supplied URLs', () => {
|
|
725
|
-
let originalEnv: Record<string, string | undefined>
|
|
726
|
-
|
|
727
|
-
beforeEach(() => {
|
|
728
|
-
originalEnv = {
|
|
729
|
-
CLIPROXY_URL: process.env.CLIPROXY_URL,
|
|
730
|
-
CLIPROXY_MANAGEMENT_KEY: process.env.CLIPROXY_MANAGEMENT_KEY,
|
|
731
|
-
}
|
|
732
|
-
process.env.CLIPROXY_MANAGEMENT_KEY = 'secret-ambient-key'
|
|
733
|
-
delete process.env.CLIPROXY_URL
|
|
734
|
-
})
|
|
735
|
-
|
|
736
|
-
afterEach(() => {
|
|
737
|
-
for (const [k, v] of Object.entries(originalEnv)) {
|
|
738
|
-
if (v === undefined) {
|
|
739
|
-
delete process.env[k]
|
|
740
|
-
} else {
|
|
741
|
-
process.env[k] = v
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
globalThis.fetch = originalFetch
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
it('does NOT send the ambient management key to a non-trusted --url override', async () => {
|
|
748
|
-
const capturedRequestsByHost: Record<string, string[]> = {}
|
|
749
|
-
|
|
750
|
-
globalThis.fetch = createFetchImplementation(async (url, init) => {
|
|
751
|
-
const {hostname} = new URL(url)
|
|
752
|
-
const hdrs = init?.headers
|
|
753
|
-
const key = hdrs instanceof Headers ? hdrs.get('x-management-key') : null
|
|
754
|
-
|
|
755
|
-
if (!capturedRequestsByHost[hostname]) capturedRequestsByHost[hostname] = []
|
|
756
|
-
if (key) capturedRequestsByHost[hostname].push(key)
|
|
757
|
-
|
|
758
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
759
|
-
if (url.includes('/v0/management/'))
|
|
760
|
-
return new Response('[]', {status: 200, headers: {'content-type': 'application/json'}})
|
|
761
|
-
return new Response('ok', {status: 200})
|
|
762
|
-
})
|
|
763
|
-
|
|
764
|
-
const {ctx} = createCapturedCtx()
|
|
765
|
-
try {
|
|
766
|
-
await cliproxyStatusAction({url: 'https://evil.example.com'}, ctx)
|
|
767
|
-
} catch {
|
|
768
|
-
// exit(1) expected or not — either way, we just care about key leakage
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// The ambient secret key must NOT appear in any request to the evil host
|
|
772
|
-
expect(capturedRequestsByHost['evil.example.com'] ?? []).not.toContain('secret-ambient-key')
|
|
773
|
-
})
|
|
774
|
-
|
|
775
|
-
it('still uses the ambient key when --url matches the default trusted URL', async () => {
|
|
776
|
-
const capturedManagementKeys: string[] = []
|
|
777
|
-
|
|
778
|
-
globalThis.fetch = createFetchImplementation(async (url, init) => {
|
|
779
|
-
const hdrs = init?.headers
|
|
780
|
-
const key = hdrs instanceof Headers ? hdrs.get('x-management-key') : null
|
|
781
|
-
if (url.includes('/v0/management/') && key) capturedManagementKeys.push(key)
|
|
782
|
-
|
|
783
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
784
|
-
if (url.includes('/v0/management/config'))
|
|
785
|
-
return new Response('{}', {status: 200, headers: {'content-type': 'application/json'}})
|
|
786
|
-
if (url.includes('/v0/management/usage-queue'))
|
|
787
|
-
return new Response('[]', {status: 200, headers: {'content-type': 'application/json'}})
|
|
788
|
-
if (url.includes('/v0/management/latest-version'))
|
|
789
|
-
return new Response(JSON.stringify({'latest-version': 'v1.0.0'}), {
|
|
790
|
-
status: 200,
|
|
791
|
-
headers: {'content-type': 'application/json'},
|
|
792
|
-
})
|
|
793
|
-
return new Response('ok', {status: 200})
|
|
794
|
-
})
|
|
795
|
-
|
|
796
|
-
const {ctx} = createCapturedCtx()
|
|
797
|
-
// Default URL is https://cliproxy.fro.bot — pass it explicitly
|
|
798
|
-
await cliproxyStatusAction({url: 'https://cliproxy.fro.bot'}, ctx)
|
|
799
|
-
|
|
800
|
-
expect(capturedManagementKeys).toContain('secret-ambient-key')
|
|
801
|
-
})
|
|
802
|
-
|
|
803
|
-
it('uses an explicit --key even when --url is a non-trusted host', async () => {
|
|
804
|
-
const capturedManagementKeys: string[] = []
|
|
805
|
-
|
|
806
|
-
globalThis.fetch = createFetchImplementation(async (url, init) => {
|
|
807
|
-
const hdrs = init?.headers
|
|
808
|
-
const key = hdrs instanceof Headers ? hdrs.get('x-management-key') : null
|
|
809
|
-
if (url.includes('/v0/management/') && key) capturedManagementKeys.push(key)
|
|
810
|
-
|
|
811
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
812
|
-
if (url.includes('/v0/management/config'))
|
|
813
|
-
return new Response('{}', {status: 200, headers: {'content-type': 'application/json'}})
|
|
814
|
-
if (url.includes('/v0/management/usage-queue'))
|
|
815
|
-
return new Response('[]', {status: 200, headers: {'content-type': 'application/json'}})
|
|
816
|
-
if (url.includes('/v0/management/latest-version'))
|
|
817
|
-
return new Response(JSON.stringify({'latest-version': 'v1.0.0'}), {
|
|
818
|
-
status: 200,
|
|
819
|
-
headers: {'content-type': 'application/json'},
|
|
820
|
-
})
|
|
821
|
-
return new Response('ok', {status: 200})
|
|
822
|
-
})
|
|
823
|
-
|
|
824
|
-
const {ctx} = createCapturedCtx()
|
|
825
|
-
await cliproxyStatusAction({url: 'https://other-cliproxy.example.com', key: 'explicit-key'}, ctx)
|
|
826
|
-
|
|
827
|
-
expect(capturedManagementKeys).toContain('explicit-key')
|
|
828
|
-
})
|
|
829
|
-
|
|
830
|
-
it('falls through to the no-key warning path when URL is untrusted and no --key given', async () => {
|
|
831
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
832
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
833
|
-
return new Response('[]', {status: 200})
|
|
834
|
-
})
|
|
835
|
-
|
|
836
|
-
const {ctx, captured} = createCapturedCtx()
|
|
837
|
-
await cliproxyStatusAction({url: 'https://other-cliproxy.example.com'}, ctx)
|
|
838
|
-
|
|
839
|
-
const output = [...captured.stdout, ...captured.stderr].join('\n')
|
|
840
|
-
expect(output).toMatch(/CLIPROXY_MANAGEMENT_KEY|no key|skipping/i)
|
|
841
|
-
})
|
|
842
|
-
})
|
|
843
|
-
|
|
844
|
-
// ─── checkProviderAuth ────────────────────────────────────────────────────────
|
|
845
|
-
|
|
846
|
-
describe('checkProviderAuth', () => {
|
|
847
|
-
afterEach(() => {
|
|
848
|
-
globalThis.fetch = originalFetch
|
|
849
|
-
})
|
|
850
|
-
|
|
851
|
-
it('returns ok when POST /v1/chat/completions returns 200', async () => {
|
|
852
|
-
globalThis.fetch = createFetchImplementation(
|
|
853
|
-
async () =>
|
|
854
|
-
new Response(JSON.stringify({choices: [{message: {content: 'pong'}}]}), {
|
|
855
|
-
status: 200,
|
|
856
|
-
headers: {'content-type': 'application/json'},
|
|
857
|
-
}),
|
|
858
|
-
)
|
|
859
|
-
|
|
860
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
861
|
-
|
|
862
|
-
expect(result.level).toBe('ok')
|
|
863
|
-
expect(result.title).toBe('Upstream provider auth (anthropic)')
|
|
864
|
-
expect(result.summary).toContain('anthropic route OK')
|
|
865
|
-
expect(result.summary).toContain('claude-sonnet-4-6')
|
|
866
|
-
})
|
|
867
|
-
|
|
868
|
-
it('returns error when POST returns 401', async () => {
|
|
869
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('Unauthorized', {status: 401}))
|
|
870
|
-
|
|
871
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
872
|
-
|
|
873
|
-
expect(result.level).toBe('error')
|
|
874
|
-
expect(result.summary).toContain('401')
|
|
875
|
-
expect(result.summary).toContain('cliproxy login claude')
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
it('returns error when POST returns 503 with auth_unavailable body', async () => {
|
|
879
|
-
globalThis.fetch = createFetchImplementation(
|
|
880
|
-
async () => new Response('auth_unavailable: no auth available (providers=claude)', {status: 503}),
|
|
881
|
-
)
|
|
882
|
-
|
|
883
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
884
|
-
|
|
885
|
-
expect(result.level).toBe('error')
|
|
886
|
-
expect(result.summary).toContain('503')
|
|
887
|
-
expect(result.summary).toContain('cliproxy login claude')
|
|
888
|
-
})
|
|
889
|
-
|
|
890
|
-
it('returns error when POST returns 503 with "no auth available" in body', async () => {
|
|
891
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('no auth available', {status: 503}))
|
|
892
|
-
|
|
893
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
894
|
-
|
|
895
|
-
expect(result.level).toBe('error')
|
|
896
|
-
expect(result.summary).toContain('cliproxy login claude')
|
|
897
|
-
})
|
|
898
|
-
|
|
899
|
-
it('returns error when POST returns 503 with "providers=claude" in body', async () => {
|
|
900
|
-
globalThis.fetch = createFetchImplementation(
|
|
901
|
-
async () => new Response('{"error":"providers=claude unavailable"}', {status: 503}),
|
|
902
|
-
)
|
|
903
|
-
|
|
904
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
905
|
-
|
|
906
|
-
expect(result.level).toBe('error')
|
|
907
|
-
expect(result.summary).toContain('cliproxy login claude')
|
|
908
|
-
})
|
|
909
|
-
|
|
910
|
-
it('returns warning when POST returns 503 without auth-unavailable markers', async () => {
|
|
911
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('Service Unavailable', {status: 503}))
|
|
912
|
-
|
|
913
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
914
|
-
|
|
915
|
-
expect(result.level).toBe('warning')
|
|
916
|
-
expect(result.summary).toContain('503')
|
|
917
|
-
expect(result.summary).not.toContain('cliproxy login claude')
|
|
918
|
-
})
|
|
919
|
-
|
|
920
|
-
it('returns warning when POST returns other non-2xx (e.g. 500)', async () => {
|
|
921
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('Internal Server Error', {status: 500}))
|
|
922
|
-
|
|
923
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
924
|
-
|
|
925
|
-
expect(result.level).toBe('warning')
|
|
926
|
-
expect(result.summary).toContain('500')
|
|
927
|
-
})
|
|
928
|
-
|
|
929
|
-
it('returns warning (not error) when fetch throws (network/timeout)', async () => {
|
|
930
|
-
globalThis.fetch = createFetchImplementation(async () => {
|
|
931
|
-
throw new Error('The operation was aborted due to timeout')
|
|
932
|
-
})
|
|
933
|
-
|
|
934
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
935
|
-
|
|
936
|
-
expect(result.level).toBe('warning')
|
|
937
|
-
expect(result.summary).toContain('Anthropic probe failed')
|
|
938
|
-
expect(result.summary).toContain('aborted')
|
|
939
|
-
})
|
|
940
|
-
|
|
941
|
-
it('never includes the apiKey in summary or details', async () => {
|
|
942
|
-
const secretKey = 'super-secret-api-key-do-not-leak'
|
|
943
|
-
|
|
944
|
-
globalThis.fetch = createFetchImplementation(async () => new Response('Unauthorized', {status: 401}))
|
|
945
|
-
|
|
946
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', secretKey)
|
|
947
|
-
|
|
948
|
-
expect(result.summary).not.toContain(secretKey)
|
|
949
|
-
const detailsText = (result.details ?? []).join('\n')
|
|
950
|
-
expect(detailsText).not.toContain(secretKey)
|
|
951
|
-
})
|
|
952
|
-
|
|
953
|
-
it('uses the default model claude-sonnet-4-6 when no model override given', async () => {
|
|
954
|
-
let capturedBody = ''
|
|
955
|
-
|
|
956
|
-
globalThis.fetch = createFetchImplementation(async (_url, init) => {
|
|
957
|
-
capturedBody = typeof init?.body === 'string' ? init.body : ''
|
|
958
|
-
return new Response(JSON.stringify({choices: []}), {
|
|
959
|
-
status: 200,
|
|
960
|
-
headers: {'content-type': 'application/json'},
|
|
961
|
-
})
|
|
962
|
-
})
|
|
963
|
-
|
|
964
|
-
await checkProviderAuth('https://cliproxy.example.com', 'test-api-key')
|
|
965
|
-
|
|
966
|
-
expect(capturedBody).toContain('claude-sonnet-4-6')
|
|
967
|
-
})
|
|
968
|
-
|
|
969
|
-
it('uses the model override when options.model is provided', async () => {
|
|
970
|
-
let capturedBody = ''
|
|
971
|
-
|
|
972
|
-
globalThis.fetch = createFetchImplementation(async (_url, init) => {
|
|
973
|
-
capturedBody = typeof init?.body === 'string' ? init.body : ''
|
|
974
|
-
return new Response(JSON.stringify({choices: []}), {
|
|
975
|
-
status: 200,
|
|
976
|
-
headers: {'content-type': 'application/json'},
|
|
977
|
-
})
|
|
978
|
-
})
|
|
979
|
-
|
|
980
|
-
await checkProviderAuth('https://cliproxy.example.com', 'test-api-key', {model: 'claude-opus-4-5'})
|
|
981
|
-
|
|
982
|
-
expect(capturedBody).toContain('claude-opus-4-5')
|
|
983
|
-
expect(capturedBody).not.toContain('claude-sonnet-4-6')
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
it('includes verbose details (URL, model, status) when verbose=true', async () => {
|
|
987
|
-
globalThis.fetch = createFetchImplementation(
|
|
988
|
-
async () =>
|
|
989
|
-
new Response(JSON.stringify({choices: []}), {
|
|
990
|
-
status: 200,
|
|
991
|
-
headers: {'content-type': 'application/json'},
|
|
992
|
-
}),
|
|
993
|
-
)
|
|
994
|
-
|
|
995
|
-
const result = await checkProviderAuth('https://cliproxy.example.com', 'test-api-key', {verbose: true})
|
|
996
|
-
|
|
997
|
-
expect(result.details).toBeDefined()
|
|
998
|
-
const detailsText = (result.details ?? []).join('\n')
|
|
999
|
-
expect(detailsText).toContain('https://cliproxy.example.com')
|
|
1000
|
-
expect(detailsText).toContain('claude-sonnet-4-6')
|
|
1001
|
-
})
|
|
1002
|
-
})
|
|
1003
|
-
|
|
1004
|
-
// ─── cliproxyStatusAction — provider auth wiring ─────────────────────────────
|
|
1005
|
-
|
|
1006
|
-
describe('cliproxyStatusAction — provider auth probe wiring', () => {
|
|
1007
|
-
let originalEnv: Record<string, string | undefined>
|
|
1008
|
-
|
|
1009
|
-
beforeEach(() => {
|
|
1010
|
-
originalEnv = {
|
|
1011
|
-
CLIPROXY_URL: process.env.CLIPROXY_URL,
|
|
1012
|
-
CLIPROXY_MANAGEMENT_KEY: process.env.CLIPROXY_MANAGEMENT_KEY,
|
|
1013
|
-
CLIPROXY_API_KEY: process.env.CLIPROXY_API_KEY,
|
|
1014
|
-
}
|
|
1015
|
-
delete process.env.CLIPROXY_URL
|
|
1016
|
-
delete process.env.CLIPROXY_MANAGEMENT_KEY
|
|
1017
|
-
delete process.env.CLIPROXY_API_KEY
|
|
1018
|
-
})
|
|
1019
|
-
|
|
1020
|
-
afterEach(() => {
|
|
1021
|
-
for (const [k, v] of Object.entries(originalEnv)) {
|
|
1022
|
-
if (v === undefined) {
|
|
1023
|
-
delete process.env[k]
|
|
1024
|
-
} else {
|
|
1025
|
-
process.env[k] = v
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
globalThis.fetch = originalFetch
|
|
1029
|
-
})
|
|
1030
|
-
|
|
1031
|
-
it('shows a warning (not error) when no provider key is available', async () => {
|
|
1032
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
1033
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
1034
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
1035
|
-
})
|
|
1036
|
-
|
|
1037
|
-
const {ctx, captured} = createCapturedCtx()
|
|
1038
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com'}, ctx)
|
|
1039
|
-
|
|
1040
|
-
const output = [...captured.stdout, ...captured.stderr].join('\n')
|
|
1041
|
-
expect(output).toMatch(/CLIPROXY_API_KEY|skipping upstream provider probe/i)
|
|
1042
|
-
// Must be a warning, not an error — no exit(1) for missing key
|
|
1043
|
-
expect(captured.exit).toBeNull()
|
|
1044
|
-
})
|
|
1045
|
-
|
|
1046
|
-
it('runs the provider auth probe when --api-key is provided', async () => {
|
|
1047
|
-
const fetchedUrls: string[] = []
|
|
1048
|
-
|
|
1049
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
1050
|
-
fetchedUrls.push(url)
|
|
1051
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
1052
|
-
if (url.includes('/v1/chat/completions'))
|
|
1053
|
-
return new Response(JSON.stringify({choices: []}), {
|
|
1054
|
-
status: 200,
|
|
1055
|
-
headers: {'content-type': 'application/json'},
|
|
1056
|
-
})
|
|
1057
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
1058
|
-
})
|
|
1059
|
-
|
|
1060
|
-
const {ctx, captured} = createCapturedCtx()
|
|
1061
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com', apiKey: 'my-api-key'}, ctx)
|
|
1062
|
-
|
|
1063
|
-
expect(fetchedUrls.some(u => u.includes('/v1/chat/completions'))).toBe(true)
|
|
1064
|
-
const output = [...captured.stdout, ...captured.stderr].join('\n')
|
|
1065
|
-
expect(output).toContain('Upstream provider auth')
|
|
1066
|
-
})
|
|
1067
|
-
|
|
1068
|
-
it('does NOT forward ambient CLIPROXY_API_KEY to an explicit --url override', async () => {
|
|
1069
|
-
process.env.CLIPROXY_API_KEY = 'ambient-api-key-secret'
|
|
1070
|
-
|
|
1071
|
-
const capturedAuthHeaders: string[] = []
|
|
1072
|
-
|
|
1073
|
-
globalThis.fetch = createFetchImplementation(async (url, init) => {
|
|
1074
|
-
const hdrs = init?.headers
|
|
1075
|
-
let auth: string | null = null
|
|
1076
|
-
if (hdrs instanceof Headers) {
|
|
1077
|
-
auth = hdrs.get('authorization')
|
|
1078
|
-
} else if (hdrs !== null && hdrs !== undefined && typeof hdrs === 'object') {
|
|
1079
|
-
const hdrsObj = hdrs as Record<string, string>
|
|
1080
|
-
auth = hdrsObj.authorization ?? hdrsObj.Authorization ?? null
|
|
1081
|
-
}
|
|
1082
|
-
if (auth) capturedAuthHeaders.push(auth)
|
|
1083
|
-
|
|
1084
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
1085
|
-
if (url.includes('/v1/chat/completions')) return new Response(JSON.stringify({choices: []}), {status: 200})
|
|
1086
|
-
return new Response('ok', {status: 200})
|
|
1087
|
-
})
|
|
1088
|
-
|
|
1089
|
-
const {ctx} = createCapturedCtx()
|
|
1090
|
-
// Explicit --url override to a non-trusted host — ambient key must NOT follow
|
|
1091
|
-
await cliproxyStatusAction({url: 'https://evil.example.com'}, ctx)
|
|
1092
|
-
|
|
1093
|
-
// The ambient API key must not appear in any Authorization header sent to evil host
|
|
1094
|
-
expect(capturedAuthHeaders.some(h => h.includes('ambient-api-key-secret'))).toBe(false)
|
|
1095
|
-
})
|
|
1096
|
-
|
|
1097
|
-
it('uses ambient CLIPROXY_API_KEY when URL is the trusted default', async () => {
|
|
1098
|
-
process.env.CLIPROXY_API_KEY = 'ambient-api-key-trusted'
|
|
1099
|
-
|
|
1100
|
-
const capturedAuthHeaders: string[] = []
|
|
1101
|
-
|
|
1102
|
-
globalThis.fetch = createFetchImplementation(async (url, init) => {
|
|
1103
|
-
const hdrs = init?.headers
|
|
1104
|
-
let auth: string | null = null
|
|
1105
|
-
if (hdrs instanceof Headers) {
|
|
1106
|
-
auth = hdrs.get('authorization')
|
|
1107
|
-
} else if (hdrs !== null && hdrs !== undefined && typeof hdrs === 'object') {
|
|
1108
|
-
const hdrsObj = hdrs as Record<string, string>
|
|
1109
|
-
auth = hdrsObj.authorization ?? hdrsObj.Authorization ?? null
|
|
1110
|
-
}
|
|
1111
|
-
if (auth) capturedAuthHeaders.push(auth)
|
|
1112
|
-
|
|
1113
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
1114
|
-
if (url.includes('/v1/chat/completions'))
|
|
1115
|
-
return new Response(JSON.stringify({choices: []}), {
|
|
1116
|
-
status: 200,
|
|
1117
|
-
headers: {'content-type': 'application/json'},
|
|
1118
|
-
})
|
|
1119
|
-
// Management endpoints — return ok so management checks don't block
|
|
1120
|
-
if (url.includes('/v0/management/config'))
|
|
1121
|
-
return new Response('{}', {status: 200, headers: {'content-type': 'application/json'}})
|
|
1122
|
-
if (url.includes('/v0/management/usage-queue'))
|
|
1123
|
-
return new Response('[]', {status: 200, headers: {'content-type': 'application/json'}})
|
|
1124
|
-
if (url.includes('/v0/management/latest-version'))
|
|
1125
|
-
return new Response(JSON.stringify({'latest-version': 'v1.0.0'}), {
|
|
1126
|
-
status: 200,
|
|
1127
|
-
headers: {'content-type': 'application/json'},
|
|
1128
|
-
})
|
|
1129
|
-
return new Response('ok', {status: 200})
|
|
1130
|
-
})
|
|
1131
|
-
|
|
1132
|
-
const {ctx} = createCapturedCtx()
|
|
1133
|
-
// Default trusted URL — ambient key SHOULD be used
|
|
1134
|
-
await cliproxyStatusAction({url: 'https://cliproxy.fro.bot'}, ctx)
|
|
1135
|
-
|
|
1136
|
-
expect(capturedAuthHeaders.some(h => h.includes('ambient-api-key-trusted'))).toBe(true)
|
|
1137
|
-
})
|
|
1138
|
-
|
|
1139
|
-
it('provider auth error (401) causes exit(1) — dead upstream is a real error', async () => {
|
|
1140
|
-
globalThis.fetch = createFetchImplementation(async url => {
|
|
1141
|
-
if (url.includes('/healthz')) return new Response('ok', {status: 200})
|
|
1142
|
-
if (url.includes('/v1/chat/completions')) return new Response('Unauthorized', {status: 401})
|
|
1143
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
1144
|
-
})
|
|
1145
|
-
|
|
1146
|
-
const {ctx, captured} = createCapturedCtx()
|
|
1147
|
-
let threw: unknown
|
|
1148
|
-
try {
|
|
1149
|
-
await cliproxyStatusAction({url: 'https://cliproxy.example.com', apiKey: 'my-api-key'}, ctx)
|
|
1150
|
-
} catch (error) {
|
|
1151
|
-
threw = error
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
expect(threw).toBeInstanceOf(MockProcessExit)
|
|
1155
|
-
expect(captured.exit?.code).toBe(1)
|
|
1156
|
-
})
|
|
1157
|
-
})
|