Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/hooks/useDashboardCtx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,48 @@ describe('useDashboardCtx', () => {
expect(result.current.envs).toEqual(envsBefore)
})

// BUG-DASH-001 (P0): pre-fix a 67-char input persisted to
// localStorage and broke every subsequent API call. Now the input is
// clipped to the api cap (32 chars). The clipped string must still
// match the api regex `^[a-z0-9-]{1,32}$`.
it('addEnv clips an over-32-char input to the api cap', async () => {
const mod = await load()
const { result } = renderHook(() => mod.useDashboardCtx())
await waitFor(() => expect(result.current.me).not.toBeNull())
const longInput = 'a'.repeat(67) // 67 chars of valid char class
await act(async () => { mod.addEnv(longInput) })
// The persisted env name must be ≤ 32 chars (api cap).
expect(result.current.env.length).toBeLessThanOrEqual(32)
expect(result.current.env).toMatch(/^[a-z0-9-]{1,32}$/)
})

// BUG-DASH-002: pre-fix the JS regex permitted underscores even
// though the api regex bans them. Underscore inputs would persist
// locally and then 400 on every API call. Now the underscore is
// stripped.
it('addEnv strips underscores (api regex forbids them)', async () => {
const mod = await load()
const { result } = renderHook(() => mod.useDashboardCtx())
await waitFor(() => expect(result.current.me).not.toBeNull())
await act(async () => { mod.addEnv('my_env_name') })
// Underscore-stripped form is "myenvname" — the only valid api shape.
expect(result.current.envs.some((e: string) => e.includes('_'))).toBe(false)
expect(result.current.env).toMatch(/^[a-z0-9-]{1,32}$/)
})

// BUG-DASH-001 belt: a paste that's entirely punctuation must NOT
// change the live env. Pre-fix the empty-string fall-through worked
// because of `if (!clean)`; we keep that guard via the final regex
// gate.
it('addEnv leaves live env unchanged on all-invalid + over-cap input', async () => {
const mod = await load()
const { result } = renderHook(() => mod.useDashboardCtx())
await waitFor(() => expect(result.current.me).not.toBeNull())
const envBefore = result.current.env
await act(async () => { mod.addEnv('!!! !!! !!!') })
expect(result.current.env).toBe(envBefore)
})

it('resetBootstrap clears state and allows a fresh bootstrap', async () => {
const mod = await load()
const { result, rerender } = renderHook(() => mod.useDashboardCtx())
Expand Down
31 changes: 29 additions & 2 deletions src/hooks/useDashboardCtx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,36 @@ export function setEnv(next: string) {
refreshCounts()
}

// BUG-DASH-001 (P0) + BUG-DASH-002:
//
// Pre-fix this function accepted env names of arbitrary length and
// stripped characters silently, then persisted the result to
// localStorage. A 67-char paste → every subsequent /api/v1/* call
// carried `?env=<67-char-name>` → vault 400 invalid_env, resources
// 200 with empty list, no UI affordance to delete the bad value
// (user had to clear localStorage from devtools).
//
// The api regex is `^[a-z0-9-]{1,32}$` (see api/internal/handlers/env.go
// + the `invalid_env` 400 branch). Underscores are NOT part of the
// api regex; the pre-fix JS regex `[^a-z0-9_-]` permitted them,
// producing names that the api would later reject — a different
// class of the same "client says yes, server says no" gap.
//
// Fix: align the JS regex with the api regex, enforce the 32-char
// cap up front, and return early if validation fails. The caller
// (EnvSwitcher) already gates `addEnv` behind a non-empty draft so
// no UI plumbing changes are required.
const ENV_REGEX = /^[a-z0-9-]{1,32}$/
const ENV_MAX_LEN = 32

export function addEnv(name: string) {
const clean = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '')
if (!clean) return
// Lowercase + strip api-invalid chars; underscores no longer survive
// (BUG-DASH-002). Clip to the api cap of 32 chars (BUG-DASH-001).
const clean = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, ENV_MAX_LEN)
// Final regex gate: empty / leading-dash-only / any drift from the
// api regex → bail out without persisting. setEnv is NOT called, so
// the live env stays on the previous valid value (no broken state).
if (!ENV_REGEX.test(clean)) return
if (!state.envs.includes(clean)) {
state = { ...state, envs: [...state.envs, clean] }
emit()
Expand Down
Loading