程序员阿江(Relakkes) 8e184c1edd Enforce H5 opt-in before exposing LAN capabilities
The desktop sidecar can bind on LAN addresses for phone access, so the H5 settings switch must be an authorization boundary for remote capability routes, not only a token-mode toggle. Remote browser API, proxy, websocket, and SDK routes now fail closed while H5 is disabled; local desktop, Tauri, WebUI, adapter, and internal SDK paths remain tokenless. When H5 is enabled, remote API, proxy, and websocket requests must use the H5 token carried by the QR link, and the server API key cannot substitute for that H5 token.

Constraint: Desktop sidecar binds 0.0.0.0 while reporting loopback to local UI.

Constraint: Client-controlled Host and Origin headers cannot prove a local request; the boundary uses Bun requestIP instead.

Constraint: Static H5 shell and /health must still load so browser bootstrap can show a recovery flow.

Rejected: Trust loopback Host headers | LAN clients can spoof Host and Origin.

Rejected: Use ANTHROPIC_API_KEY as a remote H5 credential | it is not the phone pairing token and would weaken the QR-token boundary.

Confidence: high

Scope-risk: moderate

Directive: Do not make h5Enabled=false an open remote state for /api, /proxy, /ws, or /sdk routes.

Tested: bun test src/server/__tests__/h5-access-policy.test.ts src/server/__tests__/h5-access-auth.test.ts src/server/middleware/cors.test.ts

Tested: bun run check:server
2026-05-12 16:25:54 +08:00

97 lines
2.7 KiB
TypeScript

/**
* Authentication middleware
*
* 本地桌面应用场景下,使用 Anthropic API Key 做简单鉴权。
* 验证请求头中的 Authorization: Bearer <key> 与 .env 中的 ANTHROPIC_API_KEY 是否匹配。
*/
import { H5AccessService } from '../services/h5AccessService.js'
type AuthResult = { valid: boolean; error?: string }
function parseBearerToken(authHeader: string | null): AuthResult & { token?: string } {
if (!authHeader) {
return { valid: false, error: 'Missing Authorization header' }
}
const [scheme, token] = authHeader.split(' ')
if (scheme !== 'Bearer' || !token) {
return { valid: false, error: 'Invalid Authorization format. Use: Bearer <token>' }
}
return { valid: true, token }
}
export function validateAuth(req: Request): AuthResult {
const parsedAuth = parseBearerToken(req.headers.get('Authorization'))
if (!parsedAuth.valid || !parsedAuth.token) {
return parsedAuth
}
const apiKey = process.env.ANTHROPIC_API_KEY
if (!apiKey) {
return { valid: false, error: 'Server ANTHROPIC_API_KEY not configured' }
}
if (parsedAuth.token !== apiKey) {
return { valid: false, error: 'Invalid API key' }
}
return { valid: true }
}
/**
* Helper to check auth and return 401 if invalid
*/
export async function validateRequestAuth(
req: Request,
tokenOverride?: string | null,
): Promise<AuthResult> {
const anthropicAuth = validateAuth(req)
if (anthropicAuth.valid) {
return anthropicAuth
}
const parsedAuth = parseBearerToken(req.headers.get('Authorization'))
const h5Token = tokenOverride ?? parsedAuth.token
if (h5Token) {
const h5AccessService = new H5AccessService()
if (await h5AccessService.validateToken(h5Token)) {
return { valid: true }
}
return { valid: false, error: 'Invalid H5 access token' }
}
return anthropicAuth
}
export async function requireAuth(req: Request, tokenOverride?: string | null): Promise<Response | null> {
const { valid, error } = await validateRequestAuth(req, tokenOverride)
if (!valid) {
return Response.json({ error: 'Unauthorized', message: error }, { status: 401 })
}
return null
}
export async function requireH5Token(req: Request, tokenOverride?: string | null): Promise<Response | null> {
const parsedAuth = parseBearerToken(req.headers.get('Authorization'))
const h5Token = tokenOverride ?? parsedAuth.token
if (!h5Token) {
return Response.json(
{ error: 'Unauthorized', message: 'Missing H5 access token' },
{ status: 401 },
)
}
const h5AccessService = new H5AccessService()
if (!await h5AccessService.validateToken(h5Token)) {
return Response.json(
{ error: 'Unauthorized', message: 'Invalid H5 access token' },
{ status: 401 },
)
}
return null
}