commit dd3fe3033bed21aae2aa00f7780531457838ad88 Author: lsy Date: Sun Feb 15 02:12:01 2026 +0800 init: 初始化项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67ed4a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.DS_Store +.env +.env.* +apps/admin/dist/ +apps/worker/.wrangler/ +tests/ + +helloagents/plan +helloagents/archive \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b83fc4 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# new-api-lite + +简易版 new-api:Cloudflare Workers + D1 后端,Vite 静态管理台。 + +## 目录结构 + +- `apps/worker` Cloudflare Worker (Hono) +- `apps/admin` 管理台 (Vite) +- `tests` 基础单元测试 + +## 本地开发 + +### 1) 安装依赖 + +```bash +bun install +``` + +### 2) Worker + +```bash +bun run dev:worker +``` + +环境变量(`apps/worker/wrangler.toml` 或 `wrangler secret put`): + +- `ADMIN_PASSWORD` 管理员密码 +- `SESSION_TTL_HOURS` 管理会话时长(小时) +- `LOG_RETENTION_DAYS` 日志保留天数 +- `CORS_ORIGIN` 允许的管理台来源 +- `PROXY_RETRY_ROUNDS` 代理失败轮询次数(默认 2) +- `PROXY_RETRY_DELAY_MS` 轮询间隔(毫秒,默认 200) +- `NEW_API_ADMIN_TOKEN` New API 兼容接口的管理员令牌 + +### 3) Admin UI + +```bash +bun run dev:admin +``` + +前端配置(`apps/admin/.env` 可选): + +- `VITE_API_BASE` 管理 API 基址(默认同域) +- `VITE_API_TARGET` 本地开发代理目标(默认 http://localhost:8787) + +### 4) 常用脚本 + +```bash +bun run fix +``` + +用于自动修正 package.json 常见问题(等价于 `bun pm pkg fix`)。 + +## New API 兼容接口 + +为了支持 all-api-hub 等插件同步渠道,Worker 提供 New API 风格的渠道管理接口: + +- 路径前缀:`/api/channel` +- 认证方式:`Authorization: Bearer {NEW_API_ADMIN_TOKEN}`(或管理台登录 token) +- 可选请求头:`New-Api-User: 1` + +支持的常用接口: +- `GET /api/channel` 渠道列表 +- `GET /api/channel/` 渠道列表(兼容尾斜杠) +- `GET /api/group` 渠道分组列表 +- `GET /api/channel/search` 渠道搜索 +- `POST /api/channel` 新增渠道(单条) +- `PUT /api/channel` 更新渠道 +- `DELETE /api/channel/:id` 删除渠道 +- `GET /api/channel/test/:id` 渠道连通性测试 +- `GET /api/channel/fetch_models/:id` 拉取渠道模型 +- `GET /api/user/models` 用户可用模型列表 + +## D1 数据库 + +在 `apps/worker/migrations/0001_init.sql` 定义表结构,使用 Wrangler 迁移: + +```bash +bun run --filter new-api-lite-worker db:migrate +``` + +## 生产部署 + +### 1) 准备 Cloudflare 资源 + +1. 创建 D1 数据库并回填 `apps/worker/wrangler.toml` 的 `database_id`: + +```bash +bunx wrangler d1 create new_api_lite +``` + +2. 设置 Worker 变量与密钥(推荐用 secret): + +```bash +bunx wrangler secret put ADMIN_PASSWORD +bunx wrangler secret put NEW_API_ADMIN_TOKEN +``` + +其他变量可继续放在 `apps/worker/wrangler.toml` 的 `[vars]` 中,`CORS_ORIGIN` 需指向管理台域名。 + +### 2) 执行生产迁移 + +```bash +bunx wrangler d1 migrations apply DB +``` + +### 3) 部署 Worker + +```bash +bun run --filter new-api-lite-worker deploy +``` + +### 4) 构建并部署 Admin + +```bash +bun run --filter new-api-lite-admin build +``` + +将 `apps/admin/dist` 发布到 Cloudflare Pages 或任意静态托管;如需指定 API 基址,请在构建前设置 `VITE_API_BASE`(例如 `apps/admin/.env.production`)。 diff --git a/apps/admin/index.html b/apps/admin/index.html new file mode 100644 index 0000000..37c8509 --- /dev/null +++ b/apps/admin/index.html @@ -0,0 +1,18 @@ + + + + + + api-workers + + +
+ + + diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..95b047e --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,14 @@ +{ + "name": "new-api-lite-admin", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^5.2.0" + } +} diff --git a/apps/admin/src/main.ts b/apps/admin/src/main.ts new file mode 100644 index 0000000..c822b73 --- /dev/null +++ b/apps/admin/src/main.ts @@ -0,0 +1,718 @@ +import './styles.css'; + +type Channel = { + id: string; + name: string; + base_url: string; + api_key: string; + weight: number; + status: string; + rate_limit: number; + models_json?: string; +}; + +type Token = { + id: string; + name: string; + key_prefix: string; + quota_total: number | null; + quota_used: number; + status: string; +}; + +type UsageLog = { + id: string; + model: string; + channel_id: string; + channel_name?: string | null; + token_id: string; + token_name?: string | null; + total_tokens: number; + latency_ms: number; + status: string; + created_at: string; +}; + +type DashboardData = { + summary: { + total_requests: number; + total_tokens: number; + avg_latency: number; + total_errors: number; + }; + byDay: Array<{ day: string; requests: number; tokens: number }>; + byModel: Array<{ model: string; requests: number; tokens: number }>; + byChannel: Array<{ channel_name: string; requests: number; tokens: number }>; + byToken: Array<{ token_name: string; requests: number; tokens: number }>; +}; + +type Settings = { + log_retention_days: number; +}; + +const root = document.querySelector('#app'); +const apiBase = import.meta.env.VITE_API_BASE ?? ''; +const tabs = [ + { id: 'dashboard', label: '数据面板' }, + { id: 'channels', label: '渠道管理' }, + { id: 'models', label: '模型广场' }, + { id: 'tokens', label: '令牌管理' }, + { id: 'usage', label: '使用日志' }, + { id: 'settings', label: '系统设置' } +]; + +const state = { + token: localStorage.getItem('admin_token'), + activeTab: 'dashboard', + loading: false, + notice: '', + data: { + channels: [] as Channel[], + tokens: [] as Token[], + models: [] as Array<{ id: string; channels: Array<{ id: string; name: string }> }>, + usage: [] as UsageLog[], + dashboard: null as DashboardData | null, + settings: null as Settings | null + } +}; + +if (!root) { + throw new Error('Missing #app root'); +} + +function setNotice(message: string) { + state.notice = message; + render(); +} + +function setToken(token: string | null) { + state.token = token; + if (token) { + localStorage.setItem('admin_token', token); + } else { + localStorage.removeItem('admin_token'); + } + render(); +} + +async function apiFetch(path: string, options: RequestInit = {}): Promise { + const headers = new Headers(options.headers ?? {}); + headers.set('Content-Type', 'application/json'); + if (state.token) { + headers.set('Authorization', `Bearer ${state.token}`); + } + const response = await fetch(`${apiBase}${path}`, { ...options, headers }); + if (!response.ok) { + if (response.status === 401) { + setToken(null); + } + const payload = await response.json().catch(() => null); + throw new Error(payload?.error ?? `HTTP ${response.status}`); + } + return response.json() as Promise; +} + +async function loadDashboard() { + state.data.dashboard = await apiFetch('/api/dashboard'); +} + +async function loadChannels() { + const result = await apiFetch<{ channels: Channel[] }>('/api/channels'); + state.data.channels = result.channels; +} + +async function loadModels() { + const result = await apiFetch<{ models: Array<{ id: string; channels: Array<{ id: string; name: string }> }> }>('/api/models'); + state.data.models = result.models; +} + +async function loadTokens() { + const result = await apiFetch<{ tokens: Token[] }>('/api/tokens'); + state.data.tokens = result.tokens; +} + +async function loadUsage() { + const result = await apiFetch<{ logs: UsageLog[] }>('/api/usage?limit=50'); + state.data.usage = result.logs; +} + +async function loadSettings() { + state.data.settings = await apiFetch('/api/settings'); +} + +async function loadTab(tabId: string) { + state.loading = true; + state.notice = ''; + render(); + try { + if (tabId === 'dashboard') { + await loadDashboard(); + } + if (tabId === 'channels') { + await loadChannels(); + } + if (tabId === 'models') { + await loadModels(); + } + if (tabId === 'tokens') { + await loadTokens(); + } + if (tabId === 'usage') { + await loadUsage(); + } + if (tabId === 'settings') { + await loadSettings(); + } + } catch (error) { + setNotice((error as Error).message); + } finally { + state.loading = false; + render(); + } +} + +function renderLogin() { + return ` + + `; +} + +function renderSidebar() { + return ` + + `; +} + +function renderDashboard() { + const data = state.data.dashboard; + if (!data) { + return '
暂无数据
'; + } + const errorRate = data.summary.total_requests + ? Math.round((data.summary.total_errors / data.summary.total_requests) * 100) + : 0; + return ` +
+
+ 总请求 +
${data.summary.total_requests}
+ 最近窗口 +
+
+ 总 Tokens +
${data.summary.total_tokens}
+ 累计消耗 +
+
+ 错误率 +
${errorRate}%
+ 平均延迟 ${Math.round(data.summary.avg_latency)}ms +
+
+
+
+
+

按日趋势

+
+ + + + ${data.byDay + .map((row) => ``) + .join('')} + +
日期请求Tokens
${row.day}${row.requests}${row.tokens}
+
+
+
+

模型排行

+
+ + + + ${data.byModel + .map((row) => ``) + .join('')} + +
模型请求Tokens
${row.model ?? '-'}${row.requests}${row.tokens}
+
+
+
+
+
+

渠道贡献

+
+ + + + ${data.byChannel + .map((row) => ``) + .join('')} + +
渠道请求Tokens
${row.channel_name ?? '-'}${row.requests}${row.tokens}
+
+
+
+

令牌贡献

+
+ + + + ${data.byToken + .map((row) => ``) + .join('')} + +
令牌请求Tokens
${row.token_name ?? '-'}${row.requests}${row.tokens}
+
+
+ `; +} + +function renderChannels() { + return ` +
+
+
+

新增渠道

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+

渠道列表

+
+ + + + + + ${state.data.channels + .map( + (channel) => ` + + + + + + + + ` + ) + .join('')} + +
ID名称状态权重操作
${channel.id}${channel.name}${channel.status}${channel.weight} + + + +
+
+
+ `; +} + +function renderModels() { + return ` +
+
+

模型广场

+ ${state.data.models.length} 个模型 +
+ + + + ${state.data.models + .map( + (model) => ` + + + + + ` + ) + .join('')} + +
模型渠道
${model.id}${model.channels.map((channel) => channel.name).join(' / ')}
+
+ `; +} + +function renderTokens() { + return ` +
+
+
+

生成令牌

+
+
+
+ + +
+
+ + +
+ +
+
+
+
+

令牌列表

+
+ + + + + + ${state.data.tokens + .map( + (token) => ` + + + + + + + ` + ) + .join('')} + +
名称状态已用/额度操作
${token.name}${token.status}${token.quota_used} / ${token.quota_total ?? '∞'} + + + +
+
+
+ `; +} + +function renderUsage() { + return ` +
+
+

使用日志

+ +
+ + + + ${state.data.usage + .map( + (log) => ` + + + + + + + + + ` + ) + .join('')} + +
时间模型渠道Tokens延迟状态
${log.created_at?.slice(0, 19).replace('T', ' ')}${log.model ?? '-'}${log.channel_name ?? log.channel_id ?? '-'}${log.total_tokens ?? 0}${log.latency_ms ?? 0} ms${log.status}
+
+ `; +} + +function renderSettings() { + const retention = state.data.settings?.log_retention_days ?? 30; + return ` +
+
+

系统设置

+
+
+
+ + +
+
+ +
+
+
+ `; +} + +function renderContent() { + if (state.loading) { + return '
加载中...
'; + } + if (state.activeTab === 'dashboard') { + return renderDashboard(); + } + if (state.activeTab === 'channels') { + return renderChannels(); + } + if (state.activeTab === 'models') { + return renderModels(); + } + if (state.activeTab === 'tokens') { + return renderTokens(); + } + if (state.activeTab === 'usage') { + return renderUsage(); + } + if (state.activeTab === 'settings') { + return renderSettings(); + } + return '
未知模块
'; +} + +function renderApp() { + return ` +
+ ${renderSidebar()} +
+
+
+

${tabs.find((tab) => tab.id === state.activeTab)?.label ?? '管理台'}

+

集中管理渠道、模型、令牌与使用情况。

+
+
+ ${state.token ? '已登录' : '未登录'} + +
+
+ ${state.notice ? `
${state.notice}
` : ''} + ${renderContent()} +
+
+ `; +} + +function render() { + root!.innerHTML = state.token ? renderApp() : renderLogin(); + bindEvents(); +} + +function bindEvents() { + const loginForm = document.querySelector('#login-form'); + if (loginForm) { + loginForm.addEventListener('submit', async (event) => { + event.preventDefault(); + const formData = new FormData(loginForm); + const password = String(formData.get('password') ?? ''); + try { + const result = await apiFetch<{ token: string }>('/api/auth/login', { + method: 'POST', + body: JSON.stringify({ password }) + }); + setToken(result.token); + state.notice = ''; + await loadTab(state.activeTab); + } catch (error) { + setNotice((error as Error).message); + } + }); + } + + const channelForm = document.querySelector('#channel-form'); + if (channelForm) { + channelForm.addEventListener('submit', async (event) => { + event.preventDefault(); + const formData = new FormData(channelForm); + const payload = Object.fromEntries(formData.entries()); + const channelId = String(payload.id ?? '').trim(); + try { + const body = { + ...payload, + weight: Number(payload.weight ?? 1), + rate_limit: Number(payload.rate_limit ?? 0) + } as Record; + if (channelId) { + body.id = channelId; + } else { + delete body.id; + } + await apiFetch('/api/channels', { + method: 'POST', + body: JSON.stringify(body) + }); + channelForm.reset(); + await loadChannels(); + render(); + } catch (error) { + setNotice((error as Error).message); + } + }); + } + + const tokenForm = document.querySelector('#token-form'); + if (tokenForm) { + tokenForm.addEventListener('submit', async (event) => { + event.preventDefault(); + const formData = new FormData(tokenForm); + const payload = Object.fromEntries(formData.entries()); + try { + const result = await apiFetch<{ token: string }>('/api/tokens', { + method: 'POST', + body: JSON.stringify({ + name: payload.name, + quota_total: payload.quota_total ? Number(payload.quota_total) : null + }) + }); + setNotice(`新令牌: ${result.token}`); + tokenForm.reset(); + await loadTokens(); + render(); + } catch (error) { + setNotice((error as Error).message); + } + }); + } + + const settingsForm = document.querySelector('#settings-form'); + if (settingsForm) { + settingsForm.addEventListener('submit', async (event) => { + event.preventDefault(); + const formData = new FormData(settingsForm); + const value = Number(formData.get('log_retention_days')); + try { + await apiFetch('/api/settings', { + method: 'PUT', + body: JSON.stringify({ log_retention_days: value }) + }); + await loadSettings(); + setNotice('设置已更新'); + } catch (error) { + setNotice((error as Error).message); + } + }); + } + + root!.querySelectorAll('[data-tab]').forEach((item) => { + item.addEventListener('click', () => { + const tab = (item as HTMLElement).dataset.tab ?? 'dashboard'; + state.activeTab = tab; + loadTab(tab); + }); + }); + + root!.querySelectorAll('[data-action]').forEach((item) => { + item.addEventListener('click', async () => { + const action = (item as HTMLElement).dataset.action ?? ''; + const id = (item as HTMLElement).dataset.id ?? ''; + try { + if (action === 'logout') { + await apiFetch('/api/auth/logout', { method: 'POST' }).catch(() => null); + setToken(null); + return; + } + if (action === 'channel-test') { + const result = await apiFetch<{ models: Array<{ id: string }> }>(`/api/channels/${id}/test`, { method: 'POST' }); + await loadChannels(); + setNotice(`连通测试完成,模型数 ${result.models?.length ?? 0}`); + render(); + } + if (action === 'channel-delete') { + await apiFetch(`/api/channels/${id}`, { method: 'DELETE' }); + await loadChannels(); + setNotice('渠道已删除'); + render(); + } + if (action === 'channel-toggle') { + const status = (item as HTMLElement).dataset.status === 'active' ? 'disabled' : 'active'; + await apiFetch(`/api/channels/${id}`, { + method: 'PATCH', + body: JSON.stringify({ status }) + }); + await loadChannels(); + setNotice(`渠道已${status === 'active' ? '启用' : '停用'}`); + render(); + } + if (action === 'token-delete') { + await apiFetch(`/api/tokens/${id}`, { method: 'DELETE' }); + await loadTokens(); + setNotice('令牌已删除'); + render(); + } + if (action === 'token-reveal') { + const result = await apiFetch<{ token: string | null }>(`/api/tokens/${id}/reveal`); + setNotice(result.token ? `令牌: ${result.token}` : '未找到令牌'); + } + if (action === 'token-toggle') { + const status = (item as HTMLElement).dataset.status === 'active' ? 'disabled' : 'active'; + await apiFetch(`/api/tokens/${id}`, { + method: 'PATCH', + body: JSON.stringify({ status }) + }); + await loadTokens(); + setNotice(`令牌已${status === 'active' ? '启用' : '停用'}`); + render(); + } + if (action === 'usage-refresh') { + await loadUsage(); + setNotice('日志已刷新'); + render(); + } + } catch (error) { + setNotice((error as Error).message); + } + }); + }); +} + +render(); + +if (state.token) { + loadTab(state.activeTab); +} diff --git a/apps/admin/src/styles.css b/apps/admin/src/styles.css new file mode 100644 index 0000000..8704f65 --- /dev/null +++ b/apps/admin/src/styles.css @@ -0,0 +1,353 @@ +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap'); + +:root { + --bg: #f6f4ef; + --surface: #ffffff; + --surface-alt: #f0ede7; + --text: #1b1b19; + --muted: #6b6760; + --border: #e2ddd3; + --accent: #d4af37; + --accent-dark: #b9972a; + --shadow: 0 12px 30px rgba(27, 27, 25, 0.08); + --radius: 16px; + --radius-sm: 10px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: 'IBM Plex Sans', sans-serif; + background: radial-gradient(circle at top, #ffffff 0%, #f3efe8 40%, #efeae2 100%); + color: var(--text); + min-height: 100vh; +} + +h1, +h2, +h3, +h4 { + font-family: 'Space Grotesk', sans-serif; + margin: 0 0 0.75rem 0; + letter-spacing: -0.02em; +} + +p { + margin: 0 0 1rem 0; + color: var(--muted); + line-height: 1.6; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +select, +textarea { + font-family: inherit; +} + +#app { + min-height: 100vh; +} + +.app-shell { + display: grid; + grid-template-columns: 260px 1fr; + min-height: 100vh; +} + +.sidebar { + padding: 32px 20px; + background: var(--surface); + border-right: 1px solid var(--border); + position: sticky; + top: 0; + height: 100vh; +} + +.brand { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 32px; +} + +.brand span { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.24em; + color: var(--muted); +} + +.nav-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.nav-item { + display: flex; + align-items: center; + padding: 10px 14px; + border-radius: 12px; + border: none; + background: transparent; + text-align: left; + width: 100%; + min-height: 44px; + color: var(--muted); + font-weight: 500; + transition: all 0.2s ease; + cursor: pointer; +} + +.nav-item.active { + background: var(--surface-alt); + color: var(--text); +} + +.nav-item:hover { + background: var(--surface-alt); + color: var(--text); +} + +.main { + padding: 32px 40px 60px; +} + +.top-bar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.top-bar-actions { + display: flex; + gap: 12px; + align-items: center; +} + +.badge { + padding: 4px 10px; + border-radius: 999px; + background: var(--surface-alt); + font-size: 12px; + color: var(--muted); +} + +.grid { + display: grid; + gap: 20px; +} + +.grid.two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.grid.three { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.card { + background: var(--surface); + border-radius: var(--radius); + padding: 20px; + box-shadow: var(--shadow); + border: 1px solid var(--border); +} + +.card h3 { + margin-bottom: 12px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.button { + background: var(--text); + color: #fff; + border: none; + padding: 10px 16px; + border-radius: 10px; + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; + min-height: 44px; +} + +.button.secondary { + background: var(--surface-alt); + color: var(--text); + border: 1px solid var(--border); +} + +.button.ghost { + background: transparent; + color: var(--muted); + border: 1px solid var(--border); +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.button:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 8px 18px rgba(27, 27, 25, 0.12); +} + +.button:focus-visible, +.nav-item:focus-visible { + outline: 2px solid rgba(212, 175, 55, 0.6); + outline-offset: 2px; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.table th, +.table td { + text-align: left; + padding: 10px 12px; + border-bottom: 1px solid var(--border); +} + +.table th { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--muted); +} + +.table tr:hover { + background: #faf8f3; +} + +.form-grid { + display: grid; + gap: 14px; +} + +.form-grid.two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--muted); + display: block; + margin-bottom: 6px; +} + +input, +select, +textarea { + width: 100%; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--border); + background: #fff; + color: var(--text); +} + +input:focus, +select:focus, +textarea:focus { + outline: 2px solid rgba(212, 175, 55, 0.4); + border-color: var(--accent); +} + +.notice { + padding: 12px 16px; + background: #fff8e1; + border: 1px solid #f1e0b3; + border-radius: 12px; + color: #7a5d00; + margin-top: 12px; + font-size: 14px; +} + +.login-card { + max-width: 420px; + margin: 12vh auto 0; + background: var(--surface); + padding: 32px; + border-radius: 20px; + border: 1px solid var(--border); + box-shadow: var(--shadow); +} + +.login-card h1 { + margin-bottom: 8px; +} + +.kpi { + display: flex; + flex-direction: column; + gap: 6px; +} + +.kpi .value { + font-size: 24px; + font-weight: 600; +} + +.mono { + font-family: 'Space Grotesk', sans-serif; + font-size: 13px; +} + +@media (max-width: 1024px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar { + position: static; + height: auto; + border-right: none; + border-bottom: 1px solid var(--border); + } + + .grid.two, + .grid.three, + .form-grid.two { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .main { + padding: 24px; + } + + .top-bar { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } +} + +@media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + } +} diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json new file mode 100644 index 0000000..457e12a --- /dev/null +++ b/apps/admin/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "lib": ["ES2022", "DOM"], + "types": ["vite/client"], + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts new file mode 100644 index 0000000..0b16cf9 --- /dev/null +++ b/apps/admin/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; + +const apiTarget = process.env.VITE_API_TARGET ?? 'http://localhost:8787'; + +export default defineConfig({ + build: { + outDir: 'dist' + }, + server: { + proxy: { + '/api': apiTarget, + '/v1': apiTarget + } + } +}); diff --git a/apps/worker/migrations/0001_init.sql b/apps/worker/migrations/0001_init.sql new file mode 100644 index 0000000..599ce32 --- /dev/null +++ b/apps/worker/migrations/0001_init.sql @@ -0,0 +1,55 @@ +CREATE TABLE IF NOT EXISTS channels ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + base_url TEXT NOT NULL, + api_key TEXT NOT NULL, + weight INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'active', + rate_limit INTEGER DEFAULT 0, + models_json TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS tokens ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + key_hash TEXT NOT NULL, + key_prefix TEXT NOT NULL, + quota_total INTEGER, + quota_used INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'active', + allowed_channels TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS usage_logs ( + id TEXT PRIMARY KEY, + token_id TEXT, + channel_id TEXT, + model TEXT, + request_path TEXT, + total_tokens INTEGER, + prompt_tokens INTEGER, + completion_tokens INTEGER, + cost REAL, + latency_ms INTEGER, + status TEXT, + created_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS usage_logs_created_at ON usage_logs(created_at); + +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS admin_sessions ( + id TEXT PRIMARY KEY, + token_hash TEXT NOT NULL, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL +); diff --git a/apps/worker/migrations/0002_add_token_plain.sql b/apps/worker/migrations/0002_add_token_plain.sql new file mode 100644 index 0000000..af782f2 --- /dev/null +++ b/apps/worker/migrations/0002_add_token_plain.sql @@ -0,0 +1 @@ +ALTER TABLE tokens ADD COLUMN token_plain TEXT; diff --git a/apps/worker/migrations/0003_add_channel_meta.sql b/apps/worker/migrations/0003_add_channel_meta.sql new file mode 100644 index 0000000..23a9c70 --- /dev/null +++ b/apps/worker/migrations/0003_add_channel_meta.sql @@ -0,0 +1,6 @@ +ALTER TABLE channels ADD COLUMN type INTEGER NOT NULL DEFAULT 1; +ALTER TABLE channels ADD COLUMN group_name TEXT; +ALTER TABLE channels ADD COLUMN priority INTEGER NOT NULL DEFAULT 0; +ALTER TABLE channels ADD COLUMN metadata_json TEXT; +ALTER TABLE channels ADD COLUMN test_time INTEGER; +ALTER TABLE channels ADD COLUMN response_time_ms INTEGER; diff --git a/apps/worker/package.json b/apps/worker/package.json new file mode 100644 index 0000000..4a2e4f3 --- /dev/null +++ b/apps/worker/package.json @@ -0,0 +1,18 @@ +{ + "name": "new-api-lite-worker", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "db:migrate": "wrangler d1 migrations apply DB --local" + }, + "dependencies": { + "hono": "^4.2.7" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240222.0", + "typescript": "^5.4.5", + "wrangler": "^3.48.0" + } +} diff --git a/apps/worker/src/db/schema.sql b/apps/worker/src/db/schema.sql new file mode 100644 index 0000000..64e6a69 --- /dev/null +++ b/apps/worker/src/db/schema.sql @@ -0,0 +1,62 @@ +CREATE TABLE IF NOT EXISTS channels ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + base_url TEXT NOT NULL, + api_key TEXT NOT NULL, + type INTEGER NOT NULL DEFAULT 1, + group_name TEXT, + priority INTEGER NOT NULL DEFAULT 0, + weight INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'active', + rate_limit INTEGER DEFAULT 0, + models_json TEXT, + metadata_json TEXT, + test_time INTEGER, + response_time_ms INTEGER, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS tokens ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + key_hash TEXT NOT NULL, + key_prefix TEXT NOT NULL, + token_plain TEXT, + quota_total INTEGER, + quota_used INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'active', + allowed_channels TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS usage_logs ( + id TEXT PRIMARY KEY, + token_id TEXT, + channel_id TEXT, + model TEXT, + request_path TEXT, + total_tokens INTEGER, + prompt_tokens INTEGER, + completion_tokens INTEGER, + cost REAL, + latency_ms INTEGER, + status TEXT, + created_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS usage_logs_created_at ON usage_logs(created_at); + +CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS admin_sessions ( + id TEXT PRIMARY KEY, + token_hash TEXT NOT NULL, + expires_at TEXT NOT NULL, + created_at TEXT NOT NULL +); diff --git a/apps/worker/src/env.ts b/apps/worker/src/env.ts new file mode 100644 index 0000000..fe89a30 --- /dev/null +++ b/apps/worker/src/env.ts @@ -0,0 +1,15 @@ +export type Bindings = { + DB: D1Database; + ADMIN_PASSWORD: string; + SESSION_TTL_HOURS?: string; + LOG_RETENTION_DAYS?: string; + CORS_ORIGIN?: string; + PROXY_RETRY_ROUNDS?: string; + PROXY_RETRY_DELAY_MS?: string; + NEW_API_ADMIN_TOKEN?: string; + NEWAPI_ADMIN_TOKEN?: string; +}; + +export type AppEnv = { + Bindings: Bindings; +}; diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts new file mode 100644 index 0000000..f8b69d2 --- /dev/null +++ b/apps/worker/src/index.ts @@ -0,0 +1,68 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import type { AppEnv } from './env'; +import authRoutes from './routes/auth'; +import channelRoutes from './routes/channels'; +import modelRoutes from './routes/models'; +import tokenRoutes from './routes/tokens'; +import usageRoutes from './routes/usage'; +import dashboardRoutes from './routes/dashboard'; +import settingsRoutes from './routes/settings'; +import proxyRoutes from './routes/proxy'; +import newapiChannelRoutes from './routes/newapiChannels'; +import newapiUserRoutes from './routes/newapiUsers'; +import newapiGroupRoutes from './routes/newapiGroups'; +import { adminAuth } from './middleware/adminAuth'; + +const app = new Hono({ strict: false }); + +app.use('*', logger()); +app.use( + '/api/*', + cors({ + origin: (origin, c) => { + const allowed = c.env.CORS_ORIGIN ?? '*'; + return allowed === '*' ? '*' : allowed.split(',').map((item) => item.trim()); + }, + allowHeaders: ['Content-Type', 'Authorization', 'x-api-key', 'x-admin-token', 'New-Api-User'], + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] + }) +); +app.use( + '/v1/*', + cors({ + origin: '*', + allowHeaders: ['Content-Type', 'Authorization', 'x-api-key'], + allowMethods: ['GET', 'POST', 'OPTIONS'] + }) +); + +app.use('/api/*', async (c, next) => { + if ( + c.req.path === '/api/auth/login' || + c.req.path.startsWith('/api/channel') || + c.req.path.startsWith('/api/user') || + c.req.path.startsWith('/api/group') + ) { + return next(); + } + return adminAuth(c, next); +}); + +app.get('/health', (c) => c.json({ ok: true })); + +app.route('/api/auth', authRoutes); +app.route('/api/channels', channelRoutes); +app.route('/api/models', modelRoutes); +app.route('/api/tokens', tokenRoutes); +app.route('/api/usage', usageRoutes); +app.route('/api/dashboard', dashboardRoutes); +app.route('/api/settings', settingsRoutes); +app.route('/api/channel', newapiChannelRoutes); +app.route('/api/user', newapiUserRoutes); +app.route('/api/group', newapiGroupRoutes); + +app.route('/v1', proxyRoutes); + +export default app; diff --git a/apps/worker/src/middleware/adminAuth.ts b/apps/worker/src/middleware/adminAuth.ts new file mode 100644 index 0000000..5732b17 --- /dev/null +++ b/apps/worker/src/middleware/adminAuth.ts @@ -0,0 +1,36 @@ +import { createMiddleware } from 'hono/factory'; +import { sha256Hex } from '../utils/crypto'; +import { getBearerToken } from '../utils/request'; +import { jsonError } from '../utils/http'; +import type { AppEnv } from '../env'; + +/** + * Validates admin session tokens for management APIs. + */ +export const adminAuth = createMiddleware(async (c, next) => { + const token = getBearerToken(c); + if (!token) { + return jsonError(c, 401, 'admin_token_required', 'admin_token_required'); + } + + const tokenHash = await sha256Hex(token); + const session = await c.env.DB.prepare( + 'SELECT id, expires_at FROM admin_sessions WHERE token_hash = ?' + ) + .bind(tokenHash) + .first(); + + if (!session) { + return jsonError(c, 401, 'invalid_admin_token', 'invalid_admin_token'); + } + + if (new Date(String(session.expires_at)).getTime() <= Date.now()) { + await c.env.DB.prepare('DELETE FROM admin_sessions WHERE id = ?') + .bind(String(session.id)) + .run(); + return jsonError(c, 401, 'admin_session_expired', 'admin_session_expired'); + } + + c.set('adminSessionId', String(session.id)); + await next(); +}); diff --git a/apps/worker/src/middleware/newApiAuth.ts b/apps/worker/src/middleware/newApiAuth.ts new file mode 100644 index 0000000..1c44b45 --- /dev/null +++ b/apps/worker/src/middleware/newApiAuth.ts @@ -0,0 +1,56 @@ +import { createMiddleware } from 'hono/factory'; +import { sha256Hex } from '../utils/crypto'; +import { getBearerToken } from '../utils/request'; +import type { AppEnv } from '../env'; + +function newApiError(c: any, status: number, message: string) { + return c.json( + { + success: false, + message + }, + status + ); +} + +function readNewApiUserId(c: any): string | null { + return c.req.header('New-Api-User') ?? c.req.header('new-api-user') ?? null; +} + +/** + * Validates New API admin tokens or admin sessions. + */ +export const newApiAuth = createMiddleware(async (c, next) => { + const token = getBearerToken(c); + if (!token) { + return newApiError(c, 401, 'unauthorized'); + } + + const envToken = c.env.NEW_API_ADMIN_TOKEN ?? c.env.NEWAPI_ADMIN_TOKEN; + if (envToken && token === envToken) { + c.set('newApiUserId', readNewApiUserId(c)); + await next(); + return; + } + + const tokenHash = await sha256Hex(token); + const session = await c.env.DB.prepare( + 'SELECT id, expires_at FROM admin_sessions WHERE token_hash = ?' + ) + .bind(tokenHash) + .first<{ id: string; expires_at: string }>(); + + if (!session) { + return newApiError(c, 401, 'unauthorized'); + } + + if (new Date(String(session.expires_at)).getTime() <= Date.now()) { + await c.env.DB.prepare('DELETE FROM admin_sessions WHERE id = ?') + .bind(String(session.id)) + .run(); + return newApiError(c, 401, 'session_expired'); + } + + c.set('newApiUserId', readNewApiUserId(c)); + await next(); +}); diff --git a/apps/worker/src/middleware/tokenAuth.ts b/apps/worker/src/middleware/tokenAuth.ts new file mode 100644 index 0000000..7e326da --- /dev/null +++ b/apps/worker/src/middleware/tokenAuth.ts @@ -0,0 +1,56 @@ +import { createMiddleware } from 'hono/factory'; +import { sha256Hex } from '../utils/crypto'; +import { getBearerToken } from '../utils/request'; +import { jsonError } from '../utils/http'; +import { canConsumeQuota, normalizeQuota } from '../services/quota'; +import type { AppEnv } from '../env'; + +export type TokenRecord = { + id: string; + name: string; + quota_total: number | null; + quota_used: number; + status: string; + allowed_channels: string | null; +}; + +/** + * Validates API tokens for the OpenAI-compatible proxy. + */ +export const tokenAuth = createMiddleware(async (c, next) => { + const token = getBearerToken(c); + if (!token) { + return jsonError(c, 401, 'token_required', 'token_required'); + } + + const tokenHash = await sha256Hex(token); + const record = await c.env.DB.prepare( + 'SELECT id, name, quota_total, quota_used, status, allowed_channels FROM tokens WHERE key_hash = ?' + ) + .bind(tokenHash) + .first(); + + if (!record) { + return jsonError(c, 401, 'invalid_token', 'invalid_token'); + } + + if (record.status !== 'active') { + return jsonError(c, 403, 'token_disabled', 'token_disabled'); + } + + const quotaTotal = record.quota_total === null || record.quota_total === undefined + ? null + : Number(record.quota_total); + const quotaUsed = Number(record.quota_used ?? 0); + const normalized = normalizeQuota(Number.isNaN(quotaTotal) ? null : quotaTotal, Number.isNaN(quotaUsed) ? 0 : quotaUsed); + if (!canConsumeQuota(normalized.quotaTotal, normalized.quotaUsed, 1)) { + return jsonError(c, 402, 'quota_exceeded', 'quota_exceeded'); + } + + c.set('tokenRecord', { + ...record, + quota_total: normalized.quotaTotal, + quota_used: normalized.quotaUsed + }); + await next(); +}); diff --git a/apps/worker/src/routes/auth.ts b/apps/worker/src/routes/auth.ts new file mode 100644 index 0000000..1d6a54e --- /dev/null +++ b/apps/worker/src/routes/auth.ts @@ -0,0 +1,53 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { addHours, nowIso } from '../utils/time'; +import { generateToken, sha256Hex } from '../utils/crypto'; +import { jsonError } from '../utils/http'; +import { adminAuth } from '../middleware/adminAuth'; + +const auth = new Hono(); + +/** + * Logs in the admin user with a password. + */ +auth.post('/login', async (c) => { + const body = await c.req.json().catch(() => null); + if (!body?.password) { + return jsonError(c, 400, 'password_required', 'password_required'); + } + + if (body.password !== c.env.ADMIN_PASSWORD) { + return jsonError(c, 401, 'invalid_password', 'invalid_password'); + } + + const rawToken = generateToken('admin_'); + const tokenHash = await sha256Hex(rawToken); + const ttlHours = Number(c.env.SESSION_TTL_HOURS ?? '12'); + const expiresAt = addHours(new Date(), Number.isNaN(ttlHours) ? 12 : ttlHours).toISOString(); + + await c.env.DB.prepare( + 'INSERT INTO admin_sessions (id, token_hash, expires_at, created_at) VALUES (?, ?, ?, ?)' + ) + .bind(crypto.randomUUID(), tokenHash, expiresAt, nowIso()) + .run(); + + return c.json({ + token: rawToken, + expires_at: expiresAt + }); +}); + +/** + * Invalidates the current admin session token. + */ +auth.post('/logout', adminAuth, async (c) => { + const sessionId = c.get('adminSessionId'); + if (sessionId) { + await c.env.DB.prepare('DELETE FROM admin_sessions WHERE id = ?') + .bind(String(sessionId)) + .run(); + } + return c.json({ ok: true }); +}); + +export default auth; diff --git a/apps/worker/src/routes/channels.ts b/apps/worker/src/routes/channels.ts new file mode 100644 index 0000000..3ec3811 --- /dev/null +++ b/apps/worker/src/routes/channels.ts @@ -0,0 +1,157 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { nowIso } from '../utils/time'; +import { jsonError } from '../utils/http'; +import { safeJsonParse } from '../utils/json'; +import { generateToken } from '../utils/crypto'; +import { normalizeBaseUrl } from '../utils/url'; + +const channels = new Hono(); + +/** + * Resolves a channel id from request payload. + * + * Args: + * body: Request payload. + * + * Returns: + * Channel id if provided. + */ +function resolveChannelId(body: any): string | null { + const candidate = body?.id ?? body?.channel_id ?? body?.channelId; + if (!candidate) { + return null; + } + const normalized = String(candidate).trim(); + return normalized.length > 0 ? normalized : null; +} + +/** + * Lists all channels. + */ +channels.get('/', async (c) => { + const result = await c.env.DB.prepare('SELECT * FROM channels ORDER BY created_at DESC').all(); + return c.json({ channels: result.results ?? [] }); +}); + +/** + * Creates a new channel. + */ +channels.post('/', async (c) => { + const body = await c.req.json().catch(() => null); + if (!body?.name || !body?.base_url || !body?.api_key) { + return jsonError(c, 400, 'missing_fields', 'missing_fields'); + } + + const requestedId = resolveChannelId(body); + if (requestedId) { + const exists = await c.env.DB.prepare('SELECT id FROM channels WHERE id = ?').bind(requestedId).first(); + if (exists) { + return jsonError(c, 409, 'channel_id_exists', 'channel_id_exists'); + } + } + + const id = requestedId ?? generateToken('ch_'); + const now = nowIso(); + + await c.env.DB.prepare( + 'INSERT INTO channels (id, name, base_url, api_key, weight, status, rate_limit, models_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + ) + .bind( + id, + body.name, + normalizeBaseUrl(body.base_url), + body.api_key, + Number(body.weight ?? 1), + body.status ?? 'active', + body.rate_limit ?? 0, + JSON.stringify(body.models ?? []), + now, + now + ) + .run(); + + return c.json({ id }); +}); + +/** + * Updates a channel. + */ +channels.patch('/:id', async (c) => { + const body = await c.req.json().catch(() => null); + const id = c.req.param('id'); + if (!body) { + return jsonError(c, 400, 'missing_body', 'missing_body'); + } + + const current = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first(); + if (!current) { + return jsonError(c, 404, 'channel_not_found', 'channel_not_found'); + } + + const models = body.models ?? safeJsonParse(current.models_json, []); + + await c.env.DB.prepare( + 'UPDATE channels SET name = ?, base_url = ?, api_key = ?, weight = ?, status = ?, rate_limit = ?, models_json = ?, updated_at = ? WHERE id = ?' + ) + .bind( + body.name ?? current.name, + normalizeBaseUrl(body.base_url ?? current.base_url), + body.api_key ?? current.api_key, + Number(body.weight ?? current.weight ?? 1), + body.status ?? current.status, + body.rate_limit ?? current.rate_limit ?? 0, + JSON.stringify(models), + nowIso(), + id + ) + .run(); + + return c.json({ ok: true }); +}); + +/** + * Deletes a channel. + */ +channels.delete('/:id', async (c) => { + const id = c.req.param('id'); + await c.env.DB.prepare('DELETE FROM channels WHERE id = ?').bind(id).run(); + return c.json({ ok: true }); +}); + +/** + * Tests channel connectivity and updates model list. + */ +channels.post('/:id/test', async (c) => { + const id = c.req.param('id'); + const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first(); + if (!channel) { + return jsonError(c, 404, 'channel_not_found', 'channel_not_found'); + } + + const target = `${normalizeBaseUrl(String(channel.base_url))}/v1/models`; + const response = await fetch(target, { + method: 'GET', + headers: { + Authorization: `Bearer ${channel.api_key}`, + 'x-api-key': String(channel.api_key), + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + await c.env.DB.prepare('UPDATE channels SET status = ?, updated_at = ? WHERE id = ?') + .bind('error', nowIso(), id) + .run(); + return jsonError(c, 502, 'channel_unreachable', 'channel_unreachable'); + } + + const data = await response.json().catch(() => ({ data: [] })); + await c.env.DB.prepare('UPDATE channels SET status = ?, models_json = ?, updated_at = ? WHERE id = ?') + .bind('active', JSON.stringify(data), nowIso(), id) + .run(); + + return c.json({ ok: true, models: data?.data ?? [] }); +}); + +export default channels; diff --git a/apps/worker/src/routes/dashboard.ts b/apps/worker/src/routes/dashboard.ts new file mode 100644 index 0000000..1bc4582 --- /dev/null +++ b/apps/worker/src/routes/dashboard.ts @@ -0,0 +1,66 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; + +const dashboard = new Hono(); + +function buildDateFilters(query: Record) { + let sql = ' WHERE 1=1'; + const params: Array = []; + if (query.from) { + sql += ' AND created_at >= ?'; + params.push(query.from); + } + if (query.to) { + sql += ' AND created_at <= ?'; + params.push(query.to); + } + return { sql, params }; +} + +/** + * Returns aggregated usage metrics. + */ +dashboard.get('/', async (c) => { + const query = c.req.query(); + const { sql, params } = buildDateFilters(query); + + const summary = await c.env.DB.prepare( + `SELECT COUNT(*) as total_requests, COALESCE(SUM(total_tokens), 0) as total_tokens, COALESCE(AVG(latency_ms), 0) as avg_latency, COALESCE(SUM(CASE WHEN status != 'ok' THEN 1 ELSE 0 END), 0) as total_errors FROM usage_logs${sql}` + ) + .bind(...params) + .first(); + + const byDay = await c.env.DB.prepare( + `SELECT substr(created_at, 1, 10) as day, COUNT(*) as requests, COALESCE(SUM(total_tokens), 0) as tokens FROM usage_logs${sql} GROUP BY day ORDER BY day DESC LIMIT 30` + ) + .bind(...params) + .all(); + + const byModel = await c.env.DB.prepare( + `SELECT model, COUNT(*) as requests, COALESCE(SUM(total_tokens), 0) as tokens FROM usage_logs${sql} GROUP BY model ORDER BY requests DESC LIMIT 20` + ) + .bind(...params) + .all(); + + const byChannel = await c.env.DB.prepare( + `SELECT channels.id as channel_id, channels.name as channel_name, COUNT(usage_logs.id) as requests, COALESCE(SUM(usage_logs.total_tokens), 0) as tokens FROM usage_logs LEFT JOIN channels ON channels.id = usage_logs.channel_id${sql} GROUP BY channels.id, channels.name ORDER BY requests DESC LIMIT 20` + ) + .bind(...params) + .all(); + + const byToken = await c.env.DB.prepare( + `SELECT tokens.id as token_id, tokens.name as token_name, COUNT(usage_logs.id) as requests, COALESCE(SUM(usage_logs.total_tokens), 0) as tokens FROM usage_logs LEFT JOIN tokens ON tokens.id = usage_logs.token_id${sql} GROUP BY tokens.id, tokens.name ORDER BY requests DESC LIMIT 20` + ) + .bind(...params) + .all(); + + return c.json({ + summary: summary ?? { total_requests: 0, total_tokens: 0, avg_latency: 0, total_errors: 0 }, + byDay: byDay.results ?? [], + byModel: byModel.results ?? [], + byChannel: byChannel.results ?? [], + byToken: byToken.results ?? [] + }); +}); + +export default dashboard; diff --git a/apps/worker/src/routes/models.ts b/apps/worker/src/routes/models.ts new file mode 100644 index 0000000..a207d4a --- /dev/null +++ b/apps/worker/src/routes/models.ts @@ -0,0 +1,26 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { extractModels } from '../services/channels'; + +const models = new Hono(); + +/** + * Returns aggregated models from all channels. + */ +models.get('/', async (c) => { + const result = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?').bind('active').all(); + const entries = (result.results ?? []).flatMap((channel) => extractModels(channel as any)); + + const map = new Map(); + for (const entry of entries) { + const existing = map.get(entry.id) ?? { id: entry.id, channels: [] }; + existing.channels.push({ id: entry.channelId, name: entry.channelName }); + map.set(entry.id, existing); + } + + return c.json({ + models: Array.from(map.values()) + }); +}); + +export default models; diff --git a/apps/worker/src/routes/newapiChannels.ts b/apps/worker/src/routes/newapiChannels.ts new file mode 100644 index 0000000..6ed8115 --- /dev/null +++ b/apps/worker/src/routes/newapiChannels.ts @@ -0,0 +1,466 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { newApiAuth } from '../middleware/newApiAuth'; +import { nowIso } from '../utils/time'; +import { generateToken } from '../utils/crypto'; +import { normalizeBaseUrl } from '../utils/url'; +import { + extractModelIds, + mergeMetadata, + modelsToJson, + normalizeChannelInput, + normalizeModelsInput, + normalizeBaseUrlInput, + toNewApiChannel +} from '../services/newapi'; +import { safeJsonParse } from '../utils/json'; + +const newapi = new Hono({ strict: false }); +newapi.use('*', newApiAuth); + +function success(c: any, data?: any, message = '') { + return c.json( + { + success: true, + message, + ...(data !== undefined ? { data } : {}) + }, + 200 + ); +} + +function failure(c: any, status: number, message: string) { + return c.json( + { + success: false, + message + }, + status + ); +} + +function normalizePage(value: string | undefined | null, fallback: number): number { + const parsed = Number(value); + if (Number.isNaN(parsed) || parsed <= 0) { + return fallback; + } + return Math.floor(parsed); +} + +function normalizePageSize(value: string | undefined | null, fallback: number): number { + const parsed = Number(value); + if (Number.isNaN(parsed) || parsed <= 0) { + return fallback; + } + return Math.min(200, Math.floor(parsed)); +} + +function normalizeStatusFilter(value: string | undefined | null): string | null { + if (!value) { + return null; + } + const normalized = value.trim().toLowerCase(); + if (normalized === 'all') { + return null; + } + if (['enabled', 'enable', '1', 'active'].includes(normalized)) { + return 'active'; + } + if (['disabled', 'disable', '0', '2', 'inactive'].includes(normalized)) { + return 'disabled'; + } + return null; +} + +function normalizeBoolean(value: string | undefined | null): boolean { + if (!value) { + return false; + } + const normalized = value.trim().toLowerCase(); + return normalized === '1' || normalized === 'true' || normalized === 'yes'; +} + +async function fetchModels(baseUrl: string, apiKey: string) { + const target = `${normalizeBaseUrl(baseUrl)}/v1/models`; + const start = Date.now(); + const response = await fetch(target, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'x-api-key': apiKey, + 'Content-Type': 'application/json' + } + }); + + const elapsed = Date.now() - start; + if (!response.ok) { + return { ok: false, elapsed, models: [] as string[] }; + } + + const data = await response.json().catch(() => ({ data: [] })); + const models = normalizeModelsInput(data?.data ?? data); + return { ok: true, elapsed, models }; +} + +async function updateChannelTestResult( + c: any, + id: string, + ok: boolean, + elapsed: number, + models?: string[] +) { + const now = Math.floor(Date.now() / 1000); + const status = ok ? 'active' : 'error'; + const modelsJson = models ? modelsToJson(models) : undefined; + const sql = modelsJson + ? 'UPDATE channels SET status = ?, models_json = ?, test_time = ?, response_time_ms = ?, updated_at = ? WHERE id = ?' + : 'UPDATE channels SET status = ?, test_time = ?, response_time_ms = ?, updated_at = ? WHERE id = ?'; + + const stmt = c.env.DB.prepare(sql); + if (modelsJson) { + await stmt.bind(status, modelsJson, now, elapsed, nowIso(), id).run(); + } else { + await stmt.bind(status, now, elapsed, nowIso(), id).run(); + } +} + +async function loadChannels(c: any, where: string[], bindings: Array) { + const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; + const rows = await c.env.DB.prepare( + `SELECT * FROM channels ${whereSql} ORDER BY priority DESC, created_at DESC` + ) + .bind(...bindings) + .all(); + return rows.results ?? []; +} + +newapi.get('/', async (c) => { + const page = normalizePage(c.req.query('p') ?? c.req.query('page'), 1); + const pageSize = normalizePageSize(c.req.query('page_size') ?? c.req.query('limit'), 20); + const idSort = normalizeBoolean(c.req.query('id_sort')); + const statusFilter = normalizeStatusFilter(c.req.query('status')); + const typeFilter = c.req.query('type'); + + const where: string[] = []; + const bindings: Array = []; + + if (statusFilter) { + where.push('status = ?'); + bindings.push(statusFilter); + } + if (typeFilter) { + where.push('type = ?'); + bindings.push(Number(typeFilter)); + } + + const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; + const orderSql = idSort ? 'ORDER BY id ASC' : 'ORDER BY priority DESC, created_at DESC'; + const offset = (page - 1) * pageSize; + + const totalRow = await c.env.DB.prepare( + `SELECT COUNT(*) as count FROM channels ${whereSql}` + ) + .bind(...bindings) + .first<{ count: number }>(); + + const rows = await c.env.DB.prepare( + `SELECT * FROM channels ${whereSql} ${orderSql} LIMIT ? OFFSET ?` + ) + .bind(...bindings, pageSize, offset) + .all(); + + const counts = await c.env.DB.prepare( + `SELECT type, COUNT(*) as count FROM channels ${whereSql} GROUP BY type` + ) + .bind(...bindings) + .all(); + + const typeCounts: Record = { all: Number(totalRow?.count ?? 0) }; + for (const entry of counts.results ?? []) { + typeCounts[String(entry.type)] = Number(entry.count ?? 0); + } + + const items = (rows.results ?? []).map((row) => { + const channel = toNewApiChannel(row as any); + delete (channel as any).key; + return channel; + }); + + return success(c, { + items, + total: Number(totalRow?.count ?? 0), + page, + page_size: pageSize, + type_counts: typeCounts + }); +}); + +newapi.get('/search', async (c) => { + const page = normalizePage(c.req.query('p') ?? c.req.query('page'), 1); + const pageSize = normalizePageSize(c.req.query('page_size') ?? c.req.query('limit'), 20); + const statusFilter = normalizeStatusFilter(c.req.query('status')); + const typeFilter = c.req.query('type'); + const keyword = c.req.query('keyword') ?? ''; + const group = c.req.query('group') ?? ''; + const model = c.req.query('model') ?? ''; + + const where: string[] = []; + const bindings: Array = []; + + if (statusFilter) { + where.push('status = ?'); + bindings.push(statusFilter); + } + if (typeFilter) { + where.push('type = ?'); + bindings.push(Number(typeFilter)); + } + + const rows = await loadChannels(c, where, bindings); + const filtered = rows.filter((row) => { + const channel = row as any; + const models = extractModelIds(channel as any); + if (keyword && !String(channel.name).includes(keyword) && !String(channel.id).includes(keyword)) { + return false; + } + if (group && !String(channel.group_name ?? '').includes(group)) { + return false; + } + if (model && !models.includes(model)) { + return false; + } + return true; + }); + + const total = filtered.length; + const offset = (page - 1) * pageSize; + const items = filtered.slice(offset, offset + pageSize).map((row) => { + const channel = toNewApiChannel(row as any); + delete (channel as any).key; + return channel; + }); + + return success(c, { + items, + total, + page, + page_size: pageSize + }); +}); + +newapi.get('/models', async (c) => { + const result = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?') + .bind('active') + .all(); + const models = new Set(); + for (const row of result.results ?? []) { + for (const id of extractModelIds(row as any)) { + models.add(id); + } + } + const data = Array.from(models).map((id) => ({ id, name: id })); + return success(c, data); +}); + +newapi.get('/models_enabled', async (c) => { + const result = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?') + .bind('active') + .all(); + const models = new Set(); + for (const row of result.results ?? []) { + for (const id of extractModelIds(row as any)) { + models.add(id); + } + } + const data = Array.from(models).map((id) => ({ id, name: id })); + return success(c, data); +}); + +newapi.post('/', async (c) => { + const body = await c.req.json().catch(() => null); + if (!body) { + return failure(c, 400, '请求体为空'); + } + + const mode = body.mode ?? 'single'; + if (mode !== 'single') { + return failure(c, 400, '仅支持单渠道添加'); + } + + const payload = body.channel ?? body; + const parsed = normalizeChannelInput(payload); + if (!parsed.name || !parsed.base_url || !parsed.api_key) { + return failure(c, 400, '缺少必要参数'); + } + + const existingId = parsed.id ?? generateToken('ch_'); + const exists = await c.env.DB.prepare('SELECT id FROM channels WHERE id = ?').bind(existingId).first(); + if (exists) { + return failure(c, 409, '渠道已存在'); + } + + const now = nowIso(); + const baseUrl = normalizeBaseUrlInput(parsed.base_url); + await c.env.DB.prepare( + 'INSERT INTO channels (id, name, base_url, api_key, weight, status, rate_limit, models_json, type, group_name, priority, metadata_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + ) + .bind( + existingId, + parsed.name, + baseUrl ?? normalizeBaseUrl(String(parsed.base_url)), + parsed.api_key, + parsed.weight ?? 1, + parsed.status ?? 'active', + parsed.rate_limit ?? 0, + parsed.models_json, + parsed.type ?? 1, + parsed.group_name, + parsed.priority ?? 0, + parsed.metadata_json, + now, + now + ) + .run(); + + return success(c); +}); + +newapi.put('/', async (c) => { + const body = await c.req.json().catch(() => null); + if (!body) { + return failure(c, 400, '请求体为空'); + } + const payload = body.channel ?? body; + const id = payload?.id ?? body?.id; + if (!id) { + return failure(c, 400, '缺少渠道ID'); + } + + const current = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(String(id)).first(); + if (!current) { + return failure(c, 404, '渠道不存在'); + } + + const parsed = normalizeChannelInput(payload); + const models = parsed.models.length > 0 ? parsed.models : extractModelIds(current as any); + const mergedMetadata = mergeMetadata(current.metadata_json, parsed.metadata_json); + const nextBaseUrl = normalizeBaseUrlInput(parsed.base_url ?? current.base_url) ?? String(current.base_url); + + await c.env.DB.prepare( + 'UPDATE channels SET name = ?, base_url = ?, api_key = ?, weight = ?, status = ?, rate_limit = ?, models_json = ?, type = ?, group_name = ?, priority = ?, metadata_json = ?, updated_at = ? WHERE id = ?' + ) + .bind( + parsed.name ?? current.name, + nextBaseUrl, + parsed.api_key ?? current.api_key, + parsed.weight ?? current.weight ?? 1, + parsed.status ?? current.status, + parsed.rate_limit ?? current.rate_limit ?? 0, + modelsToJson(models), + parsed.type ?? current.type ?? 1, + parsed.group_name ?? current.group_name, + parsed.priority ?? current.priority ?? 0, + mergedMetadata, + nowIso(), + String(id) + ) + .run(); + + return success(c); +}); + +newapi.delete('/:id', async (c) => { + const id = c.req.param('id'); + const existing = await c.env.DB.prepare('SELECT id FROM channels WHERE id = ?').bind(id).first(); + if (!existing) { + return failure(c, 404, '渠道不存在'); + } + await c.env.DB.prepare('DELETE FROM channels WHERE id = ?').bind(id).run(); + return success(c); +}); + +newapi.get('/test/:id', async (c) => { + const id = c.req.param('id'); + const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first(); + if (!channel) { + return failure(c, 404, '渠道不存在'); + } + + const result = await fetchModels(String(channel.base_url), String(channel.api_key)); + if (!result.ok) { + await updateChannelTestResult(c, id, false, result.elapsed); + return failure(c, 502, '渠道测试失败'); + } + + await updateChannelTestResult(c, id, true, result.elapsed); + + return success(c, undefined, '测试成功'); +}); + +newapi.post('/test', async (c) => { + const body = await c.req.json().catch(() => null); + const id = body?.id; + if (!id) { + return failure(c, 400, '缺少渠道ID'); + } + const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(String(id)).first(); + if (!channel) { + return failure(c, 404, '渠道不存在'); + } + const result = await fetchModels(String(channel.base_url), String(channel.api_key)); + if (!result.ok) { + await updateChannelTestResult(c, String(id), false, result.elapsed); + return failure(c, 502, '渠道测试失败'); + } + await updateChannelTestResult(c, String(id), true, result.elapsed); + return success(c, undefined, '测试成功'); +}); + +newapi.get('/fetch_models/:id', async (c) => { + const id = c.req.param('id'); + const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first(); + if (!channel) { + return failure(c, 404, '渠道不存在'); + } + + const result = await fetchModels(String(channel.base_url), String(channel.api_key)); + if (!result.ok) { + await updateChannelTestResult(c, id, false, result.elapsed); + return failure(c, 502, '获取模型失败'); + } + + await updateChannelTestResult(c, id, true, result.elapsed, result.models); + + return success(c, result.models); +}); + +newapi.post('/fetch_models', async (c) => { + const body = await c.req.json().catch(() => null); + if (!body?.base_url || !body?.key) { + return failure(c, 400, '缺少必要参数'); + } + + const result = await fetchModels(String(body.base_url), String(body.key)); + if (!result.ok) { + return failure(c, 502, '获取模型失败'); + } + + return success(c, result.models); +}); + +newapi.get('/:id', async (c) => { + const id = c.req.param('id'); + const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first(); + if (!channel) { + return failure(c, 404, '渠道不存在'); + } + const metadata = safeJsonParse>(channel.metadata_json, {}); + const output = toNewApiChannel(channel as any); + return success(c, { + ...output, + model_mapping: metadata.model_mapping ?? '{}', + channel_info: metadata.channel_info ?? { is_multi_key: false, multi_key_mode: 'random' } + }); +}); + +export default newapi; diff --git a/apps/worker/src/routes/newapiGroups.ts b/apps/worker/src/routes/newapiGroups.ts new file mode 100644 index 0000000..17e083f --- /dev/null +++ b/apps/worker/src/routes/newapiGroups.ts @@ -0,0 +1,37 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { newApiAuth } from '../middleware/newApiAuth'; + +const groups = new Hono({ strict: false }); +groups.use('*', newApiAuth); + +function collectGroups(rows: Array<{ group_name?: string | null }>): string[] { + const names = new Set(); + for (const row of rows) { + const raw = row.group_name; + if (!raw) { + continue; + } + raw + .split(',') + .map((item) => item.trim()) + .filter((item) => item.length > 0) + .forEach((item) => names.add(item)); + } + if (names.size === 0) { + names.add('default'); + } + return Array.from(names).sort((a, b) => a.localeCompare(b)); +} + +groups.get('/', async (c) => { + const result = await c.env.DB.prepare('SELECT group_name FROM channels').all(); + const data = collectGroups((result.results ?? []) as Array<{ group_name?: string | null }>); + return c.json({ + success: true, + message: '', + data + }); +}); + +export default groups; diff --git a/apps/worker/src/routes/newapiUsers.ts b/apps/worker/src/routes/newapiUsers.ts new file mode 100644 index 0000000..75d60ba --- /dev/null +++ b/apps/worker/src/routes/newapiUsers.ts @@ -0,0 +1,27 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { newApiAuth } from '../middleware/newApiAuth'; +import { extractModelIds } from '../services/newapi'; + +const users = new Hono({ strict: false }); +users.use('*', newApiAuth); + +users.get('/models', async (c) => { + const result = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?') + .bind('active') + .all(); + const models = new Set(); + for (const row of result.results ?? []) { + for (const id of extractModelIds(row as any)) { + models.add(id); + } + } + const data = Array.from(models).map((id) => ({ id, name: id })); + return c.json({ + success: true, + message: '', + data + }); +}); + +export default users; diff --git a/apps/worker/src/routes/proxy.ts b/apps/worker/src/routes/proxy.ts new file mode 100644 index 0000000..743c2d7 --- /dev/null +++ b/apps/worker/src/routes/proxy.ts @@ -0,0 +1,172 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { tokenAuth, type TokenRecord } from '../middleware/tokenAuth'; +import { createWeightedOrder, extractModels, type ChannelRecord } from '../services/channels'; +import { recordUsage } from '../services/usage'; +import { jsonError } from '../utils/http'; +import { safeJsonParse } from '../utils/json'; +import { normalizeBaseUrl } from '../utils/url'; + +const proxy = new Hono(); + +function channelSupportsModel(channel: ChannelRecord, model?: string | null): boolean { + if (!model) { + return true; + } + const models = extractModels(channel); + return models.some((entry) => entry.id === model); +} + +function filterAllowedChannels(channels: ChannelRecord[], tokenRecord: TokenRecord): ChannelRecord[] { + const allowed = safeJsonParse(tokenRecord.allowed_channels, null); + if (!allowed || allowed.length === 0) { + return channels; + } + const allowedSet = new Set(allowed); + return channels.filter((channel) => allowedSet.has(channel.id)); +} + +/** + * Determines whether a response status should be retried. + * + * Args: + * status: HTTP response status code. + * + * Returns: + * True if the status is retryable. + */ +function isRetryableStatus(status: number): boolean { + return status === 408 || status === 429 || status >= 500; +} + +/** + * Waits before the next retry round. + * + * Args: + * ms: Delay in milliseconds. + * + * Returns: + * Promise resolved after delay. + */ +async function sleep(ms: number): Promise { + if (ms <= 0) { + return; + } + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * OpenAI-compatible proxy handler. + */ +proxy.all('/*', tokenAuth, async (c) => { + const tokenRecord = c.get('tokenRecord') as TokenRecord; + const requestText = await c.req.text(); + const parsedBody = requestText ? safeJsonParse(requestText, null) : null; + const model = parsedBody?.model ?? null; + const isStream = parsedBody?.stream === true; + + const channelResult = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?') + .bind('active') + .all(); + const activeChannels = (channelResult.results ?? []) as ChannelRecord[]; + const allowedChannels = filterAllowedChannels(activeChannels, tokenRecord); + const modelChannels = allowedChannels.filter((channel) => channelSupportsModel(channel, model)); + const candidates = modelChannels.length > 0 ? modelChannels : allowedChannels; + + if (candidates.length === 0) { + return jsonError(c, 503, 'no_available_channels', 'no_available_channels'); + } + + const ordered = createWeightedOrder(candidates); + const targetPath = c.req.path; + const retryRounds = Math.max(1, Number(c.env.PROXY_RETRY_ROUNDS ?? '1')); + const retryDelayMs = Math.max(0, Number(c.env.PROXY_RETRY_DELAY_MS ?? '200')); + let lastResponse: Response | null = null; + let lastChannel: ChannelRecord | null = null; + const start = Date.now(); + let selectedChannel: ChannelRecord | null = null; + let usagePayload: any = null; + + let round = 0; + while (round < retryRounds && !selectedChannel) { + let shouldRetry = false; + for (const channel of ordered) { + lastChannel = channel; + const target = `${normalizeBaseUrl(channel.base_url)}${targetPath}${c.req.url.includes('?') ? `?${c.req.url.split('?')[1]}` : ''}`; + const headers = new Headers(c.req.header()); + headers.set('Authorization', `Bearer ${channel.api_key}`); + headers.set('x-api-key', String(channel.api_key)); + headers.delete('host'); + headers.delete('content-length'); + + try { + const response = await fetch(target, { + method: c.req.method, + headers, + body: requestText || undefined + }); + + lastResponse = response; + if (response.ok) { + selectedChannel = channel; + if (!isStream && response.headers.get('content-type')?.includes('application/json')) { + const data = await response.clone().json().catch(() => null); + usagePayload = data?.usage ?? null; + } + break; + } + + if (isRetryableStatus(response.status)) { + shouldRetry = true; + } + } catch { + lastResponse = null; + shouldRetry = true; + } + } + + if (selectedChannel || !shouldRetry) { + break; + } + + round += 1; + if (round < retryRounds) { + await sleep(retryDelayMs); + } + } + + const latencyMs = Date.now() - start; + + if (!lastResponse) { + await recordUsage(c.env.DB, { + tokenId: tokenRecord.id, + model, + requestPath: targetPath, + totalTokens: 0, + latencyMs, + status: 'error' + }); + return jsonError(c, 502, 'upstream_unavailable', 'upstream_unavailable'); + } + + const channelForUsage = selectedChannel ?? lastChannel; + if (channelForUsage) { + const totalTokens = Number(usagePayload?.total_tokens ?? 0); + await recordUsage(c.env.DB, { + tokenId: tokenRecord.id, + channelId: channelForUsage.id, + model, + requestPath: targetPath, + totalTokens: Number.isNaN(totalTokens) ? 0 : totalTokens, + promptTokens: Number(usagePayload?.prompt_tokens ?? 0), + completionTokens: Number(usagePayload?.completion_tokens ?? 0), + cost: 0, + latencyMs, + status: lastResponse.ok ? 'ok' : 'error' + }); + } + + return lastResponse; +}); + +export default proxy; diff --git a/apps/worker/src/routes/settings.ts b/apps/worker/src/routes/settings.ts new file mode 100644 index 0000000..5553e3a --- /dev/null +++ b/apps/worker/src/routes/settings.ts @@ -0,0 +1,33 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { getRetentionDays, setRetentionDays } from '../services/settings'; +import { jsonError } from '../utils/http'; + +const settings = new Hono(); + +/** + * Returns settings values. + */ +settings.get('/', async (c) => { + const fallback = Number(c.env.LOG_RETENTION_DAYS ?? '30'); + const retention = await getRetentionDays(c.env.DB, Number.isNaN(fallback) ? 30 : fallback); + return c.json({ log_retention_days: retention }); +}); + +/** + * Updates settings values. + */ +settings.put('/', async (c) => { + const body = await c.req.json().catch(() => null); + if (!body?.log_retention_days) { + return jsonError(c, 400, 'log_retention_days_required', 'log_retention_days_required'); + } + const days = Number(body.log_retention_days); + if (Number.isNaN(days) || days < 1) { + return jsonError(c, 400, 'invalid_log_retention_days', 'invalid_log_retention_days'); + } + await setRetentionDays(c.env.DB, days); + return c.json({ ok: true }); +}); + +export default settings; diff --git a/apps/worker/src/routes/tokens.ts b/apps/worker/src/routes/tokens.ts new file mode 100644 index 0000000..1ed68a6 --- /dev/null +++ b/apps/worker/src/routes/tokens.ts @@ -0,0 +1,127 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { generateToken, sha256Hex } from '../utils/crypto'; +import { nowIso } from '../utils/time'; +import { jsonError } from '../utils/http'; +import { safeJsonParse } from '../utils/json'; + +const tokens = new Hono(); + +/** + * Lists API tokens. + */ +tokens.get('/', async (c) => { + const result = await c.env.DB + .prepare( + 'SELECT id, name, key_prefix, quota_total, quota_used, status, allowed_channels, created_at, updated_at FROM tokens ORDER BY created_at DESC' + ) + .all(); + return c.json({ tokens: result.results ?? [] }); +}); + +/** + * Creates a new API token. + */ +tokens.post('/', async (c) => { + const body = await c.req.json().catch(() => null); + if (!body?.name) { + return jsonError(c, 400, 'name_required', 'name_required'); + } + + const rawToken = generateToken('sk-'); + const tokenHash = await sha256Hex(rawToken); + const id = crypto.randomUUID(); + const now = nowIso(); + const keyPrefix = rawToken.slice(0, 8); + const quotaTotal = body.quota_total === null || body.quota_total === undefined + ? null + : Number(body.quota_total); + + await c.env.DB + .prepare( + 'INSERT INTO tokens (id, name, key_hash, key_prefix, token_plain, quota_total, quota_used, status, allowed_channels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + ) + .bind( + id, + body.name, + tokenHash, + keyPrefix, + rawToken, + Number.isNaN(quotaTotal) ? null : quotaTotal, + 0, + body.status ?? 'active', + JSON.stringify(body.allowed_channels ?? null), + now, + now + ) + .run(); + + return c.json({ + id, + token: rawToken + }); +}); + +/** + * Updates an API token. + */ +tokens.patch('/:id', async (c) => { + const id = c.req.param('id'); + const body = await c.req.json().catch(() => null); + if (!body) { + return jsonError(c, 400, 'missing_body', 'missing_body'); + } + + const existing = await c.env.DB.prepare('SELECT * FROM tokens WHERE id = ?').bind(id).first(); + if (!existing) { + return jsonError(c, 404, 'token_not_found', 'token_not_found'); + } + + const existingAllowed = safeJsonParse(existing.allowed_channels, null); + const quotaTotalUpdate = body.quota_total === null || body.quota_total === undefined + ? existing.quota_total + : Number(body.quota_total); + const quotaUsedUpdate = body.quota_used === null || body.quota_used === undefined + ? existing.quota_used + : Number(body.quota_used); + + await c.env.DB + .prepare( + 'UPDATE tokens SET name = ?, quota_total = ?, quota_used = ?, status = ?, allowed_channels = ?, updated_at = ? WHERE id = ?' + ) + .bind( + body.name ?? existing.name, + Number.isNaN(quotaTotalUpdate) ? existing.quota_total : quotaTotalUpdate, + Number.isNaN(quotaUsedUpdate) ? existing.quota_used : quotaUsedUpdate, + body.status ?? existing.status, + JSON.stringify(body.allowed_channels ?? existingAllowed ?? null), + nowIso(), + id + ) + .run(); + + return c.json({ ok: true }); +}); + +/** + * Reveals a stored API token. + */ +tokens.get('/:id/reveal', async (c) => { + const id = c.req.param('id'); + const record = await c.env.DB.prepare('SELECT token_plain FROM tokens WHERE id = ?').bind(id).first(); + if (!record) { + return jsonError(c, 404, 'token_not_found', 'token_not_found'); + } + return c.json({ token: record.token_plain ?? null }); +}); + +/** + * Deletes an API token. + */ +tokens.delete('/:id', async (c) => { + const id = c.req.param('id'); + await c.env.DB.prepare('DELETE FROM tokens WHERE id = ?').bind(id).run(); + return c.json({ ok: true }); +}); + +export default tokens; diff --git a/apps/worker/src/routes/usage.ts b/apps/worker/src/routes/usage.ts new file mode 100644 index 0000000..4b39325 --- /dev/null +++ b/apps/worker/src/routes/usage.ts @@ -0,0 +1,49 @@ +import { Hono } from 'hono'; +import type { AppEnv } from '../env'; +import { getRetentionDays } from '../services/settings'; +import { pruneUsageLogs } from '../services/usage'; + +const usage = new Hono(); + +/** + * Lists usage logs with filters. + */ +usage.get('/', async (c) => { + const query = c.req.query(); + const params: Array = []; + let sql = 'SELECT usage_logs.*, channels.name as channel_name, tokens.name as token_name FROM usage_logs LEFT JOIN channels ON channels.id = usage_logs.channel_id LEFT JOIN tokens ON tokens.id = usage_logs.token_id WHERE 1=1'; + + if (query.from) { + sql += ' AND usage_logs.created_at >= ?'; + params.push(query.from); + } + if (query.to) { + sql += ' AND usage_logs.created_at <= ?'; + params.push(query.to); + } + if (query.model) { + sql += ' AND usage_logs.model = ?'; + params.push(query.model); + } + if (query.channel_id) { + sql += ' AND usage_logs.channel_id = ?'; + params.push(query.channel_id); + } + if (query.token_id) { + sql += ' AND usage_logs.token_id = ?'; + params.push(query.token_id); + } + + const limit = Math.min(Number(query.limit ?? 50), 200); + sql += ' ORDER BY usage_logs.created_at DESC LIMIT ?'; + params.push(Number.isNaN(limit) ? 50 : limit); + + const fallback = Number(c.env.LOG_RETENTION_DAYS ?? '30'); + const retention = await getRetentionDays(c.env.DB, Number.isNaN(fallback) ? 30 : fallback); + await pruneUsageLogs(c.env.DB, retention); + + const result = await c.env.DB.prepare(sql).bind(...params).all(); + return c.json({ logs: result.results ?? [] }); +}); + +export default usage; diff --git a/apps/worker/src/services/channels.ts b/apps/worker/src/services/channels.ts new file mode 100644 index 0000000..9d6fad4 --- /dev/null +++ b/apps/worker/src/services/channels.ts @@ -0,0 +1,62 @@ +import { safeJsonParse } from '../utils/json'; + +export type ChannelRecord = { + id: string; + name: string; + base_url: string; + api_key: string; + weight: number; + status: string; + rate_limit?: number | null; + models_json?: string | null; +}; + +export type ModelEntry = { + id: string; + label: string; + channelId: string; + channelName: string; +}; + +/** + * Returns channels in a weighted random order. + */ +export function createWeightedOrder(channels: ChannelRecord[]): ChannelRecord[] { + const pool = channels.map((channel) => ({ + ...channel, + weight: Math.max(1, Number(channel.weight) || 1) + })); + const ordered: ChannelRecord[] = []; + while (pool.length > 0) { + const total = pool.reduce((sum, channel) => sum + channel.weight, 0); + let roll = Math.random() * total; + const index = pool.findIndex((channel) => { + roll -= channel.weight; + return roll <= 0; + }); + const [selected] = pool.splice(index < 0 ? 0 : index, 1); + ordered.push(selected); + } + return ordered; +} + +/** + * Extracts model entries from a channel record. + */ +export function extractModels(channel: ChannelRecord): ModelEntry[] { + const raw = safeJsonParse(channel.models_json, null); + const models = Array.isArray(raw) + ? raw + : Array.isArray(raw?.data) + ? raw.data + : []; + + return models + .filter((model) => model?.id) + .map((model) => ({ + id: String(model.id), + label: String(model.id), + channelId: channel.id, + channelName: channel.name + })); +} diff --git a/apps/worker/src/services/newapi.ts b/apps/worker/src/services/newapi.ts new file mode 100644 index 0000000..8fd1d5b --- /dev/null +++ b/apps/worker/src/services/newapi.ts @@ -0,0 +1,253 @@ +import { safeJsonParse } from '../utils/json'; +import { normalizeBaseUrl } from '../utils/url'; + +export type ChannelRow = { + id: string; + name: string; + base_url: string; + api_key: string; + weight: number; + status: string; + rate_limit?: number | null; + models_json?: string | null; + type?: number | null; + group_name?: string | null; + priority?: number | null; + metadata_json?: string | null; + test_time?: number | string | null; + response_time_ms?: number | null; + created_at?: string | null; + updated_at?: string | null; +}; + +export type NewApiChannel = { + id: string; + name: string; + type: number; + status: number; + priority: number; + weight: number; + models: string; + group: string; + response_time: number; + test_time: number; + base_url?: string; + key?: string; + created_time?: number; + updated_time?: number; +}; + +export type ParsedChannelInput = { + id?: string; + name?: string; + type?: number; + base_url?: string; + api_key?: string; + weight?: number; + status?: string; + rate_limit?: number; + models: string[]; + models_json: string; + group_name?: string | null; + priority?: number; + metadata_json?: string | null; +}; + +const KNOWN_KEYS = new Set([ + 'id', + 'name', + 'type', + 'key', + 'api_key', + 'base_url', + 'baseUrl', + 'weight', + 'status', + 'rate_limit', + 'rateLimit', + 'models', + 'model', + 'model_list', + 'models_list', + 'group', + 'group_name', + 'groups', + 'priority' +]); + +function toNumber(value: unknown, fallback: number | null = null): number | null { + if (value === null || value === undefined) { + return fallback; + } + const parsed = Number(value); + return Number.isNaN(parsed) ? fallback : parsed; +} + +export function normalizeModelsInput(input: unknown): string[] { + if (!input) { + return []; + } + if (Array.isArray(input)) { + return input.map((item) => String(item)).filter((item) => item.length > 0); + } + if (typeof input === 'string') { + return input + .split(',') + .map((item) => item.trim()) + .filter((item) => item.length > 0); + } + if (typeof input === 'object') { + const raw = input as { data?: unknown[] }; + if (Array.isArray(raw.data)) { + return raw.data + .map((item: any) => String(item?.id ?? item)) + .filter((item) => item.length > 0); + } + } + return []; +} + +export function modelsToJson(models: string[]): string { + const normalized = models + .map((model) => String(model).trim()) + .filter((model) => model.length > 0); + return JSON.stringify(normalized.map((id) => ({ id }))); +} + +export function extractModelIds(channel: ChannelRow): string[] { + const raw = safeJsonParse(channel.models_json, null); + const models = Array.isArray(raw) + ? raw + : Array.isArray(raw?.data) + ? raw.data + : []; + return models + .map((model: any) => String(model?.id ?? model)) + .filter((model: string) => model.length > 0); +} + +export function toInternalStatus(status: unknown): string { + if (status === undefined || status === null) { + return 'active'; + } + if (typeof status === 'string') { + const normalized = status.trim().toLowerCase(); + if (['1', 'true', 'enabled', 'enable', 'active'].includes(normalized)) { + return 'active'; + } + if (['0', '2', 'false', 'disabled', 'disable', 'inactive'].includes(normalized)) { + return 'disabled'; + } + } + if (typeof status === 'number') { + return status === 1 ? 'active' : 'disabled'; + } + if (typeof status === 'boolean') { + return status ? 'active' : 'disabled'; + } + return 'active'; +} + +export function toNewApiStatus(status: string | null | undefined): number { + return status === 'active' ? 1 : 2; +} + +export function normalizeChannelInput(body: any): ParsedChannelInput { + const hasModels = + body && + (Object.prototype.hasOwnProperty.call(body, 'models') || + Object.prototype.hasOwnProperty.call(body, 'model') || + Object.prototype.hasOwnProperty.call(body, 'model_list') || + Object.prototype.hasOwnProperty.call(body, 'models_list')); + const models = hasModels + ? normalizeModelsInput(body?.models ?? body?.model ?? body?.model_list ?? body?.models_list) + : []; + const hasGroup = + body && + (Object.prototype.hasOwnProperty.call(body, 'group') || + Object.prototype.hasOwnProperty.call(body, 'group_name') || + Object.prototype.hasOwnProperty.call(body, 'groups')); + const groupInput = body?.group ?? body?.group_name ?? null; + const groupsInput = Array.isArray(body?.groups) ? body.groups.map((item: any) => String(item)) : null; + const groupName = !hasGroup + ? undefined + : groupInput + ? String(groupInput) + : groupsInput && groupsInput.length > 0 + ? groupsInput.join(',') + : null; + + const metadata: Record = {}; + if (body && typeof body === 'object') { + for (const [key, value] of Object.entries(body)) { + if (KNOWN_KEYS.has(key)) { + continue; + } + metadata[key] = value; + } + } + + const hasType = body && Object.prototype.hasOwnProperty.call(body, 'type'); + const hasWeight = body && Object.prototype.hasOwnProperty.call(body, 'weight'); + const hasStatus = body && Object.prototype.hasOwnProperty.call(body, 'status'); + const hasRateLimit = + body && + (Object.prototype.hasOwnProperty.call(body, 'rate_limit') || + Object.prototype.hasOwnProperty.call(body, 'rateLimit')); + const hasPriority = body && Object.prototype.hasOwnProperty.call(body, 'priority'); + + return { + id: body?.id !== undefined && body?.id !== null ? String(body.id) : undefined, + name: body?.name ? String(body.name) : undefined, + type: hasType ? toNumber(body?.type, 1) ?? 1 : undefined, + base_url: body?.base_url ?? body?.baseUrl, + api_key: body?.key ?? body?.api_key, + weight: hasWeight ? toNumber(body?.weight, 1) ?? 1 : undefined, + status: hasStatus ? toInternalStatus(body?.status) : undefined, + rate_limit: hasRateLimit ? toNumber(body?.rate_limit ?? body?.rateLimit, 0) ?? 0 : undefined, + models, + models_json: modelsToJson(models), + group_name: groupName, + priority: hasPriority ? toNumber(body?.priority, 0) ?? 0 : undefined, + metadata_json: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null + }; +} + +export function mergeMetadata(existing: string | null | undefined, incoming: string | null | undefined): string | null { + const base = safeJsonParse>(existing, {}); + const updates = safeJsonParse>(incoming, {}); + const merged = { ...base, ...updates }; + return Object.keys(merged).length > 0 ? JSON.stringify(merged) : null; +} + +export function toNewApiChannel(channel: ChannelRow): NewApiChannel { + const models = extractModelIds(channel); + const metadata = safeJsonParse>(channel.metadata_json, {}); + const createdTime = channel.created_at ? Date.parse(channel.created_at) : NaN; + const updatedTime = channel.updated_at ? Date.parse(channel.updated_at) : NaN; + + return { + id: channel.id, + name: channel.name, + type: Number(channel.type ?? 1), + status: toNewApiStatus(channel.status), + priority: Number(channel.priority ?? 0), + weight: Number(channel.weight ?? 1), + models: models.join(','), + group: channel.group_name ?? '', + response_time: Number(channel.response_time_ms ?? 0), + test_time: toNumber(channel.test_time, 0) ?? 0, + base_url: channel.base_url, + key: channel.api_key, + created_time: Number.isNaN(createdTime) ? undefined : Math.floor(createdTime / 1000), + updated_time: Number.isNaN(updatedTime) ? undefined : Math.floor(updatedTime / 1000), + ...metadata + }; +} + +export function normalizeBaseUrlInput(value: string | undefined | null): string | null { + if (!value) { + return null; + } + return normalizeBaseUrl(String(value)); +} diff --git a/apps/worker/src/services/quota.ts b/apps/worker/src/services/quota.ts new file mode 100644 index 0000000..601a84f --- /dev/null +++ b/apps/worker/src/services/quota.ts @@ -0,0 +1,19 @@ +/** + * Determines whether a token can consume more quota. + */ +export function canConsumeQuota(quotaTotal: number | null, quotaUsed: number, increment: number): boolean { + if (quotaTotal === null || quotaTotal === undefined) { + return true; + } + return quotaUsed + increment <= quotaTotal; +} + +/** + * Normalizes quota numbers to safe values. + */ +export function normalizeQuota(quotaTotal?: number | null, quotaUsed?: number | null) { + return { + quotaTotal: quotaTotal ?? null, + quotaUsed: quotaUsed ?? 0 + }; +} diff --git a/apps/worker/src/services/settings.ts b/apps/worker/src/services/settings.ts new file mode 100644 index 0000000..65d78dc --- /dev/null +++ b/apps/worker/src/services/settings.ts @@ -0,0 +1,41 @@ +import type { D1Database } from '@cloudflare/workers-types'; +import { nowIso } from '../utils/time'; + +const RETENTION_KEY = 'log_retention_days'; + +/** + * Returns the log retention days from settings or env fallback. + */ +export async function getRetentionDays(db: D1Database, fallback: number): Promise { + const setting = await db.prepare('SELECT value FROM settings WHERE key = ?').bind(RETENTION_KEY).first(); + if (setting?.value) { + const parsed = Number(setting.value); + if (!Number.isNaN(parsed) && parsed > 0) { + return parsed; + } + } + return fallback; +} + +/** + * Updates the log retention days setting. + */ +export async function setRetentionDays(db: D1Database, days: number): Promise { + const value = Math.max(1, Math.floor(days)).toString(); + await db + .prepare('INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at') + .bind(RETENTION_KEY, value, nowIso()) + .run(); +} + +/** + * Loads generic settings as a key/value map. + */ +export async function listSettings(db: D1Database): Promise> { + const result = await db.prepare('SELECT key, value FROM settings').all(); + const map: Record = {}; + for (const row of result.results ?? []) { + map[String(row.key)] = String(row.value); + } + return map; +} diff --git a/apps/worker/src/services/usage.ts b/apps/worker/src/services/usage.ts new file mode 100644 index 0000000..bf8a9fa --- /dev/null +++ b/apps/worker/src/services/usage.ts @@ -0,0 +1,58 @@ +import type { D1Database } from '@cloudflare/workers-types'; +import { nowIso } from '../utils/time'; + +export type UsageInput = { + tokenId?: string | null; + channelId?: string | null; + model?: string | null; + requestPath?: string | null; + totalTokens?: number | null; + promptTokens?: number | null; + completionTokens?: number | null; + cost?: number | null; + latencyMs?: number | null; + status?: string | null; +}; + +/** + * Inserts a usage record and updates token quota. + */ +export async function recordUsage(db: D1Database, input: UsageInput): Promise { + const id = crypto.randomUUID(); + const createdAt = nowIso(); + await db + .prepare( + 'INSERT INTO usage_logs (id, token_id, channel_id, model, request_path, total_tokens, prompt_tokens, completion_tokens, cost, latency_ms, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + ) + .bind( + id, + input.tokenId ?? null, + input.channelId ?? null, + input.model ?? null, + input.requestPath ?? null, + input.totalTokens ?? 0, + input.promptTokens ?? 0, + input.completionTokens ?? 0, + input.cost ?? 0, + input.latencyMs ?? 0, + input.status ?? 'ok', + createdAt + ) + .run(); + + if (input.tokenId && input.totalTokens) { + await db + .prepare('UPDATE tokens SET quota_used = quota_used + ?, updated_at = ? WHERE id = ?') + .bind(input.totalTokens, createdAt, input.tokenId) + .run(); + } +} + +/** + * Deletes usage logs older than the retention window. + */ +export async function pruneUsageLogs(db: D1Database, retentionDays: number): Promise { + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - retentionDays); + await db.prepare('DELETE FROM usage_logs WHERE created_at < ?').bind(cutoff.toISOString()).run(); +} diff --git a/apps/worker/src/utils/crypto.ts b/apps/worker/src/utils/crypto.ts new file mode 100644 index 0000000..30d10f8 --- /dev/null +++ b/apps/worker/src/utils/crypto.ts @@ -0,0 +1,23 @@ +const encoder = new TextEncoder(); + +/** + * Creates a SHA-256 hex digest from input text. + */ +export async function sha256Hex(input: string): Promise { + const data = encoder.encode(input); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +/** + * Generates a URL-safe random token with an optional prefix. + */ +export function generateToken(prefix = ''): string { + const bytes = crypto.getRandomValues(new Uint8Array(24)); + const base = btoa(String.fromCharCode(...bytes)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + return `${prefix}${base}`; +} diff --git a/apps/worker/src/utils/http.ts b/apps/worker/src/utils/http.ts new file mode 100644 index 0000000..a09321c --- /dev/null +++ b/apps/worker/src/utils/http.ts @@ -0,0 +1,14 @@ +import type { Context } from 'hono'; + +/** + * Sends a JSON error response with a consistent shape. + */ +export function jsonError(c: Context, status: number, message: string, code?: string) { + return c.json( + { + error: message, + code + }, + status + ); +} diff --git a/apps/worker/src/utils/json.ts b/apps/worker/src/utils/json.ts new file mode 100644 index 0000000..1818a63 --- /dev/null +++ b/apps/worker/src/utils/json.ts @@ -0,0 +1,13 @@ +/** + * Safely parses JSON or returns a fallback value. + */ +export function safeJsonParse(value: string | null | undefined, fallback: T): T { + if (!value) { + return fallback; + } + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } +} diff --git a/apps/worker/src/utils/request.ts b/apps/worker/src/utils/request.ts new file mode 100644 index 0000000..c386502 --- /dev/null +++ b/apps/worker/src/utils/request.ts @@ -0,0 +1,12 @@ +import type { Context } from 'hono'; + +/** + * Reads a bearer token from common headers. + */ +export function getBearerToken(c: Context): string | null { + const auth = c.req.header('Authorization'); + if (auth && auth.toLowerCase().startsWith('bearer ')) { + return auth.slice(7).trim(); + } + return c.req.header('x-api-key') ?? c.req.header('x-admin-token') ?? null; +} diff --git a/apps/worker/src/utils/time.ts b/apps/worker/src/utils/time.ts new file mode 100644 index 0000000..9174ddc --- /dev/null +++ b/apps/worker/src/utils/time.ts @@ -0,0 +1,15 @@ +/** + * Returns the current ISO timestamp. + */ +export function nowIso(): string { + return new Date().toISOString(); +} + +/** + * Adds hours to a date and returns a new Date. + */ +export function addHours(date: Date, hours: number): Date { + const copy = new Date(date.getTime()); + copy.setHours(copy.getHours() + hours); + return copy; +} diff --git a/apps/worker/src/utils/url.ts b/apps/worker/src/utils/url.ts new file mode 100644 index 0000000..f82d268 --- /dev/null +++ b/apps/worker/src/utils/url.ts @@ -0,0 +1,13 @@ +/** + * Normalizes upstream base URLs and strips a trailing /v1 segment. + * + * Args: + * baseUrl: Raw upstream base URL. + * + * Returns: + * Normalized base URL without trailing slashes or /v1. + */ +export function normalizeBaseUrl(baseUrl: string): string { + const trimmed = baseUrl.trim().replace(/\/+$/, ''); + return trimmed.replace(/\/v1$/i, ''); +} diff --git a/apps/worker/tsconfig.json b/apps/worker/tsconfig.json new file mode 100644 index 0000000..ec193bf --- /dev/null +++ b/apps/worker/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["@cloudflare/workers-types"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/apps/worker/wrangler.toml b/apps/worker/wrangler.toml new file mode 100644 index 0000000..ad18309 --- /dev/null +++ b/apps/worker/wrangler.toml @@ -0,0 +1,17 @@ +name = "new-api-lite" +main = "src/index.ts" +compatibility_date = "2026-02-14" + +[vars] +ADMIN_PASSWORD = "lsy" +CORS_ORIGIN = "http://localhost:5173" +SESSION_TTL_HOURS = "12" +LOG_RETENTION_DAYS = "30" +PROXY_RETRY_ROUNDS = "2" +PROXY_RETRY_DELAY_MS = "200" +NEW_API_ADMIN_TOKEN = "sk-newapi-admin" + +[[d1_databases]] +binding = "DB" +database_name = "new_api_lite" +database_id = "00000000-0000-0000-0000-000000000000" diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..427fb35 --- /dev/null +++ b/bun.lock @@ -0,0 +1,652 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "new-api-lite", + "dependencies": { + "wrangler": "4", + }, + "devDependencies": { + "@types/node": "^20.11.30", + "typescript": "^5.4.5", + "vitest": "^1.5.3", + }, + }, + "apps/admin": { + "name": "new-api-lite-admin", + "devDependencies": { + "typescript": "^5.4.5", + "vite": "^5.2.0", + }, + }, + "apps/worker": { + "name": "new-api-lite-worker", + "dependencies": { + "hono": "^4.2.7", + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240222.0", + "typescript": "^5.4.5", + "wrangler": "^3.48.0", + }, + }, + }, + "packages": { + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "https://registry.npmmirror.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.12.1", "https://registry.npmmirror.com/@cloudflare/unenv-preset/-/unenv-preset-2.12.1.tgz", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260115.0" }, "optionalPeers": ["workerd"] }, "sha512-tP/Wi+40aBJovonSNJSsS7aFJY0xjuckKplmzDs2Xat06BJ68B6iG7YDUWXJL8gNn0gqW7YC5WhlYhO3QbugQA=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260212.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-kLxuYutk88Wlo7edp8mlkN68TgZZ9237SUnuX9kNaD5jcOdblUqiBctMRZeRcPsuoX/3g2t0vS4ga02NBEVRNg=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260212.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-fqoqQWMA1D0ZzDOD8sp0allREM2M8GHdpxMXQ8EdZpZ70z5bJbJ9Vr4qe35++FNIZJspsDHfTw3Xm/M4ELm/dQ=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260212.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bCSQoZzDzV5MSh4ueWo1DgmOn4Hf3QBu4Yo3eQFXA2llYFIu/sZgRtkEehw1X2/SY5Sn6O0EMCqxJYRf82Wdeg=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260212.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-GPvp1iiKQodtbUDi6OmR5I0vD75lawB54tdYGtmypuHC7ZOI2WhBmhb3wCxgnQNOG1z7mhCQrzRCoqrKwYbVWQ=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260212.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-wHRI218Xn4ndgWJCUHH4Zx0YlU5q/o6OmcxXkcw95tJOsQn4lDrhppioPh4eScxJZALf2X+ODeZcyQTCq5exGw=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260214.0", "https://registry.npmmirror.com/@cloudflare/workers-types/-/workers-types-4.20260214.0.tgz", {}, "sha512-qb8rgbAdJR4BAPXolXhFL/wuGtecHLh1veOyZ1mK6QqWuCdI3vK1biKC0i3lzmzdLR/DZvsN3mNtpUE8zpWGEg=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "https://registry.npmmirror.com/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="], + + "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "https://registry.npmmirror.com/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@fastify/busboy": ["@fastify/busboy@2.1.1", "https://registry.npmmirror.com/@fastify/busboy/-/busboy-2.1.1.tgz", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + + "@img/colour": ["@img/colour@1.0.0", "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@poppinss/colors": ["@poppinss/colors@4.1.6", "https://registry.npmmirror.com/@poppinss/colors/-/colors-4.1.6.tgz", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "https://registry.npmmirror.com/@poppinss/dumper/-/dumper-0.6.5.tgz", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "https://registry.npmmirror.com/@poppinss/exception/-/exception-1.2.3.tgz", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.10", "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.10.tgz", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], + + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "https://registry.npmmirror.com/@sindresorhus/is/-/is-7.2.0.tgz", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "https://registry.npmmirror.com/@speed-highlight/core/-/core-1.2.14.tgz", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], + + "@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@20.19.33", "https://registry.npmmirror.com/@types/node/-/node-20.19.33.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + + "@vitest/expect": ["@vitest/expect@1.6.1", "https://registry.npmmirror.com/@vitest/expect/-/expect-1.6.1.tgz", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog=="], + + "@vitest/runner": ["@vitest/runner@1.6.1", "https://registry.npmmirror.com/@vitest/runner/-/runner-1.6.1.tgz", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA=="], + + "@vitest/snapshot": ["@vitest/snapshot@1.6.1", "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-1.6.1.tgz", { "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", "pretty-format": "^29.7.0" } }, "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ=="], + + "@vitest/spy": ["@vitest/spy@1.6.1", "https://registry.npmmirror.com/@vitest/spy/-/spy-1.6.1.tgz", { "dependencies": { "tinyspy": "^2.2.0" } }, "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw=="], + + "@vitest/utils": ["@vitest/utils@1.6.1", "https://registry.npmmirror.com/@vitest/utils/-/utils-1.6.1.tgz", { "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" } }, "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g=="], + + "acorn": ["acorn@8.15.0", "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-walk": ["acorn-walk@8.3.4", "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.4.tgz", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "ansi-styles": ["ansi-styles@5.2.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "as-table": ["as-table@1.0.55", "https://registry.npmmirror.com/as-table/-/as-table-1.0.55.tgz", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="], + + "assertion-error": ["assertion-error@1.1.0", "https://registry.npmmirror.com/assertion-error/-/assertion-error-1.1.0.tgz", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], + + "blake3-wasm": ["blake3-wasm@2.1.5", "https://registry.npmmirror.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + + "cac": ["cac@6.7.14", "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@4.5.0", "https://registry.npmmirror.com/chai/-/chai-4.5.0.tgz", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], + + "check-error": ["check-error@1.0.3", "https://registry.npmmirror.com/check-error/-/check-error-1.0.3.tgz", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], + + "color": ["color@4.2.3", "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "confbox": ["confbox@0.1.8", "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], + + "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@4.1.4", "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.4.tgz", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg=="], + + "defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "diff-sequences": ["diff-sequences@29.6.3", "https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + + "esbuild": ["esbuild@0.27.3", "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "estree-walker": ["estree-walker@3.0.3", "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "execa": ["execa@8.0.1", "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "exit-hook": ["exit-hook@2.2.1", "https://registry.npmmirror.com/exit-hook/-/exit-hook-2.2.1.tgz", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + + "exsolve": ["exsolve@1.0.8", "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-func-name": ["get-func-name@2.0.2", "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz", {}, "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="], + + "get-source": ["get-source@2.0.12", "https://registry.npmmirror.com/get-source/-/get-source-2.0.12.tgz", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="], + + "get-stream": ["get-stream@8.0.1", "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "hono": ["hono@4.11.9", "https://registry.npmmirror.com/hono/-/hono-4.11.9.tgz", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + + "human-signals": ["human-signals@5.0.0", "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "is-arrayish": ["is-arrayish@0.3.4", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + + "is-stream": ["is-stream@3.0.0", "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@9.0.1", "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "kleur": ["kleur@4.1.5", "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "local-pkg": ["local-pkg@0.5.1", "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="], + + "loupe": ["loupe@2.3.7", "https://registry.npmmirror.com/loupe/-/loupe-2.3.7.tgz", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="], + + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "merge-stream": ["merge-stream@2.0.0", "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "mime": ["mime@3.0.0", "https://registry.npmmirror.com/mime/-/mime-3.0.0.tgz", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "mimic-fn": ["mimic-fn@4.0.0", "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "miniflare": ["miniflare@4.20260212.0", "https://registry.npmmirror.com/miniflare/-/miniflare-4.20260212.0.tgz", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260212.0", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-Lgxq83EuR2q/0/DAVOSGXhXS1V7GDB04HVggoPsenQng8sqEDR3hO4FigIw5ZI2Sv2X7kIc30NCzGHJlCFIYWg=="], + + "mlly": ["mlly@1.8.0", "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mustache": ["mustache@4.2.0", "https://registry.npmmirror.com/mustache/-/mustache-4.2.0.tgz", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + + "nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "new-api-lite-admin": ["new-api-lite-admin@workspace:apps/admin"], + + "new-api-lite-worker": ["new-api-lite-worker@workspace:apps/worker"], + + "npm-run-path": ["npm-run-path@5.3.0", "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "ohash": ["ohash@2.0.11", "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "onetime": ["onetime@6.0.0", "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "p-limit": ["p-limit@5.0.0", "https://registry.npmmirror.com/p-limit/-/p-limit-5.0.0.tgz", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ=="], + + "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@1.1.2", "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@1.1.1", "https://registry.npmmirror.com/pathval/-/pathval-1.1.1.tgz", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + + "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "pkg-types": ["pkg-types@1.3.1", "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss": ["postcss@8.5.6", "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "pretty-format": ["pretty-format@29.7.0", "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "printable-characters": ["printable-characters@1.0.42", "https://registry.npmmirror.com/printable-characters/-/printable-characters-1.0.42.tgz", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="], + + "react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "rollup": ["rollup@4.57.1", "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + + "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "https://registry.npmmirror.com/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="], + + "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "https://registry.npmmirror.com/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="], + + "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "https://registry.npmmirror.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + + "semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "sharp": ["sharp@0.33.5", "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + + "shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-swizzle": ["simple-swizzle@0.2.4", "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + + "source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sourcemap-codec": ["sourcemap-codec@1.4.8", "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], + + "stackback": ["stackback@0.0.2", "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "stacktracey": ["stacktracey@2.1.8", "https://registry.npmmirror.com/stacktracey/-/stacktracey-2.1.8.tgz", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="], + + "std-env": ["std-env@3.10.0", "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "stoppable": ["stoppable@1.1.0", "https://registry.npmmirror.com/stoppable/-/stoppable-1.1.0.tgz", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "strip-literal": ["strip-literal@2.1.1", "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q=="], + + "supports-color": ["supports-color@10.2.2", "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "tinybench": ["tinybench@2.9.0", "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinypool": ["tinypool@0.8.4", "https://registry.npmmirror.com/tinypool/-/tinypool-0.8.4.tgz", {}, "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ=="], + + "tinyspy": ["tinyspy@2.2.1", "https://registry.npmmirror.com/tinyspy/-/tinyspy-2.2.1.tgz", {}, "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A=="], + + "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-detect": ["type-detect@4.1.0", "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + + "typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.3", "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "undici": ["undici@7.18.2", "https://registry.npmmirror.com/undici/-/undici-7.18.2.tgz", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], + + "undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unenv": ["unenv@2.0.0-rc.24", "https://registry.npmmirror.com/unenv/-/unenv-2.0.0-rc.24.tgz", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "vite": ["vite@5.4.21", "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node": ["vite-node@1.6.1", "https://registry.npmmirror.com/vite-node/-/vite-node-1.6.1.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA=="], + + "vitest": ["vitest@1.6.1", "https://registry.npmmirror.com/vitest/-/vitest-1.6.1.tgz", { "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", "@vitest/snapshot": "1.6.1", "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", "local-pkg": "^0.5.0", "magic-string": "^0.30.5", "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "1.6.1", "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag=="], + + "which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "workerd": ["workerd@1.20260212.0", "https://registry.npmmirror.com/workerd/-/workerd-1.20260212.0.tgz", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260212.0", "@cloudflare/workerd-darwin-arm64": "1.20260212.0", "@cloudflare/workerd-linux-64": "1.20260212.0", "@cloudflare/workerd-linux-arm64": "1.20260212.0", "@cloudflare/workerd-windows-64": "1.20260212.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-4B9BoZUzKSRv3pVZGEPh7OX+Q817hpUqAUtz5O0TxJVqo4OsYJAUA/sY177Q5ha/twjT9KaJt2DtQzE+oyCOzw=="], + + "wrangler": ["wrangler@4.65.0", "https://registry.npmmirror.com/wrangler/-/wrangler-4.65.0.tgz", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.12.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260212.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260212.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260212.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-R+n3o3tlGzLK9I4fGocPReOuvcnjhtOL2aCVKkHMeuEwt9pPbOO4FxJtx/ec5cIUG/otRyJnfQGCAr9DplBVng=="], + + "ws": ["ws@8.18.0", "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "yocto-queue": ["yocto-queue@1.2.2", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-1.2.2.tgz", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "youch": ["youch@4.1.0-beta.10", "https://registry.npmmirror.com/youch/-/youch-4.1.0-beta.10.tgz", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "https://registry.npmmirror.com/youch-core/-/youch-core-0.3.3.tgz", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + + "zod": ["zod@3.22.3", "https://registry.npmmirror.com/zod/-/zod-3.22.3.tgz", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + + "miniflare/sharp": ["sharp@0.34.5", "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "mlly/pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "new-api-lite-worker/wrangler": ["wrangler@3.114.17", "https://registry.npmmirror.com/wrangler/-/wrangler-3.114.17.tgz", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/unenv-preset": "2.0.2", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250718.3", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.14", "workerd": "1.20250718.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250408.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "pkg-types/pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "https://registry.npmmirror.com/estree-walker/-/estree-walker-0.6.1.tgz", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + + "rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + + "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "https://registry.npmmirror.com/estree-walker/-/estree-walker-0.6.1.tgz", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + + "unenv/pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "vite/esbuild": ["esbuild@0.21.5", "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "new-api-lite-worker/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "https://registry.npmmirror.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="], + + "new-api-lite-worker/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.0.2", "https://registry.npmmirror.com/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", { "peerDependencies": { "unenv": "2.0.0-rc.14", "workerd": "^1.20250124.0" }, "optionalPeers": ["workerd"] }, "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg=="], + + "new-api-lite-worker/wrangler/esbuild": ["esbuild@0.17.19", "https://registry.npmmirror.com/esbuild/-/esbuild-0.17.19.tgz", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + + "new-api-lite-worker/wrangler/miniflare": ["miniflare@3.20250718.3", "https://registry.npmmirror.com/miniflare/-/miniflare-3.20250718.3.tgz", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="], + + "new-api-lite-worker/wrangler/unenv": ["unenv@2.0.0-rc.14", "https://registry.npmmirror.com/unenv/-/unenv-2.0.0-rc.14.tgz", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.1", "ohash": "^2.0.10", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q=="], + + "new-api-lite-worker/wrangler/workerd": ["workerd@1.20250718.0", "https://registry.npmmirror.com/workerd/-/workerd-1.20250718.0.tgz", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250718.0", "@cloudflare/workerd-darwin-arm64": "1.20250718.0", "@cloudflare/workerd-linux-64": "1.20250718.0", "@cloudflare/workerd-linux-arm64": "1.20250718.0", "@cloudflare/workerd-windows-64": "1.20250718.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="], + + "new-api-lite-worker/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="], + + "new-api-lite-worker/wrangler/miniflare/acorn": ["acorn@8.14.0", "https://registry.npmmirror.com/acorn/-/acorn-8.14.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "new-api-lite-worker/wrangler/miniflare/acorn-walk": ["acorn-walk@8.3.2", "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.2.tgz", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + + "new-api-lite-worker/wrangler/miniflare/undici": ["undici@5.29.0", "https://registry.npmmirror.com/undici/-/undici-5.29.0.tgz", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "new-api-lite-worker/wrangler/miniflare/youch": ["youch@3.3.4", "https://registry.npmmirror.com/youch/-/youch-3.3.4.tgz", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], + + "new-api-lite-worker/wrangler/unenv/pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g=="], + + "new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q=="], + + "new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg=="], + + "new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog=="], + + "new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg=="], + + "new-api-lite-worker/wrangler/miniflare/youch/cookie": ["cookie@0.7.2", "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + } +} diff --git a/helloagents/CHANGELOG.md b/helloagents/CHANGELOG.md new file mode 100644 index 0000000..eaf96e4 --- /dev/null +++ b/helloagents/CHANGELOG.md @@ -0,0 +1,72 @@ +# 变更日志 + +## [Unreleased] + +### 微调 +- **[admin-ui]**: 本地开发增加 Vite proxy 解决前后端端口不一致 + - 类型: 微调(无方案包) + - 文件: apps/admin/vite.config.ts, README.md +- **[worker]**: 补充 wrangler.toml 示例配置占位 + - 类型: 微调(无方案包) + - 文件: apps/worker/wrangler.toml +- **[admin-ui]**: 渠道 ID 与日志渠道可见,操作反馈更清晰 + - 类型: 微调(无方案包) + - 文件: apps/admin/src/main.ts +- **[worker]**: 使用日志关联渠道/令牌,base_url 自动纠正 + - 类型: 微调(无方案包) + - 文件: apps/worker/src/routes/usage.ts, apps/worker/src/routes/channels.ts, apps/worker/src/routes/proxy.ts +- **[worker]**: 渠道 ID 支持自定义,令牌可二次查看 + - 类型: 微调(无方案包) + - 文件: apps/worker/src/routes/channels.ts, apps/worker/src/routes/tokens.ts, apps/worker/src/db/schema.sql, apps/worker/migrations/0002_add_token_plain.sql +- **[admin-ui]**: 渠道 ID 可录入、令牌查看按钮 + - 类型: 微调(无方案包) + - 文件: apps/admin/src/main.ts +- **[tests]**: 补充 URL 规范化单测 + - 类型: 微调(无方案包) + - 文件: tests/worker/url.test.ts +- **[proxy]**: 增加失败轮询重试与相关配置 + - 类型: 微调(无方案包) + - 文件: apps/worker/src/routes/proxy.ts, apps/worker/src/env.ts, apps/worker/wrangler.toml +- **[docs]**: 更新代理重试与本地配置说明 + - 类型: 微调(无方案包) + - 文件: README.md, helloagents/modules/proxy.md +- **[worker]**: 放宽路由严格匹配以兼容 `/api/channel/` 尾斜杠 + - 类型: 微调(无方案包) + - 文件: apps/worker/src/index.ts +- **[worker]**: 新增 `/api/group` 兼容接口并放行鉴权 + - 类型: 微调(无方案包) + - 文件: apps/worker/src/index.ts, apps/worker/src/routes/newapiGroups.ts, tests/worker/newapi.test.ts + +## [0.2.1] - 2026-02-15 + +### 变更 +- **[tooling]**: 切换为 Bun 作为包管理器,补充部署说明与 fix 命令 + - 方案: [202602150153_bun-tooling](archive/2026-02/202602150153_bun-tooling/) + +## [0.2.0] - 2026-02-15 + +### 新增 +- **[channels/auth/models]**: 新增 New API 兼容渠道管理接口、用户模型接口与管理员令牌鉴权 + - 方案: [202602150127_newapi-channel-compat](archive/2026-02/202602150127_newapi-channel-compat/) + - 决策: newapi-channel-compat#D001(新增兼容层并保留扩展字段) + +## [0.1.0] - 2026-02-14 + +### 新增 +- **[核心服务]**: 初始化 Worker + D1 后端与 Vite 管理台,提供渠道/模型/令牌/日志/面板与 OpenAI 兼容代理 + - 方案: [202602142217_new-api-lite](archive/2026-02/202602142217_new-api-lite/) + - 决策: new-api-lite#D001(单 Worker + Hono), new-api-lite#D002(Vite + Pages), new-api-lite#D003(Token 默认全渠道), new-api-lite#D004(日志保留可配置) + +### 修复 +- **[{模块名}]**: {修复描述} + - 方案: [{YYYYMMDDHHMM}_{fix}](archive/{YYYY-MM}/{YYYYMMDDHHMM}_{fix}/) + +### 微调 +- **[{模块名}]**: {微调描述} + - 类型: 微调(无方案包) + - 文件: {文件路径}:{行号范围} + +### 回滚 +- **[{模块名}]**: 回滚至 {版本/提交} + - 原因: {回滚原因} + - 方案: [{原方案包}](archive/{YYYY-MM}/{原方案包}/) diff --git a/helloagents/INDEX.md b/helloagents/INDEX.md new file mode 100644 index 0000000..2ac0a4c --- /dev/null +++ b/helloagents/INDEX.md @@ -0,0 +1,36 @@ +# new-api-lite 知识库 + +> 本文件是知识库的入口点 + +## 快速导航 + +| 需要了解 | 读取文件 | +|---------|---------| +| 项目概况、技术栈、开发约定 | [context.md](context.md) | +| 模块索引 | [modules/_index.md](modules/_index.md) | +| 某个模块的职责和接口 | [modules/{模块名}.md](modules/{模块名}.md) | +| 项目变更历史 | [CHANGELOG.md](CHANGELOG.md) | +| 历史方案索引 | [archive/_index.md](archive/_index.md) | +| 当前待执行的方案 | [plan/](plan/) | + +## 知识库状态 + +```yaml +最后更新: 2026-02-15 02:03 +模块数量: 9 +待执行方案: 0 +``` + +## 读取指引 + +```yaml +启动任务: + 1. 读取本文件获取导航 + 2. 读取 context.md 获取项目上下文 + 3. 检查 plan/ 是否有进行中方案包 + +任务相关: + - 涉及特定模块: 读取 modules/{模块名}.md + - 需要历史决策: 搜索 CHANGELOG.md → 读取对应 archive/{YYYY-MM}/{方案包}/proposal.md + - 继续之前任务: 读取 plan/{方案包}/* +``` diff --git a/helloagents/config.toml b/helloagents/config.toml new file mode 100644 index 0000000..ee16ccb --- /dev/null +++ b/helloagents/config.toml @@ -0,0 +1,17 @@ +version = 1 + +[commands] +# mode: +# - "allowlist": 只允许 allow 里的命令 +# - "denylist": 允许除 deny 以外的命令 +mode = "denylist" + +# 默认策略:允许全部命令(deny 为空) +allow = [ + "~auto", "~plan", "~exec", + "~init", "~upgrade", "~clean", + "~commit", "~test", "~review", + "~validate", "~rollback", "~help" +] + +deny = [] diff --git a/helloagents/context.md b/helloagents/context.md new file mode 100644 index 0000000..42915b5 --- /dev/null +++ b/helloagents/context.md @@ -0,0 +1,89 @@ +# 项目上下文 + +## 1. 基本信息 + +```yaml +名称: new-api-lite +描述: 基于 Cloudflare Workers + D1 的简易版 new-api +类型: Web服务 +状态: 开发中 +``` + +## 2. 技术上下文 + +```yaml +语言: TypeScript +框架: Hono + Vite +包管理器: bun +构建工具: Wrangler + Vite +``` + +### 主要依赖 +| 依赖 | 版本 | 用途 | +|------|------|------| +| Hono | 待定 | Worker 路由与中间件 | +| Wrangler | 待定 | Workers 构建与部署 | +| Vite | 待定 | 管理台构建 | + +## 3. 项目概述 + +### 核心功能 +- OpenAI 兼容代理(/v1/*) +- 渠道管理、模型广场、令牌管理、使用日志、数据面板 +- 管理员登录与权限 +- New API 兼容渠道管理接口(/api/channel) +- 系统设置(日志保留天数) + +### 项目边界 +```yaml +范围内: + - 单管理员密码登录 + - Token 默认全渠道 + - 使用日志可配置保留天数 +范围外: + - 多租户/复杂角色体系 + - 复杂计费结算与对账 +``` + +## 4. 开发约定 + +### 代码规范 +```yaml +命名风格: camelCase +文件命名: kebab-case +目录组织: apps/worker + apps/admin +``` + +### 错误处理 +```yaml +错误码格式: ERR_{模块}_{含义} +日志级别: info/warn/error +``` + +### 测试要求 +```yaml +测试框架: vitest +覆盖率要求: 核心逻辑覆盖 +测试文件位置: tests/ +``` + +### Git规范 +```yaml +分支策略: trunk-based +提交格式: conventional commits +``` + +## 5. 当前约束(源自历史决策) + +| 约束 | 原因 | 决策来源 | +|------|------|---------| +| Cloudflare Workers + D1 | 技术栈约束 | new-api-lite#D001 | +| 登录密码来自环境变量 | 安全要求 | new-api-lite#D002 | +| Token 默认全渠道 | 产品规则 | new-api-lite#D003 | +| 使用日志保留天数可配置 | 合规/成本 | new-api-lite#D004 | + +## 6. 已知技术债务(可选) + +| 债务描述 | 优先级 | 来源 | 建议处理时机 | +|---------|--------|------|-------------| +| 暂无 | - | - | - | diff --git a/helloagents/modules/_index.md b/helloagents/modules/_index.md new file mode 100644 index 0000000..fe5884c --- /dev/null +++ b/helloagents/modules/_index.md @@ -0,0 +1,32 @@ +# 模块索引 + +> 通过此文件快速定位模块文档 + +## 模块清单 + +| 模块 | 职责 | 状态 | 文档 | +|------|------|------|------| +| auth | 管理员登录与会话 | 🚧 | [auth.md](./auth.md) | +| proxy | OpenAI 兼容代理与转发 | 🚧 | [proxy.md](./proxy.md) | +| channels | 渠道管理与健康检查 | 🚧 | [channels.md](./channels.md) | +| models | 模型广场与聚合 | 🚧 | [models.md](./models.md) | +| tokens | 令牌管理与额度 | 🚧 | [tokens.md](./tokens.md) | +| usage | 使用日志与留存 | 🚧 | [usage.md](./usage.md) | +| dashboard | 数据面板统计 | 🚧 | [dashboard.md](./dashboard.md) | +| settings | 系统配置 | 🚧 | [settings.md](./settings.md) | +| admin-ui | 管理台前端 | 🚧 | [admin-ui.md](./admin-ui.md) | + +## 模块依赖关系 + +``` +admin-ui → auth, channels, models, tokens, usage, dashboard, settings +proxy → channels, tokens, usage +dashboard → usage, tokens +models → channels +usage → settings +``` + +## 状态说明 +- ✅ 稳定 +- 🚧 开发中 +- 📝 规划中 diff --git a/helloagents/modules/admin-ui.md b/helloagents/modules/admin-ui.md new file mode 100644 index 0000000..bd8d846 --- /dev/null +++ b/helloagents/modules/admin-ui.md @@ -0,0 +1,17 @@ +# admin-ui 模块 + +## 职责 +- 管理台前端展示与操作 +- 覆盖渠道、模型、令牌、日志、面板与设置 + +## 接口定义 +- 调用 `/api/*` 管理接口 +- 登录后使用 Bearer token + +## 行为规范 +- Vite 构建静态文件 +- 前端状态集中在单页 +- Token 仅本地保存,不回显 + +## 依赖关系 +- `auth` / `channels` / `models` / `tokens` / `usage` / `dashboard` / `settings` 模块 diff --git a/helloagents/modules/auth.md b/helloagents/modules/auth.md new file mode 100644 index 0000000..3798c81 --- /dev/null +++ b/helloagents/modules/auth.md @@ -0,0 +1,20 @@ +# auth 模块 + +## 职责 +- 管理员密码登录与会话签发 +- 管理员会话注销与失效处理 +- New API 兼容接口鉴权(管理员令牌或会话) + +## 接口定义 +- `POST /api/auth/login` 管理员登录 +- `POST /api/auth/logout` 管理员退出 + +## 行为规范 +- 密码仅来自环境变量 `ADMIN_PASSWORD` +- 会话 token 使用哈希后存储 +- 过期会话自动清理 +- New API 兼容接口允许使用 `NEW_API_ADMIN_TOKEN` 直接鉴权 + +## 依赖关系 +- `admin_sessions` 表 +- `ADMIN_PASSWORD` / `SESSION_TTL_HOURS` / `NEW_API_ADMIN_TOKEN` 环境变量 diff --git a/helloagents/modules/channels.md b/helloagents/modules/channels.md new file mode 100644 index 0000000..9dda324 --- /dev/null +++ b/helloagents/modules/channels.md @@ -0,0 +1,34 @@ +# channels 模块 + +## 职责 +- 渠道配置的增删改查 +- 渠道连通性测试与模型同步 +- 渠道权重与状态管理 + +## 接口定义 +- `GET /api/channels` 渠道列表 +- `POST /api/channels` 新建渠道 +- `PATCH /api/channels/:id` 更新渠道 +- `DELETE /api/channels/:id` 删除渠道 +- `POST /api/channels/:id/test` 连通性测试 +- `GET /api/channel` New API 兼容列表 +- `GET /api/channel/search` New API 兼容搜索 +- `POST /api/channel` New API 兼容新增 +- `PUT /api/channel` New API 兼容更新 +- `DELETE /api/channel/:id` New API 兼容删除 +- `GET /api/channel/test/:id` New API 兼容连通性测试 +- `GET /api/channel/fetch_models/:id` New API 兼容模型拉取 +- `GET /api/group` New API 兼容分组列表 + +## 行为规范 +- `base_url` 保存为无尾斜杠格式 +- 创建渠道时可传入自定义 `id`,未提供则自动生成 +- 连通性测试调用 `/v1/models` +- 测试成功后更新 `models_json` +- New API 兼容层支持 `type`/`group`/`priority` 等字段映射,并保留扩展字段到 `metadata_json` +- New API 分组列表从 `group_name` 字段解析,空时返回 `default` + +## 依赖关系 +- `channels` 表 +- `models` 模块(聚合读取) +- `newApiAuth` 中间件 diff --git a/helloagents/modules/dashboard.md b/helloagents/modules/dashboard.md new file mode 100644 index 0000000..fb75726 --- /dev/null +++ b/helloagents/modules/dashboard.md @@ -0,0 +1,16 @@ +# dashboard 模块 + +## 职责 +- 汇总调用统计指标 +- 输出按时间、模型、渠道、令牌的聚合数据 + +## 接口定义 +- `GET /api/dashboard` 数据面板汇总 + +## 行为规范 +- 以 `usage_logs` 为统计来源 +- 默认返回最近 30 条趋势数据 + +## 依赖关系 +- `usage_logs` 表 +- `tokens` / `channels` 表 diff --git a/helloagents/modules/models.md b/helloagents/modules/models.md new file mode 100644 index 0000000..73d3709 --- /dev/null +++ b/helloagents/modules/models.md @@ -0,0 +1,16 @@ +# models 模块 + +## 职责 +- 汇总各渠道模型列表 +- 输出模型与渠道的映射关系 + +## 接口定义 +- `GET /api/models` 模型广场列表 +- `GET /api/user/models` New API 兼容用户模型列表 + +## 行为规范 +- 仅聚合启用状态的渠道 +- 优先使用渠道连通性测试返回的模型数据 + +## 依赖关系 +- `channels` 表 diff --git a/helloagents/modules/proxy.md b/helloagents/modules/proxy.md new file mode 100644 index 0000000..40eb96c --- /dev/null +++ b/helloagents/modules/proxy.md @@ -0,0 +1,19 @@ +# proxy 模块 + +## 职责 +- 提供 OpenAI 兼容 API 代理 +- 根据权重选择渠道并支持故障回退 +- 记录用量日志并回写额度 + +## 接口定义 +- `/v1/*` OpenAI 兼容代理路径 + +## 行为规范 +- 基于令牌鉴权 +- 按渠道权重随机排序 +- 非流式响应解析 usage 字段 +- 可配置失败重试轮询(响应 5xx/429 时触发) + +## 依赖关系 +- `channels` / `tokens` / `usage_logs` 表 +- `tokenAuth` 中间件 diff --git a/helloagents/modules/settings.md b/helloagents/modules/settings.md new file mode 100644 index 0000000..e84dd0a --- /dev/null +++ b/helloagents/modules/settings.md @@ -0,0 +1,16 @@ +# settings 模块 + +## 职责 +- 维护系统级配置(如日志保留天数) +- 为使用日志与面板提供统一配置读取 + +## 接口定义 +- `GET /api/settings` 读取设置 +- `PUT /api/settings` 更新设置 + +## 行为规范 +- 配置写入后即时生效 +- 未配置时使用环境变量默认值 + +## 依赖关系 +- `settings` 表 diff --git a/helloagents/modules/tokens.md b/helloagents/modules/tokens.md new file mode 100644 index 0000000..82157a9 --- /dev/null +++ b/helloagents/modules/tokens.md @@ -0,0 +1,22 @@ +# tokens 模块 + +## 职责 +- 管理 API 令牌的生成与生命周期 +- 维护额度与已用额度统计 +- 控制令牌启用/禁用状态 + +## 接口定义 +- `GET /api/tokens` 令牌列表 +- `POST /api/tokens` 生成令牌 +- `PATCH /api/tokens/:id` 更新令牌 +- `DELETE /api/tokens/:id` 删除令牌 +- `GET /api/tokens/:id/reveal` 再次查看令牌 + +## 行为规范 +- 令牌可通过 reveal 接口再次查看 +- 默认允许所有渠道 +- 额度消耗由代理调用记录 + +## 依赖关系 +- `tokens` 表 +- `usage` 模块(用量回写) diff --git a/helloagents/modules/usage.md b/helloagents/modules/usage.md new file mode 100644 index 0000000..5adeee7 --- /dev/null +++ b/helloagents/modules/usage.md @@ -0,0 +1,18 @@ +# usage 模块 + +## 职责 +- 记录代理调用日志 +- 提供日志查询与筛选 +- 执行日志保留策略 + +## 接口定义 +- `GET /api/usage` 使用日志查询 + +## 行为规范 +- 代理调用统一写入 `usage_logs` +- 保留天数可配置,查询时触发清理 +- 查询结果附带渠道与令牌名称 + +## 依赖关系 +- `usage_logs` 表 +- `settings` 模块 diff --git a/package.json b/package.json new file mode 100644 index 0000000..081c17d --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "new-api-lite", + "private": true, + "packageManager": "bun@1.3.9", + "workspaces": [ + "apps/*" + ], + "scripts": { + "dev:worker": "npm --workspace apps/worker run dev", + "dev:admin": "npm --workspace apps/admin run dev", + "test": "vitest", + "fix": "bun pm pkg fix" + }, + "devDependencies": { + "@types/node": "^20.11.30", + "typescript": "^5.4.5", + "vitest": "^1.5.3" + }, + "dependencies": { + "wrangler": "4" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e680d00 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "types": ["vitest/globals"], + "skipLibCheck": true + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..49d9552 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/**/*.test.ts'], + environment: 'node' + } +});