mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day minimum-release-age supply-chain guard; nothing here needs it). The bump is a no-op on the existing config. Enable 3 error rules (backlog autofixed to zero in this commit) and 4 warn rules (surface signal without gating CI): error (autofixed, behavior-preserving): - unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:) - typescript/no-import-type-side-effects (~36: all-inline-type -> import type) - unicorn/no-array-reverse (19: copy-then-reverse -> toReversed) warn (real signal, current fires are test-only/correct): - unicorn/no-array-fill-with-reference-type (aliasing footgun guard) - typescript/no-unsafe-function-type (bans bare Function type) - unicorn/prefer-array-flat-map (map().flat() -> flatMap()) - unicorn/prefer-regexp-test (.match() in bool ctx -> .test()) mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix ran from root and covered mobile/ too. Verification (all green): oxlint 0 errors (root+mobile+aux configs), oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed, builds (electron-vite + web + cli) succeed. node: rewrites confirmed to skip embedded SSH/CLI string payloads (AST-only); all toReversed sites verified to operate on fresh copies or write-once locals. * chore(lint): bump mobile oxlint to 1.71 so inherited rules parse mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile && oxlint' failed to parse the new rule. Bump mobile to match root (1.71). Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit pass, vitest 978 passed / 0 failed. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
140 lines
3.9 KiB
TypeScript
140 lines
3.9 KiB
TypeScript
import { createReadStream } from 'node:fs'
|
|
import { stat } from 'node:fs/promises'
|
|
import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http'
|
|
import { extname, isAbsolute, posix, relative, resolve } from 'node:path'
|
|
|
|
const STATIC_WEB_ALLOWED_PATHS = new Set(['/web-index.html'])
|
|
const STATIC_WEB_ALLOWED_PREFIXES = ['/assets/']
|
|
const STATIC_WEB_CONTENT_TYPES = new Map([
|
|
['.css', 'text/css; charset=utf-8'],
|
|
['.html', 'text/html; charset=utf-8'],
|
|
['.js', 'text/javascript; charset=utf-8'],
|
|
['.json', 'application/json; charset=utf-8'],
|
|
['.png', 'image/png'],
|
|
['.svg', 'image/svg+xml; charset=utf-8'],
|
|
['.wasm', 'application/wasm'],
|
|
['.webp', 'image/webp'],
|
|
['.woff2', 'font/woff2']
|
|
])
|
|
|
|
export function createStaticWebClientHandler(staticRoot: string): RequestListener {
|
|
const resolvedRoot = resolve(staticRoot)
|
|
return (request, response) => {
|
|
void handleStaticRequest(resolvedRoot, request, response)
|
|
}
|
|
}
|
|
|
|
async function handleStaticRequest(
|
|
staticRoot: string,
|
|
request: IncomingMessage,
|
|
response: ServerResponse
|
|
): Promise<void> {
|
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
response.setHeader('Allow', 'GET, HEAD')
|
|
writeHttpStatus(response, 405)
|
|
return
|
|
}
|
|
|
|
const pathname = parseStaticPathname(request.url)
|
|
if (!pathname) {
|
|
writeHttpStatus(response, 400)
|
|
return
|
|
}
|
|
if (!isAllowedStaticWebPath(pathname)) {
|
|
writeHttpStatus(response, 404)
|
|
return
|
|
}
|
|
|
|
const absolutePath = resolve(staticRoot, pathname.slice(1))
|
|
const relativePath = relative(staticRoot, absolutePath)
|
|
if (relativePath === '' || relativePath.startsWith('..') || isAbsolute(relativePath)) {
|
|
writeHttpStatus(response, 404)
|
|
return
|
|
}
|
|
|
|
let fileStat
|
|
try {
|
|
fileStat = await stat(absolutePath)
|
|
} catch {
|
|
writeHttpStatus(response, 404)
|
|
return
|
|
}
|
|
if (!fileStat.isFile()) {
|
|
writeHttpStatus(response, 404)
|
|
return
|
|
}
|
|
|
|
response.statusCode = 200
|
|
response.setHeader(
|
|
'Content-Type',
|
|
STATIC_WEB_CONTENT_TYPES.get(extname(absolutePath)) ?? 'application/octet-stream'
|
|
)
|
|
response.setHeader('Content-Length', fileStat.size)
|
|
response.setHeader(
|
|
'Cache-Control',
|
|
pathname.startsWith('/assets/') ? 'public, max-age=31536000, immutable' : 'no-cache'
|
|
)
|
|
if (request.method === 'HEAD') {
|
|
response.end()
|
|
return
|
|
}
|
|
|
|
const stream = createReadStream(absolutePath)
|
|
stream.on('error', () => {
|
|
if (!response.headersSent) {
|
|
writeHttpStatus(response, 500)
|
|
return
|
|
}
|
|
response.destroy()
|
|
})
|
|
stream.pipe(response)
|
|
}
|
|
|
|
function parseStaticPathname(rawUrl: string | undefined): string | null {
|
|
if (!rawUrl) {
|
|
return '/web-index.html'
|
|
}
|
|
let pathname: string
|
|
try {
|
|
pathname = decodeURIComponent(new URL(rawUrl, 'http://127.0.0.1').pathname)
|
|
} catch {
|
|
return null
|
|
}
|
|
if (pathname === '/' || pathname === '/index.html') {
|
|
return '/web-index.html'
|
|
}
|
|
if (pathname.includes('\0') || pathname.includes('\\') || pathname.split('/').includes('..')) {
|
|
return null
|
|
}
|
|
if (posix.normalize(pathname) !== pathname) {
|
|
return null
|
|
}
|
|
return mapProxyPrefixedStaticPathname(pathname)
|
|
}
|
|
|
|
function mapProxyPrefixedStaticPathname(pathname: string): string {
|
|
if (pathname === '/web-index.html' || pathname.endsWith('/web-index.html')) {
|
|
return '/web-index.html'
|
|
}
|
|
const assetMarker = '/assets/'
|
|
const assetIndex = pathname.indexOf(assetMarker)
|
|
if (assetIndex !== -1) {
|
|
// Why: reverse proxies may forward the external path prefix through to
|
|
// Orca. Only the bundled /assets subtree is served after the prefix.
|
|
return pathname.slice(assetIndex)
|
|
}
|
|
return pathname
|
|
}
|
|
|
|
function isAllowedStaticWebPath(pathname: string): boolean {
|
|
return (
|
|
STATIC_WEB_ALLOWED_PATHS.has(pathname) ||
|
|
STATIC_WEB_ALLOWED_PREFIXES.some((prefix) => pathname.startsWith(prefix))
|
|
)
|
|
}
|
|
|
|
function writeHttpStatus(response: ServerResponse, statusCode: number): void {
|
|
response.statusCode = statusCode
|
|
response.end()
|
|
}
|