程序员阿江(Relakkes) e2f299dcb3 Protect remote H5 capability endpoints without breaking local desktop flows
Wire request-scoped H5 auth and CORS decisions through the server so LAN browser API, proxy, and websocket access require the H5 token while loopback desktop, Tauri WebView, local adapter, SDK, and static shell paths keep their existing bootstrap behavior.

Constraint: Task 2 scope is limited to server auth/CORS wiring and the four requested files
Rejected: Global auth toggle for all requests | would incorrectly block loopback desktop and bootstrap asset flows
Directive: Keep static H5 shell and assets tokenless unless the browser-side bootstrap model changes
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/h5-access-policy.test.ts src/server/middleware/cors.test.ts
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 | h5-access-auth blocked by missing zod dependency in src/server/api/haha-oauth.ts
Tested: LSP diagnostics clean for src/server/index.ts, src/server/middleware/cors.ts, src/server/__tests__/h5-access-auth.test.ts, src/server/middleware/cors.test.ts
Not-tested: h5-access-auth runtime assertions because this checkout is missing zod for haha-oauth imports
2026-05-12 14:33:58 +08:00

101 lines
2.2 KiB
TypeScript

/**
* CORS middleware for desktop and temporary open H5 access.
*/
import { isLoopbackHost } from '../h5AccessPolicy.js'
export function corsHeaders(origin?: string | null): Record<string, string> {
const allowedOrigin = origin || 'http://localhost:3000'
return {
'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
Vary: 'Origin',
}
}
function baseCorsHeaders(): Record<string, string> {
return {
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
Vary: 'Origin',
}
}
export type CorsResolution = {
allowed: boolean
rejected: boolean
headers: Record<string, string>
}
export type CorsResolutionOptions = {
h5Enabled?: boolean
isOriginAllowed?: (origin: string) => Promise<boolean>
}
const LOCAL_ORIGINS = new Set([
'http://tauri.localhost',
'https://tauri.localhost',
'tauri://localhost',
])
function isLocalOrigin(origin?: string | null): boolean {
if (!origin) {
return true
}
if (LOCAL_ORIGINS.has(origin)) {
return true
}
try {
return isLoopbackHost(new URL(origin).hostname)
} catch {
return false
}
}
export async function resolveCors(
origin?: string | null,
requestOrigin?: string | null,
options: CorsResolutionOptions = {},
): Promise<CorsResolution> {
if (!origin) {
return {
allowed: true,
rejected: false,
headers: corsHeaders(origin),
}
}
if (!options.h5Enabled || isLocalOrigin(origin) || origin === requestOrigin) {
return {
allowed: true,
rejected: false,
headers: {
...baseCorsHeaders(),
'Access-Control-Allow-Origin': origin,
},
}
}
if (options.isOriginAllowed && await options.isOriginAllowed(origin)) {
return {
allowed: true,
rejected: false,
headers: {
...baseCorsHeaders(),
'Access-Control-Allow-Origin': origin,
},
}
}
return {
allowed: false,
rejected: true,
headers: baseCorsHeaders(),
}
}