From 955c380c4377667f8c5ee8c620cbd47168c77154 Mon Sep 17 00:00:00 2001 From: Bob Webb Date: Mon, 2 Mar 2026 13:24:39 +0000 Subject: [PATCH 1/4] feat(server): replace Node http with Hono framework Migrate from Node's native http.createServer() with custom regex-based routing to Hono, providing proper routing, middleware patterns, and cleaner request/response handling via @hono/node-server. Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 23 ++ package.json | 2 + server/cors.ts | 20 +- server/http_helpers.ts | 27 -- server/index.ts | 78 +++-- server/rate_limit.ts | 29 +- server/routes.ts | 632 ++++++++++++++++--------------------- server/security_headers.ts | 15 +- server/session.ts | 75 ++--- server/static_files.ts | 45 ++- 10 files changed, 421 insertions(+), 525 deletions(-) diff --git a/package-lock.json b/package-lock.json index fedca0d..ab1b295 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@fontsource-variable/inter": "^5.2.8", "@fontsource/source-code-pro": "^5.2.7", + "@hono/node-server": "^1.19.9", "@preact/preset-vite": "^2.10.3", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-context-menu": "^2.2.16", @@ -20,6 +21,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "dompurify": "^3.3.1", "dotenv": "^17.3.1", + "hono": "^4.12.3", "lucide-react": "^0.575.0", "marked": "^17.0.3", "preact": "^10.28.3" @@ -961,6 +963,18 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2579,6 +2593,15 @@ "he": "bin/he" } }, + "node_modules/hono": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", + "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 7bc7184..0e89334 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@fontsource-variable/inter": "^5.2.8", "@fontsource/source-code-pro": "^5.2.7", + "@hono/node-server": "^1.19.9", "@preact/preset-vite": "^2.10.3", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-context-menu": "^2.2.16", @@ -32,6 +33,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "dompurify": "^3.3.1", "dotenv": "^17.3.1", + "hono": "^4.12.3", "lucide-react": "^0.575.0", "marked": "^17.0.3", "preact": "^10.28.3" diff --git a/server/cors.ts b/server/cors.ts index 41f8394..6162559 100644 --- a/server/cors.ts +++ b/server/cors.ts @@ -1,14 +1,10 @@ -import type http from 'node:http'; +import { cors } from 'hono/cors'; import { ALLOWED_ORIGINS } from './config'; -export function applyCors(req: http.IncomingMessage, res: http.ServerResponse): void { - const origin = req.headers.origin; - res.setHeader('Vary', 'Origin'); - if (origin && ALLOWED_ORIGINS.has(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin); - res.setHeader('Access-Control-Allow-Credentials', 'true'); - res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization'); - res.setHeader('Access-Control-Max-Age', '600'); - } -} +export const corsMiddleware = cors({ + origin: (origin) => (ALLOWED_ORIGINS.has(origin) ? origin : ''), + credentials: true, + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization'], + maxAge: 600, +}); diff --git a/server/http_helpers.ts b/server/http_helpers.ts index 57a827f..86d9a73 100644 --- a/server/http_helpers.ts +++ b/server/http_helpers.ts @@ -1,32 +1,5 @@ -import type http from 'node:http'; -import { MAX_BODY_BYTES } from './config'; import { ClientError } from './errors'; -export function json(res: http.ServerResponse, statusCode: number, data: unknown): void { - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(data)); -} - -export async function readJson(req: http.IncomingMessage): Promise | null> { - const chunks: Buffer[] = []; - let totalBytes = 0; - - for await (const chunk of req) { - totalBytes += (chunk as Buffer).length; - if (totalBytes > MAX_BODY_BYTES) throw new ClientError('Request body too large'); - chunks.push(chunk as Buffer); - } - - const raw = Buffer.concat(chunks).toString('utf8'); - if (!raw) return null; - - try { - return JSON.parse(raw) as Record; - } catch { - throw new ClientError('Invalid JSON body'); - } -} - export function requireEnv(name: string): string { const value = process.env[name]; if (!value) throw new Error(`Missing env var: ${name}`); diff --git a/server/index.ts b/server/index.ts index 118d0a2..73d0b52 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,65 +1,59 @@ import './env'; -import http from 'node:http'; +import { serve } from '@hono/node-server'; +import { Hono } from 'hono'; import { PORT } from './config'; -import { applyCors } from './cors'; +import { corsMiddleware } from './cors'; import { ClientError } from './errors'; import { startGistCacheCleanup } from './gist_cache'; import { startInstallationTokenCacheCleanup } from './github_client'; -import { json } from './http_helpers'; -import { startRateLimitCleanup } from './rate_limit'; -import { handleApiRequest } from './routes'; -import { applySecurityHeaders } from './security_headers'; +import { startRateLimitCleanup, rateLimitMiddleware } from './rate_limit'; +import { api } from './routes'; +import { securityHeaders } from './security_headers'; import { startSessionCleanup } from './session'; -import { serveStatic } from './static_files'; +import { serveStaticMiddleware } from './static_files'; startInstallationTokenCacheCleanup(); startGistCacheCleanup(); startRateLimitCleanup(); startSessionCleanup(); -const server = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => { - try { - applySecurityHeaders(res); - applyCors(req, res); +const app = new Hono(); - if (req.method === 'OPTIONS') { - res.end(); - return; - } +// Global middleware +app.use('*', securityHeaders); +app.use('*', corsMiddleware); - const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`); - const pathname = url.pathname; +// Rate limiting on API routes +app.use('/api/*', rateLimitMiddleware); - const handledApi = await handleApiRequest(req, res, url, pathname); - if (handledApi) return; +// API routes +app.route('/api', api); - if (req.method === 'GET') { - if (await serveStatic(res, pathname)) return; - if (await serveStatic(res, '/index.html')) return; - } +// Static files + SPA fallback +app.use('*', serveStaticMiddleware); - json(res, 404, { error: 'Not found' }); - } catch (err) { - if (err instanceof ClientError) { - json(res, err.statusCode, { error: err.message }); - return; - } - - console.error('Unhandled server error:', err); - json(res, 500, { error: 'Internal server error' }); +// Error handler +app.onError((err, c) => { + if (err instanceof ClientError) { + return c.json({ error: err.message }, err.statusCode as 400); } + console.error('Unhandled server error:', err); + return c.json({ error: 'Internal server error' }, 500); }); -server.listen(PORT, '0.0.0.0', () => { - const configured = Boolean( - process.env.GITHUB_APP_ID && - (process.env.GITHUB_APP_PRIVATE_KEY || process.env.GITHUB_APP_PRIVATE_KEY_PATH) && - process.env.GITHUB_APP_SLUG && - process.env.GITHUB_CLIENT_ID && - process.env.GITHUB_CLIENT_SECRET, - ); - console.log(`GitHub App auth server listening on http://0.0.0.0:${PORT} (configured=${configured})`); -}); +const server = serve( + { fetch: app.fetch, port: PORT, hostname: '0.0.0.0' }, + () => { + const configured = Boolean( + process.env.GITHUB_APP_ID && + (process.env.GITHUB_APP_PRIVATE_KEY || process.env.GITHUB_APP_PRIVATE_KEY_PATH) && + process.env.GITHUB_APP_SLUG && + process.env.GITHUB_CLIENT_ID && + process.env.GITHUB_CLIENT_SECRET, + ); + console.log(`GitHub App auth server listening on http://0.0.0.0:${PORT} (configured=${configured})`); + }, +); function gracefulShutdown(signal: string): void { console.log(`\n${signal} received, shutting down gracefully...`); diff --git a/server/rate_limit.ts b/server/rate_limit.ts index 63317bc..8ea7295 100644 --- a/server/rate_limit.ts +++ b/server/rate_limit.ts @@ -1,5 +1,5 @@ -import type http from 'node:http'; -import { json } from './http_helpers'; +import { getConnInfo } from '@hono/node-server/conninfo'; +import { createMiddleware } from 'hono/factory'; import type { RateLimitEntry } from './types'; const RATE_LIMIT_MAX = 30; @@ -20,24 +20,24 @@ export function startRateLimitCleanup(): void { ).unref(); } -function getClientIp(req: http.IncomingMessage): string { - const forwarded = req.headers['x-forwarded-for']; - if (typeof forwarded === 'string') { +function getClientIp(c: Parameters[0]>[0]): string { + const forwarded = c.req.header('x-forwarded-for'); + if (forwarded) { const first = forwarded.split(',')[0].trim(); if (first) return first; } - return req.socket.remoteAddress || 'unknown'; + const info = getConnInfo(c); + return info.remote.address || 'unknown'; } -export function checkRateLimit(req: http.IncomingMessage, res: http.ServerResponse): boolean { - const ip = getClientIp(req); +export const rateLimitMiddleware = createMiddleware(async (c, next) => { + const ip = getClientIp(c); const now = Date.now(); let entry = rateLimitWindows.get(ip); if (!entry || now >= entry.resetAtMs) { if (!entry && rateLimitWindows.size >= MAX_RATE_LIMIT_ENTRIES) { - json(res, 429, { error: 'Too many requests' }); - return false; + return c.json({ error: 'Too many requests' }, 429); } entry = { count: 0, resetAtMs: now + RATE_LIMIT_WINDOW_MS }; rateLimitWindows.set(ip, entry); @@ -46,10 +46,9 @@ export function checkRateLimit(req: http.IncomingMessage, res: http.ServerRespon entry.count++; if (entry.count > RATE_LIMIT_MAX) { const retryAfter = Math.ceil((entry.resetAtMs - now) / 1000); - res.setHeader('Retry-After', String(retryAfter)); - json(res, 429, { error: 'Too many requests' }); - return false; + c.header('Retry-After', String(retryAfter)); + return c.json({ error: 'Too many requests' }, 429); } - return true; -} + await next(); +}); diff --git a/server/routes.ts b/server/routes.ts index 4221df4..1cf9816 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1,10 +1,10 @@ -import type http from 'node:http'; +import { Hono } from 'hono'; +import type { Context } from 'hono'; import { APP_URL, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_FETCH_TIMEOUT_MS, GITHUB_TOKEN } from './config'; import { ClientError } from './errors'; import { getGistCacheEntry, isFresh, markRevalidated, setGistCacheEntry } from './gist_cache'; import { createAppJwt, encodePathPreserveSlashes, githubFetchWithInstallationToken } from './github_client'; -import { json, readJson, requireEnv, requireString } from './http_helpers'; -import { checkRateLimit } from './rate_limit'; +import { requireEnv, requireString } from './http_helpers'; import { clearRememberedInstallationForUser, consumeOAuthState, @@ -18,22 +18,6 @@ import { } from './session'; import type { Session } from './types'; -interface RouteContext { - req: http.IncomingMessage; - res: http.ServerResponse; - url: URL; - pathname: string; - match: RegExpMatchArray; -} - -type RouteHandler = (ctx: RouteContext) => Promise; - -interface RouteDef { - method: string; - pattern: RegExp; - handler: RouteHandler; -} - type OAuthTokenResponse = { access_token?: string; token_type?: string; @@ -45,21 +29,15 @@ type GitHubApiError = { message?: string; }; -function redirect(res: http.ServerResponse, location: string): void { - res.statusCode = 302; - res.setHeader('Location', location); - res.end(); -} - -function requestBaseUrl(req: http.IncomingMessage): string { - const proto = req.headers['x-forwarded-proto']; - const scheme = typeof proto === 'string' ? proto.split(',')[0].trim() : 'http'; - const host = req.headers.host ?? 'localhost'; +function requestBaseUrl(c: Context): string { + const proto = c.req.header('x-forwarded-proto'); + const scheme = proto ? proto.split(',')[0].trim() : 'http'; + const host = c.req.header('host') ?? 'localhost'; return `${scheme}://${host}`; } -function oauthBaseUrl(req: http.IncomingMessage): string { - return APP_URL || requestBaseUrl(req); +function oauthBaseUrl(c: Context): string { + return APP_URL || requestBaseUrl(c); } function normalizeReturnTo(raw: string | null): string { @@ -69,14 +47,14 @@ function normalizeReturnTo(raw: string | null): string { return raw; } -function requireAuthSession(ctx: RouteContext): Session { - const session = getSession(ctx.req); +function requireAuthSession(c: Context): Session { + const session = getSession(c); if (!session) throw new ClientError('Unauthorized', 401); return session; } -function requireMatchedInstallation(ctx: RouteContext, session: Session, matchIndex: number): string { - const installationId = ctx.match[matchIndex]; +function requireMatchedInstallation(c: Context, session: Session, paramName: string): string { + const installationId = c.req.param(paramName); if (!session.installationId || session.installationId !== installationId) { throw new ClientError('Forbidden', 403); } @@ -100,20 +78,19 @@ async function githubFetchWithUserToken(session: Session, path: string, init: Re } async function proxyGitHubJson( - ctx: RouteContext, + c: Context, session: Session, path: string, init: RequestInit = {}, -): Promise { +): Promise { const ghRes = await githubFetchWithUserToken(session, path, init); const data = (await ghRes.json().catch(() => null)) as unknown; if (!ghRes.ok) { const err = data as GitHubApiError | null; if (ghRes.status === 401) throw new ClientError('Unauthorized', 401); - json(ctx.res, ghRes.status, { error: err?.message ?? 'GitHub API error' }); - return; + return c.json({ error: err?.message ?? 'GitHub API error' }, ghRes.status as 400); } - json(ctx.res, 200, data); + return c.json(data, 200); } async function fetchPublicGitHub(path: string, init: RequestInit = {}): Promise { @@ -131,46 +108,59 @@ async function fetchPublicGitHub(path: string, init: RequestInit = {}): Promise< }); } -async function handleAuthStart(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; +type GitTreeEntry = { path: string; type: string; sha: string }; + +function mdFilesFromTree(tree: GitTreeEntry[]): { name: string; path: string; sha: string }[] { + const files: { name: string; path: string; sha: string }[] = []; + for (const entry of tree) { + if (entry.type !== 'blob' || !entry.path.toLowerCase().endsWith('.md')) continue; + const slash = entry.path.lastIndexOf('/'); + files.push({ name: slash === -1 ? entry.path : entry.path.slice(slash + 1), path: entry.path, sha: entry.sha }); + } + files.sort((a, b) => a.path.localeCompare(b.path)); + return files; +} + +// --- Route handlers --- + +const api = new Hono(); + +// Auth routes + +api.get('/auth/github/start', async (c) => { if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET) { - json(ctx.res, 503, { error: 'GitHub OAuth is not configured' }); - return; + return c.json({ error: 'GitHub OAuth is not configured' }, 503); } - const returnTo = normalizeReturnTo(ctx.url.searchParams.get('return_to')); + const returnTo = normalizeReturnTo(c.req.query('return_to') ?? null); const state = createOAuthState(returnTo); - const redirectUri = `${oauthBaseUrl(ctx.req)}/api/auth/github/callback`; + const redirectUri = `${oauthBaseUrl(c)}/api/auth/github/callback`; console.log(`[auth] OAuth start: redirect_uri=${redirectUri}, return_to=${returnTo}`); const authUrl = new URL('https://github.com/login/oauth/authorize'); authUrl.searchParams.set('client_id', GITHUB_CLIENT_ID); authUrl.searchParams.set('redirect_uri', redirectUri); authUrl.searchParams.set('scope', 'gist read:user'); authUrl.searchParams.set('state', state); - redirect(ctx.res, authUrl.toString()); -} + return c.redirect(authUrl.toString(), 302); +}); -async function handleAuthCallback(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; +api.get('/auth/github/callback', async (c) => { if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET) { - json(ctx.res, 503, { error: 'GitHub OAuth is not configured' }); - return; + return c.json({ error: 'GitHub OAuth is not configured' }, 503); } - const code = ctx.url.searchParams.get('code'); - const state = ctx.url.searchParams.get('state'); + const code = c.req.query('code'); + const state = c.req.query('state'); if (!code || !state) { - json(ctx.res, 400, { error: 'Missing OAuth callback parameters' }); - return; + return c.json({ error: 'Missing OAuth callback parameters' }, 400); } const returnTo = consumeOAuthState(state); if (!returnTo) { - json(ctx.res, 400, { error: 'Invalid or expired OAuth state' }); - return; + return c.json({ error: 'Invalid or expired OAuth state' }, 400); } - const redirectUri = `${oauthBaseUrl(ctx.req)}/api/auth/github/callback`; + const redirectUri = `${oauthBaseUrl(c)}/api/auth/github/callback`; const tokenRes = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, @@ -186,8 +176,10 @@ async function handleAuthCallback(ctx: RouteContext): Promise { const tokenData = (await tokenRes.json().catch(() => null)) as OAuthTokenResponse | null; if (!tokenRes.ok || !tokenData?.access_token) { - json(ctx.res, 502, { error: tokenData?.error_description ?? tokenData?.error ?? 'Failed to exchange OAuth code' }); - return; + return c.json( + { error: tokenData?.error_description ?? tokenData?.error ?? 'Failed to exchange OAuth code' }, + 502, + ); } const userRes = await fetch('https://api.github.com/user', { @@ -200,8 +192,7 @@ async function handleAuthCallback(ctx: RouteContext): Promise { signal: AbortSignal.timeout(GITHUB_FETCH_TIMEOUT_MS), }); if (!userRes.ok) { - json(ctx.res, 502, { error: 'Failed to fetch GitHub user profile' }); - return; + return c.json({ error: 'Failed to fetch GitHub user profile' }, 502); } const ghUser = (await userRes.json()) as { @@ -211,7 +202,7 @@ async function handleAuthCallback(ctx: RouteContext): Promise { name: string | null; }; - const session = createSession(ctx.res, { + const session = createSession(c, { githubUserId: ghUser.id, githubAccessToken: tokenData.access_token, githubLogin: ghUser.login, @@ -221,56 +212,117 @@ async function handleAuthCallback(ctx: RouteContext): Promise { }); console.log(`[auth] Created session for ${ghUser.login} (id=${session.id.slice(0, 8)}…), redirecting to ${returnTo}`); - redirect(ctx.res, returnTo); -} + return c.redirect(returnTo, 302); +}); -async function handleAuthSession(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = getSession(ctx.req); +api.get('/auth/session', (c) => { + const session = getSession(c); if (!session) { - const hasCookie = Boolean(ctx.req.headers.cookie?.includes('input_session_id')); + const cookieHeader = c.req.header('cookie') ?? ''; + const hasCookie = cookieHeader.includes('input_session_id'); console.log(`[auth] Session check: no valid session (cookie present=${hasCookie})`); - json(ctx.res, 200, { authenticated: false }); - return; + return c.json({ authenticated: false }, 200); } - refreshSession(session, ctx.res); - json(ctx.res, 200, { - authenticated: true, - user: { + refreshSession(session, c); + return c.json( + { + authenticated: true, + user: { + login: session.githubLogin, + avatar_url: session.githubAvatarUrl, + name: session.githubName, + }, + installationId: session.installationId, + }, + 200, + ); +}); + +api.post('/auth/logout', (c) => { + destroySession(c); + return c.json({ ok: true }, 200); +}); + +// GitHub user routes + +api.get('/github/user', (c) => { + const session = requireAuthSession(c); + return c.json( + { login: session.githubLogin, avatar_url: session.githubAvatarUrl, name: session.githubName, }, - installationId: session.installationId, + 200, + ); +}); + +// Gist routes (authenticated) + +api.get('/github/gists', async (c) => { + const session = requireAuthSession(c); + const qs = new URLSearchParams(); + const page = c.req.query('page') ?? '1'; + const perPage = c.req.query('per_page') ?? '30'; + qs.set('page', page); + qs.set('per_page', perPage); + return proxyGitHubJson(c, session, `/gists?${qs.toString()}`); +}); + +api.get('/github/gists/:id', async (c) => { + const session = requireAuthSession(c); + return proxyGitHubJson(c, session, `/gists/${encodeURIComponent(c.req.param('id'))}`); +}); + +api.post('/github/gists', async (c) => { + const session = requireAuthSession(c); + const body = await c.req.json(); + return proxyGitHubJson(c, session, '/gists', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body ?? {}), }); -} +}); -async function handleAuthLogout(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - // We only destroy the server-side session; the GitHub access token is not - // revoked. It remains valid until GitHub's own expiry or the user revokes - // it manually at https://github.com/settings/applications. - destroySession(ctx.req, ctx.res); - json(ctx.res, 200, { ok: true }); -} +api.patch('/github/gists/:id', async (c) => { + const session = requireAuthSession(c); + const body = await c.req.json(); + return proxyGitHubJson(c, session, `/gists/${encodeURIComponent(c.req.param('id'))}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body ?? {}), + }); +}); + +api.delete('/github/gists/:id', async (c) => { + const session = requireAuthSession(c); + const ghRes = await githubFetchWithUserToken(session, `/gists/${encodeURIComponent(c.req.param('id'))}`, { + method: 'DELETE', + }); + if (!ghRes.ok) { + const data = (await ghRes.json().catch(() => null)) as GitHubApiError | null; + if (ghRes.status === 401) throw new ClientError('Unauthorized', 401); + return c.json({ error: data?.message ?? 'GitHub API error' }, ghRes.status as 400); + } + return c.json({ ok: true }, 200); +}); -async function handleInstallUrl(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; +// GitHub App routes + +api.get('/github-app/install-url', (c) => { const slug = requireEnv('GITHUB_APP_SLUG'); - const state = ctx.url.searchParams.get('state'); + const state = c.req.query('state'); const installUrl = new URL(`https://github.com/apps/${slug}/installations/new`); if (state) installUrl.searchParams.set('state', state); - json(ctx.res, 200, { url: installUrl.toString() }); -} + return c.json({ url: installUrl.toString() }, 200); +}); -async function handleCreateSession(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const body = await readJson(ctx.req); +api.post('/github-app/sessions', async (c) => { + const session = requireAuthSession(c); + const body = await c.req.json(); const installationId = body?.installationId; if (!installationId || typeof installationId !== 'string') { - json(ctx.res, 400, { error: 'installationId is required' }); - return; + return c.json({ error: 'installationId is required' }, 400); } const jwt = await createAppJwt(); @@ -287,94 +339,29 @@ async function handleCreateSession(ctx: RouteContext): Promise { if (!ghRes.ok) { clearRememberedInstallationForUser(session.githubUserId); session.installationId = null; - refreshSession(session, ctx.res); - json(ctx.res, 403, { error: 'Invalid installation' }); - return; + refreshSession(session, c); + return c.json({ error: 'Invalid installation' }, 403); } session.installationId = installationId; rememberInstallationForUser(session.githubUserId, installationId); - refreshSession(session, ctx.res); - json(ctx.res, 200, { installationId }); -} + refreshSession(session, c); + return c.json({ installationId }, 200); +}); -async function handleDisconnectInstallation(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); +api.post('/github-app/disconnect', (c) => { + const session = requireAuthSession(c); session.installationId = null; clearRememberedInstallationForUser(session.githubUserId); - refreshSession(session, ctx.res); - json(ctx.res, 200, { ok: true }); -} - -async function handleGitHubUser(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - json(ctx.res, 200, { - login: session.githubLogin, - avatar_url: session.githubAvatarUrl, - name: session.githubName, - }); -} - -async function handleListGists(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const qs = new URLSearchParams(); - const page = ctx.url.searchParams.get('page') ?? '1'; - const perPage = ctx.url.searchParams.get('per_page') ?? '30'; - qs.set('page', page); - qs.set('per_page', perPage); - await proxyGitHubJson(ctx, session, `/gists?${qs.toString()}`); -} - -async function handleGetAuthedGist(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - await proxyGitHubJson(ctx, session, `/gists/${encodeURIComponent(ctx.match[1])}`); -} - -async function handleCreateGist(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const body = await readJson(ctx.req); - await proxyGitHubJson(ctx, session, '/gists', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body ?? {}), - }); -} + refreshSession(session, c); + return c.json({ ok: true }, 200); +}); -async function handlePatchGist(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const body = await readJson(ctx.req); - await proxyGitHubJson(ctx, session, `/gists/${encodeURIComponent(ctx.match[1])}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body ?? {}), - }); -} - -async function handleDeleteGist(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const ghRes = await githubFetchWithUserToken(session, `/gists/${encodeURIComponent(ctx.match[1])}`, { - method: 'DELETE', - }); - if (!ghRes.ok) { - const data = (await ghRes.json().catch(() => null)) as GitHubApiError | null; - if (ghRes.status === 401) throw new ClientError('Unauthorized', 401); - json(ctx.res, ghRes.status, { error: data?.message ?? 'GitHub API error' }); - return; - } - json(ctx.res, 200, { ok: true }); -} +// Installation repos -async function handleListRepos(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const installationId = requireMatchedInstallation(ctx, session, 1); +api.get('/github-app/installations/:installationId/repositories', async (c) => { + const session = requireAuthSession(c); + const installationId = requireMatchedInstallation(c, session, 'installationId'); const allRepos: unknown[] = []; const MAX_PAGES = 50; @@ -390,18 +377,19 @@ async function handleListRepos(ctx: RouteContext): Promise { page++; } - json(ctx.res, 200, { total_count: allRepos.length, repositories: allRepos }); -} + return c.json({ total_count: allRepos.length, repositories: allRepos }, 200); +}); + +// Repo contents (authenticated via installation) -async function handleGetContents(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const installationId = requireMatchedInstallation(ctx, session, 1); - const owner = ctx.match[2]; - const repo = ctx.match[3]; +api.get('/github-app/installations/:installationId/repos/:owner/:repo/contents', async (c) => { + const session = requireAuthSession(c); + const installationId = requireMatchedInstallation(c, session, 'installationId'); + const owner = c.req.param('owner'); + const repo = c.req.param('repo'); - const pathParam = ctx.url.searchParams.get('path') ?? ''; - const ref = ctx.url.searchParams.get('ref'); + const pathParam = c.req.query('path') ?? ''; + const ref = c.req.query('ref'); const encodedPath = encodePathPreserveSlashes(pathParam); const ghPath = encodedPath ? `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodedPath}` @@ -412,20 +400,18 @@ async function handleGetContents(ctx: RouteContext): Promise { if (!ghRes.ok) { const err = data as GitHubApiError | null; if (ghRes.status === 401) throw new ClientError('Unauthorized', 401); - json(ctx.res, ghRes.status, { error: err?.message ?? 'GitHub API error' }); - return; + return c.json({ error: err?.message ?? 'GitHub API error' }, ghRes.status as 400); } - json(ctx.res, 200, data); -} + return c.json(data, 200); +}); -async function handlePutContents(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const installationId = requireMatchedInstallation(ctx, session, 1); - const owner = ctx.match[2]; - const repo = ctx.match[3]; +api.put('/github-app/installations/:installationId/repos/:owner/:repo/contents', async (c) => { + const session = requireAuthSession(c); + const installationId = requireMatchedInstallation(c, session, 'installationId'); + const owner = c.req.param('owner'); + const repo = c.req.param('repo'); - const body = await readJson(ctx.req); + const body = await c.req.json(); const pathParam = requireString(body, 'path'); const message = requireString(body, 'message'); const content = body?.content; @@ -438,17 +424,16 @@ async function handlePutContents(ctx: RouteContext): Promise { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, content, sha, branch }), }); - json(ctx.res, 200, await ghRes.json()); -} + return c.json(await ghRes.json(), 200); +}); -async function handleDeleteContents(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const installationId = requireMatchedInstallation(ctx, session, 1); - const owner = ctx.match[2]; - const repo = ctx.match[3]; +api.delete('/github-app/installations/:installationId/repos/:owner/:repo/contents', async (c) => { + const session = requireAuthSession(c); + const installationId = requireMatchedInstallation(c, session, 'installationId'); + const owner = c.req.param('owner'); + const repo = c.req.param('repo'); - const body = await readJson(ctx.req); + const body = await c.req.json(); const pathParam = requireString(body, 'path'); const message = requireString(body, 'message'); const sha = requireString(body, 'sha'); @@ -459,16 +444,17 @@ async function handleDeleteContents(ctx: RouteContext): Promise { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, sha, branch }), }); - json(ctx.res, 200, await ghRes.json()); -} + return c.json(await ghRes.json(), 200); +}); -async function handleGetRawContent(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const installationId = requireMatchedInstallation(ctx, session, 1); - const owner = ctx.match[2]; - const repo = ctx.match[3]; - const pathParam = ctx.url.searchParams.get('path'); +// Raw content (authenticated) + +api.get('/github-app/installations/:installationId/repos/:owner/:repo/raw', async (c) => { + const session = requireAuthSession(c); + const installationId = requireMatchedInstallation(c, session, 'installationId'); + const owner = c.req.param('owner'); + const repo = c.req.param('repo'); + const pathParam = c.req.query('path'); if (!pathParam) throw new ClientError('path is required'); const ghPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodePathPreserveSlashes(pathParam)}`; @@ -476,19 +462,38 @@ async function handleGetRawContent(ctx: RouteContext): Promise { headers: { Accept: 'application/vnd.github.raw' }, }); - ctx.res.statusCode = 200; - ctx.res.setHeader('Content-Type', ghRes.headers.get('content-type') ?? 'application/octet-stream'); - ctx.res.setHeader('Cache-Control', 'private, max-age=300'); const body = Buffer.from(await ghRes.arrayBuffer()); - ctx.res.end(body); -} + return new Response(body, { + status: 200, + headers: { + 'Content-Type': ghRes.headers.get('content-type') ?? 'application/octet-stream', + 'Cache-Control': 'private, max-age=300', + }, + }); +}); + +// Tree (authenticated) + +api.get('/github-app/installations/:installationId/repos/:owner/:repo/tree', async (c) => { + const session = requireAuthSession(c); + const installationId = requireMatchedInstallation(c, session, 'installationId'); + const owner = c.req.param('owner'); + const repo = c.req.param('repo'); + const ref = c.req.query('ref') || 'HEAD'; + + const ghPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`; + const ghRes = await githubFetchWithInstallationToken(installationId, ghPath); + const data = (await ghRes.json()) as { tree: GitTreeEntry[]; truncated: boolean }; + return c.json({ files: mdFilesFromTree(data.tree), truncated: data.truncated }, 200); +}); + +// Public repo routes -async function handleGetPublicRepoContents(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const owner = decodeURIComponent(ctx.match[1]); - const repo = decodeURIComponent(ctx.match[2]); - const pathParam = ctx.url.searchParams.get('path') ?? ''; - const ref = ctx.url.searchParams.get('ref'); +api.get('/public/repos/:owner/:repo/contents', async (c) => { + const owner = c.req.param('owner'); + const repo = c.req.param('repo'); + const pathParam = c.req.query('path') ?? ''; + const ref = c.req.query('ref'); const encodedPath = encodePathPreserveSlashes(pathParam); const ghPath = encodedPath ? `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodedPath}` @@ -498,17 +503,15 @@ async function handleGetPublicRepoContents(ctx: RouteContext): Promise { const data = (await ghRes.json().catch(() => null)) as GitHubApiError | unknown; if (!ghRes.ok) { const err = data as GitHubApiError | null; - json(ctx.res, ghRes.status, { error: err?.message ?? 'GitHub API error' }); - return; + return c.json({ error: err?.message ?? 'GitHub API error' }, ghRes.status as 400); } - json(ctx.res, 200, data); -} + return c.json(data, 200); +}); -async function handleGetPublicRepoRaw(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const owner = decodeURIComponent(ctx.match[1]); - const repo = decodeURIComponent(ctx.match[2]); - const pathParam = ctx.url.searchParams.get('path'); +api.get('/public/repos/:owner/:repo/raw', async (c) => { + const owner = c.req.param('owner'); + const repo = c.req.param('repo'); + const pathParam = c.req.query('path'); if (!pathParam) throw new ClientError('path is required'); const ghPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodePathPreserveSlashes(pathParam)}`; @@ -526,49 +529,23 @@ async function handleGetPublicRepoRaw(ctx: RouteContext): Promise { message = text; } } - json(ctx.res, ghRes.status, { error: message }); - return; + return c.json({ error: message }, ghRes.status as 400); } - ctx.res.statusCode = 200; - ctx.res.setHeader('Content-Type', ghRes.headers.get('content-type') ?? 'application/octet-stream'); - ctx.res.setHeader('Cache-Control', 'public, max-age=300'); const body = Buffer.from(await ghRes.arrayBuffer()); - ctx.res.end(body); -} - -type GitTreeEntry = { path: string; type: string; sha: string }; - -function mdFilesFromTree(tree: GitTreeEntry[]): { name: string; path: string; sha: string }[] { - const files: { name: string; path: string; sha: string }[] = []; - for (const entry of tree) { - if (entry.type !== 'blob' || !entry.path.toLowerCase().endsWith('.md')) continue; - const slash = entry.path.lastIndexOf('/'); - files.push({ name: slash === -1 ? entry.path : entry.path.slice(slash + 1), path: entry.path, sha: entry.sha }); - } - files.sort((a, b) => a.path.localeCompare(b.path)); - return files; -} - -async function handleGetTree(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const session = requireAuthSession(ctx); - const installationId = requireMatchedInstallation(ctx, session, 1); - const owner = ctx.match[2]; - const repo = ctx.match[3]; - const ref = ctx.url.searchParams.get('ref') || 'HEAD'; - - const ghPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`; - const ghRes = await githubFetchWithInstallationToken(installationId, ghPath); - const data = (await ghRes.json()) as { tree: GitTreeEntry[]; truncated: boolean }; - json(ctx.res, 200, { files: mdFilesFromTree(data.tree), truncated: data.truncated }); -} + return new Response(body, { + status: 200, + headers: { + 'Content-Type': ghRes.headers.get('content-type') ?? 'application/octet-stream', + 'Cache-Control': 'public, max-age=300', + }, + }); +}); -async function handleGetPublicTree(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const owner = decodeURIComponent(ctx.match[1]); - const repo = decodeURIComponent(ctx.match[2]); - const ref = ctx.url.searchParams.get('ref') || 'HEAD'; +api.get('/public/repos/:owner/:repo/tree', async (c) => { + const owner = c.req.param('owner'); + const repo = c.req.param('repo'); + const ref = c.req.query('ref') || 'HEAD'; const ghPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`; const ghRes = await fetchPublicGitHub(ghPath); @@ -579,22 +556,21 @@ async function handleGetPublicTree(ctx: RouteContext): Promise { } | null; if (!ghRes.ok) { const err = data as GitHubApiError | null; - json(ctx.res, ghRes.status, { error: err?.message ?? 'GitHub API error' }); - return; + return c.json({ error: err?.message ?? 'GitHub API error' }, ghRes.status as 400); } - json(ctx.res, 200, { files: mdFilesFromTree(data?.tree ?? []), truncated: data?.truncated ?? false }); -} + return c.json({ files: mdFilesFromTree(data?.tree ?? []), truncated: data?.truncated ?? false }, 200); +}); -async function handleGetPublicGist(ctx: RouteContext): Promise { - if (!checkRateLimit(ctx.req, ctx.res)) return; - const gistId = ctx.match[1]; +// Public gist route + +api.get('/gists/:id', async (c) => { + const gistId = c.req.param('id'); const cached = getGistCacheEntry(gistId); const now = Date.now(); if (cached && isFresh(cached, now)) { - ctx.res.setHeader('X-Cache', 'hit'); - json(ctx.res, 200, cached.data); - return; + c.header('X-Cache', 'hit'); + return c.json(cached.data, 200); } const ghHeaders: Record = { @@ -613,94 +589,34 @@ async function handleGetPublicGist(ctx: RouteContext): Promise { if (ghRes.status === 304 && cached) { markRevalidated(cached, now); - ctx.res.setHeader('X-Cache', 'revalidated'); - json(ctx.res, 200, cached.data); - return; + c.header('X-Cache', 'revalidated'); + return c.json(cached.data, 200); } if (!ghRes.ok) { if (cached) { - ctx.res.setHeader('X-Cache', 'stale'); - json(ctx.res, 200, cached.data); - return; + c.header('X-Cache', 'stale'); + return c.json(cached.data, 200); } - json(ctx.res, ghRes.status === 404 ? 404 : 502, { - error: ghRes.status === 404 ? 'Gist not found' : 'GitHub API error', - }); - return; + return c.json( + { error: ghRes.status === 404 ? 'Gist not found' : 'GitHub API error' }, + ghRes.status === 404 ? 404 : 502, + ); } const data: unknown = await ghRes.json(); const etag = ghRes.headers.get('etag'); setGistCacheEntry(gistId, data, etag, now); - ctx.res.setHeader('X-Cache', 'miss'); - json(ctx.res, 200, data); + c.header('X-Cache', 'miss'); + return c.json(data, 200); } catch (err) { if (cached) { - ctx.res.setHeader('X-Cache', 'stale'); - json(ctx.res, 200, cached.data); - return; + c.header('X-Cache', 'stale'); + return c.json(cached.data, 200); } console.error('Gist fetch failed:', err); throw new ClientError('Failed to load gist', 502); } -} - -const CONTENTS_PATTERN = /^\/api\/github-app\/installations\/([^/]+)\/repos\/([^/]+)\/([^/]+)\/contents$/; -const RAW_CONTENT_PATTERN = /^\/api\/github-app\/installations\/([^/]+)\/repos\/([^/]+)\/([^/]+)\/raw$/; -const TREE_PATTERN = /^\/api\/github-app\/installations\/([^/]+)\/repos\/([^/]+)\/([^/]+)\/tree$/; -const PUBLIC_REPO_CONTENTS_PATTERN = /^\/api\/public\/repos\/([^/]+)\/([^/]+)\/contents$/; -const PUBLIC_REPO_RAW_PATTERN = /^\/api\/public\/repos\/([^/]+)\/([^/]+)\/raw$/; -const PUBLIC_REPO_TREE_PATTERN = /^\/api\/public\/repos\/([^/]+)\/([^/]+)\/tree$/; - -const routes: RouteDef[] = [ - { method: 'GET', pattern: /^\/api\/auth\/github\/start$/, handler: handleAuthStart }, - { method: 'GET', pattern: /^\/api\/auth\/github\/callback$/, handler: handleAuthCallback }, - { method: 'GET', pattern: /^\/api\/auth\/session$/, handler: handleAuthSession }, - { method: 'POST', pattern: /^\/api\/auth\/logout$/, handler: handleAuthLogout }, - { method: 'GET', pattern: /^\/api\/github\/user$/, handler: handleGitHubUser }, - { method: 'GET', pattern: /^\/api\/github\/gists$/, handler: handleListGists }, - { method: 'POST', pattern: /^\/api\/github\/gists$/, handler: handleCreateGist }, - { method: 'GET', pattern: /^\/api\/github\/gists\/([a-f0-9]+)$/i, handler: handleGetAuthedGist }, - { method: 'PATCH', pattern: /^\/api\/github\/gists\/([a-f0-9]+)$/i, handler: handlePatchGist }, - { method: 'DELETE', pattern: /^\/api\/github\/gists\/([a-f0-9]+)$/i, handler: handleDeleteGist }, - { method: 'GET', pattern: /^\/api\/github-app\/install-url$/, handler: handleInstallUrl }, - { method: 'POST', pattern: /^\/api\/github-app\/sessions$/, handler: handleCreateSession }, - { method: 'POST', pattern: /^\/api\/github-app\/disconnect$/, handler: handleDisconnectInstallation }, - { method: 'GET', pattern: /^\/api\/github-app\/installations\/([^/]+)\/repositories$/, handler: handleListRepos }, - { method: 'GET', pattern: CONTENTS_PATTERN, handler: handleGetContents }, - { method: 'PUT', pattern: CONTENTS_PATTERN, handler: handlePutContents }, - { method: 'DELETE', pattern: CONTENTS_PATTERN, handler: handleDeleteContents }, - { method: 'GET', pattern: RAW_CONTENT_PATTERN, handler: handleGetRawContent }, - { method: 'GET', pattern: TREE_PATTERN, handler: handleGetTree }, - { method: 'GET', pattern: PUBLIC_REPO_CONTENTS_PATTERN, handler: handleGetPublicRepoContents }, - { method: 'GET', pattern: PUBLIC_REPO_RAW_PATTERN, handler: handleGetPublicRepoRaw }, - { method: 'GET', pattern: PUBLIC_REPO_TREE_PATTERN, handler: handleGetPublicTree }, - { method: 'GET', pattern: /^\/api\/gists\/([a-f0-9]+)$/i, handler: handleGetPublicGist }, -]; - -export async function handleApiRequest( - req: http.IncomingMessage, - res: http.ServerResponse, - url: URL, - pathname: string, -): Promise { - for (const route of routes) { - const match = pathname.match(route.pattern); - if (!match) continue; - - if (req.method !== route.method) { - const hasMethodMatch = routes.some((r) => r.method === req.method && pathname.match(r.pattern)); - if (!hasMethodMatch) { - json(res, 405, { error: 'Method not allowed' }); - return true; - } - continue; - } - - await route.handler({ req, res, url, pathname, match }); - return true; - } +}); - return false; -} +export { api }; diff --git a/server/security_headers.ts b/server/security_headers.ts index cdc78e2..69f2ac6 100644 --- a/server/security_headers.ts +++ b/server/security_headers.ts @@ -1,9 +1,10 @@ -import type http from 'node:http'; +import { createMiddleware } from 'hono/factory'; import { CONTENT_SECURITY_POLICY } from './config'; -export function applySecurityHeaders(res: http.ServerResponse): void { - res.setHeader('X-Content-Type-Options', 'nosniff'); - res.setHeader('X-Frame-Options', 'DENY'); - res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload'); - res.setHeader('Content-Security-Policy', CONTENT_SECURITY_POLICY); -} +export const securityHeaders = createMiddleware(async (c, next) => { + await next(); + c.header('X-Content-Type-Options', 'nosniff'); + c.header('X-Frame-Options', 'DENY'); + c.header('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload'); + c.header('Content-Security-Policy', CONTENT_SECURITY_POLICY); +}); diff --git a/server/session.ts b/server/session.ts index ba439a6..9112079 100644 --- a/server/session.ts +++ b/server/session.ts @@ -1,8 +1,9 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; -import type http from 'node:http'; import path from 'node:path'; import { DatabaseSync } from 'node:sqlite'; +import type { Context } from 'hono'; +import { deleteCookie, getCookie, setCookie } from 'hono/cookie'; import { DATABASE_PATH, SESSION_MAX_LIFETIME_SECONDS, SESSION_TTL_SECONDS } from './config'; import type { Session } from './types'; @@ -123,43 +124,6 @@ const selectInstallationStmt = db.prepare(` const deleteInstallationStmt = db.prepare('DELETE FROM user_installations WHERE github_user_id = ?'); -function parseCookies(req: http.IncomingMessage): Record { - const raw = req.headers.cookie; - if (!raw) return {}; - const result: Record = {}; - for (const pair of raw.split(';')) { - const idx = pair.indexOf('='); - if (idx === -1) continue; - const key = pair.slice(0, idx).trim(); - const value = pair.slice(idx + 1).trim(); - if (!key) continue; - result[key] = decodeURIComponent(value); - } - return result; -} - -function appendSetCookie(res: http.ServerResponse, cookie: string): void { - const existing = res.getHeader('Set-Cookie'); - if (!existing) { - res.setHeader('Set-Cookie', cookie); - return; - } - if (Array.isArray(existing)) { - res.setHeader('Set-Cookie', [...existing, cookie]); - return; - } - res.setHeader('Set-Cookie', [String(existing), cookie]); -} - -function cookieOptions(maxAgeSeconds: number): string { - const secure = process.env.NODE_ENV === 'production' ? 'Secure; ' : ''; - return `${secure}HttpOnly; SameSite=Lax; Path=/; Max-Age=${maxAgeSeconds}`; -} - -function makeCookie(value: string, maxAgeSeconds: number): string { - return `${SESSION_COOKIE_NAME}=${encodeURIComponent(value)}; ${cookieOptions(maxAgeSeconds)}`; -} - function createSessionId(): string { return crypto.randomBytes(32).toString('base64url'); } @@ -194,7 +158,7 @@ function rowToSession(row: Record): Session { }; } -function upsertSession(res: http.ServerResponse, id: string, input: SessionInput, createdAtMs: number): Session { +function upsertSession(c: Context, id: string, input: SessionInput, createdAtMs: number): Session { const session = buildSession(id, input, createdAtMs); sessionUpsertStmt.run( session.id, @@ -208,24 +172,33 @@ function upsertSession(res: http.ServerResponse, id: string, input: SessionInput session.expiresAtMs, ); const cookieMaxAge = Math.max(0, Math.ceil((session.expiresAtMs - Date.now()) / 1000)); - appendSetCookie(res, makeCookie(id, cookieMaxAge)); + setCookie(c, SESSION_COOKIE_NAME, id, { + httpOnly: true, + sameSite: 'Lax', + path: '/', + maxAge: cookieMaxAge, + secure: process.env.NODE_ENV === 'production', + }); return session; } -export function createSession(res: http.ServerResponse, input: SessionInput): Session { - return upsertSession(res, createSessionId(), input, Date.now()); +export function createSession(c: Context, input: SessionInput): Session { + return upsertSession(c, createSessionId(), input, Date.now()); } -export function destroySession(req: http.IncomingMessage, res: http.ServerResponse): void { - const cookies = parseCookies(req); - const id = cookies[SESSION_COOKIE_NAME]; +export function destroySession(c: Context): void { + const id = getCookie(c, SESSION_COOKIE_NAME); if (id) deleteSessionByIdStmt.run(id); - appendSetCookie(res, makeCookie('', 0)); + deleteCookie(c, SESSION_COOKIE_NAME, { + httpOnly: true, + sameSite: 'Lax', + path: '/', + secure: process.env.NODE_ENV === 'production', + }); } -export function getSession(req: http.IncomingMessage): Session | null { - const cookies = parseCookies(req); - const id = cookies[SESSION_COOKIE_NAME]; +export function getSession(c: Context): Session | null { + const id = getCookie(c, SESSION_COOKIE_NAME); if (!id) return null; const row = sessionByIdStmt.get(id) as Record | undefined; @@ -239,9 +212,9 @@ export function getSession(req: http.IncomingMessage): Session | null { return session; } -export function refreshSession(session: Session, res: http.ServerResponse): Session { +export function refreshSession(session: Session, c: Context): Session { return upsertSession( - res, + c, session.id, { githubUserId: session.githubUserId, diff --git a/server/static_files.ts b/server/static_files.ts index 83e1811..1643e09 100644 --- a/server/static_files.ts +++ b/server/static_files.ts @@ -1,6 +1,6 @@ import { readFile, stat } from 'node:fs/promises'; -import type http from 'node:http'; import path from 'node:path'; +import { createMiddleware } from 'hono/factory'; const DIST_DIR = path.resolve(new URL('../dist', import.meta.url).pathname); @@ -17,24 +17,43 @@ const MIME_TYPES: Record = { '.txt': 'text/plain', }; -export async function serveStatic(res: http.ServerResponse, pathname: string): Promise { - const safePath = path.normalize(decodeURIComponent(pathname)); - const filePath = path.join(DIST_DIR, safePath); - if (!filePath.startsWith(DIST_DIR)) return false; - +async function tryServeFile(filePath: string): Promise { + if (!filePath.startsWith(DIST_DIR)) return null; try { const s = await stat(filePath); - if (!s.isFile()) return false; + if (!s.isFile()) return null; const ext = path.extname(filePath); const content = await readFile(filePath); - res.writeHead(200, { + const headers: Record = { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream', - ...(ext !== '.html' ? { 'Cache-Control': 'public, max-age=31536000, immutable' } : {}), - }); - res.end(content); - return true; + }; + if (ext !== '.html') { + headers['Cache-Control'] = 'public, max-age=31536000, immutable'; + } + return new Response(content, { status: 200, headers }); } catch { - return false; + return null; } } + +export const serveStaticMiddleware = createMiddleware(async (c, next) => { + if (c.req.method !== 'GET') { + await next(); + return; + } + + const pathname = decodeURIComponent(new URL(c.req.url).pathname); + const safePath = path.normalize(pathname); + const filePath = path.join(DIST_DIR, safePath); + + const res = await tryServeFile(filePath); + if (res) return res; + + // SPA fallback: serve index.html for unmatched GET requests + const indexPath = path.join(DIST_DIR, 'index.html'); + const indexRes = await tryServeFile(indexPath); + if (indexRes) return indexRes; + + await next(); +}); From aeb191c18faf5c5c51bc9d19c015a9d68c8645f0 Mon Sep 17 00:00:00 2001 From: Bob Webb Date: Mon, 2 Mar 2026 13:36:08 +0000 Subject: [PATCH 2/4] feat(server): replace custom code with built-in Hono middleware Use secureHeaders(), serveStatic(), HTTPException, and bodyLimit() from Hono instead of hand-rolled equivalents. This removes ~70 lines of custom code (MIME map, file serving, ClientError class, manual security headers) and restores the 2 MB body size limit lost during migration. Co-Authored-By: Claude Opus 4.6 --- server/config.ts | 3 -- server/errors.ts | 9 ------ server/github_client.ts | 6 ++-- server/http_helpers.ts | 4 +-- server/index.ts | 15 +++++---- server/routes.ts | 20 ++++++------ server/security_headers.ts | 20 +++++++----- server/static_files.ts | 66 +++++++------------------------------- 8 files changed, 47 insertions(+), 96 deletions(-) delete mode 100644 server/errors.ts diff --git a/server/config.ts b/server/config.ts index 9924b73..414f5ff 100644 --- a/server/config.ts +++ b/server/config.ts @@ -15,6 +15,3 @@ export const GITHUB_FETCH_TIMEOUT_MS = 15_000; export const MAX_BODY_BYTES = 2 * 1024 * 1024; // 2 MB export const ALLOWED_ORIGINS = new Set(['https://input.md', `http://localhost:${CLIENT_PORT}`]); - -export const CONTENT_SECURITY_POLICY = - "default-src 'self'; script-src 'self' 'sha256-wBdtWdXsHnAU2DdByySW4LlXFAScrBvmBgkXtydwJdg='; style-src 'self' 'unsafe-inline'; img-src 'self' https://avatars.githubusercontent.com; connect-src 'self' https://api.github.com https://gist.githubusercontent.com; font-src 'self'"; diff --git a/server/errors.ts b/server/errors.ts deleted file mode 100644 index 4965155..0000000 --- a/server/errors.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class ClientError extends Error { - statusCode: number; - - constructor(message: string, statusCode = 400) { - super(message); - this.name = 'ClientError'; - this.statusCode = statusCode; - } -} diff --git a/server/github_client.ts b/server/github_client.ts index c6b9162..6558b5b 100644 --- a/server/github_client.ts +++ b/server/github_client.ts @@ -1,7 +1,7 @@ import crypto from 'node:crypto'; import { readFile } from 'node:fs/promises'; import { GITHUB_FETCH_TIMEOUT_MS } from './config'; -import { ClientError } from './errors'; +import { HTTPException } from 'hono/http-exception'; import { base64url, requireEnv } from './http_helpers'; import type { TokenCacheRecord } from './types'; @@ -95,7 +95,7 @@ async function getInstallationToken(installationId: string, repositoryIds?: numb if (!res.ok) { const body = await res.text().catch(() => ''); console.error(`Failed to mint installation token: ${res.status} ${res.statusText}`, body); - throw new ClientError(`GitHub API error: ${res.status}`, 502); + throw new HTTPException(502, { message: `GitHub API error: ${res.status}` }); } const data = (await res.json()) as { token: string; expires_at: string }; @@ -135,7 +135,7 @@ export async function githubFetchWithInstallationToken( const body = await res.text().catch(() => ''); console.error(`GitHub API error on ${ghPath}: ${res.status} ${res.statusText}`, body); const statusCode = res.status >= 400 && res.status < 500 ? res.status : 502; - throw new ClientError(`GitHub API error: ${res.status}`, statusCode); + throw new HTTPException(statusCode as 400, { message: `GitHub API error: ${res.status}` }); } return res; diff --git a/server/http_helpers.ts b/server/http_helpers.ts index 86d9a73..9207095 100644 --- a/server/http_helpers.ts +++ b/server/http_helpers.ts @@ -1,4 +1,4 @@ -import { ClientError } from './errors'; +import { HTTPException } from 'hono/http-exception'; export function requireEnv(name: string): string { const value = process.env[name]; @@ -8,7 +8,7 @@ export function requireEnv(name: string): string { export function requireString(body: Record | null, key: string): string { const value = body?.[key]; - if (typeof value !== 'string' || !value.trim()) throw new ClientError(`${key} is required`); + if (typeof value !== 'string' || !value.trim()) throw new HTTPException(400, { message: `${key} is required` }); return value; } diff --git a/server/index.ts b/server/index.ts index 73d0b52..e2b8fb2 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,16 +1,17 @@ import './env'; import { serve } from '@hono/node-server'; import { Hono } from 'hono'; -import { PORT } from './config'; +import { bodyLimit } from 'hono/body-limit'; +import { MAX_BODY_BYTES, PORT } from './config'; import { corsMiddleware } from './cors'; -import { ClientError } from './errors'; +import { HTTPException } from 'hono/http-exception'; import { startGistCacheCleanup } from './gist_cache'; import { startInstallationTokenCacheCleanup } from './github_client'; import { startRateLimitCleanup, rateLimitMiddleware } from './rate_limit'; import { api } from './routes'; import { securityHeaders } from './security_headers'; import { startSessionCleanup } from './session'; -import { serveStaticMiddleware } from './static_files'; +import { serveStaticMiddleware, spaFallback } from './static_files'; startInstallationTokenCacheCleanup(); startGistCacheCleanup(); @@ -23,19 +24,21 @@ const app = new Hono(); app.use('*', securityHeaders); app.use('*', corsMiddleware); -// Rate limiting on API routes +// Rate limiting and body size limit on API routes app.use('/api/*', rateLimitMiddleware); +app.use('/api/*', bodyLimit({ maxSize: MAX_BODY_BYTES })); // API routes app.route('/api', api); // Static files + SPA fallback app.use('*', serveStaticMiddleware); +app.use('*', spaFallback); // Error handler app.onError((err, c) => { - if (err instanceof ClientError) { - return c.json({ error: err.message }, err.statusCode as 400); + if (err instanceof HTTPException) { + return c.json({ error: err.message }, err.status); } console.error('Unhandled server error:', err); return c.json({ error: 'Internal server error' }, 500); diff --git a/server/routes.ts b/server/routes.ts index 1cf9816..90ae5f6 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import type { Context } from 'hono'; import { APP_URL, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_FETCH_TIMEOUT_MS, GITHUB_TOKEN } from './config'; -import { ClientError } from './errors'; +import { HTTPException } from 'hono/http-exception'; import { getGistCacheEntry, isFresh, markRevalidated, setGistCacheEntry } from './gist_cache'; import { createAppJwt, encodePathPreserveSlashes, githubFetchWithInstallationToken } from './github_client'; import { requireEnv, requireString } from './http_helpers'; @@ -49,14 +49,14 @@ function normalizeReturnTo(raw: string | null): string { function requireAuthSession(c: Context): Session { const session = getSession(c); - if (!session) throw new ClientError('Unauthorized', 401); + if (!session) throw new HTTPException(401, { message: 'Unauthorized' }); return session; } function requireMatchedInstallation(c: Context, session: Session, paramName: string): string { const installationId = c.req.param(paramName); if (!session.installationId || session.installationId !== installationId) { - throw new ClientError('Forbidden', 403); + throw new HTTPException(403, { message: 'Forbidden' }); } return installationId; } @@ -87,7 +87,7 @@ async function proxyGitHubJson( const data = (await ghRes.json().catch(() => null)) as unknown; if (!ghRes.ok) { const err = data as GitHubApiError | null; - if (ghRes.status === 401) throw new ClientError('Unauthorized', 401); + if (ghRes.status === 401) throw new HTTPException(401, { message: 'Unauthorized' }); return c.json({ error: err?.message ?? 'GitHub API error' }, ghRes.status as 400); } return c.json(data, 200); @@ -301,7 +301,7 @@ api.delete('/github/gists/:id', async (c) => { }); if (!ghRes.ok) { const data = (await ghRes.json().catch(() => null)) as GitHubApiError | null; - if (ghRes.status === 401) throw new ClientError('Unauthorized', 401); + if (ghRes.status === 401) throw new HTTPException(401, { message: 'Unauthorized' }); return c.json({ error: data?.message ?? 'GitHub API error' }, ghRes.status as 400); } return c.json({ ok: true }, 200); @@ -399,7 +399,7 @@ api.get('/github-app/installations/:installationId/repos/:owner/:repo/contents', const data = (await ghRes.json().catch(() => null)) as unknown; if (!ghRes.ok) { const err = data as GitHubApiError | null; - if (ghRes.status === 401) throw new ClientError('Unauthorized', 401); + if (ghRes.status === 401) throw new HTTPException(401, { message: 'Unauthorized' }); return c.json({ error: err?.message ?? 'GitHub API error' }, ghRes.status as 400); } return c.json(data, 200); @@ -415,7 +415,7 @@ api.put('/github-app/installations/:installationId/repos/:owner/:repo/contents', const pathParam = requireString(body, 'path'); const message = requireString(body, 'message'); const content = body?.content; - if (typeof content !== 'string') throw new ClientError('content is required'); + if (typeof content !== 'string') throw new HTTPException(400, { message: 'content is required' }); const sha = typeof body?.sha === 'string' ? body.sha : undefined; const branch = typeof body?.branch === 'string' ? body.branch : undefined; const ghPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodePathPreserveSlashes(pathParam)}`; @@ -455,7 +455,7 @@ api.get('/github-app/installations/:installationId/repos/:owner/:repo/raw', asyn const owner = c.req.param('owner'); const repo = c.req.param('repo'); const pathParam = c.req.query('path'); - if (!pathParam) throw new ClientError('path is required'); + if (!pathParam) throw new HTTPException(400, { message: 'path is required' }); const ghPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodePathPreserveSlashes(pathParam)}`; const ghRes = await githubFetchWithInstallationToken(installationId, ghPath, { @@ -512,7 +512,7 @@ api.get('/public/repos/:owner/:repo/raw', async (c) => { const owner = c.req.param('owner'); const repo = c.req.param('repo'); const pathParam = c.req.query('path'); - if (!pathParam) throw new ClientError('path is required'); + if (!pathParam) throw new HTTPException(400, { message: 'path is required' }); const ghPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodePathPreserveSlashes(pathParam)}`; const ghRes = await fetchPublicGitHub(ghPath, { @@ -615,7 +615,7 @@ api.get('/gists/:id', async (c) => { return c.json(cached.data, 200); } console.error('Gist fetch failed:', err); - throw new ClientError('Failed to load gist', 502); + throw new HTTPException(502, { message: 'Failed to load gist' }); } }); diff --git a/server/security_headers.ts b/server/security_headers.ts index 69f2ac6..ddbfe7b 100644 --- a/server/security_headers.ts +++ b/server/security_headers.ts @@ -1,10 +1,14 @@ -import { createMiddleware } from 'hono/factory'; -import { CONTENT_SECURITY_POLICY } from './config'; +import { secureHeaders } from 'hono/secure-headers'; -export const securityHeaders = createMiddleware(async (c, next) => { - await next(); - c.header('X-Content-Type-Options', 'nosniff'); - c.header('X-Frame-Options', 'DENY'); - c.header('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload'); - c.header('Content-Security-Policy', CONTENT_SECURITY_POLICY); +export const securityHeaders = secureHeaders({ + xFrameOptions: 'DENY', + strictTransportSecurity: 'max-age=63072000; includeSubDomains; preload', + contentSecurityPolicy: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'sha256-wBdtWdXsHnAU2DdByySW4LlXFAScrBvmBgkXtydwJdg='"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", "https://avatars.githubusercontent.com"], + connectSrc: ["'self'", "https://api.github.com", "https://gist.githubusercontent.com"], + fontSrc: ["'self'"], + }, }); diff --git a/server/static_files.ts b/server/static_files.ts index 1643e09..e44530d 100644 --- a/server/static_files.ts +++ b/server/static_files.ts @@ -1,59 +1,15 @@ -import { readFile, stat } from 'node:fs/promises'; -import path from 'node:path'; -import { createMiddleware } from 'hono/factory'; +import { serveStatic } from '@hono/node-server/serve-static'; -const DIST_DIR = path.resolve(new URL('../dist', import.meta.url).pathname); - -const MIME_TYPES: Record = { - '.html': 'text/html; charset=utf-8', - '.js': 'application/javascript', - '.css': 'text/css', - '.json': 'application/json', - '.png': 'image/png', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - '.woff': 'font/woff', - '.woff2': 'font/woff2', - '.txt': 'text/plain', -}; - -async function tryServeFile(filePath: string): Promise { - if (!filePath.startsWith(DIST_DIR)) return null; - try { - const s = await stat(filePath); - if (!s.isFile()) return null; - - const ext = path.extname(filePath); - const content = await readFile(filePath); - const headers: Record = { - 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream', - }; - if (ext !== '.html') { - headers['Cache-Control'] = 'public, max-age=31536000, immutable'; +export const serveStaticMiddleware = serveStatic({ + root: 'dist', + onFound: (_path, c) => { + if (!_path.endsWith('.html')) { + c.header('Cache-Control', 'public, max-age=31536000, immutable'); } - return new Response(content, { status: 200, headers }); - } catch { - return null; - } -} - -export const serveStaticMiddleware = createMiddleware(async (c, next) => { - if (c.req.method !== 'GET') { - await next(); - return; - } - - const pathname = decodeURIComponent(new URL(c.req.url).pathname); - const safePath = path.normalize(pathname); - const filePath = path.join(DIST_DIR, safePath); - - const res = await tryServeFile(filePath); - if (res) return res; - - // SPA fallback: serve index.html for unmatched GET requests - const indexPath = path.join(DIST_DIR, 'index.html'); - const indexRes = await tryServeFile(indexPath); - if (indexRes) return indexRes; + }, +}); - await next(); +export const spaFallback = serveStatic({ + root: 'dist', + path: 'index.html', }); From f1c20e01ad81c61a5dd7a703d3ea0f1b3302d7de Mon Sep 17 00:00:00 2001 From: Bob Webb Date: Mon, 2 Mar 2026 14:25:15 +0000 Subject: [PATCH 3/4] feat(server): add @teekit/tunnel integration for TEE encrypted channels When TEEKIT_GET_QUOTE_URL is set, the server initializes a TunnelServer with the Hono app (via getRequestListener) for encrypted communication inside a trusted execution environment. Standard mode is unchanged. Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 495 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + server/index.ts | 82 ++++++-- 3 files changed, 557 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab1b295..334a4aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", + "@teekit/tunnel": "^0.0.1", "dompurify": "^3.3.1", "dotenv": "^17.3.1", "hono": "^4.12.3", @@ -491,6 +492,84 @@ "node": ">=14.21.3" } }, + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1020,6 +1099,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@preact/preset-vite": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.3.tgz", @@ -2203,6 +2294,46 @@ "win32" ] }, + "node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@teekit/qvl": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@teekit/qvl/-/qvl-0.0.1.tgz", + "integrity": "sha512-PfzS5xlRvH9EJcoufsqbUJEgq9WEZmp1vl0esuecwInEtKvpaIvL9TWTiYUa/tA9lKgWk0FOF64Rm3jVSeSqRg==", + "dependencies": { + "@scure/base": "^2.0.0", + "asn1js": "^3.0.5", + "pkijs": "^3.0.18", + "restructure": "^3.0.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@teekit/tunnel": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@teekit/tunnel/-/tunnel-0.0.1.tgz", + "integrity": "sha512-zzs1SGWSRUcpMhgx0RI4Xzw/GJm7A/ytWFNBeBEh9pCKZ9QjyPUjK/M4mhoh3S/zi2TnzcY1zXgnBbRRHtn5FA==", + "dependencies": { + "@teekit/qvl": "0.0.1", + "cbor-x": "^1.6.0", + "libsodium-wrappers": "^0.7.15", + "node-mocks-http": "^1.17.2", + "uint8array-extras": "^1.5.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2226,6 +2357,19 @@ "license": "MIT", "optional": true }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -2238,6 +2382,20 @@ "node": ">=10" } }, + "node_modules/asn1js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", + "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/babel-plugin-transform-hook-names": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", @@ -2295,6 +2453,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001770", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", @@ -2315,6 +2482,49 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cbor-extract": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.1.1" + }, + "bin": { + "download-cbor-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", + "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", + "@cbor-extract/cbor-extract-linux-x64": "2.2.0", + "@cbor-extract/cbor-extract-win32-x64": "2.2.0" + } + }, + "node_modules/cbor-x": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz", + "integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==", + "license": "MIT", + "optionalDependencies": { + "cbor-extract": "^2.2.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2366,6 +2576,25 @@ } } }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -2539,6 +2768,15 @@ } } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2638,6 +2876,21 @@ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", "license": "MIT" }, + "node_modules/libsodium": { + "version": "0.7.16", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.16.tgz", + "integrity": "sha512-3HrzSPuzm6Yt9aTYCDxYEG8x8/6C0+ag655Y7rhhWZM9PT4NpdnbqlzXhGZlDnkgR6MeSTnOt/VIyHLs9aSf+Q==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.16", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.16.tgz", + "integrity": "sha512-Gtr/WBx4dKjvRL1pvfwZqu7gO6AfrQ0u9vFL+kXihtHf6NfkROR8pjYWn98MFDI3jN19Ii1ZUfPR9afGiPyfHg==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.7.16" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2677,6 +2930,66 @@ "node": ">= 20" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2701,6 +3014,30 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-html-parser": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", @@ -2711,6 +3048,39 @@ "he": "1.2.0" } }, + "node_modules/node-mocks-http": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.17.2.tgz", + "integrity": "sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@types/express": "^4.17.21 || ^5.0.0", + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -2729,6 +3099,15 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2747,6 +3126,23 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkijs": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", + "integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==", + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2785,6 +3181,33 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -2887,6 +3310,12 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -2931,6 +3360,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3025,6 +3474,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3039,6 +3501,18 @@ "node": ">=14.17" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3210,6 +3684,27 @@ "vite": "5.x || 6.x || 7.x" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 0e89334..dd8ea32 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@fontsource-variable/inter": "^5.2.8", "@fontsource/source-code-pro": "^5.2.7", "@hono/node-server": "^1.19.9", + "@teekit/tunnel": "^0.0.1", "@preact/preset-vite": "^2.10.3", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-context-menu": "^2.2.16", diff --git a/server/index.ts b/server/index.ts index e2b8fb2..163702a 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,13 +1,16 @@ import './env'; -import { serve } from '@hono/node-server'; +import type http from 'node:http'; +import { getRequestListener, serve } from '@hono/node-server'; +import type { QuoteData } from '@teekit/tunnel'; +import { TunnelServer } from '@teekit/tunnel'; import { Hono } from 'hono'; import { bodyLimit } from 'hono/body-limit'; +import { HTTPException } from 'hono/http-exception'; import { MAX_BODY_BYTES, PORT } from './config'; import { corsMiddleware } from './cors'; -import { HTTPException } from 'hono/http-exception'; import { startGistCacheCleanup } from './gist_cache'; import { startInstallationTokenCacheCleanup } from './github_client'; -import { startRateLimitCleanup, rateLimitMiddleware } from './rate_limit'; +import { rateLimitMiddleware, startRateLimitCleanup } from './rate_limit'; import { api } from './routes'; import { securityHeaders } from './security_headers'; import { startSessionCleanup } from './session'; @@ -44,25 +47,62 @@ app.onError((err, c) => { return c.json({ error: 'Internal server error' }, 500); }); -const server = serve( - { fetch: app.fetch, port: PORT, hostname: '0.0.0.0' }, - () => { - const configured = Boolean( - process.env.GITHUB_APP_ID && - (process.env.GITHUB_APP_PRIVATE_KEY || process.env.GITHUB_APP_PRIVATE_KEY_PATH) && - process.env.GITHUB_APP_SLUG && - process.env.GITHUB_CLIENT_ID && - process.env.GITHUB_CLIENT_SECRET, +// Convert Hono app to a Node.js-compatible request listener for TunnelServer +const nodeHandler = getRequestListener(app.fetch); + +async function startServer(): Promise { + const getQuote = process.env.TEEKIT_GET_QUOTE_URL; + + if (getQuote) { + // Running inside a TEE — use TunnelServer for encrypted channels + const tunnelServer = await TunnelServer.initialize( + nodeHandler as any, + async (x25519PublicKey: Uint8Array): Promise => { + const res = await fetch(getQuote, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + report_data: Buffer.from(x25519PublicKey).toString('base64'), + }), + }); + if (!res.ok) { + throw new Error(`Failed to get quote: ${res.status} ${res.statusText}`); + } + return (await res.json()) as QuoteData; + }, ); - console.log(`GitHub App auth server listening on http://0.0.0.0:${PORT} (configured=${configured})`); - }, -); + return new Promise((resolve) => { + tunnelServer.server.listen(PORT, '0.0.0.0', () => { + resolve(tunnelServer.server); + }); + }); + } -function gracefulShutdown(signal: string): void { - console.log(`\n${signal} received, shutting down gracefully...`); - server.close(() => process.exit(0)); - setTimeout(() => process.exit(1), 5000).unref(); + // Standard mode — use Hono's built-in serve + return new Promise((resolve) => { + const s = serve({ fetch: app.fetch, port: PORT, hostname: '0.0.0.0' }, () => { + resolve(s); + }); + }); } -process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); -process.on('SIGINT', () => gracefulShutdown('SIGINT')); +startServer().then((server) => { + const configured = Boolean( + process.env.GITHUB_APP_ID && + (process.env.GITHUB_APP_PRIVATE_KEY || process.env.GITHUB_APP_PRIVATE_KEY_PATH) && + process.env.GITHUB_APP_SLUG && + process.env.GITHUB_CLIENT_ID && + process.env.GITHUB_CLIENT_SECRET, + ); + const mode = process.env.TEEKIT_GET_QUOTE_URL ? 'teekit' : 'standard'; + console.log(`GitHub App auth server listening on http://0.0.0.0:${PORT} (configured=${configured}, mode=${mode})`); + + function gracefulShutdown(signal: string): void { + console.log(`\n${signal} received, shutting down gracefully...`); + server.close(() => process.exit(0)); + setTimeout(() => process.exit(1), 5000).unref(); + } + + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + process.on('SIGINT', () => gracefulShutdown('SIGINT')); +}); From 8d8439f33d62c7bd59a3ff925e0342dcae5351d3 Mon Sep 17 00:00:00 2001 From: Bob Webb Date: Mon, 2 Mar 2026 14:58:40 +0000 Subject: [PATCH 4/4] chore: add teekitpriv dependencies as relative paths --- package-lock.json | 591 ++++++++-------------------------------------- package.json | 5 +- 2 files changed, 104 insertions(+), 492 deletions(-) diff --git a/package-lock.json b/package-lock.json index 334a4aa..a657c0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,9 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", - "@teekit/tunnel": "^0.0.1", + "@teekit/kettle": "file:../teekitpriv/packages/kettle", + "@teekit/kettle-sgx": "file:../teekitpriv/packages/kettle-sgx", + "@teekit/tunnel": "file:../teekitpriv/packages/tunnel", "dompurify": "^3.3.1", "dotenv": "^17.3.1", "hono": "^4.12.3", @@ -35,6 +37,95 @@ "vite": "^7.3.1" } }, + "../teekitpriv/packages/kettle": { + "name": "@teekit/kettle", + "version": "0.0.5", + "dependencies": { + "@scure/base": "^2.0.0", + "@teekit/qvl": "0.0.5", + "@teekit/tunnel": "0.0.5", + "cbor-x": "^1.6.0", + "colorette": "^2.0.20", + "hono": "^4.7.5", + "yargs": "^18.0.0" + }, + "bin": { + "kettle": "bin/kettle.mjs" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/node": "^24.3.1", + "@types/ws": "^8.18.1", + "@types/yargs": "^17.0.34", + "ava": "^6.4.1", + "esbuild": "^0.24.0", + "eslint": "^9.33.0", + "globals": "^16.3.0", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "workerd": "^1.20251105.0", + "ws": "^8.18.3" + }, + "optionalDependencies": { + "@esbuild/linux-x64": "^0.25.12", + "@rollup/rollup-linux-x64-gnu": "^4.50.1" + } + }, + "../teekitpriv/packages/kettle-sgx": { + "name": "@teekit/kettle-sgx", + "version": "0.0.1", + "dependencies": { + "@noble/hashes": "^1.7.0", + "@scure/base": "^2.0.0" + }, + "bin": { + "kettle-sgx": "scripts/kettle-sgx.sh" + }, + "devDependencies": { + "@types/node": "^24.3.1", + "@types/ws": "^8.18.1", + "ava": "^6.4.1", + "colorette": "^2.0.20", + "esbuild": "^0.24.0", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "ws": "^8.18.3" + } + }, + "../teekitpriv/packages/tunnel": { + "name": "@teekit/tunnel", + "version": "0.0.5", + "dependencies": { + "@cloudflare/workers-types": "^4.20251011.0", + "@noble/ciphers": "^2.0.1", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^1.8.0", + "@teekit/qvl": "0.0.5", + "cbor-x": "^1.6.0", + "isomorphic-ws": "^5.0.0", + "node-mocks-http": "^1.17.2", + "uint8array-extras": "^1.5.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@hono/node-server": "^1.19.5", + "@hono/node-ws": "^1.2.0", + "@types/debug": "^4.1.12", + "@types/express": "^5.0.3", + "@types/node": "^24.3.1", + "@types/ws": "^8.18.1", + "ava": "^6.4.1", + "concurrently": "^9.2.1", + "esbuild": "^0.24.0", + "express": "^5.1.0", + "hono": "^4.6.9", + "tsx": "^4.20.5", + "typescript": "^5.9.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -492,84 +583,6 @@ "node": ">=14.21.3" } }, - "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", - "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@cbor-extract/cbor-extract-darwin-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", - "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-arm": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", - "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-arm64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", - "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", - "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-win32-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", - "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1099,18 +1112,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@preact/preset-vite": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.3.tgz", @@ -2294,45 +2295,17 @@ "win32" ] }, - "node_modules/@scure/base": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", - "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } + "node_modules/@teekit/kettle": { + "resolved": "../teekitpriv/packages/kettle", + "link": true }, - "node_modules/@teekit/qvl": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@teekit/qvl/-/qvl-0.0.1.tgz", - "integrity": "sha512-PfzS5xlRvH9EJcoufsqbUJEgq9WEZmp1vl0esuecwInEtKvpaIvL9TWTiYUa/tA9lKgWk0FOF64Rm3jVSeSqRg==", - "dependencies": { - "@scure/base": "^2.0.0", - "asn1js": "^3.0.5", - "pkijs": "^3.0.18", - "restructure": "^3.0.2", - "uint8array-extras": "^1.5.0" - }, - "engines": { - "node": ">=22.0.0" - } + "node_modules/@teekit/kettle-sgx": { + "resolved": "../teekitpriv/packages/kettle-sgx", + "link": true }, "node_modules/@teekit/tunnel": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@teekit/tunnel/-/tunnel-0.0.1.tgz", - "integrity": "sha512-zzs1SGWSRUcpMhgx0RI4Xzw/GJm7A/ytWFNBeBEh9pCKZ9QjyPUjK/M4mhoh3S/zi2TnzcY1zXgnBbRRHtn5FA==", - "dependencies": { - "@teekit/qvl": "0.0.1", - "cbor-x": "^1.6.0", - "libsodium-wrappers": "^0.7.15", - "node-mocks-http": "^1.17.2", - "uint8array-extras": "^1.5.0", - "ws": "^8.18.3" - }, - "engines": { - "node": ">=22.0.0" - } + "resolved": "../teekitpriv/packages/tunnel", + "link": true }, "node_modules/@types/estree": { "version": "1.0.8", @@ -2357,19 +2330,6 @@ "license": "MIT", "optional": true }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -2382,20 +2342,6 @@ "node": ">=10" } }, - "node_modules/asn1js": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", - "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", - "license": "BSD-3-Clause", - "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/babel-plugin-transform-hook-names": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", @@ -2453,15 +2399,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bytestreamjs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001770", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", @@ -2482,49 +2419,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/cbor-extract": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", - "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.1.1" - }, - "bin": { - "download-cbor-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", - "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", - "@cbor-extract/cbor-extract-linux-arm": "2.2.0", - "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", - "@cbor-extract/cbor-extract-linux-x64": "2.2.0", - "@cbor-extract/cbor-extract-win32-x64": "2.2.0" - } - }, - "node_modules/cbor-x": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz", - "integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==", - "license": "MIT", - "optionalDependencies": { - "cbor-extract": "^2.2.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2576,25 +2470,6 @@ } } }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -2768,15 +2643,6 @@ } } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2876,21 +2742,6 @@ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", "license": "MIT" }, - "node_modules/libsodium": { - "version": "0.7.16", - "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.16.tgz", - "integrity": "sha512-3HrzSPuzm6Yt9aTYCDxYEG8x8/6C0+ag655Y7rhhWZM9PT4NpdnbqlzXhGZlDnkgR6MeSTnOt/VIyHLs9aSf+Q==", - "license": "ISC" - }, - "node_modules/libsodium-wrappers": { - "version": "0.7.16", - "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.16.tgz", - "integrity": "sha512-Gtr/WBx4dKjvRL1pvfwZqu7gO6AfrQ0u9vFL+kXihtHf6NfkROR8pjYWn98MFDI3jN19Ii1ZUfPR9afGiPyfHg==", - "license": "ISC", - "dependencies": { - "libsodium": "^0.7.16" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2930,66 +2781,6 @@ "node": ">= 20" } }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3014,30 +2805,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", - "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, "node_modules/node-html-parser": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", @@ -3048,39 +2815,6 @@ "he": "1.2.0" } }, - "node_modules/node-mocks-http": { - "version": "1.17.2", - "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.17.2.tgz", - "integrity": "sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA==", - "license": "MIT", - "dependencies": { - "accepts": "^1.3.7", - "content-disposition": "^0.5.3", - "depd": "^1.1.0", - "fresh": "^0.5.2", - "merge-descriptors": "^1.0.1", - "methods": "^1.1.2", - "mime": "^1.3.4", - "parseurl": "^1.3.3", - "range-parser": "^1.2.0", - "type-is": "^1.6.18" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@types/express": "^4.17.21 || ^5.0.0", - "@types/node": "*" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - }, - "@types/node": { - "optional": true - } - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -3099,15 +2833,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3126,23 +2851,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkijs": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", - "integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==", - "license": "BSD-3-Clause", - "dependencies": { - "@noble/hashes": "1.4.0", - "asn1js": "^3.0.6", - "bytestreamjs": "^2.0.1", - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3181,33 +2889,6 @@ "url": "https://opencollective.com/preact" } }, - "node_modules/pvtsutils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/pvutils": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", - "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -3310,12 +2991,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restructure": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", - "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", - "license": "MIT" - }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -3360,26 +3035,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3474,19 +3129,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3501,18 +3143,6 @@ "node": ">=14.17" } }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3684,27 +3314,6 @@ "vite": "5.x || 6.x || 7.x" } }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index dd8ea32..f4fa666 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "server": "tsx server/index.ts", "dev": "vite", "build": "tsc && tsc -p tsconfig.server.json && vite build", + "build:worker": "tsc -p tsconfig.server.json && kettle build-app server/index.ts && kettle build-worker", "preview": "vite preview", "deploy": "fly deploy", "lint": "biome check .", @@ -23,7 +24,9 @@ "@fontsource-variable/inter": "^5.2.8", "@fontsource/source-code-pro": "^5.2.7", "@hono/node-server": "^1.19.9", - "@teekit/tunnel": "^0.0.1", + "@teekit/kettle": "file:../teekitpriv/packages/kettle", + "@teekit/kettle-sgx": "file:../teekitpriv/packages/kettle-sgx", + "@teekit/tunnel": "file:../teekitpriv/packages/tunnel", "@preact/preset-vite": "^2.10.3", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-context-menu": "^2.2.16",