Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import { request, type Server } from 'node:http'
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { startHookServer, type HookServerHandlers, chooseHookAgent, knownTranscriptFor } from './hookServer.js'
import type { RegisteredSession } from './lib/registry.js'
import { env } from './config/env.js'
import { readHookCredential } from './lib/hookAuth.js'
import { CommandBarService } from './lib/commandBar.js'
import { ENGINES } from './engines/types.js'
let server: Server | null = null
describe('native command bar endpoints', () => {
it('requires a native local header and rejects browser origins before evaluating', async () => {
const decide = vi.fn()
const { base } = await start({ onCommandBar: { status: vi.fn(), decide } })
const attempts: Record<string, string>[] = [{}, { 'x-adapter-local': '1', origin: 'https://example.com' }]
for (const headers of attempts) {
const response = await fetch(`${base}/api/command-bar/resolve`, { method: 'POST', headers, body: '{}' })
expect(response.status).toBe(403)
}
expect(decide).not.toHaveBeenCalled()
})
it('returns configuration without credentials and wraps useful setup errors', async () => {
const { base } = await start({ onCommandBar: new CommandBarService({ key: async () => null }) })
const headers = { 'x-adapter-local': '1', 'content-type': 'application/json' }
const status = await fetch(`${base}/api/command-bar/status`, { headers })
expect(await status.json()).toMatchObject({ success: true, data: { configured: false, provider: 'OpenRouter' } })
const response = await fetch(`${base}/api/command-bar/resolve`, { method: 'POST', headers, body: JSON.stringify({ prompt: 'hello', candidates: [] }) })
expect(response.status).toBe(503)
expect(await response.json()).toMatchObject({ success: false, error: { code: 'OPENROUTER_REQUIRED' } })
const bad = await fetch(`${base}/api/command-bar/resolve`, { method: 'POST', headers, body: '{' })
expect(bad.status).toBe(400)
const large = await fetch(`${base}/api/command-bar/resolve`, { method: 'POST', headers, body: 'x'.repeat(129_000) })
expect(large.status).toBe(413)
})
})
afterEach(async () => {
if (!server) return
await new Promise<void>((resolve) => server!.close(() => resolve()))
server = null
})
async function start(overrides: Partial<HookServerHandlers> = {}) {
const handlers: HookServerHandlers = {
onRegistered: vi.fn(),
onSessionEnd: vi.fn(),
...overrides,
}
const started = await startHookServer(0, handlers)
server = started.server
const credential = readHookCredential(env.ADAPTER_DATA_DIR)
if (!credential) throw new Error('hook credential was not created')
return {
handlers,
base: `http://127.0.0.1:${started.port}`,
headers: { 'content-type': 'application/json', 'x-harness-hook-token': credential },
}
}
describe('process-owned hook server', () => {
it('runs targeted resolution and rejects a hook without a matching pane engine process', async () => {
const resolveHookAgent = vi.fn(async () => null)
const { handlers, base, headers } = await start({ resolveHookAgent })
const response = await fetch(`${base}/api/hook/session-start`, {
method: 'POST',
headers,
body: JSON.stringify({
engine: 'codex',
tmuxPane: '%41',
sessionId: '019fea92-e31a-7692-9c35-f616e9d458b7',
cwd: '/work/demo',
}),
})
expect(await response.json()).toEqual({ ignored: true, reason: 'no_matching_engine_process' })
expect(resolveHookAgent).toHaveBeenCalledWith({
engine: 'codex', tmuxPane: '%41', runtimeHints: [{ backend: 'tmux', paneId: '%41' }], callerPid: undefined,
})
expect(handlers.onRegistered).not.toHaveBeenCalled()
})
it('accepts a Herdr hint from a hook installed by an earlier build, and resolves by its tmux pane only', async () => {
const resolveHookAgent = vi.fn(async () => null)
const { base, headers } = await start({ resolveHookAgent })
const response = await fetch(`${base}/api/hook/session-start`, {
method: 'POST',
headers,
body: JSON.stringify({
engine: 'codex',
tmuxPane: '%41',
sessionId: '019fea92-e31a-7692-9c35-f616e9d458b7',
runtimeHints: [
{ backend: 'tmux', paneId: '%41' },
{ backend: 'herdr', paneId: 'w1:p1', sessionName: 'default', socketPath: '/tmp/herdr.sock' },
],
}),
})
expect(response.status).toBe(200)
expect(resolveHookAgent).toHaveBeenCalledWith({
engine: 'codex', tmuxPane: '%41', runtimeHints: [{ backend: 'tmux', paneId: '%41' }], callerPid: undefined,
})
})
it('ignores a hook whose only terminal is a Herdr pane', async () => {
const resolveHookAgent = vi.fn(async () => null)
const { base, headers } = await start({ resolveHookAgent })
const response = await fetch(`${base}/api/hook/session-start`, {
method: 'POST',
headers,
body: JSON.stringify({
engine: 'claude',
sessionId: 'session-1',
runtimeHints: [{ backend: 'herdr', paneId: 'w1:p1', sessionName: 'default' }],
}),
})
expect(await response.json()).toEqual({ ignored: true, reason: 'not_in_terminal' })
expect(resolveHookAgent).not.toHaveBeenCalled()
})
it('rejects hooks outside configured terminal contexts before attempting process resolution', async () => {
const resolveHookAgent = vi.fn(async () => null)
const { handlers, base, headers } = await start({ resolveHookAgent })
const response = await fetch(`${base}/api/hook/session-start`, {
method: 'POST',
headers,
body: JSON.stringify({ engine: 'claude', sessionId: 'session-1', cwd: '/work/demo' }),
})
expect(await response.json()).toEqual({ ignored: true, reason: 'not_in_terminal' })
expect(resolveHookAgent).not.toHaveBeenCalled()
expect(handlers.onRegistered).not.toHaveBeenCalled()
})
it('treats SessionEnd as a reconciliation hint and exposes no launcher websocket endpoint', async () => {
const onSessionEnd = vi.fn()
const resolveHookAgent = vi.fn(async () => ({
engine: 'claude', sessionId: 'session-1', agentId: 'agent-1',
} as never))
const { base, headers } = await start({ onSessionEnd, resolveHookAgent })
const ended = await fetch(`${base}/api/hook/session-end`, {
method: 'POST',
headers,
body: JSON.stringify({
engine: 'claude', sessionId: 'session-1', reason: 'clear', tmuxPane: '%1', callerPid: 123,
}),
})
expect(await ended.json()).toEqual({ ok: true })
expect(onSessionEnd).toHaveBeenCalledWith('session-1', 'clear')
const legacy = await fetch(`${base}/api/machine-ws`)
expect(legacy.status).toBe(404)
})
it('rejects every unauthenticated mutating hook request', async () => {
const { base, handlers } = await start()
const response = await fetch(`${base}/api/hook/session-end`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ sessionId: 'session-1' }),
})
expect(response.status).toBe(401)
expect(await response.json()).toEqual({ error: 'UNAUTHORIZED' })
expect(handlers.onSessionEnd).not.toHaveBeenCalled()
})
it.each([
[{ engine: 'claude', sessionId: 'session-1', tmuxPane: '%1', unknown: true }],
[{ engine: 'claude', sessionId: 'session-1', runtimeHints: [{ backend: 'tmux', paneId: '%1', extra: true }] }],
[{ engine: 'claude', sessionId: 'session-1', tmuxPane: '%1', source: { forged: true } }],
[{ engine: 'claude', sessionId: 'session-1', tmuxPane: '%1', input: 'x'.repeat(128 * 1024 + 1) }],
[null],
])('rejects malformed or unknown hook fields before process resolution', async (body) => {
const resolveHookAgent = vi.fn(async () => null)
const { base, headers } = await start({ resolveHookAgent })
const response = await fetch(`${base}/api/hook/session-start`, {
method: 'POST', headers, body: JSON.stringify(body),
})
expect(response.status).toBe(400)
expect(await response.json()).toEqual({ error: 'invalid hook body' })
expect(resolveHookAgent).not.toHaveBeenCalled()
})
it.each([
['/api/hook/session-end', { reason: 'clear' }, 'onSessionEnd'],
['/api/hook/turn-start', {}, 'onTurnStart'],
['/api/hook/tool-start', { toolUseId: 'tool-1', toolName: 'Task' }, 'onToolStart'],
['/api/hook/turn-stop', { status: 'error' }, 'onTurnStop'],
] as const)('rejects a forged bound-session mutation on %s', async (path, extra, handlerName) => {
const handler = vi.fn()
const resolveHookAgent = vi.fn(async () => ({
engine: 'claude', sessionId: 'real-session', agentId: 'agent-1',
} as never))
const { base, headers } = await start({ [handlerName]: handler, resolveHookAgent })
const response = await fetch(`${base}${path}`, {
method: 'POST',
headers,
body: JSON.stringify({
engine: 'claude', sessionId: 'forged-session', tmuxPane: '%1', callerPid: 123, ...extra,
}),
})
expect(response.status).toBe(403)
expect(await response.json()).toEqual({ error: 'UNBOUND_HOOK' })
expect(handler).not.toHaveBeenCalled()
})
it('answers a proxied control-plane read whose handler throws, instead of hanging it', async () => {
// The handler is a void-discarded async: a throw used to be an unhandledRejection and a request
// with no response, which the desktop app reported 30s later as its own receive timeout.
const { base } = await start({ onMachinesList: async () => { throw new TypeError('fetch failed') } })
const response = await fetch(`${base}/api/machines`)
expect(response.status).toBe(502)
expect(await response.json()).toEqual({ success: false, error: { code: 'PROXY_FAILED', message: 'fetch failed' } })
})
it('forwards a proxied answer verbatim, status and body alike', async () => {
const { base } = await start({
onAuthMe: async () => ({ status: 504, body: { success: false, error: { code: 'BACKEND_TIMEOUT', message: 'slow' } } }),
})
const response = await fetch(`${base}/api/auth/me`)
expect(response.status).toBe(504)
expect(await response.json()).toEqual({ success: false, error: { code: 'BACKEND_TIMEOUT', message: 'slow' } })
})
})
describe('the desk proxy', () => {
it('reads the desk ungated and writes its ops only with the local header, body passed through', async () => {
const ops = vi.fn(async (body: unknown) => ({ status: 200, body: { success: true, data: { revision: 2, tabs: [], echo: body } } }))
const { base } = await start({
onDeskRead: async () => ({ status: 200, body: { success: true, data: { revision: 1, tabs: [] } } }),
onDeskOps: ops,
})
const read = await fetch(`${base}/api/desk`)
expect(await read.json()).toEqual({ success: true, data: { revision: 1, tabs: [] } })
const refused = await fetch(`${base}/api/desk/ops`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"ops":[]}' })
expect(refused.status).toBe(403)
expect(ops).not.toHaveBeenCalled()
const body = { ops: [{ op: 'tab.create', id: 'a', name: 'Local' }] }
const written = await fetch(`${base}/api/desk/ops`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-adapter-local': '1' }, body: JSON.stringify(body) })
expect(((await written.json()) as { data: unknown }).data).toMatchObject({ revision: 2, echo: body })
expect(ops).toHaveBeenCalledWith(body)
const bad = await fetch(`${base}/api/desk/ops`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-adapter-local': '1' }, body: '{nope' })
expect(bad.status).toBe(400)
})
})
describe('the Harness Store proxy', () => {
it('forwards a store read with its path and query, and a store write only with the local header', async () => {
const calls: Array<[string, string, unknown]> = []
const { base } = await start({
onStore: async (method, path, body) => { calls.push([method, path, body]); return { status: 200, body: { success: true, data: { ok: method } } } },
})
const read = await fetch(`${base}/api/store/harnesses/autonomous/marp/reviews?limit=5`)
expect(read.status).toBe(200)
expect(await read.json()).toEqual({ success: true, data: { ok: 'GET' } })
const crossOrigin = await fetch(`${base}/api/store/harnesses/autonomous/marp/review`, { method: 'PUT', body: JSON.stringify({ rating: 5 }) })
expect(crossOrigin.status).toBe(403)
const write = await fetch(`${base}/api/store/harnesses/autonomous/marp/review`, {
method: 'PUT', headers: { 'x-adapter-local': '1', 'content-type': 'application/json' }, body: JSON.stringify({ rating: 5, title: 'Keynote' }),
})
expect(write.status).toBe(200)
const gone = await fetch(`${base}/api/store/harnesses/autonomous/marp/review`, { method: 'DELETE', headers: { 'x-adapter-local': '1' } })
expect(gone.status).toBe(200)
expect(calls).toEqual([
['GET', '/api/store/harnesses/autonomous/marp/reviews?limit=5', undefined],
['PUT', '/api/store/harnesses/autonomous/marp/review', { rating: 5, title: 'Keynote' }],
['DELETE', '/api/store/harnesses/autonomous/marp/review', undefined],
])
})
it('refuses a store path with anything but id characters in it', async () => {
const { base } = await start({ onStore: async () => ({ status: 200, body: {} }) })
expect((await fetch(`${base}/api/store/harnesses/a%20b/reviews`)).status).toBe(400)
expect((await fetch(`${base}/api/store/ratings`, { method: 'POST', headers: { 'x-adapter-local': '1' } })).status).toBe(405)
})
})
describe('knownTranscriptFor', () => {
const sessionId = 'eae0ba40-3d0a-4340-9dd5-0a56ecbc080c'
const root = mkdtempSync(join(tmpdir(), 'hook-transcript-'))
const known = join(root, 'openharness', `${sessionId}.jsonl`)
const announced = join(root, 'openharness-cli', `${sessionId}.jsonl`)
mkdirSync(join(root, 'openharness'))
writeFileSync(known, '{}\n')
const row = { sessionId, transcriptPath: known } as RegisteredSession
afterAll(() => rmSync(root, { recursive: true, force: true }))
it('takes the resumed row\'s transcript when Claude Code announced one under the wrong project dir', () => {
expect(knownTranscriptFor({ engine: 'claude', sessionId, transcriptPath: announced }, row)).toBe(known)
})
it('keeps an announced transcript that exists', () => {
expect(knownTranscriptFor({ engine: 'claude', sessionId, transcriptPath: known }, { ...row, transcriptPath: join(root, 'other.jsonl') })).toBe(known)
})
it.each<[string, Parameters<typeof knownTranscriptFor>[0], RegisteredSession | undefined]>([
['another conversation', { engine: 'claude', sessionId: 'other-conversation', transcriptPath: announced }, row],
['another engine', { engine: 'codex', sessionId, transcriptPath: announced }, row],
['a row whose file is not named by the conversation', { engine: 'claude', sessionId, transcriptPath: announced }, { ...row, transcriptPath: join(root, 'openharness', 'renamed.jsonl') }],
['a row whose file is gone', { engine: 'claude', sessionId, transcriptPath: announced }, { ...row, transcriptPath: join(root, 'gone', `${sessionId}.jsonl`) }],
['no row', { engine: 'claude', sessionId, transcriptPath: announced }, undefined],
['no announcement', { engine: 'claude', sessionId }, row],
])('leaves the announcement alone for %s', (_case, body, agent) => {
expect(knownTranscriptFor(body, agent)).toBe(body.transcriptPath)
})
})
describe('chooseHookAgent', () => {
it('prefers caller ancestry, the evidence that cannot be guessed at', () => {
expect(chooseHookAgent(['strong'], ['weak'], 'codex')).toEqual({ agent: 'strong', reason: 'ancestry' })
})
it('accepts the runtime alone when ancestry is unavailable and the pane is unambiguous', () => {
// Cursor posts its hooks from outside the pane's process tree — on tmux and on Herdr alike — so
// demanding ancestry rejected every hook it ever sent and no session bound. The pane is the proof:
// the hook named a runtime, and that runtime carries exactly one agent of this engine.
expect(chooseHookAgent([], ['only-agent-on-that-pane'], 'cursor')).toEqual({
agent: 'only-agent-on-that-pane', reason: 'runtime',
})
})
it('answers nothing rather than guessing', () => {
expect(chooseHookAgent([], [], 'cursor')).toEqual({ agent: null, reason: 'none' })
expect(chooseHookAgent([], ['a', 'b'], 'cursor')).toEqual({ agent: null, reason: 'ambiguous' })
expect(chooseHookAgent(['a', 'b'], ['c'], 'cursor')).toEqual({ agent: null, reason: 'ambiguous' })
})
it.each(ENGINES.filter((engine) => engine !== 'cursor'))(
'rejects a late %s hook when the pane now belongs to a replacement process',
(engine) => {
const replacement = { agentId: 'replacement-agent', sessionId: 'new-session' }
expect(chooseHookAgent([], [replacement], engine)).toEqual({ agent: null, reason: 'none' })
expect(chooseHookAgent([replacement], [replacement], engine))
.toEqual({ agent: replacement, reason: 'ancestry' })
},
)
})
describe('/api/status', () => {
it('serves whatever the daemon reports — including the pid the desktop uses to tell daemons apart', async () => {
const { base } = await start({
onStatus: () => ({ pid: process.pid, connected: false, restarting: false, discoveryReady: true }),
})
const response = await fetch(`${base}/api/status`)
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ pid: process.pid, connected: false, restarting: false, discoveryReady: true })
})
})
describe('browser setup links are gone', () => {
// They served the retired web client; the route that minted them must not come back.
it('mints nothing at the old /api/e2ee/setup-link route', async () => {
const { base } = await start()
const response = await fetch(`${base}/api/e2ee/setup-link`, { method: 'POST', headers: { 'x-adapter-local': '1' } })
expect(response.status).not.toBe(200)
expect(await response.text()).not.toContain('setup=browser')
})
})
describe('requests must name this server', () => {
// A page that re-points its hostname at 127.0.0.1 is same-origin to this port; only its Host gives it away.
async function send(base: string, method: string, path: string, headers: Record<string, string>): Promise<number> {
const url = new URL(path, base)
return new Promise((resolve, reject) => {
const r = request({ host: '127.0.0.1', port: url.port, path: url.pathname, method, headers }, (res) => { res.resume(); resolve(res.statusCode ?? 0) })
r.on('error', reject)
r.end()
})
}
it('refuses every route, reads included, when Host is not a loopback name for this port', async () => {
const onStatus = vi.fn(() => ({ ok: true }))
const onLogs = vi.fn(() => 'secret log')
const { base } = await start({ onStatus, onLogs })
const evil = { host: 'rebind.evil.example:' + new URL(base).port }
for (const [method, path, extra] of [
['GET', '/api/status', {}], ['GET', '/api/logs', {}], ['GET', '/', {}], ['GET', '/api/machines', {}],
['POST', '/api/remote-password/set', { 'x-adapter-local': '1' }], ['POST', '/api/stop', { 'x-adapter-local': '1' }],
] as const) {
expect(await send(base, method, path, { ...evil, ...extra }), `${method} ${path}`).toBe(403)
}
expect(onStatus).not.toHaveBeenCalled()
expect(onLogs).not.toHaveBeenCalled()
})
it('serves a status that has to read before it answers', async () => {
// A harness's `updatedAt` is when its conversation last moved, which is read from its transcript.
const { base } = await start({ onStatus: async () => ({ sessions: [{ id: 'a', updatedAt: 42 }] }) })
const res = await fetch(`${base}/api/status`)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ sessions: [{ id: 'a', updatedAt: 42 }] })
})
it('still serves loopback names, and the dashboard from its own origin', async () => {
const { base } = await start({ onStatus: () => ({ ok: true }) })
const port = new URL(base).port
for (const host of [`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]) {
expect(await send(base, 'GET', '/api/status', { host }), host).toBe(200)
}
expect(await send(base, 'GET', '/api/status', { host: `localhost:${port}`, origin: `http://localhost:${port}` })).toBe(200)
expect(await send(base, 'GET', '/api/status', { host: `127.0.0.1:${port}`, origin: 'http://evil.example' })).toBe(403)
})
})
describe('the trust group endpoints', () => {
const local = { 'x-adapter-local': '1', 'content-type': 'application/json' }
const key = Buffer.alloc(32, 7).toString('base64')
const machineId = 'a'.repeat(32)
it('trust-peer takes only a real key and machine id, and trims the label', async () => {
const onTrustLinkedPeer = vi.fn(() => ({ status: 200, body: { ok: true } }))
const { base } = await start({ onTrustLinkedPeer })
const post = (body: unknown, headers: Record<string, string> = local) =>
fetch(`${base}/api/link/trust-peer`, { method: 'POST', headers, body: JSON.stringify(body) })
for (const bad of [{ pub: 'x', machineId }, { pub: key, machineId: 'nope' }, { machineId }, { pub: `${key}AA`, machineId }]) {
expect((await post(bad)).status, JSON.stringify(bad)).toBe(400)
}
expect((await post({ pub: key, machineId }, { 'content-type': 'application/json' })).status).toBe(403)
expect((await post({ pub: key, machineId, label: ` ${'n'.repeat(80)} ` })).status).toBe(200)
expect(onTrustLinkedPeer).toHaveBeenCalledTimes(1)
expect(onTrustLinkedPeer).toHaveBeenCalledWith({ pub: key, machineId, label: 'n'.repeat(60) })
})
it('group remove and sync are local writes; list is readable', async () => {
const onGroupRemove = vi.fn(() => ({ status: 200, body: { label: 'b', fingerprint: 'fp' } }))
const onGroupSync = vi.fn(() => ({ status: 200, body: { ok: true } }))
const onGroupList = vi.fn(() => ({ status: 200, body: { members: [] } }))
const { base } = await start({ onGroupRemove, onGroupSync, onGroupList })
expect((await fetch(`${base}/api/group/remove`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"selector":"1"}' })).status).toBe(403)
expect((await fetch(`${base}/api/group/sync`, { method: 'POST' })).status).toBe(403)
expect((await fetch(`${base}/api/group/remove`, { method: 'POST', headers: local, body: '{"selector":" "}' })).status).toBe(400)
expect((await fetch(`${base}/api/group/remove`, { method: 'POST', headers: local, body: '{"selector":" 1 "}' })).status).toBe(200)
expect(onGroupRemove).toHaveBeenCalledWith('1')
expect((await fetch(`${base}/api/group/sync`, { method: 'POST', headers: local })).status).toBe(200)
expect(await (await fetch(`${base}/api/group`)).json()).toEqual({ members: [] })
})
})
describe('the daemon socket', () => {
function viaSocket(socketPath: string, method: string, path: string, headers: Record<string, string> = {}): Promise<number> {
return new Promise((resolve, reject) => {
const r = request({ socketPath, path, method, headers }, (res) => { res.resume(); resolve(res.statusCode ?? 0) })
r.on('error', reject)
r.end()
})
}
it('serves the same routes with no loopback Host, and keeps every other guard', async () => {
const dir = mkdtempSync('/tmp/hsock-')
const socketPath = join(dir, 'daemon.sock')
const onStatus = vi.fn(() => ({ ok: true }))
const commandBar = { status: vi.fn(async () => ({ configured: false })), decide: vi.fn() }
const started = await startHookServer(0, { onRegistered: vi.fn(), onSessionEnd: vi.fn(), onStatus, onCommandBar: commandBar as never }, { socketPath })
server = started.server
try {
expect(started.localSocket?.path).toBe(socketPath)
// Node sends `Host: localhost` without a port over a socket — refused on TCP, fine here.
expect(await viaSocket(socketPath, 'GET', '/api/status')).toBe(200)
expect(await viaSocket(socketPath, 'GET', '/api/status', { host: 'rebind.evil.example' })).toBe(200)
expect(onStatus).toHaveBeenCalledTimes(2)
// The command bar asked for a loopback PEER; a socket peer has no address and is let in.
expect(await viaSocket(socketPath, 'GET', '/api/command-bar/status', { 'x-adapter-local': '1' })).toBe(200)
expect(await viaSocket(socketPath, 'GET', '/api/command-bar/status')).toBe(403)
// Mutations still need the CSRF header, hooks still need their credential.
expect(await viaSocket(socketPath, 'POST', '/api/stop')).toBe(403)
expect(await viaSocket(socketPath, 'POST', '/api/hook/session-start')).not.toBe(200)
// The TCP port is untouched: a foreign Host is still refused there.
const port = (started.server.address() as { port: number }).port
const tcp = await new Promise<number>((resolve, reject) => {
const r = request({ host: '127.0.0.1', port, path: '/api/status', headers: { host: 'rebind.evil.example' } }, (res) => { res.resume(); resolve(res.statusCode ?? 0) })
r.on('error', reject)
r.end()
})
expect(tcp).toBe(403)
} finally {
await started.localSocket?.close()
rmSync(dir, { recursive: true, force: true })
}
})
it('hands out a phone sign-in code over the socket only — never on the loopback port', async () => {
const dir = mkdtempSync('/tmp/hsock-')
const socketPath = join(dir, 'daemon.sock')
const onAuthHandoff = vi.fn(async () => ({ status: 200, body: { success: true, data: { code: 'hnh_x', expiresIn: 90 } } }))
const started = await startHookServer(0, { onRegistered: vi.fn(), onSessionEnd: vi.fn(), onAuthHandoff }, { socketPath })
server = started.server
try {
const local = { 'x-adapter-local': '1' }
expect(await viaSocket(socketPath, 'POST', '/api/auth/handoff', local)).toBe(200)
expect(await viaSocket(socketPath, 'POST', '/api/auth/handoff')).toBe(403)
const port = (started.server.address() as { port: number }).port
const tcp = await fetch(`http://127.0.0.1:${port}/api/auth/handoff`, { method: 'POST', headers: local })
expect(tcp.status).toBe(403)
expect(onAuthHandoff).toHaveBeenCalledTimes(1)
} finally {
await started.localSocket?.close()
rmSync(dir, { recursive: true, force: true })
}
})
it('starts on TCP alone when the socket cannot be opened', async () => {
const dir = mkdtempSync('/tmp/hsock-')
const blocked = join(dir, 'daemon.sock')
writeFileSync(blocked, 'not a socket')
const started = await startHookServer(0, { onRegistered: vi.fn(), onSessionEnd: vi.fn() }, { socketPath: blocked })
server = started.server
expect(started.localSocket).toBeNull()
expect(started.port).toBeGreaterThan(0)
rmSync(dir, { recursive: true, force: true })
})
})
670fd7200This file is pinned to the selected release. Download the original to open it in your design tool.