* Loopback handler — full surface for the spawning agent. No auth (the * loopback bind itself is the boundary).
(ctx: HandlerCtx)
| 228 | * loopback bind itself is the boundary). |
| 229 | */ |
| 230 | async function handleLoopback(ctx: HandlerCtx): Promise<void> { |
| 231 | const { req, res, tokenStore, getTunnel } = ctx; |
| 232 | const url = parseUrl(req.url ?? '/'); |
| 233 | const path = url.pathname ?? '/'; |
| 234 | const method = req.method ?? 'GET'; |
| 235 | |
| 236 | try { |
| 237 | // /healthz — public on loopback. |
| 238 | if (method === 'GET' && path === '/healthz') { |
| 239 | sendJson(res, 200, { version: '1.0.0', mode: 'loopback' }); |
| 240 | return; |
| 241 | } |
| 242 | |
| 243 | // /auth/sessions — list active sessions (owner only). |
| 244 | if (method === 'GET' && path === '/auth/sessions') { |
| 245 | sendJson(res, 200, { sessions: tokenStore.list() }); |
| 246 | return; |
| 247 | } |
| 248 | |
| 249 | // /auth/revoke — revoke a token. |
| 250 | if (method === 'POST' && path === '/auth/revoke') { |
| 251 | const body = await readBody(req); |
| 252 | if ('error' in body) { sendJson(res, 413, body); return; } |
| 253 | const parsed = JSON.parse(body.toString('utf-8') || '{}') as { token?: string; identity?: string }; |
| 254 | let count = 0; |
| 255 | if (parsed.token) { |
| 256 | count = tokenStore.revoke(parsed.token) ? 1 : 0; |
| 257 | } else if (parsed.identity) { |
| 258 | count = tokenStore.revokeByIdentity(parsed.identity); |
| 259 | } |
| 260 | sendJson(res, 200, { revoked: count }); |
| 261 | return; |
| 262 | } |
| 263 | |
| 264 | // Other endpoints — proxy to the device. |
| 265 | const tunnel = await getTunnel(); |
| 266 | if (!tunnel) { |
| 267 | sendJson(res, 503, { error: 'device_not_connected' }); |
| 268 | return; |
| 269 | } |
| 270 | const body = await readBody(req); |
| 271 | if ('error' in body) { sendJson(res, 413, body); return; } |
| 272 | const sessionId = (req.headers['x-session-id'] as string | undefined) ?? null; |
| 273 | const agentIdentity = (req.headers['x-agent-identity'] as string | undefined) ?? undefined; |
| 274 | const upstream = await proxyToDevice({ inbound: req, body, tunnel, sessionId, agentIdentity }); |
| 275 | res.writeHead(upstream.status, upstream.headers); |
| 276 | res.end(upstream.body); |
| 277 | } catch (err) { |
| 278 | sendJson(res, 500, { error: 'internal_error', detail: (err as Error).message }); |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | interface TailnetCtx extends HandlerCtx { |
| 283 | whoIsImpl: (addr: string) => Promise<{ identity: string; raw: unknown }>; |
no test coverage detected