style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,17 +1,17 @@
import { createHash, timingSafeEqual } from "node:crypto";
import { config } from "../config";
import { createHash, timingSafeEqual } from 'node:crypto'
import { config } from '../config'
function sha256(value: string): Buffer {
return createHash("sha256").update(value).digest();
return createHash('sha256').update(value).digest()
}
/** Validate a raw API key token string */
export function validateApiKey(token: string | undefined): boolean {
if (!token) return false;
const tokenHash = sha256(token);
return config.apiKeys.some((key) => timingSafeEqual(tokenHash, sha256(key)));
if (!token) return false
const tokenHash = sha256(token)
return config.apiKeys.some(key => timingSafeEqual(tokenHash, sha256(key)))
}
export function hashApiKey(key: string): string {
return createHash("sha256").update(key).digest("hex");
return createHash('sha256').update(key).digest('hex')
}

View File

@@ -1,34 +1,34 @@
import { config } from "../config";
import { config } from '../config'
function originFromUrl(rawUrl: string): string | undefined {
try {
return new URL(rawUrl).origin;
return new URL(rawUrl).origin
} catch {
return undefined;
return undefined
}
}
export function getAllowedWebCorsOrigins(): string[] {
const origins = new Set<string>(config.webCorsOrigins);
const origins = new Set<string>(config.webCorsOrigins)
const baseOrigin = config.baseUrl ? originFromUrl(config.baseUrl) : undefined;
const baseOrigin = config.baseUrl ? originFromUrl(config.baseUrl) : undefined
if (baseOrigin) {
origins.add(baseOrigin);
origins.add(baseOrigin)
}
origins.add(`http://localhost:${config.port}`);
origins.add(`http://127.0.0.1:${config.port}`);
origins.add(`http://localhost:${config.port}`)
origins.add(`http://127.0.0.1:${config.port}`)
return [...origins];
return [...origins]
}
export function resolveWebCorsOrigin(origin: string): string | undefined {
return getAllowedWebCorsOrigins().includes(origin) ? origin : undefined;
return getAllowedWebCorsOrigins().includes(origin) ? origin : undefined
}
export const webCorsOptions = {
origin: resolveWebCorsOrigin,
allowHeaders: ["Authorization", "Content-Type", "X-UUID"],
allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ['Authorization', 'Content-Type', 'X-UUID'],
allowMethods: ['GET', 'POST', 'OPTIONS'],
credentials: false,
};
}

View File

@@ -1,4 +1,4 @@
import { createHmac, timingSafeEqual } from "node:crypto";
import { createHmac, timingSafeEqual } from 'node:crypto'
/**
* Lightweight JWT implementation using HMAC-SHA256.
@@ -9,29 +9,29 @@ import { createHmac, timingSafeEqual } from "node:crypto";
*/
interface JwtPayload {
session_id: string;
role: string;
iat: number;
exp: number;
session_id: string
role: string
iat: number
exp: number
}
function base64url(data: string | Buffer): string {
return Buffer.from(data as unknown as ArrayLike<number>)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}
function base64urlDecode(str: string): string {
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
return Buffer.from(padded, "base64").toString("utf-8");
const padded = str.replace(/-/g, '+').replace(/_/g, '/')
return Buffer.from(padded, 'base64').toString('utf-8')
}
function getSigningKey(): string {
const key = process.env.RCS_API_KEYS?.split(",").filter(Boolean)[0];
if (!key) throw new Error("No API key configured for JWT signing");
return key;
const key = process.env.RCS_API_KEYS?.split(',').filter(Boolean)[0]
if (!key) throw new Error('No API key configured for JWT signing')
return key
}
/** Generate a JWT for worker authentication. */
@@ -39,23 +39,23 @@ export function generateWorkerJwt(
sessionId: string,
expiresInSeconds: number,
): string {
const header = { alg: "HS256", typ: "JWT" };
const header = { alg: 'HS256', typ: 'JWT' }
const payload: JwtPayload = {
session_id: sessionId,
role: "worker",
role: 'worker',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + expiresInSeconds,
};
}
const headerB64 = base64url(JSON.stringify(header));
const payloadB64 = base64url(JSON.stringify(payload));
const signingInput = `${headerB64}.${payloadB64}`;
const headerB64 = base64url(JSON.stringify(header))
const payloadB64 = base64url(JSON.stringify(payload))
const signingInput = `${headerB64}.${payloadB64}`
const signature = createHmac("sha256", getSigningKey())
const signature = createHmac('sha256', getSigningKey())
.update(signingInput)
.digest();
.digest()
return `${signingInput}.${base64url(signature)}`;
return `${signingInput}.${base64url(signature)}`
}
/**
@@ -63,30 +63,30 @@ export function generateWorkerJwt(
* Uses timing-safe comparison to prevent timing attacks.
*/
export function verifyWorkerJwt(token: string): JwtPayload | null {
const parts = token.split(".");
if (parts.length !== 3) return null;
const parts = token.split('.')
if (parts.length !== 3) return null
const [headerB64, payloadB64, signatureB64] = parts;
const [headerB64, payloadB64, signatureB64] = parts
// Verify signature
const signingInput = `${headerB64}.${payloadB64}`;
const expectedSig = createHmac("sha256", getSigningKey())
const signingInput = `${headerB64}.${payloadB64}`
const expectedSig = createHmac('sha256', getSigningKey())
.update(signingInput)
.digest();
.digest()
const actualSig = Buffer.from(
signatureB64.replace(/-/g, "+").replace(/_/g, "/"),
"base64",
);
signatureB64.replace(/-/g, '+').replace(/_/g, '/'),
'base64',
)
if (expectedSig.length !== actualSig.length) return null;
if (!timingSafeEqual(expectedSig, actualSig)) return null;
if (expectedSig.length !== actualSig.length) return null
if (!timingSafeEqual(expectedSig, actualSig)) return null
// Decode payload
try {
const payload: JwtPayload = JSON.parse(base64urlDecode(payloadB64));
if (payload.exp < Math.floor(Date.now() / 1000)) return null;
return payload;
const payload: JwtPayload = JSON.parse(base64urlDecode(payloadB64))
if (payload.exp < Math.floor(Date.now() / 1000)) return null
return payload
} catch {
return null;
return null
}
}

