diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b84408..19b3e47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,12 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`. ### English - Replace native Logs and Models filter dropdowns with compact HeroUI Select controls that match the 32px toolbar +- Add named client API keys with optional provider allowlists, and show the console administrator key on System ### 中文 - Logs 和模型页的筛选下拉改用紧凑 HeroUI Select,高度对齐 32px 工具栏 +- 新增可限制供应商的客户端 API 密钥,并在系统页显示控制台管理员密钥 ## 0.2.20 - 2026-08-29 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index b5a1a6d3..0fc06dc8 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -347,12 +347,13 @@ They are not the same password. | Gate | Secret | Where | Unlocks | |------|--------|-------|---------| -| Console | SQLite API key | `/login` | Overview, accounts, models, API test | +| Console | SQLite `proxy_api_key` | `/login` | Overview, accounts, models, keys, logs, API test, updates | +| Client | SQLite `api_keys` | `/v1` only | Chat and model list, optionally limited to selected providers | | Qoder | device-flow / PAT | `/accounts` per worker | Upstream chat for that HOME | -`/health` stays open. `/api/*`, `/v1`, worker `/admin/*` and chat require the API key stored in SQLite. +`/health` stays open. Console `/api/*` requires the administrator key. `/v1` accepts the administrator key or a named client key. -The console API key is stored in SQLite `app_secrets` under `proxy_api_key`. A blank database generates a random key on first startup; the key has no environment-variable bootstrap path. +The console API key is stored in SQLite `app_secrets` under `proxy_api_key`. A blank database generates a random key on first startup; the key has no environment-variable bootstrap path. Named client keys live in `api_keys` and only unlock `/v1`. Empty `providers` means every family; a non-empty list is applied in auth, then passed to `PickRoute` as `AllowedProviders`. Do not put key lookup inside the scheduler. ## Account routing @@ -459,7 +460,8 @@ Keep the menu short. Login is a gate, not a nav item. | `/providers` | Models | Catalog + per-model context-window defaults; filter by provider and paginate | | `/access` | Access | Base URL + quick chat; account and model dropdowns follow the selected account catalog | | `/logs` | Logs | Request history + runtime process output | -| `/system` | System | Next-version update + SQLite protection | +| `/keys` | API keys | Named client keys with optional provider allowlists | +| `/system` | System | Next-version update, SQLite protection, console administrator key | | `/auth` | redirect | Legacy → `/accounts` | Public model IDs are lowercase request identifiers. Qoder CLI names remain display labels, while internal Qoder keys are shown only for routing diagnostics. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 56c3f35f..7df7ae9b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,7 @@ import { AccountsPage } from '@/pages/AccountsPage' import { LoginPage } from '@/pages/LoginPage' import { SystemPage } from '@/pages/SystemPage' import { LogsPage } from '@/pages/LogsPage' +import { KeysPage } from '@/pages/KeysPage' export default function App() { return ( @@ -30,6 +31,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/api/keys.ts b/frontend/src/api/keys.ts new file mode 100644 index 00000000..668c6547 --- /dev/null +++ b/frontend/src/api/keys.ts @@ -0,0 +1,54 @@ +import { api } from './client' + +export type APIKeyRecord = { + id: string + name: string + prefix: string + providers: string[] + enabled: boolean + last_used_at?: string + created_at: string + updated_at: string + secret?: string + secret_once?: boolean +} + +export type ConsoleKeyView = { + prefix: string + hint?: string + rotated?: boolean + secret?: string +} + +export function fetchAPIKeys() { + return api<{ object: string; data: APIKeyRecord[] }>('/api/keys') +} + +export function createAPIKey(input: { name: string; providers: string[]; enabled?: boolean }) { + return api('/api/keys', { + method: 'POST', + body: JSON.stringify(input), + }) +} + +export function updateAPIKey(id: string, input: { name?: string; providers?: string[]; enabled?: boolean }) { + return api(`/api/keys/${encodeURIComponent(id)}`, { + method: 'PATCH', + body: JSON.stringify(input), + }) +} + +export function deleteAPIKey(id: string) { + return api(`/api/keys/${encodeURIComponent(id)}`, { method: 'DELETE' }) +} + +export function fetchConsoleKey() { + return api('/api/system/console-key') +} + +export function rotateConsoleKey() { + return api('/api/system/console-key', { + method: 'POST', + body: JSON.stringify({ rotate: true }), + }) +} diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index 82f7ae08..2cfae388 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -12,6 +12,7 @@ const routeKey: Record = { '/access': 'access', '/accounts': 'accounts', '/logs': 'logs', + '/keys': 'keys', '/system': 'system', } diff --git a/frontend/src/components/layout/AppSidebar.tsx b/frontend/src/components/layout/AppSidebar.tsx index 7c766b37..9778d041 100644 --- a/frontend/src/components/layout/AppSidebar.tsx +++ b/frontend/src/components/layout/AppSidebar.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import { NavLink, useLocation } from 'react-router-dom' import { Drawer, Button, Chip, Tooltip } from '@heroui/react' -import { Cube, Gauge, GearSix, Lightning, Scroll, SidebarSimple, UsersThree, X } from '@phosphor-icons/react' +import { Cube, Gauge, GearSix, Key, Lightning, Scroll, SidebarSimple, UsersThree, X } from '@phosphor-icons/react' import { BrandMark } from '@/components/BrandMark' import { gsap } from 'gsap' import { pressScale } from '@/hooks/useGsapReveal' @@ -14,6 +14,7 @@ const nav = [ { to: '/providers', key: 'navProviders', icon: Cube, section: 'develop' }, { to: '/access', key: 'navAccess', icon: Lightning, section: 'develop' }, { to: '/logs', key: 'navLogs', icon: Scroll, section: 'system' }, + { to: '/keys', key: 'navKeys', icon: Key, section: 'system' }, { to: '/system', key: 'navSystem', icon: GearSix, section: 'system' }, ] as const diff --git a/frontend/src/components/ui/PageSkeletons.tsx b/frontend/src/components/ui/PageSkeletons.tsx index f4d510be..cde924d4 100644 --- a/frontend/src/components/ui/PageSkeletons.tsx +++ b/frontend/src/components/ui/PageSkeletons.tsx @@ -71,6 +71,15 @@ export function ProvidersPageSkeleton() { ) } +export function KeysPageSkeleton() { + return ( +
+
+
{Array.from({ length: 3 }, (_, index) =>
)}
+
+ ) +} + export function AccessPageSkeleton() { return (
diff --git a/frontend/src/i18n/messages.ts b/frontend/src/i18n/messages.ts index 1d885484..84e50da6 100644 --- a/frontend/src/i18n/messages.ts +++ b/frontend/src/i18n/messages.ts @@ -30,6 +30,7 @@ export const messages: Record = { navAccess: 'Access', navAccounts: 'Accounts', navLogs: 'Logs', + navKeys: 'API keys', navSystem: 'System', sidebarFoot: 'CLI login-state gateway. More providers later.', systemUpdateTitle: 'Managed update', @@ -86,7 +87,7 @@ export const messages: Record = { loggingInConsole: 'Checking…', loginHint: 'Self-hosted only. Account login happens after this, on Accounts.', loginUnlocks: 'Unlocks', - loginUnlocksValue: 'Overview, accounts, models, logs, API test', + loginUnlocksValue: 'Overview, accounts, models, keys, logs, API test', loginNot: 'Not this', loginNotValue: 'Qoder Global account', loginNext: 'Next', @@ -103,7 +104,8 @@ export const messages: Record = { access: { title: 'Access', desc: 'Copy Base URL and run a quick Chat Completions check.' }, accounts: { title: 'Accounts', desc: 'One login per account. Browser sign-in first; PAT only when needed.' }, logs: { title: 'Logs', desc: 'Recent proxy requests and process output.' }, - system: { title: 'System', desc: 'Check and apply the next stable release with SQLite protection.' }, + keys: { title: 'API keys', desc: 'Create client keys and limit them to selected providers.' }, + system: { title: 'System', desc: 'Managed updates, SQLite protection, and the console API key.' }, login: { title: 'Enter the console', desc: 'Same API key as /v1.' }, }, logsLead: 'Request history and runtime output from this host.', @@ -448,6 +450,36 @@ export const messages: Record = { disabled: 'Disabled', delete: 'Delete', deleteAccountConfirm: 'Delete account {name}? This removes its local runtime and stored credential.', + edit: 'Edit', + enabled: 'Enabled', + keysLead: 'Client keys call /v1. Leave providers empty to allow every family. The console key stays on System.', + keysCreate: 'Create key', + keysEmpty: 'No client keys yet', + keysEmptyHint: 'Create a named key for OpenAI-compatible clients. It cannot sign in to this console.', + keysCreateTitle: 'Create API key', + keysCreateHint: 'The secret is shown once. Store it in the client that will call /v1.', + keysEditTitle: 'Edit API key', + keysEditHint: 'Rename the key or change which providers it may route to.', + keysName: 'Name', + keysNamePh: 'CI bot', + keysNameRequired: 'Name is required.', + keysProviders: 'Providers', + keysProvidersHint: 'Leave all unchecked to allow every provider.', + keysAllProviders: 'All providers', + keysLastUsed: 'Last used {time}', + keysNeverUsed: 'Not used yet', + keysSecretTitle: 'Store this secret now', + keysSecretHint: 'This value is not stored in plaintext. It will not be shown again.', + keysSecretOnce: 'Copy the key before closing this dialog.', + keysDeleteConfirm: 'Delete API key {name}? Clients using it will fail immediately.', + consoleKeyTitle: 'Console API key', + consoleKeyHint: 'This is the administrator key used to sign in here.', + consoleKeyLead: 'It can call every provider and manage accounts, keys, and updates. Client keys live on the API keys page.', + consoleKeyRotate: 'Rotate key', + consoleKeyRotateHint: 'Rotating signs you out of other sessions and restarts Qoder workers so they pick up the new secret.', + consoleKeyRotateNow: 'Rotate now', + consoleKeySecretTitle: 'New console key', + consoleKeySecretHint: 'This browser session is already updated. Copy the key for any other clients that used the old console secret.', }, zh: { brandSub: '登录态网关', @@ -476,6 +508,7 @@ export const messages: Record = { navAccess: 'API 接入', navAccounts: '账号', navLogs: '日志', + navKeys: 'API 密钥', navSystem: '系统更新', sidebarFoot: 'CLI 登录态网关,后续扩展更多提供方。', systemUpdateTitle: '管理更新', @@ -532,7 +565,7 @@ export const messages: Record = { loggingInConsole: '核对中…', loginHint: '只解锁本机控制台。账号登录在进入后的「账号」页。', loginUnlocks: '解锁', - loginUnlocksValue: '概览、账号、模型、日志、接口测试', + loginUnlocksValue: '概览、账号、模型、密钥、日志、接口测试', loginNot: '不是这个', loginNotValue: 'Qoder 国际版账号', loginNext: '下一步', @@ -549,7 +582,8 @@ export const messages: Record = { access: { title: '接入', desc: '复制 Base URL,并快速验证 Chat Completions。' }, accounts: { title: '账号', desc: '一个账号一个登录态。先走浏览器登录;浏览器不方便再用 PAT。' }, logs: { title: '日志', desc: '查看近期代理请求和进程输出。' }, - system: { title: '系统更新', desc: '按顺序更新到下一个稳定版本,并保护 SQLite 数据。' }, + keys: { title: 'API 密钥', desc: '创建客户端密钥,并限制可用的供应商。' }, + system: { title: '系统更新', desc: '管理更新、SQLite 保护,以及控制台管理员密钥。' }, login: { title: '进入控制台', desc: '和 /v1 使用同一把 API Key。' }, }, logsLead: '本机请求历史与运行时输出。', @@ -894,6 +928,36 @@ export const messages: Record = { disabled: '已停用', delete: '删除', deleteAccountConfirm: '确定删除账号 {name} 吗?这会移除它的本地运行时和已保存凭据。', + edit: '编辑', + enabled: '已启用', + keysLead: '客户端密钥只用于 /v1。供应商留空表示全部可用。控制台管理员密钥在系统页。', + keysCreate: '创建密钥', + keysEmpty: '还没有客户端密钥', + keysEmptyHint: '为 OpenAI 兼容客户端创建命名密钥。它不能登录这个控制台。', + keysCreateTitle: '创建 API 密钥', + keysCreateHint: '明文密钥只显示一次,请保存在会调用 /v1 的客户端里。', + keysEditTitle: '编辑 API 密钥', + keysEditHint: '可以改名称,或限制它能调度到哪些供应商。', + keysName: '名称', + keysNamePh: 'CI 机器人', + keysNameRequired: '请填写名称。', + keysProviders: '供应商', + keysProvidersHint: '全部不勾选表示允许所有供应商。', + keysAllProviders: '全部供应商', + keysLastUsed: '最近使用 {time}', + keysNeverUsed: '尚未使用', + keysSecretTitle: '请立刻保存这把密钥', + keysSecretHint: '服务端只存哈希,关闭后无法再查看明文。', + keysSecretOnce: '关闭弹窗前请先复制。', + keysDeleteConfirm: '确定删除 API 密钥 {name} 吗?正在使用它的客户端会立刻失败。', + consoleKeyTitle: '控制台 API 密钥', + consoleKeyHint: '这是登录本控制台的管理员密钥。', + consoleKeyLead: '它可以调用全部供应商,并管理账号、密钥和更新。客户端密钥在「API 密钥」页。', + consoleKeyRotate: '轮换密钥', + consoleKeyRotateHint: '轮换后其他会话会失效,并重启 Qoder worker 以使用新密钥。', + consoleKeyRotateNow: '立即轮换', + consoleKeySecretTitle: '新的控制台密钥', + consoleKeySecretHint: '当前浏览器会话已更新。如果还有客户端在用旧的控制台密钥,请把新值复制过去。', }, } diff --git a/frontend/src/pages/KeysPage.tsx b/frontend/src/pages/KeysPage.tsx new file mode 100644 index 00000000..67eba61a --- /dev/null +++ b/frontend/src/pages/KeysPage.tsx @@ -0,0 +1,367 @@ +import { useEffect, useMemo, useState } from 'react' +import { Alert, Button, Checkbox, Chip, Description, Form, Input, Label, Modal } from '@heroui/react' +import { Copy, Key, Plus, TrashSimple, WarningCircle, X } from '@phosphor-icons/react' +import { createAPIKey, deleteAPIKey, fetchAPIKeys, updateAPIKey, type APIKeyRecord } from '@/api/keys' +import { BrandMark } from '@/components/BrandMark' +import { ProviderMark } from '@/components/ProviderMark' +import { CompactSwitch } from '@/components/ui/CompactSwitch' +import { KeysPageSkeleton } from '@/components/ui/PageSkeletons' +import { useI18n } from '@/hooks/useI18n' +import { useOverview } from '@/hooks/useOverview' +import { accountProviderFamilyLabel } from '@/lib/provider' + +const PROVIDER_IDS = ['qoder', 'workbuddy', 'trae'] + +function providersLabel(ids: string[], t: (key: string) => string) { + if (!ids.length) return t('keysAllProviders') + return ids.map((id) => accountProviderFamilyLabel(id, t)).join(' · ') +} + +function formatTime(value?: string) { + if (!value) return '' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return value + return date.toLocaleString() +} + +export function KeysPage() { + const { t } = useI18n() + const { overview } = useOverview() + const [keys, setKeys] = useState([]) + const [loading, setLoading] = useState(true) + const [busyId, setBusyId] = useState('') + const [error, setError] = useState('') + const [createOpen, setCreateOpen] = useState(false) + const [editKey, setEditKey] = useState(null) + const [deleteKey, setDeleteKey] = useState(null) + const [revealed, setRevealed] = useState(null) + const [copied, setCopied] = useState(false) + + const availableProviders = useMemo(() => { + const ids = new Set(PROVIDER_IDS) + for (const account of overview?.accounts || []) { + if (account.provider) ids.add(account.provider) + } + return [...ids] + }, [overview?.accounts]) + + async function load() { + setLoading(true) + try { + const result = await fetchAPIKeys() + setKeys(result.data || []) + setError('') + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setLoading(false) + } + } + + useEffect(() => { + const timer = window.setTimeout(() => void load(), 0) + return () => window.clearTimeout(timer) + }, []) + + async function copySecret(value: string) { + await navigator.clipboard.writeText(value) + setCopied(true) + window.setTimeout(() => setCopied(false), 1200) + } + + async function onToggle(key: APIKeyRecord, enabled: boolean) { + setBusyId(key.id) + setKeys((current) => current.map((item) => item.id === key.id ? { ...item, enabled } : item)) + try { + const updated = await updateAPIKey(key.id, { enabled }) + setKeys((current) => current.map((item) => item.id === key.id ? { ...item, ...updated, secret: undefined } : item)) + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + await load() + } finally { + setBusyId('') + } + } + + async function onDelete() { + if (!deleteKey) return + setBusyId(deleteKey.id) + try { + await deleteAPIKey(deleteKey.id) + setKeys((current) => current.filter((item) => item.id !== deleteKey.id)) + setDeleteKey(null) + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setBusyId('') + } + } + + if (loading && !keys.length && !error) return + + return ( +
+
+
+

{t('navKeys')}

+

{t('keysLead')}

+
+ +
+ + {error ? ( +
+ {error} +
+ ) : null} + + {!keys.length ? ( +
+
+ +
{t('keysEmpty')}
+
{t('keysEmptyHint')}
+ +
+
+ ) : ( +
+ {keys.map((key) => ( +
+
+
+
+
{key.name}
+
{key.prefix}
+
+ + {key.enabled ? t('enabled') : t('disabled')} + +
+
+ {(key.providers.length ? key.providers : ['all']).map((provider) => ( + + + {provider !== 'all' ? : } + {provider === 'all' ? t('keysAllProviders') : accountProviderFamilyLabel(provider, t)} + + + ))} +
+

+ {key.last_used_at ? t('keysLastUsed', { time: formatTime(key.last_used_at) }) : t('keysNeverUsed')} +

+
+
+ + +
+ void onToggle(key, selected)} + /> +
+
+
+ ))} +
+ )} + + setCreateOpen(false)} + onSave={async (input) => { + const created = await createAPIKey(input) + setKeys((current) => [created, ...current.map((item) => ({ ...item, secret: undefined }))]) + setCreateOpen(false) + setRevealed(created) + }} + /> + setEditKey(null)} + onSave={async (input) => { + if (!editKey) return + const updated = await updateAPIKey(editKey.id, input) + setKeys((current) => current.map((item) => item.id === editKey.id ? { ...item, ...updated, secret: undefined } : item)) + setEditKey(null) + }} + /> + + { if (!open) setRevealed(null) }}> + + + + +
+ {t('keysSecretTitle')} +

{t('keysSecretHint')}

+
+ +
+ + + + {t('keysSecretOnce')} + + {revealed?.secret} + + + + + +
+
+
+
+ + { if (!open) setDeleteKey(null) }}> + + + + +
{t('delete')}
+ +
+ +

{t('keysDeleteConfirm', { name: deleteKey?.name || '' })}

+
+ + + + +
+
+
+
+
+ ) +} + +function KeyEditorModal({ + isOpen, + title, + hint, + initial, + providers, + t, + onClose, + onSave, +}: { + isOpen: boolean + title: string + hint: string + initial?: APIKeyRecord | null + providers: string[] + t: (key: string, vars?: Record) => string + onClose: () => void + onSave: (input: { name: string; providers: string[]; enabled?: boolean }) => Promise +}) { + const [name, setName] = useState(initial?.name || '') + const [selected, setSelected] = useState(initial?.providers || []) + const [error, setError] = useState('') + const [busy, setBusy] = useState(false) + + useEffect(() => { + if (!isOpen) return + setName(initial?.name || '') + setSelected(initial?.providers || []) + setError('') + }, [initial, isOpen]) + + function toggle(id: string) { + setSelected((current) => current.includes(id) ? current.filter((item) => item !== id) : [...current, id]) + } + + async function submit(event?: { preventDefault(): void }) { + event?.preventDefault() + const trimmed = name.trim() + if (!trimmed) { + setError(t('keysNameRequired')) + return + } + setBusy(true) + setError('') + try { + await onSave({ name: trimmed, providers: selected }) + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setBusy(false) + } + } + + return ( + { if (!open && !busy) onClose() }}> + + + +
+ +
+ {title} +

{hint}

+
+ +
+ + {error ? ( + + + {error} + + ) : null} +
+ + setName(event.target.value)} placeholder={t('keysNamePh')} /> +
+
+ + {t('keysProvidersHint')} +
+ {providers.map((id) => { + const checked = selected.includes(id) + return ( + toggle(id)} className="rounded-lg border border-[var(--app-line)] px-3 py-2.5"> + + + + + + {accountProviderFamilyLabel(id, t)} + + + ) + })} +
+

{providersLabel(selected, t)}

+
+
+ + + + +
+
+
+
+
+ ) +} diff --git a/frontend/src/pages/SystemPage.tsx b/frontend/src/pages/SystemPage.tsx index 1140f0ae..d420a5ca 100644 --- a/frontend/src/pages/SystemPage.tsx +++ b/frontend/src/pages/SystemPage.tsx @@ -4,12 +4,16 @@ import { ArrowClockwise, ArrowCircleUp, CheckCircle, + Copy, Database, + Key, ShieldCheck, WarningCircle, X, } from '@phosphor-icons/react' +import { fetchConsoleKey, rotateConsoleKey, type ConsoleKeyView } from '@/api/keys' import { fetchSystemUpdate, startSystemUpdate, type StartUpdateResult, type SystemUpdateInfo } from '@/api/system' +import { useApiKey } from '@/hooks/useApiKey' import { ReleaseNotes } from '@/components/ReleaseNotes' import { useI18n } from '@/hooks/useI18n' import { extractReleaseNotes } from '@/lib/releaseNotes' @@ -25,7 +29,13 @@ function statusColor(state?: string): 'success' | 'warning' | 'danger' | 'defaul export function SystemPage() { const { lang, t } = useI18n() + const { setApiKey } = useApiKey() const [info, setInfo] = useState(null) + const [consoleKey, setConsoleKey] = useState(null) + const [consoleBusy, setConsoleBusy] = useState(false) + const [rotateOpen, setRotateOpen] = useState(false) + const [rotatedSecret, setRotatedSecret] = useState('') + const [copied, setCopied] = useState(false) const [loading, setLoading] = useState(true) const [checking, setChecking] = useState(false) const [submitting, setSubmitting] = useState(false) @@ -52,7 +62,10 @@ export function SystemPage() { }, []) useEffect(() => { - const timer = window.setTimeout(() => void load(false), 0) + const timer = window.setTimeout(() => { + void load(false) + void fetchConsoleKey().then(setConsoleKey).catch(() => undefined) + }, 0) return () => window.clearTimeout(timer) }, [load]) @@ -202,6 +215,23 @@ export function SystemPage() { {started?.backup?.name ?
{started.backup.name}
: null} + +
+
+
+
+

{t('consoleKeyTitle')}

+

{t('consoleKeyHint')}

+
+
+
+ {consoleKey?.prefix || '—'} +

{t('consoleKeyLead')}

+
+ +
+
+
@@ -215,6 +245,65 @@ export function SystemPage() {
+ { if (!open && !consoleBusy) setRotateOpen(false) }}> + + + + +
+ {t('consoleKeyRotate')} +

{t('consoleKeyRotateHint')}

+
+ +
+ + + + +
+
+
+
+ + { if (!open) setRotatedSecret('') }}> + + + + +
+ {t('consoleKeySecretTitle')} +

{t('consoleKeySecretHint')}

+
+ +
+ + {rotatedSecret} + + + + + +
+
+
+
+ { if (!open && !submitting && reloadIn == null) setConfirmOpen(false) }}> diff --git a/internal/accounts/api_keys.go b/internal/accounts/api_keys.go new file mode 100644 index 00000000..86485a54 --- /dev/null +++ b/internal/accounts/api_keys.go @@ -0,0 +1,278 @@ +package accounts + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "database/sql" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/caigee-cmd/cli2api/internal/providers" +) + +type APIKey struct { + ID string `json:"id"` + Name string `json:"name"` + Prefix string `json:"prefix"` + Providers []string `json:"providers"` + Enabled bool `json:"enabled"` + LastUsedAt *time.Time `json:"last_used_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Secret string `json:"secret,omitempty"` + SecretOnce bool `json:"secret_once,omitempty"` +} + +type CreateAPIKey struct { + Name string + Providers []string + Enabled bool +} + +type UpdateAPIKey struct { + Name string + Providers []string + Enabled *bool +} + +func HashAPIKey(secret string) string { + sum := sha256.Sum256([]byte(strings.TrimSpace(secret))) + return hex.EncodeToString(sum[:]) +} + +func APIKeyPrefix(secret string) string { + secret = strings.TrimSpace(secret) + if len(secret) <= 12 { + return secret + } + return secret[:8] + "…" + secret[len(secret)-4:] +} + +func NormalizeAPIKeyProviders(ids []string) ([]string, error) { + if len(ids) == 0 { + return nil, nil + } + seen := map[string]struct{}{} + out := make([]string, 0, len(ids)) + for _, id := range ids { + id = strings.ToLower(strings.TrimSpace(id)) + if id == "" { + continue + } + if _, _, err := providers.Resolve(id, ""); err != nil { + return nil, fmt.Errorf("unknown provider %q", id) + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + out = append(out, id) + } + return out, nil +} + +func GenerateAPIKeySecret() (string, error) { + raw := make([]byte, 32) + if _, err := rand.Read(raw); err != nil { + return "", fmt.Errorf("generate api key: %w", err) + } + return "sk_" + base64.RawURLEncoding.EncodeToString(raw), nil +} + +func newAPIKeyID() string { + raw := make([]byte, 6) + if _, err := rand.Read(raw); err != nil { + return fmt.Sprintf("key_%d", time.Now().UnixNano()) + } + return "key_" + hex.EncodeToString(raw) +} + +func (s *Store) CreateAPIKey(ctx context.Context, input CreateAPIKey) (APIKey, error) { + name := strings.TrimSpace(input.Name) + if name == "" { + return APIKey{}, fmt.Errorf("api key name required") + } + allowed, err := NormalizeAPIKeyProviders(input.Providers) + if err != nil { + return APIKey{}, err + } + secret, err := GenerateAPIKeySecret() + if err != nil { + return APIKey{}, err + } + now := time.Now().UTC() + key := APIKey{ + ID: newAPIKeyID(), + Name: name, + Prefix: APIKeyPrefix(secret), + Providers: allowed, + Enabled: input.Enabled, + CreatedAt: now, + UpdatedAt: now, + Secret: secret, + SecretOnce: true, + } + payload, err := json.Marshal(allowed) + if err != nil { + return APIKey{}, err + } + _, err = s.db.ExecContext(ctx, ` +INSERT INTO api_keys (id, name, key_hash, prefix, providers_json, enabled, created_at, updated_at) +VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + key.ID, key.Name, HashAPIKey(secret), key.Prefix, string(payload), boolToInt(key.Enabled), + formatTime(key.CreatedAt), formatTime(key.UpdatedAt), + ) + if err != nil { + return APIKey{}, fmt.Errorf("create api key: %w", err) + } + return key, nil +} + +func (s *Store) ListAPIKeys(ctx context.Context) ([]APIKey, error) { + rows, err := s.db.QueryContext(ctx, ` +SELECT id, name, prefix, providers_json, enabled, last_used_at, created_at, updated_at +FROM api_keys ORDER BY created_at DESC, id DESC`) + if err != nil { + return nil, fmt.Errorf("list api keys: %w", err) + } + defer rows.Close() + var keys []APIKey + for rows.Next() { + key, err := scanAPIKey(rows) + if err != nil { + return nil, err + } + keys = append(keys, key) + } + return keys, rows.Err() +} + +func (s *Store) GetAPIKey(ctx context.Context, id string) (APIKey, error) { + row := s.db.QueryRowContext(ctx, ` +SELECT id, name, prefix, providers_json, enabled, last_used_at, created_at, updated_at +FROM api_keys WHERE id = ?`, strings.TrimSpace(id)) + key, err := scanAPIKey(row) + if errors.Is(err, sql.ErrNoRows) { + return APIKey{}, ErrAPIKeyNotFound + } + if err != nil { + return APIKey{}, fmt.Errorf("get api key: %w", err) + } + return key, nil +} + +func (s *Store) LookupAPIKey(ctx context.Context, secret string) (APIKey, bool, error) { + secret = strings.TrimSpace(secret) + if secret == "" { + return APIKey{}, false, nil + } + row := s.db.QueryRowContext(ctx, ` +SELECT id, name, prefix, providers_json, enabled, last_used_at, created_at, updated_at +FROM api_keys WHERE key_hash = ?`, HashAPIKey(secret)) + key, err := scanAPIKey(row) + if errors.Is(err, sql.ErrNoRows) { + return APIKey{}, false, nil + } + if err != nil { + return APIKey{}, false, fmt.Errorf("lookup api key: %w", err) + } + return key, true, nil +} + +func (s *Store) UpdateAPIKey(ctx context.Context, id string, input UpdateAPIKey) (APIKey, error) { + key, err := s.GetAPIKey(ctx, id) + if err != nil { + return APIKey{}, err + } + if name := strings.TrimSpace(input.Name); name != "" { + key.Name = name + } + if input.Providers != nil { + allowed, err := NormalizeAPIKeyProviders(input.Providers) + if err != nil { + return APIKey{}, err + } + key.Providers = allowed + } + if input.Enabled != nil { + key.Enabled = *input.Enabled + } + key.UpdatedAt = time.Now().UTC() + payload, err := json.Marshal(key.Providers) + if err != nil { + return APIKey{}, err + } + result, err := s.db.ExecContext(ctx, ` +UPDATE api_keys SET name = ?, providers_json = ?, enabled = ?, updated_at = ? +WHERE id = ?`, key.Name, string(payload), boolToInt(key.Enabled), formatTime(key.UpdatedAt), key.ID) + if err != nil { + return APIKey{}, fmt.Errorf("update api key: %w", err) + } + changed, _ := result.RowsAffected() + if changed == 0 { + return APIKey{}, ErrAPIKeyNotFound + } + return key, nil +} + +func (s *Store) DeleteAPIKey(ctx context.Context, id string) error { + result, err := s.db.ExecContext(ctx, `DELETE FROM api_keys WHERE id = ?`, strings.TrimSpace(id)) + if err != nil { + return fmt.Errorf("delete api key: %w", err) + } + changed, _ := result.RowsAffected() + if changed == 0 { + return ErrAPIKeyNotFound + } + return nil +} + +func (s *Store) TouchAPIKey(ctx context.Context, id string) error { + id = strings.TrimSpace(id) + if id == "" { + return nil + } + now := formatTime(time.Now().UTC()) + _, err := s.db.ExecContext(ctx, `UPDATE api_keys SET last_used_at = ? WHERE id = ?`, now, id) + if err != nil { + return fmt.Errorf("touch api key: %w", err) + } + return nil +} + +func scanAPIKey(row rowScanner) (APIKey, error) { + var key APIKey + var providersJSON string + var lastUsed, created, updated sql.NullString + var enabled int + if err := row.Scan(&key.ID, &key.Name, &key.Prefix, &providersJSON, &enabled, &lastUsed, &created, &updated); err != nil { + return APIKey{}, err + } + key.Enabled = enabled != 0 + if strings.TrimSpace(providersJSON) != "" && providersJSON != "null" { + if err := json.Unmarshal([]byte(providersJSON), &key.Providers); err != nil { + return APIKey{}, fmt.Errorf("decode api key providers: %w", err) + } + } + if key.Providers == nil { + key.Providers = []string{} + } + if lastUsed.Valid { + parsed := parseTime(lastUsed.String) + key.LastUsedAt = &parsed + } + key.CreatedAt = parseTime(created.String) + key.UpdatedAt = parseTime(updated.String) + return key, nil +} + +func ConstantTimeEqual(left, right string) bool { + return subtle.ConstantTimeCompare([]byte(left), []byte(right)) == 1 +} diff --git a/internal/accounts/manager.go b/internal/accounts/manager.go index 1a7fec64..68ac5619 100644 --- a/internal/accounts/manager.go +++ b/internal/accounts/manager.go @@ -105,6 +105,39 @@ func (m *Manager) Start(ctx context.Context) error { func (m *Manager) Pool() *Pool { return m.pool } func (m *Manager) Store() *Store { return m.store } +func (m *Manager) ReplaceProxyAPIKey(ctx context.Context, key string) error { + if m == nil { + return nil + } + m.mu.Lock() + m.config.ProxyAPIKey = key + if starter, ok := m.starter.(ExecStarter); ok { + starter.Config.ProxyAPIKey = key + m.starter = starter + } + accounts := make([]Account, 0, len(m.processes)) + for id := range m.processes { + account, err := m.store.Get(ctx, id) + if err != nil { + m.mu.Unlock() + return err + } + if account.Enabled { + accounts = append(accounts, account) + } + } + m.mu.Unlock() + for _, account := range accounts { + if err := m.stopAccount(account.ID); err != nil { + return err + } + if err := m.startAccount(ctx, account); err != nil { + return err + } + } + return nil +} + // SetProviders wires optional in-process account probers (WorkBuddy, etc.). func (m *Manager) SetProviders(registry *providers.Registry) { if m == nil { diff --git a/internal/accounts/migrations.go b/internal/accounts/migrations.go index db3c1099..0fe35daa 100644 --- a/internal/accounts/migrations.go +++ b/internal/accounts/migrations.go @@ -129,13 +129,27 @@ CREATE INDEX IF NOT EXISTS request_logs_provider ON request_logs(provider);`, // v0.2.19 retabbed this SQL; keep that checksum bootable. legacyChecksums: []string{"9b96b8d63286519b20791d2a3688c0b71efba6204015250ae9864c5cbcd1f0b4"}}, {filename: "007_provider_model_settings.sql", sql: ` - CREATE TABLE IF NOT EXISTS provider_model_settings ( - provider TEXT NOT NULL, - model_id TEXT NOT NULL, - max_mode INTEGER NOT NULL DEFAULT 0, - updated_at TEXT NOT NULL, - PRIMARY KEY (provider, model_id) - );`}, + CREATE TABLE IF NOT EXISTS provider_model_settings ( + provider TEXT NOT NULL, + model_id TEXT NOT NULL, + max_mode INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL, + PRIMARY KEY (provider, model_id) + );`}, + {filename: "008_api_keys.sql", sql: ` +CREATE TABLE IF NOT EXISTS api_keys ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + key_hash TEXT NOT NULL UNIQUE, + prefix TEXT NOT NULL, + providers_json TEXT NOT NULL DEFAULT '[]', + enabled INTEGER NOT NULL DEFAULT 1, + last_used_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS api_keys_hash ON api_keys(key_hash); +CREATE INDEX IF NOT EXISTS api_keys_created_at ON api_keys(created_at DESC);`}, } const schemaMigrationsDDL = ` diff --git a/internal/accounts/pool.go b/internal/accounts/pool.go index 88423657..e76a8cb8 100644 --- a/internal/accounts/pool.go +++ b/internal/accounts/pool.go @@ -50,11 +50,12 @@ type Item struct { // RouteQuery selects candidates for one public model request. Empty fields are // not filtered; excluded account IDs are honored before anything else. type RouteQuery struct { - PublicModel string - PreferAccount string - ProviderFilter string - RegionFilter string - Excluded map[string]struct{} + PublicModel string + PreferAccount string + ProviderFilter string + RegionFilter string + AllowedProviders []string + Excluded map[string]struct{} } func itemRegion(item Item) string { @@ -111,10 +112,29 @@ func NativeModelID(item Item, publicModel string) string { return strings.TrimSpace(publicModel) } +func providerAllowed(provider string, allowed []string) bool { + if len(allowed) == 0 { + return true + } + family := strings.ToLower(strings.TrimSpace(provider)) + if family == "" { + family = "qoder" + } + for _, item := range allowed { + if strings.ToLower(strings.TrimSpace(item)) == family { + return true + } + } + return false +} + func routeMatches(item Item, q RouteQuery) bool { if _, skip := q.Excluded[item.ID]; skip { return false } + if !providerAllowed(item.Provider, q.AllowedProviders) { + return false + } if q.ProviderFilter != "" && item.Provider != q.ProviderFilter { return false } diff --git a/internal/accounts/pool_route_test.go b/internal/accounts/pool_route_test.go index b83dc3e7..3c07440c 100644 --- a/internal/accounts/pool_route_test.go +++ b/internal/accounts/pool_route_test.go @@ -37,6 +37,24 @@ func TestPickRouteRespectsProviderFamilyAndCooldown(t *testing.T) { } } +func TestPickRouteHonorsAPIKeyAllowlist(t *testing.T) { + p := NewPool(nil, nil) + p.Upsert(Item{ID: "q1", URL: "http://q1", Provider: "qoder", Runtime: "child_process"}) + p.Upsert(Item{ID: "w1", Provider: "workbuddy", Runtime: "in_process"}) + p.Upsert(Item{ID: "t1", Provider: "trae", Runtime: "in_process"}) + + got, ok := p.PickRoute(RouteQuery{AllowedProviders: []string{"trae"}}) + if !ok || got.ID != "t1" { + t.Fatalf("allowlist pick = %+v ok=%v", got, ok) + } + if n := p.LenRoute(RouteQuery{AllowedProviders: []string{"qoder", "workbuddy"}}); n != 2 { + t.Fatalf("two-family allowlist count = %d", n) + } + if _, ok := p.PickRoute(RouteQuery{ProviderFilter: "trae", AllowedProviders: []string{"qoder"}}); ok { + t.Fatal("key limited to qoder must not pick trae") + } +} + func TestPickRouteKeepsQoderFailoverInsideRegion(t *testing.T) { p := NewPool(nil, nil) p.Upsert(Item{ID: "g1", URL: "http://g1", Provider: "qoder", Region: "global", Runtime: "child_process"}) diff --git a/internal/accounts/store.go b/internal/accounts/store.go index e12041a9..aae3e36e 100644 --- a/internal/accounts/store.go +++ b/internal/accounts/store.go @@ -19,6 +19,7 @@ import ( var ErrAccountNotFound = errors.New("account not found") var ErrSecretNotFound = errors.New("secret not found") +var ErrAPIKeyNotFound = errors.New("api key not found") type Account struct { ID string `json:"id"` diff --git a/internal/accounts/store_test.go b/internal/accounts/store_test.go index 2268339b..8bfa091e 100644 --- a/internal/accounts/store_test.go +++ b/internal/accounts/store_test.go @@ -177,6 +177,48 @@ func TestProviderModelMaxModeIsIndependentOfQoderContext(t *testing.T) { } } +func TestStoreCreatesNamedAPIKeys(t *testing.T) { + ctx := context.Background() + store, err := OpenStore(filepath.Join(t.TempDir(), "qoder.db")) + if err != nil { + t.Fatal(err) + } + defer store.Close() + + created, err := store.CreateAPIKey(ctx, CreateAPIKey{Name: "CI", Providers: []string{"qoder"}, Enabled: true}) + if err != nil { + t.Fatal(err) + } + if created.Secret == "" || created.ID == "" || created.Prefix == "" { + t.Fatalf("created key missing secret fields: %+v", created) + } + listed, err := store.ListAPIKeys(ctx) + if err != nil { + t.Fatal(err) + } + if len(listed) != 1 || listed[0].Secret != "" || listed[0].ID != created.ID { + t.Fatalf("listed = %+v", listed) + } + got, ok, err := store.LookupAPIKey(ctx, created.Secret) + if err != nil || !ok || got.ID != created.ID { + t.Fatalf("lookup = %+v ok=%v err=%v", got, ok, err) + } + disabled := false + updated, err := store.UpdateAPIKey(ctx, created.ID, UpdateAPIKey{Name: "CI prod", Providers: []string{"qoder", "trae"}, Enabled: &disabled}) + if err != nil { + t.Fatal(err) + } + if updated.Name != "CI prod" || updated.Enabled || len(updated.Providers) != 2 { + t.Fatalf("updated = %+v", updated) + } + if err := store.DeleteAPIKey(ctx, created.ID); err != nil { + t.Fatal(err) + } + if _, err := store.GetAPIKey(ctx, created.ID); !errors.Is(err, ErrAPIKeyNotFound) { + t.Fatalf("deleted key err = %v", err) + } +} + func TestStoreSavesNativeCredential(t *testing.T) { ctx := context.Background() store, err := OpenStore(filepath.Join(t.TempDir(), "qoder.db")) diff --git a/internal/api/auth_test.go b/internal/api/auth_test.go index 3e8a197f..011697e3 100644 --- a/internal/api/auth_test.go +++ b/internal/api/auth_test.go @@ -34,6 +34,8 @@ func TestManagementRoutesRequireAPIKey(t *testing.T) { "/api/logs/requests", "/api/logs/runtime", "/api/system/update", + "/api/keys", + "/api/system/console-key", } { method := http.MethodGet if path == "/api/chat" { @@ -430,3 +432,83 @@ func TestAccountsAPIUpdatesExportsAndDeletesAccount(t *testing.T) { t.Fatalf("delete: %d %s", rec.Code, rec.Body.String()) } } + +func TestNamedAPIKeyCannotManageConsoleOrKeys(t *testing.T) { + srv := New(config.Config{ + Host: "127.0.0.1", Port: 3010, ProxyAPIKey: "secret", + QoderHome: t.TempDir(), DataDir: t.TempDir(), + }) + defer srv.Close() + created, err := srv.manager.Store().CreateAPIKey(context.Background(), accounts.CreateAPIKey{ + Name: "ci", Providers: []string{"qoder"}, Enabled: true, + }) + if err != nil { + t.Fatal(err) + } + req := httptest.NewRequest(http.MethodGet, "/api/keys", nil) + req.Header.Set("Authorization", "Bearer "+created.Secret) + rec := httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("named key GET /api/keys = %d %s", rec.Code, rec.Body.String()) + } + req = httptest.NewRequest(http.MethodGet, "/api/overview", nil) + req.Header.Set("Authorization", "Bearer "+created.Secret) + rec = httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("named key GET /api/overview = %d %s", rec.Code, rec.Body.String()) + } + req = httptest.NewRequest(http.MethodGet, "/v1/models", nil) + req.Header.Set("Authorization", "Bearer "+created.Secret) + rec = httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("named key GET /v1/models = %d %s", rec.Code, rec.Body.String()) + } +} + +func TestAPIKeysCRUDAndConsoleKeyPrefix(t *testing.T) { + srv := New(config.Config{ + Host: "127.0.0.1", Port: 3010, ProxyAPIKey: "secret", + QoderHome: t.TempDir(), DataDir: t.TempDir(), + }) + defer srv.Close() + + req := httptest.NewRequest(http.MethodGet, "/api/system/console-key", nil) + req.Header.Set("Authorization", "Bearer secret") + rec := httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusOK || !bytes.Contains(rec.Body.Bytes(), []byte(`"prefix"`)) { + t.Fatalf("console key: %d %s", rec.Code, rec.Body.String()) + } + + body := bytes.NewBufferString(`{"name":"CI","providers":["qoder"]}`) + req = httptest.NewRequest(http.MethodPost, "/api/keys", body) + req.Header.Set("Authorization", "Bearer secret") + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusCreated || !bytes.Contains(rec.Body.Bytes(), []byte(`"secret"`)) { + t.Fatalf("create key: %d %s", rec.Code, rec.Body.String()) + } + var created accounts.APIKey + if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil { + t.Fatal(err) + } + req = httptest.NewRequest(http.MethodPatch, "/api/keys/"+created.ID, bytes.NewBufferString(`{"name":"CI bot"}`)) + req.Header.Set("Authorization", "Bearer secret") + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusOK || !bytes.Contains(rec.Body.Bytes(), []byte(`"name":"CI bot"`)) { + t.Fatalf("patch key: %d %s", rec.Code, rec.Body.String()) + } + req = httptest.NewRequest(http.MethodDelete, "/api/keys/"+created.ID, nil) + req.Header.Set("Authorization", "Bearer secret") + rec = httptest.NewRecorder() + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusNoContent { + t.Fatalf("delete key: %d %s", rec.Code, rec.Body.String()) + } +} diff --git a/internal/api/chat.go b/internal/api/chat.go index ac157634..23cd896d 100644 --- a/internal/api/chat.go +++ b/internal/api/chat.go @@ -163,7 +163,7 @@ func parseStreamUsageLine(line string) (streamRelayStats, bool) { func (s *Server) handleModels(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "object": "list", - "data": s.decorateModelsWithContext(r.Context(), s.fetchWorkerModels(false)), + "data": s.decorateModelsWithContext(r.Context(), s.filterModelsForIdentity(r, s.fetchWorkerModels(false))), }) } @@ -187,6 +187,24 @@ func (s *Server) resolveProviderFilter(req *translate.ChatRequest) string { return "qoder" } +func (s *Server) filterModelsForIdentity(r *http.Request, models []map[string]any) []map[string]any { + identity := s.requestIdentity(r) + if len(identity.AllowedProviders) == 0 { + return models + } + filtered := make([]map[string]any, 0, len(models)) + for _, model := range models { + provider, _ := model["provider"].(string) + if provider == "" { + provider, _ = model["owned_by"].(string) + } + if identity.AllowsProvider(provider) { + filtered = append(filtered, model) + } + } + return filtered +} + func (s *Server) handleModelsAPI(w http.ResponseWriter, r *http.Request) { refresh := r.URL.Query().Get("refresh") == "1" models, err := s.fetchWorkerModelsFor(refresh, s.requestedAccount(r)) @@ -199,7 +217,7 @@ func (s *Server) handleModelsAPI(w http.ResponseWriter, r *http.Request) { } writeJSON(w, http.StatusOK, map[string]any{ "object": "list", - "data": s.decorateModelsWithContext(r.Context(), models), + "data": s.decorateModelsWithContext(r.Context(), s.filterModelsForIdentity(r, models)), }) } @@ -219,6 +237,11 @@ func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) { } publicModel := req.Model providerFilter := s.resolveProviderFilter(&req) + identity := s.requestIdentity(r) + if providerFilter != "" && !identity.AllowsProvider(providerFilter) { + writeErr(w, http.StatusForbidden, "provider_not_allowed", "This API key cannot use provider "+providerFilter) + return + } if err := s.applyModelContextDefaults(r.Context(), &req, providerFilter); err != nil { writeErr(w, http.StatusInternalServerError, "model_setting_failed", err.Error()) return @@ -234,7 +257,7 @@ func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) { ID: requestID, CreatedAt: started, Stream: req.Stream, Status: accounts.RequestStatusStarted, RequestedModel: firstNonEmpty(publicModel, req.Model), }) - ctx := executor.WithRequestID(r.Context(), requestID) + ctx := executor.WithAllowedProviders(executor.WithRequestID(r.Context(), requestID), identity.AllowedProviders) w.Header().Set("X-Request-Id", requestID) if req.Stream { diff --git a/internal/api/keys.go b/internal/api/keys.go new file mode 100644 index 00000000..9ffc8890 --- /dev/null +++ b/internal/api/keys.go @@ -0,0 +1,158 @@ +package api + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "strings" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/auth" +) + +type consoleKeyView struct { + Prefix string `json:"prefix"` + Hint string `json:"hint"` + Rotated bool `json:"rotated,omitempty"` + Secret string `json:"secret,omitempty"` +} + +func (s *Server) handleAPIKeys(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + keys, err := s.manager.Store().ListAPIKeys(r.Context()) + if err != nil { + writeErr(w, http.StatusInternalServerError, "api_key_list_failed", err.Error()) + return + } + if keys == nil { + keys = []accounts.APIKey{} + } + writeJSON(w, http.StatusOK, map[string]any{"object": "list", "data": keys}) + case http.MethodPost: + var input struct { + Name string `json:"name"` + Providers []string `json:"providers"` + Enabled *bool `json:"enabled"` + } + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + writeErr(w, http.StatusBadRequest, "invalid_request", err.Error()) + return + } + enabled := true + if input.Enabled != nil { + enabled = *input.Enabled + } + key, err := s.manager.Store().CreateAPIKey(r.Context(), accounts.CreateAPIKey{ + Name: input.Name, Providers: input.Providers, Enabled: enabled, + }) + if err != nil { + writeErr(w, http.StatusBadRequest, "api_key_create_failed", err.Error()) + return + } + writeJSON(w, http.StatusCreated, key) + default: + writeErr(w, http.StatusMethodNotAllowed, "method_not_allowed", "GET or POST only") + } +} + +func (s *Server) handleAPIKeyByID(w http.ResponseWriter, r *http.Request) { + id := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/keys/"), "/") + if id == "" || strings.Contains(id, "/") { + writeErr(w, http.StatusNotFound, "api_key_not_found", "api key id required") + return + } + switch r.Method { + case http.MethodGet: + key, err := s.manager.Store().GetAPIKey(r.Context(), id) + if errors.Is(err, accounts.ErrAPIKeyNotFound) { + writeErr(w, http.StatusNotFound, "api_key_not_found", err.Error()) + return + } + if err != nil { + writeErr(w, http.StatusInternalServerError, "api_key_get_failed", err.Error()) + return + } + writeJSON(w, http.StatusOK, key) + case http.MethodPatch: + var input struct { + Name string `json:"name"` + Providers []string `json:"providers"` + Enabled *bool `json:"enabled"` + } + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + writeErr(w, http.StatusBadRequest, "invalid_request", err.Error()) + return + } + key, err := s.manager.Store().UpdateAPIKey(r.Context(), id, accounts.UpdateAPIKey{ + Name: input.Name, Providers: input.Providers, Enabled: input.Enabled, + }) + if errors.Is(err, accounts.ErrAPIKeyNotFound) { + writeErr(w, http.StatusNotFound, "api_key_not_found", err.Error()) + return + } + if err != nil { + writeErr(w, http.StatusBadRequest, "api_key_update_failed", err.Error()) + return + } + writeJSON(w, http.StatusOK, key) + case http.MethodDelete: + if err := s.manager.Store().DeleteAPIKey(r.Context(), id); errors.Is(err, accounts.ErrAPIKeyNotFound) { + writeErr(w, http.StatusNotFound, "api_key_not_found", err.Error()) + return + } else if err != nil { + writeErr(w, http.StatusInternalServerError, "api_key_delete_failed", err.Error()) + return + } + w.WriteHeader(http.StatusNoContent) + default: + writeErr(w, http.StatusMethodNotAllowed, "method_not_allowed", "GET, PATCH, or DELETE only") + } +} + +func (s *Server) handleConsoleKey(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + writeJSON(w, http.StatusOK, consoleKeyView{ + Prefix: accounts.APIKeyPrefix(s.cfg.ProxyAPIKey), + Hint: "This key unlocks the console and can call every provider.", + }) + case http.MethodPost: + var input struct { + Rotate bool `json:"rotate"` + } + if err := json.NewDecoder(r.Body).Decode(&input); err != nil && err != io.EOF { + writeErr(w, http.StatusBadRequest, "invalid_request", err.Error()) + return + } + if !input.Rotate { + writeErr(w, http.StatusBadRequest, "invalid_request", "set rotate=true to mint a new console key") + return + } + secret, err := generateAPIKey() + if err != nil { + writeErr(w, http.StatusInternalServerError, "console_key_rotate_failed", err.Error()) + return + } + if err := s.manager.Store().SetSecret(r.Context(), proxyAPIKeySecret, secret); err != nil { + writeErr(w, http.StatusInternalServerError, "console_key_rotate_failed", err.Error()) + return + } + s.cfg.ProxyAPIKey = secret + s.auth = auth.NewVerifier(secret, s.manager.Store()) + s.executor.WorkerKey = secret + if err := s.manager.ReplaceProxyAPIKey(r.Context(), secret); err != nil { + writeErr(w, http.StatusInternalServerError, "console_key_rotate_failed", err.Error()) + return + } + writeJSON(w, http.StatusOK, consoleKeyView{ + Prefix: accounts.APIKeyPrefix(secret), + Hint: "Store this value now. The console will ask for it on the next sign-in.", + Rotated: true, + Secret: secret, + }) + default: + writeErr(w, http.StatusMethodNotAllowed, "method_not_allowed", "GET or POST only") + } +} diff --git a/internal/api/server.go b/internal/api/server.go index 568f6930..28a0d8d6 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -99,7 +99,7 @@ func New(cfg config.Config) *Server { chatExecutor.OnAttempt = recorder.Attempt s := &Server{ cfg: cfg, - auth: auth.NewVerifier(proxyAPIKey), + auth: auth.NewVerifier(proxyAPIKey, store), executor: chatExecutor, pool: pool, manager: manager, @@ -165,17 +165,20 @@ func (s *Server) Close() error { func (s *Server) routes() { s.mux.HandleFunc(endpoint.HealthPath, s.handleHealth) - s.mux.HandleFunc("/api/overview", s.withAPIKey(s.handleOverview)) - s.mux.HandleFunc("/api/system/update", s.withAPIKey(s.handleSystemUpdate)) - s.mux.HandleFunc("/api/models", s.withAPIKey(s.handleModelsAPI)) - s.mux.HandleFunc("/api/models/", s.withAPIKey(s.handleModelSetting)) - s.mux.HandleFunc("/api/providers", s.withAPIKey(s.handleProviders)) - s.mux.HandleFunc("/api/accounts", s.withAPIKey(s.handleAccounts)) - s.mux.HandleFunc("/api/accounts/import", s.withAPIKey(s.handleAccountImport)) - s.mux.HandleFunc("/api/accounts/", s.withAPIKey(s.handleAccountByID)) - s.mux.HandleFunc("/api/logs", s.withAPIKey(s.handleLogs)) - s.mux.HandleFunc("/api/logs/", s.withAPIKey(s.handleLogs)) - s.mux.HandleFunc("/api/chat", s.withAPIKey(s.handleChatCompletions)) + s.mux.HandleFunc("/api/overview", s.withConsoleKey(s.handleOverview)) + s.mux.HandleFunc("/api/system/update", s.withConsoleKey(s.handleSystemUpdate)) + s.mux.HandleFunc("/api/system/console-key", s.withConsoleKey(s.handleConsoleKey)) + s.mux.HandleFunc("/api/keys", s.withConsoleKey(s.handleAPIKeys)) + s.mux.HandleFunc("/api/keys/", s.withConsoleKey(s.handleAPIKeyByID)) + s.mux.HandleFunc("/api/models", s.withConsoleKey(s.handleModelsAPI)) + s.mux.HandleFunc("/api/models/", s.withConsoleKey(s.handleModelSetting)) + s.mux.HandleFunc("/api/providers", s.withConsoleKey(s.handleProviders)) + s.mux.HandleFunc("/api/accounts", s.withConsoleKey(s.handleAccounts)) + s.mux.HandleFunc("/api/accounts/import", s.withConsoleKey(s.handleAccountImport)) + s.mux.HandleFunc("/api/accounts/", s.withConsoleKey(s.handleAccountByID)) + s.mux.HandleFunc("/api/logs", s.withConsoleKey(s.handleLogs)) + s.mux.HandleFunc("/api/logs/", s.withConsoleKey(s.handleLogs)) + s.mux.HandleFunc("/api/chat", s.withConsoleKey(s.handleChatCompletions)) s.mux.HandleFunc(endpoint.ModelsPath, s.withAPIKey(s.handleModels)) s.mux.HandleFunc(endpoint.ChatCompletionsPath, s.withAPIKey(s.handleChatCompletions)) @@ -195,7 +198,7 @@ func (s *Server) routes() { r.URL.Path != "/og-card.svg" && r.URL.Path != "/site.webmanifest" { switch r.URL.Path { - case "/login", "/auth", "/providers", "/access", "/accounts", "/system", "/logs": + case "/login", "/auth", "/providers", "/access", "/accounts", "/system", "/logs", "/keys": default: http.NotFound(w, r) return @@ -213,12 +216,35 @@ func (s *Server) routes() { func (s *Server) withAPIKey(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if !s.auth.Authorized(r) { + identity, ok := s.auth.Authenticate(r.Context(), r) + if !ok { writeErr(w, http.StatusUnauthorized, "invalid_api_key", "Missing/invalid API key") return } + if identity.Kind == auth.KindKey && s.manager != nil { + _ = s.manager.Store().TouchAPIKey(r.Context(), identity.KeyID) + } + next(w, r.WithContext(auth.WithIdentity(r.Context(), identity))) + } +} + +func (s *Server) withConsoleKey(next http.HandlerFunc) http.HandlerFunc { + return s.withAPIKey(func(w http.ResponseWriter, r *http.Request) { + identity, _ := auth.IdentityFrom(r.Context()) + if !identity.Console() { + writeErr(w, http.StatusForbidden, "console_key_required", "This endpoint requires the console API key") + return + } next(w, r) + }) +} + +func (s *Server) requestIdentity(r *http.Request) auth.Identity { + identity, ok := auth.IdentityFrom(r.Context()) + if ok { + return identity } + return auth.Identity{Kind: auth.KindNone} } func providerIDs() []string { @@ -257,7 +283,7 @@ func (s *Server) handleOverview(w http.ResponseWriter, r *http.Request) { hotCount++ } } - models := s.decorateModelsWithContext(r.Context(), s.fetchWorkerModels(false)) + models := s.decorateModelsWithContext(r.Context(), s.filterModelsForIdentity(r, s.fetchWorkerModels(false))) writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "time": time.Now().Format(time.RFC3339), diff --git a/internal/auth/identity.go b/internal/auth/identity.go new file mode 100644 index 00000000..d1757997 --- /dev/null +++ b/internal/auth/identity.go @@ -0,0 +1,65 @@ +package auth + +import ( + "context" + "strings" + + "github.com/caigee-cmd/cli2api/internal/accounts" +) + +const ( + KindNone = "none" + KindConsole = "console" + KindKey = "key" +) + +type Identity struct { + Kind string + KeyID string + Name string + AllowedProviders []string +} + +func (i Identity) Console() bool { + return i.Kind == KindNone || i.Kind == KindConsole +} + +func (i Identity) AllowsProvider(provider string) bool { + if len(i.AllowedProviders) == 0 { + return true + } + family := strings.ToLower(strings.TrimSpace(provider)) + if family == "" { + return true + } + for _, item := range i.AllowedProviders { + if strings.ToLower(strings.TrimSpace(item)) == family { + return true + } + } + return false +} + +type ctxKey struct{} + +func WithIdentity(ctx context.Context, identity Identity) context.Context { + return context.WithValue(ctx, ctxKey{}, identity) +} + +func IdentityFrom(ctx context.Context) (Identity, bool) { + identity, ok := ctx.Value(ctxKey{}).(Identity) + return identity, ok +} + +func ConsoleIdentity() Identity { + return Identity{Kind: KindConsole, Name: "console"} +} + +func KeyIdentity(key accounts.APIKey) Identity { + return Identity{ + Kind: KindKey, + KeyID: key.ID, + Name: key.Name, + AllowedProviders: append([]string{}, key.Providers...), + } +} diff --git a/internal/auth/verifier.go b/internal/auth/verifier.go index bac8b73f..09515a5b 100644 --- a/internal/auth/verifier.go +++ b/internal/auth/verifier.go @@ -1,26 +1,56 @@ package auth import ( - "crypto/subtle" + "context" "net/http" "strings" + + "github.com/caigee-cmd/cli2api/internal/accounts" ) +type KeyLookup interface { + LookupAPIKey(context.Context, string) (accounts.APIKey, bool, error) +} + type Verifier struct { - key string + consoleKey string + keys KeyLookup } -func NewVerifier(key string) Verifier { - return Verifier{key: strings.TrimSpace(key)} +func NewVerifier(consoleKey string, keys KeyLookup) Verifier { + return Verifier{consoleKey: strings.TrimSpace(consoleKey), keys: keys} } -func (v Verifier) Authorized(r *http.Request) bool { - if v.key == "" { - return true - } +func bearerSecret(r *http.Request) string { got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if got == "" { got = r.Header.Get("x-api-key") } - return subtle.ConstantTimeCompare([]byte(got), []byte(v.key)) == 1 + return strings.TrimSpace(got) +} + +func (v Verifier) Authenticate(ctx context.Context, r *http.Request) (Identity, bool) { + if v.consoleKey == "" && v.keys == nil { + return Identity{Kind: KindNone}, true + } + secret := bearerSecret(r) + if secret == "" { + return Identity{}, false + } + if v.consoleKey != "" && accounts.ConstantTimeEqual(secret, v.consoleKey) { + return ConsoleIdentity(), true + } + if v.keys == nil { + return Identity{}, false + } + key, ok, err := v.keys.LookupAPIKey(ctx, secret) + if err != nil || !ok || !key.Enabled { + return Identity{}, false + } + return KeyIdentity(key), true +} + +func (v Verifier) Authorized(r *http.Request) bool { + _, ok := v.Authenticate(r.Context(), r) + return ok } diff --git a/internal/auth/verifier_test.go b/internal/auth/verifier_test.go index 64d43cf8..c2cba6cb 100644 --- a/internal/auth/verifier_test.go +++ b/internal/auth/verifier_test.go @@ -1,13 +1,16 @@ package auth import ( + "context" "net/http" "net/http/httptest" "testing" + + "github.com/caigee-cmd/cli2api/internal/accounts" ) func TestVerifierAcceptsConfiguredAPIKeyHeaders(t *testing.T) { - verifier := NewVerifier("secret") + verifier := NewVerifier("secret", nil) for _, header := range []struct { name string value string @@ -17,7 +20,8 @@ func TestVerifierAcceptsConfiguredAPIKeyHeaders(t *testing.T) { } { req := httptest.NewRequest(http.MethodGet, "/api/overview", nil) req.Header.Set(header.name, header.value) - if !verifier.Authorized(req) { + identity, ok := verifier.Authenticate(context.Background(), req) + if !ok || !identity.Console() { t.Fatalf("header %s was rejected", header.name) } } @@ -26,10 +30,43 @@ func TestVerifierAcceptsConfiguredAPIKeyHeaders(t *testing.T) { func TestVerifierRejectsInvalidKeyAndAllowsDisabledGate(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/overview", nil) req.Header.Set("Authorization", "Bearer wrong") - if NewVerifier("secret").Authorized(req) { + if _, ok := NewVerifier("secret", nil).Authenticate(context.Background(), req); ok { t.Fatal("invalid key was accepted") } - if !NewVerifier("").Authorized(req) { + if identity, ok := NewVerifier("", nil).Authenticate(context.Background(), req); !ok || identity.Kind != KindNone { t.Fatal("disabled gate rejected request") } } + +type stubKeys struct { + key accounts.APIKey + ok bool +} + +func (s stubKeys) LookupAPIKey(_ context.Context, secret string) (accounts.APIKey, bool, error) { + if secret != "named-secret" { + return accounts.APIKey{}, false, nil + } + return s.key, s.ok, nil +} + +func TestVerifierAcceptsNamedAPIKey(t *testing.T) { + verifier := NewVerifier("console", stubKeys{ + ok: true, + key: accounts.APIKey{ + ID: "key_1", Name: "ci", Providers: []string{"qoder"}, Enabled: true, + }, + }) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil) + req.Header.Set("Authorization", "Bearer named-secret") + identity, ok := verifier.Authenticate(context.Background(), req) + if !ok || identity.Kind != KindKey || identity.KeyID != "key_1" { + t.Fatalf("named key identity = %+v ok=%v", identity, ok) + } + if identity.Console() { + t.Fatal("named key must not unlock console routes") + } + if !identity.AllowsProvider("qoder") || identity.AllowsProvider("trae") { + t.Fatalf("provider allowlist = %+v", identity.AllowedProviders) + } +} diff --git a/internal/executor/chat.go b/internal/executor/chat.go index f5648ea3..cc9c940e 100644 --- a/internal/executor/chat.go +++ b/internal/executor/chat.go @@ -73,6 +73,24 @@ func RequestIDFromContext(ctx context.Context) string { return id } +type allowedProvidersKey struct{} + +func WithAllowedProviders(ctx context.Context, providers []string) context.Context { + if len(providers) == 0 { + return ctx + } + copied := append([]string{}, providers...) + return context.WithValue(ctx, allowedProvidersKey{}, copied) +} + +func allowedProvidersFrom(ctx context.Context) []string { + if ctx == nil { + return nil + } + providers, _ := ctx.Value(allowedProvidersKey{}).([]string) + return providers +} + func NewChatExecutor(pool *accounts.Pool, workerKey string) ChatExecutor { if pool == nil { pool = accounts.NewPool(nil, nil) @@ -128,18 +146,19 @@ func buildWorkerPayload(req translate.ChatRequest, stream bool) map[string]any { return payload } -func (e ChatExecutor) routeQuery(prefer, providerFilter, regionFilter, publicModel string, excluded map[string]struct{}) accounts.RouteQuery { +func (e ChatExecutor) routeQuery(prefer, providerFilter, regionFilter, publicModel string, allowed []string, excluded map[string]struct{}) accounts.RouteQuery { return accounts.RouteQuery{ - PublicModel: publicModel, - PreferAccount: prefer, - ProviderFilter: providerFilter, - RegionFilter: regionFilter, - Excluded: excluded, + PublicModel: publicModel, + PreferAccount: prefer, + ProviderFilter: providerFilter, + RegionFilter: regionFilter, + AllowedProviders: allowed, + Excluded: excluded, } } -func (e ChatExecutor) pick(prefer, providerFilter, regionFilter, publicModel string, excluded map[string]struct{}) (accounts.Item, error) { - query := e.routeQuery(prefer, providerFilter, regionFilter, publicModel, excluded) +func (e ChatExecutor) pick(prefer, providerFilter, regionFilter, publicModel string, allowed []string, excluded map[string]struct{}) (accounts.Item, error) { + query := e.routeQuery(prefer, providerFilter, regionFilter, publicModel, allowed, excluded) if e.Pool != nil { if item, ok := e.Pool.PickRoute(query); ok { return item, nil @@ -152,24 +171,40 @@ func (e ChatExecutor) pick(prefer, providerFilter, regionFilter, publicModel str } } } + if len(allowed) > 0 && providerFilter != "" && !containsFold(allowed, providerFilter) { + return accounts.Item{}, fmt.Errorf("api key cannot use provider %s", providerFilter) + } if providerFilter != "" && regionFilter != "" { return accounts.Item{}, fmt.Errorf("no %s/%s accounts available", providerFilter, regionFilter) } if providerFilter != "" { return accounts.Item{}, fmt.Errorf("no %s accounts available", providerFilter) } + if len(allowed) > 0 { + return accounts.Item{}, fmt.Errorf("no accounts available for this api key") + } return accounts.Item{}, fmt.Errorf("no worker accounts configured") } -func (e ChatExecutor) attemptsFor(providerFilter, regionFilter, publicModel string) int { +func (e ChatExecutor) attemptsFor(providerFilter, regionFilter, publicModel string, allowed []string) int { if e.Pool != nil { - if n := e.Pool.LenRoute(e.routeQuery("", providerFilter, regionFilter, publicModel, nil)); n > 0 { + if n := e.Pool.LenRoute(e.routeQuery("", providerFilter, regionFilter, publicModel, allowed, nil)); n > 0 { return n } } return 1 } +func containsFold(values []string, want string) bool { + want = strings.ToLower(strings.TrimSpace(want)) + for _, value := range values { + if strings.ToLower(strings.TrimSpace(value)) == want { + return true + } + } + return false +} + func pinRegion(current, next string) string { if current != "" { return current @@ -266,10 +301,11 @@ func (e ChatExecutor) ChatNonStream(ctx context.Context, req translate.ChatReque regionFilter = pinRegion("", pinnedItem.Region) } } - attempts := e.attemptsFor(providerFilter, regionFilter, req.Model) + allowed := allowedProvidersFrom(ctx) + attempts := e.attemptsFor(providerFilter, regionFilter, req.Model, allowed) pinned := prefer for i := 0; i < attempts; i++ { - item, err := e.pick(prefer, providerFilter, regionFilter, req.Model, excluded) + item, err := e.pick(prefer, providerFilter, regionFilter, req.Model, allowed, excluded) if err != nil { if lastErr != nil { return ChatResult{AttemptCount: i, AccountID: lastAccountID(excluded), Provider: providerFilter}, lastErr @@ -279,7 +315,7 @@ func (e ChatExecutor) ChatNonStream(ctx context.Context, req translate.ChatReque prefer = "" if regionFilter == "" { regionFilter = pinRegion(regionFilter, item.Region) - attempts = e.attemptsFor(providerFilter, regionFilter, req.Model) + attempts = e.attemptsFor(providerFilter, regionFilter, req.Model, allowed) } if isInProcessItem(item) { result, classified, err := e.chatInProcessNonStreamAttempt(ctx, item, req, i) @@ -604,11 +640,12 @@ func (e ChatExecutor) ChatStreamProxy(ctx context.Context, req translate.ChatReq regionFilter = pinRegion("", pinnedItem.Region) } } - attempts := e.attemptsFor(providerFilter, regionFilter, req.Model) + allowed := allowedProvidersFrom(ctx) + attempts := e.attemptsFor(providerFilter, regionFilter, req.Model, allowed) pinned := prefer startedAll := time.Now() for i := 0; i < attempts; i++ { - item, err := e.pick(prefer, providerFilter, regionFilter, req.Model, excluded) + item, err := e.pick(prefer, providerFilter, regionFilter, req.Model, allowed, excluded) if err != nil { if lastErr != nil { return StreamResult{AttemptCount: i, AccountID: lastAccountID(excluded), Provider: providerFilter}, lastErr @@ -618,7 +655,7 @@ func (e ChatExecutor) ChatStreamProxy(ctx context.Context, req translate.ChatReq prefer = "" if regionFilter == "" { regionFilter = pinRegion(regionFilter, item.Region) - attempts = e.attemptsFor(providerFilter, regionFilter, req.Model) + attempts = e.attemptsFor(providerFilter, regionFilter, req.Model, allowed) } if isInProcessItem(item) { result, classified, err := e.chatInProcessStreamAttempt(ctx, item, req, i) diff --git a/internal/executor/providers_test.go b/internal/executor/providers_test.go index 1c4a0239..24e1e903 100644 --- a/internal/executor/providers_test.go +++ b/internal/executor/providers_test.go @@ -78,6 +78,26 @@ func TestInProcessProviderFilterRoutesWithoutPin(t *testing.T) { } } +func TestAPIKeyAllowlistBlocksOtherProviderFamily(t *testing.T) { + pool := accounts.NewPool(nil, nil) + pool.Upsert(accounts.Item{ID: "wb1", Provider: "workbuddy", Region: "cn", Runtime: "in_process"}) + registry := providers.NewRegistry() + fake := &fakeInProcessChat{} + registry.Register(providers.Adapter{ID: "workbuddy", Chat: fake}) + ex := NewChatExecutor(pool, "") + ex.Providers = registry + ctx := WithAllowedProviders(context.Background(), []string{"qoder"}) + _, err := ex.ChatNonStream(ctx, translate.ChatRequest{ + Model: "glm-5.2", Messages: []translate.ChatMessage{{Role: "user", Content: "hi"}}, + }, "", "workbuddy") + if err == nil { + t.Fatal("expected qoder-only key to miss workbuddy") + } + if fake.calls != 0 { + t.Fatalf("unexpected workbuddy calls=%d", fake.calls) + } +} + func TestInProcessProviderOnlyAccountRoutesWithoutPin(t *testing.T) { pool := accounts.NewPool(nil, nil) pool.Upsert(accounts.Item{ID: "wb1", Provider: "workbuddy", Runtime: "in_process"}) diff --git a/internal/webui/static/assets/index-C1Ful2UA.js b/internal/webui/static/assets/index-C1Ful2UA.js new file mode 100644 index 00000000..ca78bf2b --- /dev/null +++ b/internal/webui/static/assets/index-C1Ful2UA.js @@ -0,0 +1,28 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m){if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1ne||(e.current=te[ne],te[ne]=null,ne--)}function re(e,t){ne++,te[ne]=e.current,e.current=t}var ie=R(null),ae=R(null),oe=R(null),se=R(null);function ce(e,t){switch(re(oe,t),re(ae,e),re(ie,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?qd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=qd(t),e=Jd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}z(ie),re(ie,e)}function le(){z(ie),z(ae),z(oe)}function ue(e){e.memoizedState!==null&&re(se,e);var t=ie.current,n=Jd(t,e.type);t!==n&&(re(ae,e),re(ie,n))}function de(e){ae.current===e&&(z(ie),z(ae)),se.current===e&&(z(se),ip._currentValue=ee)}var fe,pe;function me(e){if(fe===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);fe=t&&t[1]||``,pe=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{he=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?me(n):``}function _e(e,t){switch(e.tag){case 26:case 27:case 5:return me(e.type);case 16:return me(`Lazy`);case 13:return e.child!==t&&t!==null?me(`Suspense Fallback`):me(`Suspense`);case 19:return me(`SuspenseList`);case 0:case 15:return ge(e.type,!1);case 11:return ge(e.type.render,!1);case 1:return ge(e.type,!0);case 31:return me(`Activity`);default:return``}}function ve(e){try{var t=``,n=null;do t+=_e(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var ye=Object.prototype.hasOwnProperty,be=t.unstable_scheduleCallback,xe=t.unstable_cancelCallback,Se=t.unstable_shouldYield,Ce=t.unstable_requestPaint,we=t.unstable_now,Te=t.unstable_getCurrentPriorityLevel,Ee=t.unstable_ImmediatePriority,De=t.unstable_UserBlockingPriority,Oe=t.unstable_NormalPriority,ke=t.unstable_LowPriority,Ae=t.unstable_IdlePriority,je=t.log,Me=t.unstable_setDisableYieldValue,Ne=null,Pe=null;function Fe(e){if(typeof je==`function`&&Me(e),Pe&&typeof Pe.setStrictMode==`function`)try{Pe.setStrictMode(Ne,e)}catch{}}var Ie=Math.clz32?Math.clz32:ze,Le=Math.log,Re=Math.LN2;function ze(e){return e>>>=0,e===0?32:31-(Le(e)/Re|0)|0}var Be=256,Ve=262144,He=4194304;function Ue(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function We(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Ue(n))):i=Ue(o):i=Ue(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Ue(n))):i=Ue(o)):i=Ue(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ge(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Ke(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function qe(){var e=He;return He<<=1,!(He&62914560)&&(He=4194304),e}function Je(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Ye(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Xe(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),ln=!1;if(cn)try{var un={};Object.defineProperty(un,"passive",{get:function(){ln=!0}}),window.addEventListener(`test`,un,un),window.removeEventListener(`test`,un,un)}catch{ln=!1}var dn=null,fn=null,pn=null;function mn(){if(pn)return pn;var e,t=fn,n=t.length,r,i=`value`in dn?dn.value:dn.textContent,a=i.length;for(e=0;e=Kn),Yn=` `,Xn=!1;function Zn(e,t){switch(e){case`keyup`:return Wn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Qn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var $n=!1;function er(e,t){switch(e){case`compositionend`:return Qn(t);case`keypress`:return t.which===32?(Xn=!0,Yn):null;case`textInput`:return e=t.data,e===Yn&&Xn?null:e;default:return null}}function tr(e,t){if($n)return e===`compositionend`||!Gn&&Zn(e,t)?(e=mn(),pn=fn=dn=null,$n=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Cr(n)}}function Tr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Tr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Er(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ft(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ft(e.document)}return t}function Dr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Or=cn&&`documentMode`in document&&11>=document.documentMode,kr=null,Ar=null,jr=null,Mr=!1;function Nr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Mr||kr==null||kr!==Ft(r)||(r=kr,`selectionStart`in r&&Dr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),jr&&Sr(jr,r)||(jr=r,r=jd(Ar,`onSelect`),0>=o,i-=o,Ti=1<<32-Ie(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),Pi&&Di(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),Pi&&Di(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return Pi&&Di(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),Pi&&Di(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&Oa(l)===r.type){n(e,r.sibling),c=a(r,o.props),Fa(c,o),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}o.type===y?(c=fi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=di(o.type,o.key,o.props,null,e.mode,c),Fa(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l){if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}c=hi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=Oa(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Pa(o),c);if(o.$$typeof===C)return b(e,r,na(e,o),c);Ia(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=pi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Na=0;var i=b(e,t,n,r);return Ma=null,i}catch(t){if(t===Sa||t===wa)throw t;var a=si(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ra=La(!0),za=La(!1),Ba=!1;function Va(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ha(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ua(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Wa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Bl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ii(e),ri(e,null,n),t}return ei(e,r,t,n),ii(e)}function Ga(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Qe(e,n)}}function Ka(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var qa=!1;function Ja(){if(qa){var e=pa;if(e!==null)throw e}}function Ya(e,t,n,r){qa=!1;var i=e.updateQueue;Ba=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Hl&f)===f:(r&f)===f){f!==0&&f===fa&&(qa=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Ba=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Yl|=o,e.lanes=o,e.memoizedState=d}}function Xa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Za(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,Ns(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ms(e,t,ga(c,r),_u(e)):Ms(e,t,r,_u(e))}catch(n){Ms(e,t,{then:function(){},status:`rejected`,reason:n},_u())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function Cs(){}function ws(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Ts(e).queue;Ss(e,a,t,ee,n===null?Cs:function(){return Es(e),n(r)})}function Ts(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ee,baseState:ee,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Fo,lastRenderedState:ee},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Fo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Es(e){var t=Ts(e);t.next===null&&(t=e.alternate.memoizedState),Ms(e,t.next.queue,{},_u())}function W(){return ta(ip)}function Ds(){return Ao().memoizedState}function Os(){return Ao().memoizedState}function ks(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=_u();e=Ua(n);var r=Wa(t,e,n);r!==null&&(yu(r,t,n),Ga(r,t,n)),t={cache:ca()},e.payload=t;return}t=t.return}}function As(e,t,n){var r=_u();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ps(e)?Fs(t,n):(n=ti(e,t,n,r),n!==null&&(yu(n,e,r),Is(n,t,r)))}function js(e,t,n){Ms(e,t,n,_u())}function Ms(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ps(e))Fs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,xr(s,o))return ei(e,t,i,0),Vl===null&&$r(),!1}catch{}if(n=ti(e,t,i,r),n!==null)return yu(n,e,r),Is(n,t,r),!0}return!1}function Ns(e,t,n,r){if(r={lane:2,revertLane:gd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ps(e)){if(t)throw Error(i(479))}else t=ti(e,n,r,2),t!==null&&yu(t,e,2)}function Ps(e){var t=e.alternate;return e===fo||t!==null&&t===fo}function Fs(e,t){go=ho=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Is(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Qe(e,n)}}var Ls={readContext:ta,use:No,useCallback:xo,useContext:xo,useEffect:xo,useImperativeHandle:xo,useLayoutEffect:xo,useInsertionEffect:xo,useMemo:xo,useReducer:xo,useRef:xo,useState:xo,useDebugValue:xo,useDeferredValue:xo,useTransition:xo,useSyncExternalStore:xo,useId:xo,useHostTransitionStatus:xo,useFormState:xo,useActionState:xo,useOptimistic:xo,useMemoCache:xo,useCacheRefresh:xo};Ls.useEffectEvent=xo;var Rs={readContext:ta,use:No,useCallback:function(e,t){return ko().memoizedState=[e,t===void 0?null:t],e},useContext:ta,useEffect:ls,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),ss(4194308,4,hs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ss(4194308,4,e,t)},useInsertionEffect:function(e,t){ss(4,2,e,t)},useMemo:function(e,t){var n=ko();t=t===void 0?null:t;var r=e();if(_o){Fe(!0);try{e()}finally{Fe(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=ko();if(n!==void 0){var i=n(t);if(_o){Fe(!0);try{n(t)}finally{Fe(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=As.bind(null,fo,e),[r.memoizedState,e]},useRef:function(e){var t=ko();return e={current:e},t.memoizedState=e},useState:function(e){e=Go(e);var t=e.queue,n=js.bind(null,fo,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:_s,useDeferredValue:function(e,t){return bs(ko(),e,t)},useTransition:function(){var e=Go(!1);return e=Ss.bind(null,fo,e.queue,!0,!1),ko().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=fo,a=ko();if(Pi){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Vl===null)throw Error(i(349));Hl&127||Bo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,ls(Ho.bind(null,r,o,e),[e]),r.flags|=2048,as(9,{destroy:void 0},Vo.bind(null,r,o,n,t),null),n},useId:function(){var e=ko(),t=Vl.identifierPrefix;if(Pi){var n=Ei,r=Ti;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=H++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[at]=t,o[ot]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Bd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Mc(t)}}return Lc(t),Nc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Mc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=oe.current,Vi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Mi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[at]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Ld(e.nodeValue,n)),e||Ri(t,!0)}else e=Kd(e).createTextNode(r),e[at]=t,t.stateNode=e}return Lc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Vi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[at]=t}else Hi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Lc(t),e=!1}else n=Ui(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(co(t),t):(co(t),null);if(t.flags&128)throw Error(i(558))}return Lc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Vi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[at]=t}else Hi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Lc(t),a=!1}else a=Ui(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(co(t),t):(co(t),null)}return co(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Fc(t,t.updateQueue),Lc(t),null);case 4:return le(),e===null&&Dd(t.stateNode.containerInfo),Lc(t),null;case 10:return Yi(t.type),Lc(t),null;case 19:if(z(V),r=t.memoizedState,r===null)return Lc(t),null;if(a=!!(t.flags&128),o=r.rendering,o===null){if(a)Ic(r,!1);else{if(K!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=lo(e),o!==null){for(t.flags|=128,Ic(r,!1),e=o.updateQueue,t.updateQueue=e,Fc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ui(n,e),n=n.sibling;return re(V,V.current&1|2),Pi&&Di(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&we()>au&&(t.flags|=128,a=!0,Ic(r,!1),t.lanes=4194304)}}else{if(!a){if(e=lo(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Fc(t,e),Ic(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Pi)return Lc(t),null}else 2*we()-r.renderingStartTime>au&&n!==536870912&&(t.flags|=128,a=!0,Ic(r,!1),t.lanes=4194304)}r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Lc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=we(),e.sibling=null,n=V.current,re(V,a?n&1|2:n&1),Pi&&Di(t,r.treeForkCount),e);case 22:case 23:return co(t),no(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Lc(t),t.subtreeFlags&6&&(t.flags|=8192)):Lc(t),n=t.updateQueue,n!==null&&Fc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&z(va),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Yi(sa),Lc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function zc(e,t){switch(Ai(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Yi(sa),le(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return de(t),null;case 31:if(t.memoizedState!==null){if(co(t),t.alternate===null)throw Error(i(340));Hi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(co(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Hi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return z(V),null;case 4:return le(),null;case 10:return Yi(t.type),null;case 22:case 23:return co(t),no(),e!==null&&z(va),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Yi(sa),null;case 25:return null;default:return null}}function Bc(e,t){switch(Ai(t),t.tag){case 3:Yi(sa),le();break;case 26:case 27:case 5:de(t);break;case 4:le();break;case 31:t.memoizedState!==null&&co(t);break;case 13:co(t);break;case 19:z(V);break;case 10:Yi(t.type);break;case 22:case 23:co(t),no(),e!==null&&z(va);break;case 24:Yi(sa)}}function Vc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Yu(t,t.return,e)}}function Hc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Yu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Yu(t,t.return,e)}}function Uc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Za(t,n)}catch(t){Yu(e,e.return,t)}}}function Wc(e,t,n){n.props=Gs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Yu(e,t,n)}}function Gc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Yu(e,t,n)}}function Kc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null){if(typeof r==`function`)try{r()}catch(n){Yu(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Yu(e,t,n)}else n.current=null}}function qc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Yu(e,e.return,t)}}function Jc(e,t,n){try{var r=e.stateNode;Vd(r,e.type,n,t),r[ot]=t}catch(t){Yu(e,e.return,t)}}function Yc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&rf(e.type)||e.tag===4}function Xc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Yc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&rf(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Zc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Qt));else if(r!==4&&(r===27&&rf(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Zc(e,t,n),e=e.sibling;e!==null;)Zc(e,t,n),e=e.sibling}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&rf(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Bd(t,r,n),t[at]=e,t[ot]=n}catch(t){Yu(e,e.return,t)}}var el=!1,tl=!1,nl=!1,rl=typeof WeakSet==`function`?WeakSet:Set,il=null;function al(e,t){if(e=e.containerInfo,Wd=pp,e=Er(e),Dr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(Gd={focusedElem:e,selectionRange:n},pp=!1,il=t;il!==null;)if(t=il,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,il=e;else for(;il!==null;){switch(t=il,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Bd(o,r,n),o[at]=e,vt(o),r=o;break a;case`link`:var s=qf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=wr(s,h),v=wr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=pu,pu=null;var o=lu,s=du;if(cu=0,uu=lu=null,du=0,Bl&6)throw Error(i(331));var c=Bl;if(Bl|=4,Fl(o.current),Dl(o,o.current,s,n),Bl=c,ld(0,!1),Pe&&typeof Pe.onPostCommitFiberRoot==`function`)try{Pe.onPostCommitFiberRoot(Ne,o)}catch{}return!0}finally{L.p=a,I.T=r,Gu(e,t)}}function Ju(e,t,n){t=_i(n,t),t=Zs(e.stateNode,t,2),e=Wa(e,t,2),e!==null&&(Ye(e,2),cd(e))}function Yu(e,t,n){if(e.tag===3)Ju(e,e,n);else for(;t!==null;){if(t.tag===3){Ju(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(su===null||!su.has(r))){e=_i(n,e),n=Qs(2),r=Wa(t,n,2),r!==null&&($s(n,r,t,e),Ye(r,2),cd(r));break}}t=t.return}}function Xu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(ql=!0,i.add(n),e=Zu.bind(null,e,t,n),t.then(e,e))}function Zu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Vl===e&&(Hl&n)===n&&(K===4||K===3&&(Hl&62914560)===Hl&&300>we()-ru?!(Bl&2)&&Eu(e,0):Zl|=n,$l===Hl&&($l=0)),cd(e)}function Qu(e,t){t===0&&(t=qe()),e=ni(e,t),e!==null&&(Ye(e,t),cd(e))}function $u(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Qu(e,n)}function ed(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Qu(e,n)}function td(e,t){return be(e,t)}var nd=null,rd=null,id=!1,ad=!1,od=!1,sd=0;function cd(e){e!==rd&&e.next===null&&(rd===null?nd=rd=e:rd=rd.next=e),ad=!0,id||(id=!0,hd())}function ld(e,t){if(!od&&ad){od=!0;do for(var n=!1,r=nd;r!==null;){if(!t){if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Ie(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,md(r,a))}else a=Hl,a=We(r,r===Vl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ge(r,a)||(n=!0,md(r,a))}r=r.next}while(n);od=!1}}function ud(){dd()}function dd(){ad=id=!1;var e=0;sd!==0&&Zd()&&(e=sd);for(var t=we(),n=null,r=nd;r!==null;){var i=r.next,a=fd(r,t);a===0?(r.next=null,n===null?nd=i:n.next=i,i===null&&(rd=n)):(n=r,(e!==0||a&3)&&(ad=!0)),r=i}cu!==0&&cu!==5||ld(e,!1),sd!==0&&(sd=0)}function fd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Hd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Df(e,t,n){var r=Ef;if(r&&typeof t==`string`&&t){var i=Lt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),xf.has(i)||(xf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Bd(t,`link`,e),vt(t),r.head.appendChild(t)))}}function Of(e){Cf.D(e),Df(`dns-prefetch`,e,null)}function kf(e,t){Cf.C(e,t),Df(`preconnect`,e,t)}function Af(e,t,n){Cf.L(e,t,n);var r=Ef;if(r&&e&&t){var i=`link[rel="preload"][as="`+Lt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Lt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Lt(n.imageSizes)+`"]`)):i+=`[href="`+Lt(e)+`"]`;var a=i;switch(t){case`style`:a=If(e);break;case`script`:a=Bf(e)}bf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),bf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Lf(a))||t===`script`&&r.querySelector(Vf(a))||(t=r.createElement(`link`),Bd(t,`link`,e),vt(t),r.head.appendChild(t)))}}function jf(e,t){Cf.m(e,t);var n=Ef;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Lt(r)+`"][href="`+Lt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Bf(e)}if(!bf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),bf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Vf(a)))return}r=n.createElement(`link`),Bd(r,`link`,e),vt(r),n.head.appendChild(r)}}}function Mf(e,t,n){Cf.S(e,t,n);var r=Ef;if(r&&e){var i=_t(r).hoistableStyles,a=If(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Lf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=bf.get(a))&&Wf(e,n);var c=o=r.createElement(`link`);vt(c),Bd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Uf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Nf(e,t){Cf.X(e,t);var n=Ef;if(n&&e){var r=_t(n).hoistableScripts,i=Bf(e),a=r.get(i);a||(a=n.querySelector(Vf(i)),a||(e=h({src:e,async:!0},t),(t=bf.get(i))&&Gf(e,t),a=n.createElement(`script`),vt(a),Bd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Pf(e,t){Cf.M(e,t);var n=Ef;if(n&&e){var r=_t(n).hoistableScripts,i=Bf(e),a=r.get(i);a||(a=n.querySelector(Vf(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=bf.get(i))&&Gf(e,t),a=n.createElement(`script`),vt(a),Bd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Ff(e,t,n,r){var a=(a=oe.current)?Sf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=If(n.href),n=_t(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=If(n.href);var o=_t(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Lf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),bf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},bf.set(e,n),o||zf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Bf(n),n=_t(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function If(e){return`href="`+Lt(e)+`"`}function Lf(e){return`link[rel="stylesheet"][`+e+`]`}function Rf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function zf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Bd(t,`link`,n),vt(t),e.head.appendChild(t))}function Bf(e){return`[src="`+Lt(e)+`"]`}function Vf(e){return`script[async]`+e}function Hf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Lt(n.href)+`"]`);if(r)return t.instance=r,vt(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),vt(r),Bd(r,`style`,a),Uf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=If(n.href);var o=e.querySelector(Lf(a));if(o)return t.state.loading|=4,t.instance=o,vt(o),o;r=Rf(n),(a=bf.get(a))&&Wf(r,a),o=(e.ownerDocument||e).createElement(`link`),vt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Bd(o,`link`,r),t.state.loading|=4,Uf(o,n.precedence,e),t.instance=o;case`script`:return o=Bf(n.src),(a=e.querySelector(Vf(o)))?(t.instance=a,vt(a),a):(r=n,(a=bf.get(o))&&(r=h({},n),Gf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),vt(a),Bd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Uf(r,n.precedence,e));return t.instance}function Uf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Yf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Xf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Zf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=If(r.href),a=t.querySelector(Lf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=ep.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,vt(a);return}a=t.ownerDocument||t,r=Rf(r),(i=bf.get(i))&&Wf(r,i),a=a.createElement(`link`),vt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Bd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=ep.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Qf=0;function $f(e,t){return e.stylesheets&&e.count===0&&np(e,e.stylesheets),0Qf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function ep(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)np(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var tp=null;function np(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,tp=new Map,t.forEach(rp,e),tp=null,ep.call(e))}function rp(e,t){if(!(t.state.loading&4)){var n=tp.get(e);if(n)var r=n.get(null);else{n=new Map,tp.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=c(u(),1),v=g(),y=`modulepreload`,b=function(e){return`/`+e},x={},S=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=b(t,n),t=s(t),t in x)return;x[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:y,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},C=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,w=/^[\\/]{2}/;function T(e,t){return t+e.replace(/\\/g,`/`)}var E=`popstate`;function D(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function O(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return N(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:P(t)}return I(t,n,null,e)}function k(e,t){if(e===!1||e==null)throw Error(t)}function A(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function j(){return Math.random().toString(36).substring(2,10)}function M(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function N(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?F(t):t,state:n,key:t&&t.key||r||j(),mask:i}}function P({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function F(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function I(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=D(e)?e:N(h.location,e,t);n&&n(r,e),l=u()+1;let d=M(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=D(e)?e:N(h.location,e,t);n&&n(r,e),l=u();let i=M(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return L(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(E,d),c=e,()=>{i.removeEventListener(E,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function L(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),k(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:P(t);return i=i.replace(/ $/,`%20`),!n&&w.test(i)&&(i=r+i),new URL(i,r)}function ee(e,t,n=`/`){return te(e,t,n,!1)}function te(e,t,n,r,i){let a=ve((typeof t==`string`?F(t):t).pathname||`/`,n);if(a==null)return null;let o=i??ne(e),s=null,c=_e(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;k(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=Ee([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(k(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),R(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:de(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=ge(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of z(e.path))a(e,t,!0,n)}),t}function z(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=z(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function re(e){e.sort((e,t)=>e.score===t.score?fe(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var ie=/^:[\w-]+$/,ae=3,oe=2,se=1,ce=10,le=-2,ue=e=>e===`*`;function de(e,t){let n=e.split(`/`),r=n.length;return n.some(ue)&&(r+=le),t&&(r+=oe),n.filter(e=>!ue(e)).reduce((e,t)=>e+(ie.test(t)?ae:t===``?se:ce),r)}function fe(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function pe(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return e[t]=n&&!i?void 0:(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function ge(e,t=!1,n=!0){A(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function _e(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return A(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function ve(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function ye(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?F(e):e,a;return n?(n=Te(n),a=n.startsWith(`/`)?be(n.substring(1),`/`):be(n,t)):a=t,{pathname:a,search:ke(r),hash:Ae(i)}}function be(e,t){let n=De(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function xe(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Se(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Ce(e){let t=Se(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function we(e,t,n,r=!1){let i;typeof e==`string`?i=F(e):(i={...e},k(!i.pathname||!i.pathname.includes(`?`),xe(`?`,`pathname`,`search`,i)),k(!i.pathname||!i.pathname.includes(`#`),xe(`#`,`pathname`,`hash`,i)),k(!i.search||!i.search.includes(`#`),xe(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=ye(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Te=e=>e.replace(/[\\/]{2,}/g,`/`),Ee=e=>Te(e.join(`/`)),De=e=>e.replace(/\/+$/,``),Oe=e=>De(e).replace(/^\/*/,`/`),ke=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Ae=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,je=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Me(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Ne(e){return Ee(e.map(e=>e.route.path).filter(Boolean))||`/`}var Pe=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Fe(e,t){let n=e;if(typeof n!=`string`||!C.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Pe)try{let e=new URL(window.location.href),r=w.test(n)?new URL(T(n,e.protocol)):new URL(n),a=ve(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{A(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ie=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ie);var Le=[`GET`,...Ie];new Set(Le);var Re=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function ze(e){try{return Re.includes(new URL(e).protocol)}catch{return!1}}var Be=_.createContext(null);Be.displayName=`DataRouter`;var Ve=_.createContext(null);Ve.displayName=`DataRouterState`;var He=_.createContext(!1);function Ue(){return _.useContext(He)}var We=_.createContext({isTransitioning:!1});We.displayName=`ViewTransition`;var Ge=_.createContext(new Map);Ge.displayName=`Fetchers`;var Ke=_.createContext(null);Ke.displayName=`Await`;var qe=_.createContext(null);qe.displayName=`Navigation`;var Je=_.createContext(null);Je.displayName=`Location`;var Ye=_.createContext({outlet:null,matches:[],isDataRoute:!1});Ye.displayName=`Route`;var Xe=_.createContext(null);Xe.displayName=`RouteError`;var Ze=`REACT_ROUTER_ERROR`,Qe=`REDIRECT`,$e=`ROUTE_ERROR_RESPONSE`;function et(e){if(e.startsWith(`${Ze}:${Qe}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function tt(e){if(e.startsWith(`${Ze}:${$e}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new je(t.status,t.statusText,t.data)}catch{}}function nt(e,{relative:t}={}){k(rt(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=_.useContext(qe),{hash:i,pathname:a,search:o}=dt(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Ee([n,a])),r.createHref({pathname:s,search:o,hash:i})}function rt(){return _.useContext(Je)!=null}function it(){return k(rt(),`useLocation() may be used only in the context of a component.`),_.useContext(Je).location}var at=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function ot(e){_.useContext(qe).static||_.useLayoutEffect(e)}function st(){let{isDataRoute:e}=_.useContext(Ye);return e?Ot():ct()}function ct(){k(rt(),`useNavigate() may be used only in the context of a component.`);let e=_.useContext(Be),{basename:t,navigator:n}=_.useContext(qe),{matches:r}=_.useContext(Ye),{pathname:i}=it(),a=JSON.stringify(Ce(r)),o=_.useRef(!1);return ot(()=>{o.current=!0}),_.useCallback((r,s={})=>{if(A(o.current,at),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=we(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Ee([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var lt=_.createContext(null);function ut(e){let t=_.useContext(Ye).outlet;return _.useMemo(()=>t&&_.createElement(lt.Provider,{value:e},t),[t,e])}function dt(e,{relative:t}={}){let{matches:n}=_.useContext(Ye),{pathname:r}=it(),i=JSON.stringify(Ce(n));return _.useMemo(()=>we(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function ft(e,t){return pt(e,t)}function pt(e,t,n){k(rt(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=_.useContext(qe),{matches:i}=_.useContext(Ye),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;At(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let u=it(),d;if(t){let e=typeof t==`string`?F(t):t;k(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):ee(e,{pathname:p});A(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),A(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=bt(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Ee([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Ee([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?_.createElement(Je.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function mt(){let e=Dt(),t=Me(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=_.createElement(_.Fragment,null,_.createElement(`p`,null,`💿 Hey developer 👋`),_.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,_.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,_.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),_.createElement(_.Fragment,null,_.createElement(`h2`,null,`Unexpected Application Error!`),_.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?_.createElement(`pre`,{style:i},n):null,o)}var ht=_.createElement(mt,null),gt=class extends _.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=tt(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:_.createElement(Ye.Provider,{value:this.props.routeContext},_.createElement(Xe.Provider,{value:e,children:this.props.component}));return this.context?_.createElement(vt,{error:e},t):t}};gt.contextType=He;var _t=new WeakMap;function vt({children:e,error:t}){let{basename:n}=_.useContext(qe);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=et(t.digest);if(e){let r=_t.get(t);if(r)throw r;let i=Fe(e.location,n),a=i.absoluteURL||i.to;if(ze(a))throw Error(`Invalid redirect location`);if(Pe&&!_t.get(t)){if(i.isExternal||e.reloadDocument)window.location.href=a;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw _t.set(t,n),n}}return _.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${a}`})}}return e}function yt({routeContext:e,match:t,children:n}){let r=_.useContext(Be);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),_.createElement(Ye.Provider,{value:e},n)}function bt(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);k(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Ne(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||ht,o&&(s<0&&c===0?(At(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?_.createElement(n.route.Component,null):n.route.element?n.route.element:e,_.createElement(yt,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?_.createElement(gt,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function xt(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function St(e){let t=_.useContext(Be);return k(t,xt(e)),t}function Ct(e){let t=_.useContext(Ve);return k(t,xt(e)),t}function wt(e){let t=_.useContext(Ye);return k(t,xt(e)),t}function Tt(e){let t=wt(e),n=t.matches[t.matches.length-1];return k(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Et(){return Tt(`useRouteId`)}function Dt(){let e=_.useContext(Xe),t=Ct(`useRouteError`),n=Tt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function Ot(){let{router:e}=St(`useNavigate`),t=Tt(`useNavigate`),n=_.useRef(!1);return ot(()=>{n.current=!0}),_.useCallback(async(r,i={})=>{A(n.current,at),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var kt={};function At(e,t,n){!t&&!kt[e]&&(kt[e]=!0,A(!1,n))}_.memo(jt);function jt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return pt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Mt({to:e,replace:t,state:n,relative:r}){k(rt(),` may be used only in the context of a component.`);let{static:i}=_.useContext(qe);A(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=_.useContext(Ye),{pathname:o}=it(),s=st(),c=we(e,Ce(a),o,r===`path`),l=JSON.stringify(c);return _.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function Nt(e){return ut(e.context)}function Pt(e){k(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Ft({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){k(!rt(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=_.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=F(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=_.useMemo(()=>{let e=ve(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return A(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:_.createElement(qe.Provider,{value:c},_.createElement(Je.Provider,{children:t,value:h}))}function It({children:e,location:t}){return ft(Lt(e),t)}_.Component;function Lt(e,t=[]){let n=[];return _.Children.forEach(e,(e,r)=>{if(!_.isValidElement(e))return;let i=[...t,r];if(e.type===_.Fragment){n.push.apply(n,Lt(e.props.children,i));return}k(e.type===Pt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),k(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Lt(e.props.children,i)),n.push(a)}),n}var Rt=`get`,zt=`application/x-www-form-urlencoded`;function Bt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Vt(e){return Bt(e)&&e.tagName.toLowerCase()===`button`}function Ht(e){return Bt(e)&&e.tagName.toLowerCase()===`form`}function Ut(e){return Bt(e)&&e.tagName.toLowerCase()===`input`}function Wt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Gt(e,t){return e.button===0&&(!t||t===`_self`)&&!Wt(e)}var Kt=null;function qt(){if(Kt===null)try{new FormData(document.createElement(`form`),0),Kt=!1}catch{Kt=!0}return Kt}var Jt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Yt(e){return e!=null&&!Jt.has(e)?(A(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${zt}"`),null):e}function Xt(e,t){let n,r,i,a,o;if(Ht(e)){let o=e.getAttribute(`action`);r=o?ve(o,t):null,n=e.getAttribute(`method`)||Rt,i=Yt(e.getAttribute(`enctype`))||zt,a=new FormData(e)}else if(Vt(e)||Ut(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a