mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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
101 lines
2.2 KiB
TypeScript
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(),
|
|
}
|
|
}
|