View File

@@ -1,51 +1,58 @@
import type { Context, Next } from "hono";
import { validateApiKey } from "./api-key";
import { verifyWorkerJwt } from "./jwt";
import { resolveToken } from "./token";
import type { Context, Next } from 'hono'
import { validateApiKey } from './api-key'
import { verifyWorkerJwt } from './jwt'
import { resolveToken } from './token'
const WS_AUTH_PROTOCOL_PREFIX = "rcs.auth.";
const WS_AUTH_PROTOCOL_PREFIX = 'rcs.auth.'
/** Encode a bearer token for WebSocket clients that cannot send auth headers. */
export function encodeWebSocketAuthProtocol(token: string): string {
return `${WS_AUTH_PROTOCOL_PREFIX}${Buffer.from(token, "utf8").toString("base64url")}`;
return `${WS_AUTH_PROTOCOL_PREFIX}${Buffer.from(token, 'utf8').toString('base64url')}`
}
function decodeWebSocketAuthProtocol(protocolHeader: string | undefined): string | undefined {
function decodeWebSocketAuthProtocol(
protocolHeader: string | undefined,
): string | undefined {
if (!protocolHeader) {
return undefined;
return undefined
}
for (const protocol of protocolHeader.split(",")) {
const trimmed = protocol.trim();
for (const protocol of protocolHeader.split(',')) {
const trimmed = protocol.trim()
if (!trimmed.startsWith(WS_AUTH_PROTOCOL_PREFIX)) {
continue;
continue
}
const encoded = trimmed.slice(WS_AUTH_PROTOCOL_PREFIX.length);
const encoded = trimmed.slice(WS_AUTH_PROTOCOL_PREFIX.length)
if (!encoded) {
return undefined;
return undefined
}
try {
const token = Buffer.from(encoded, "base64url").toString("utf8");
return token.length > 0 ? token : undefined;
const token = Buffer.from(encoded, 'base64url').toString('utf8')
return token.length > 0 ? token : undefined
} catch {
return undefined;
return undefined
}
}
return undefined;
return undefined
}
/** Extract a Bearer token from the Authorization header only. */
export function extractBearerToken(c: Context): string | undefined {
const authHeader = c.req.header("Authorization");
return authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : undefined;
const authHeader = c.req.header('Authorization')
return authHeader?.startsWith('Bearer ')
? authHeader.slice('Bearer '.length)
: undefined
}
/** Extract auth for WebSocket upgrades without putting secrets in query strings. */
export function extractWebSocketAuthToken(c: Context): string | undefined {
return extractBearerToken(c) ?? decodeWebSocketAuthProtocol(c.req.header("Sec-WebSocket-Protocol"));
return (
extractBearerToken(c) ??
decodeWebSocketAuthProtocol(c.req.header('Sec-WebSocket-Protocol'))
)
}
/**
@@ -55,28 +62,33 @@ export function extractWebSocketAuthToken(c: Context): string | undefined {
* 2. **API Key mode** (CLI bridge): Valid API key + X-Username header → username injected
*/
export async function apiKeyAuth(c: Context, next: Next) {
const token = extractBearerToken(c);
const token = extractBearerToken(c)
// Try token authentication (Web UI)
const tokenUsername = resolveToken(token);
const tokenUsername = resolveToken(token)
if (tokenUsername) {
c.set("username", tokenUsername);
await next();
return;
c.set('username', tokenUsername)
await next()
return
}
// Try API Key authentication (CLI bridge)
if (validateApiKey(token)) {
// Extract username from X-Username header or ?username= query param
const username = c.req.header("X-Username") || c.req.query("username");
const username = c.req.header('X-Username') || c.req.query('username')
if (username) {
c.set("username", username);
c.set('username', username)
}
await next();
return;
await next()
return
}
return c.json({ error: { type: "unauthorized", message: "Invalid or missing auth token" } }, 401);
return c.json(
{
error: { type: 'unauthorized', message: 'Invalid or missing auth token' },
},
401,
)
}
/**
@@ -87,43 +99,57 @@ export async function apiKeyAuth(c: Context, next: Next) {
* downstream handlers to inspect session_id if needed.
*/
export async function sessionIngressAuth(c: Context, next: Next) {
const token = extractWebSocketAuthToken(c);
const token = extractWebSocketAuthToken(c)
if (!token) {
return c.json({ error: { type: "unauthorized", message: "Missing auth token" } }, 401);
return c.json(
{ error: { type: 'unauthorized', message: 'Missing auth token' } },
401,
)
}
// Try API key first (backward compatible)
if (validateApiKey(token)) {
await next();
return;
await next()
return
}
// Try JWT verification — validate session_id matches route param
const payload = verifyWorkerJwt(token);
const payload = verifyWorkerJwt(token)
if (payload) {
const routeSessionId = c.req.param("id") || c.req.param("sessionId");
const routeSessionId = c.req.param('id') || c.req.param('sessionId')
if (routeSessionId && payload.session_id !== routeSessionId) {
return c.json({ error: { type: "forbidden", message: "JWT session_id does not match target session" } }, 403);
return c.json(
{
error: {
type: 'forbidden',
message: 'JWT session_id does not match target session',
},
},
403,
)
}
c.set("jwtPayload", payload);
await next();
return;
c.set('jwtPayload', payload)
await next()
return
}
return c.json({ error: { type: "unauthorized", message: "Invalid API key or JWT" } }, 401);
return c.json(
{ error: { type: 'unauthorized', message: 'Invalid API key or JWT' } },
401,
)
}
/** Accept CLI headers but don't validate them */
export async function acceptCliHeaders(c: Context, next: Next) {
await next();
await next()
}
/**
* Extract UUID from request — query param ?uuid= or header X-UUID
*/
export function getUuidFromRequest(c: Context): string | undefined {
return c.req.query("uuid") || c.req.header("X-UUID");
return c.req.query('uuid') || c.req.header('X-UUID')
}
/**
@@ -132,20 +158,23 @@ export function getUuidFromRequest(c: Context): string | undefined {
*/
export async function uuidAuth(c: Context, next: Next) {
// Try API key auth via Authorization header
const bearer = extractBearerToken(c);
const bearer = extractBearerToken(c)
if (bearer && validateApiKey(bearer)) {
// Valid API key — generate a stable UUID from the key for downstream use
const uuid = getUuidFromRequest(c);
c.set("uuid", uuid || bearer);
await next();
return;
const uuid = getUuidFromRequest(c)
c.set('uuid', uuid || bearer)
await next()
return
}
// Fall back to UUID auth
const uuid = getUuidFromRequest(c);
const uuid = getUuidFromRequest(c)
if (!uuid) {
return c.json({ error: { type: "unauthorized", message: "Missing UUID" } }, 401);
return c.json(
{ error: { type: 'unauthorized', message: 'Missing UUID' } },
401,
)
}
c.set("uuid", uuid);
await next();
c.set('uuid', uuid)
await next()
}

View File

@@ -1,24 +1,27 @@
import { storeCreateToken, storeGetUserByToken } from "../store";
import { storeCreateToken, storeGetUserByToken } from '../store'
let tokenCounter = 0;
let tokenCounter = 0
/** Generate a random session token and associate it with a user */
export function issueToken(username: string): { token: string; expires_in: number } {
export function issueToken(username: string): {
token: string
expires_in: number
} {
// Use crypto.getRandomValues for uniqueness
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
const hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
const token = `rct_${tokenCounter++}_${hex}`;
storeCreateToken(username, token);
return { token, expires_in: 86400 };
.map(b => b.toString(16).padStart(2, '0'))
.join('')
const token = `rct_${tokenCounter++}_${hex}`
storeCreateToken(username, token)
return { token, expires_in: 86400 }
}
/** Resolve a token to a username. Returns null if invalid. */
export function resolveToken(token: string | undefined): string | null {
if (!token) return null;
const entry = storeGetUserByToken(token);
if (!entry) return null;
return entry.username;
if (!token) return null
const entry = storeGetUserByToken(token)
if (!entry) return null
return entry.username
}