diff --git a/.codex b/.codex
new file mode 100644
index 0000000..e69de29
diff --git a/README.md b/README.md
index e215bc4..803db29 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,114 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+# HY2 Panel
-## Getting Started
+单机自托管的 Next 16 管理面板,面向 Hysteria 2 HTTP 鉴权与多节点流量聚合场景。
-First, run the development server:
+功能包括:
+
+- 单管理员本地登录,基于 SQLite 持久化会话
+- hy2 用户管理:启用/禁用、重置密码、到期时间、总流量上限、稳定 `auth_id`
+- 多节点管理:每节点独立鉴权 URL + token、`trafficStats` URL/secret、轮询间隔
+- 公开 HTTP 鉴权接口:返回标准 `200 + { ok, id }`
+- 后台轮询 `trafficStats`、聚合用户流量、记录同步错误与鉴权审计
+- 对禁用、过期、超额用户调用节点 `kick` 做准实时断开
+
+## Environment
+
+复制 `.env.example` 为 `.env`,至少设置:
```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
+APP_URL=http://127.0.0.1:3000
+SESSION_SECRET=your-long-random-secret
+SESSION_COOKIE_SECURE=auto
+ADMIN_PASSWORD=your-strong-admin-password
```
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+说明:
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+- `ADMIN_USERNAME` / `ADMIN_PASSWORD` 是单管理员账号,启动时会同步到数据库中的唯一管理员记录
+- `SESSION_COOKIE_SECURE=auto` 时会按 `APP_URL` 协议决定是否开启 `Secure` cookie;如果你是直接用 `http://IP:3000` 访问,不要强制设成 `true`
+- `DATABASE_PATH` 默认是 `./data/hy2-panel.db`
+- `POLLER_ENABLED=true` 时,Next `instrumentation.ts` 会在 Node 进程启动后启动轮询器
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+## Development
-## Learn More
+```bash
+npm install
+npm run dev
+```
-To learn more about Next.js, take a look at the following resources:
+打开 `http://127.0.0.1:3000`。
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+## Build
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+```bash
+npm run lint
+npm run build
+```
-## Deploy on Vercel
+这里将生产构建固定为 `next build --webpack`。原因是 Next 16 默认 Turbopack,但在部分受限环境下会触发构建期端口绑定问题;Webpack 构建已验证通过。
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+## HY2 Node Access
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+面板里的每个节点都会生成独立鉴权 URL,例如:
+
+```yaml
+auth:
+ type: http
+ http:
+ url: http://127.0.0.1:3000/api/hy2/nodes/tokyo-01/auth?token=YOUR_TOKEN
+ insecure: false
+```
+
+根据 Hysteria 官方文档,HTTP 鉴权回调请求体是:
+
+```json
+{
+ "addr": "123.123.123.123:44556",
+ "auth": "username:password",
+ "tx": 123456
+}
+```
+
+面板响应:
+
+```json
+{
+ "ok": true,
+ "id": "immutable-auth-id"
+}
+```
+
+多节点识别依赖“不同的 URL 路径”,不是依赖 hy2 自动附带节点 ID。
+
+## trafficStats
+
+节点需启用:
+
+```yaml
+trafficStats:
+ listen: :9999
+ secret: some_secret
+```
+
+面板会轮询:
+
+- `GET /traffic?clear=1`
+- `GET /online`
+- `GET /dump/streams`
+- `POST /kick`
+
+## systemd
+
+已提供示例单元文件 [deploy/hy2-panel.service](/root/hy2-panel/deploy/hy2-panel.service)。
+
+典型流程:
+
+```bash
+npm install
+npm run build
+sudo cp deploy/hy2-panel.service /etc/systemd/system/hy2-panel.service
+sudo systemctl daemon-reload
+sudo systemctl enable --now hy2-panel
+```
+
+如果你需要对外开放,再在 Nginx/Caddy 前面反代 `127.0.0.1:3000` 即可。
diff --git a/app/admin/admin-nav.tsx b/app/admin/admin-nav.tsx
new file mode 100644
index 0000000..473936b
--- /dev/null
+++ b/app/admin/admin-nav.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+type NavItem = {
+ href: string;
+ label: string;
+ meta: string;
+};
+
+export function AdminNav({ items }: { items: NavItem[] }) {
+ const pathname = usePathname();
+
+ return (
+
+ );
+}
diff --git a/app/admin/audits/page.tsx b/app/admin/audits/page.tsx
new file mode 100644
index 0000000..c403aae
--- /dev/null
+++ b/app/admin/audits/page.tsx
@@ -0,0 +1,51 @@
+import { formatBytes, listAudits } from "@/lib/store";
+
+export default async function AuditsPage() {
+ const audits = await listAudits();
+
+ return (
+
+
+
+
AUDIT
+
鉴权审计
+
记录最近的 HTTP 鉴权回调结果、失败原因和请求来源。
+
+
+
+ 最近记录
+ {audits.length}
+
+
+
+
+
+
+ {audits.map((audit) => (
+
+
+
+
+ {audit.ok ? "允许" : "拒绝"}
+
+
+
+ {audit.node_name ?? "未知节点"} / {audit.username ?? audit.auth_id ?? "-"}
+
+
{audit.addr ?? "-"}
+
+
+
{audit.reason}
+
+ 请求速率 {audit.requested_tx != null ? formatBytes(audit.requested_tx) : "-"}
+ /s
+
+
{audit.created_at}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx
new file mode 100644
index 0000000..2aea561
--- /dev/null
+++ b/app/admin/dashboard/page.tsx
@@ -0,0 +1,193 @@
+import { formatBytes, getDashboardMetrics } from "@/lib/store";
+
+function getNodePill(node: {
+ enabled: number;
+ last_error_message: string | null;
+}) {
+ if (!node.enabled) {
+ return { label: "已禁用", className: "status-pill status-pill--off" };
+ }
+
+ if (node.last_error_message) {
+ return { label: "同步异常", className: "status-pill status-pill--danger" };
+ }
+
+ return { label: "运行健康", className: "status-pill status-pill--ok" };
+}
+
+export default async function DashboardPage() {
+ const data = await getDashboardMetrics();
+
+ return (
+
+
+
+
OVERVIEW
+
仪表盘
+
+ 汇总多节点在线、鉴权失败、同步错误和用户用量,作为当前运行态的统一入口。
+
+
+ HTTP 鉴权在线
+ 轮询聚合已启用
+ 节点 {data.totals.enabled_nodes}
+
+
+
+
+ 活动连接
+ {data.totals.total_online_connections}
+
+
+ 累计流量
+ {formatBytes(data.totals.total_traffic)}
+
+
+
+
+
+
+ 总用户
+ {data.totals.total_users}
+ 启用 {data.totals.enabled_users} / 管理员 {data.totals.admin_users}
+
+
+ 活跃用户
+ {data.totals.active_presence_rows}
+ 在线连接 {data.totals.total_online_connections}
+
+
+ 启用节点
+ {data.totals.enabled_nodes}
+ 轮询与 kick 生效范围
+
+
+ 累计流量
+ {formatBytes(data.totals.total_traffic)}
+ 已聚合到 SQLite
+
+
+
+
+
+
+
节点健康
+
+
+ {data.nodeHealth.map((node) => (
+
+
+
{node.name}
+
/{node.slug}
+
+
+
+
+ {getNodePill(node).label}
+
+
+
+ 在线 {node.last_online_users} / 流 {node.last_stream_count}
+
+
+ 最近成功 {node.last_sync_ok_at ?? "无"}
+
+ {node.last_error_message ? (
+
{node.last_error_message}
+ ) : null}
+
+
+ ))}
+
+
+
+
+
+
用量排行
+
+
+ {data.topUsers.map((user) => (
+
+
+
{user.username}
+
{user.auth_id}
+
+
+
{formatBytes(user.total_bytes)}
+
+ TX {formatBytes(user.used_tx_bytes)} / RX {formatBytes(user.used_rx_bytes)}
+
+
+
+ ))}
+
+
+
+
+
+
最近鉴权失败
+
+
+ {data.recentFailures.map((item, index) => (
+
+
+
{item.reason}
+
+ {item.node_name ?? "未知节点"} / {item.username ?? "未知用户"}
+
+
+
+
{item.addr ?? "-"}
+
{item.created_at}
+
+
+ ))}
+
+
+
+
+
+
最近流量入账
+
+
+ {data.recentUsage.map((item, index) => (
+
+
+
{item.username ?? item.auth_id}
+
{item.node_name ?? "未知节点"}
+
+
+
+ +{formatBytes(item.tx_bytes + item.rx_bytes)}
+
+
{item.created_at}
+
+
+ ))}
+
+
+
+
+
+
节点同步错误
+
+
+ {data.syncErrors.length === 0 ? (
+
暂无同步错误。
+ ) : (
+ data.syncErrors.map((item, index) => (
+
+
+
{item.node_name}
+
{item.error_message ?? "unknown error"}
+
+
{item.created_at}
+
+ ))
+ )}
+
+
+
+
+ );
+}
diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx
new file mode 100644
index 0000000..12856b5
--- /dev/null
+++ b/app/admin/layout.tsx
@@ -0,0 +1,42 @@
+import { requireAdminAccess } from "@/lib/session";
+
+import { AdminNav } from "./admin-nav";
+
+const navItems = [
+ { href: "/admin/dashboard", label: "仪表盘", meta: "运行态与健康度" },
+ { href: "/admin/users", label: "用户管理", meta: "凭证、额度与订阅" },
+ { href: "/admin/nodes", label: "节点管理", meta: "接入、鉴权与导出" },
+ { href: "/admin/subscriptions", label: "订阅配置", meta: "模板与代理分组" },
+ { href: "/admin/audits", label: "审计日志", meta: "鉴权失败与追踪" },
+];
+
+export default async function AdminLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ const session = await requireAdminAccess();
+
+ return (
+
+ );
+}
diff --git a/app/admin/nodes/actions.ts b/app/admin/nodes/actions.ts
new file mode 100644
index 0000000..11dc9fa
--- /dev/null
+++ b/app/admin/nodes/actions.ts
@@ -0,0 +1,75 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+
+import { requireAdminAccess } from "@/lib/session";
+import { createNode, deleteNode, updateNode } from "@/lib/store";
+
+function boolFromForm(value: FormDataEntryValue | null) {
+ return value === "on";
+}
+
+export async function createNodeAction(formData: FormData) {
+ await requireAdminAccess();
+ await createNode({
+ slug: String(formData.get("slug") ?? "").trim(),
+ name: String(formData.get("name") ?? "").trim(),
+ authToken: String(formData.get("auth_token") ?? "").trim() || null,
+ hy2Listen: String(formData.get("hy2_listen") ?? "").trim() || "",
+ serverHost: String(formData.get("server_host") ?? "").trim(),
+ serverPort: Number(formData.get("server_port") ?? 443),
+ clientName:
+ String(formData.get("client_name") ?? "").trim() ||
+ String(formData.get("name") ?? "").trim(),
+ bandwidthUpMbps: Number(formData.get("bandwidth_up_mbps") ?? 30),
+ bandwidthDownMbps: Number(formData.get("bandwidth_down_mbps") ?? 30),
+ skipCertVerify: boolFromForm(formData.get("skip_cert_verify")),
+ sni: String(formData.get("sni") ?? "").trim() || null,
+ trafficStatsUrl: String(formData.get("traffic_stats_url") ?? "").trim(),
+ trafficStatsListen:
+ String(formData.get("traffic_stats_listen") ?? "").trim() || ":9999",
+ trafficStatsSecret:
+ String(formData.get("traffic_stats_secret") ?? "").trim(),
+ enabled: boolFromForm(formData.get("enabled")),
+ pollIntervalSeconds: Number(formData.get("poll_interval_seconds") ?? 15),
+ });
+
+ revalidatePath("/admin/nodes");
+ revalidatePath("/admin/dashboard");
+}
+
+export async function updateNodeAction(formData: FormData) {
+ await requireAdminAccess();
+ await updateNode({
+ id: Number(formData.get("id")),
+ slug: String(formData.get("slug") ?? "").trim(),
+ name: String(formData.get("name") ?? "").trim(),
+ authToken: String(formData.get("auth_token") ?? "").trim(),
+ hy2Listen: String(formData.get("hy2_listen") ?? "").trim() || "",
+ serverHost: String(formData.get("server_host") ?? "").trim(),
+ serverPort: Number(formData.get("server_port") ?? 443),
+ clientName:
+ String(formData.get("client_name") ?? "").trim() ||
+ String(formData.get("name") ?? "").trim(),
+ bandwidthUpMbps: Number(formData.get("bandwidth_up_mbps") ?? 30),
+ bandwidthDownMbps: Number(formData.get("bandwidth_down_mbps") ?? 30),
+ skipCertVerify: boolFromForm(formData.get("skip_cert_verify")),
+ sni: String(formData.get("sni") ?? "").trim() || null,
+ trafficStatsUrl: String(formData.get("traffic_stats_url") ?? "").trim(),
+ trafficStatsListen:
+ String(formData.get("traffic_stats_listen") ?? "").trim() || ":9999",
+ trafficStatsSecret: String(formData.get("traffic_stats_secret") ?? "").trim(),
+ enabled: boolFromForm(formData.get("enabled")),
+ pollIntervalSeconds: Number(formData.get("poll_interval_seconds") ?? 15),
+ });
+
+ revalidatePath("/admin/nodes");
+ revalidatePath("/admin/dashboard");
+}
+
+export async function deleteNodeAction(formData: FormData) {
+ await requireAdminAccess();
+ await deleteNode(Number(formData.get("id")));
+ revalidatePath("/admin/nodes");
+ revalidatePath("/admin/dashboard");
+}
diff --git a/app/admin/nodes/page.tsx b/app/admin/nodes/page.tsx
new file mode 100644
index 0000000..1d43e4d
--- /dev/null
+++ b/app/admin/nodes/page.tsx
@@ -0,0 +1,347 @@
+import { getRequestOrigin } from "@/lib/request-origin";
+import { buildNodeHy2Config } from "@/lib/subscription";
+import { buildNodeAuthUrl, buildTrafficStatsUrl, listNodes } from "@/lib/store";
+
+import { createNodeAction, deleteNodeAction, updateNodeAction } from "./actions";
+
+function getNodePills(node: {
+ enabled: number;
+ last_error_message: string | null;
+}) {
+ return [
+ {
+ label: node.enabled ? "已启用" : "已禁用",
+ className: `status-pill ${node.enabled ? "status-pill--ok" : "status-pill--off"}`,
+ },
+ node.last_error_message
+ ? { label: "同步异常", className: "status-pill status-pill--danger" }
+ : { label: "同步正常", className: "status-pill status-pill--sage" },
+ ];
+}
+
+export default async function NodesPage() {
+ const [nodes, origin] = await Promise.all([listNodes(), getRequestOrigin()]);
+
+ return (
+
+
+
+
NODES
+
节点管理
+
+ 统一维护 hy2 节点监听参数、鉴权 URL、trafficStats 接入和客户端导出配置。
+
+
+ URL 自动推导
+ Secret 自动生成
+
+
+
+
+ 节点总数
+ {nodes.length}
+
+
+ 启用中
+ {nodes.filter((node) => node.enabled).length}
+
+
+
+
+
+
+
+ {nodes.map((node) => {
+ const authUrl = buildNodeAuthUrl(node.slug, node.auth_token, origin);
+ const suggestedTrafficStatsUrl = buildTrafficStatsUrl(
+ node.server_host,
+ node.traffic_stats_listen,
+ );
+
+ return (
+
+
+
+
{node.name}
+
/{node.slug}
+
+
+
+ {getNodePills(node).map((pill) => (
+
+ {pill.label}
+
+ ))}
+
+
+ 最近同步 {node.last_sync_ok_at ?? "无"} / 错误{" "}
+ {node.last_error_at ?? "无"}
+
+
+
+
+
+ hy2 监听
+ {node.hy2_listen}
+
+
+ 对外地址
+ {node.server_host}:{node.server_port}
+
+
+ trafficStats
+ {node.traffic_stats_url}
+
+
+
+
+
+
+
节点接入说明
+
+ hy2 HTTP 鉴权 URL 必须独立配置到本节点。面板通过路径识别节点来源,不依赖请求体。
+
+
+{buildNodeHy2Config(node, origin)}
+
+
+ 面板轮询地址:{node.traffic_stats_url}
+
+
+ 自动推导地址:{suggestedTrafficStatsUrl}
+
+
+ 鉴权 URL:{authUrl}
+
+
+ 下载 hy2 config.yaml
+
+ {node.last_error_message ? (
+
{node.last_error_message}
+ ) : null}
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
new file mode 100644
index 0000000..5587a1b
--- /dev/null
+++ b/app/admin/page.tsx
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export default function AdminIndexPage() {
+ redirect("/admin/dashboard");
+}
diff --git a/app/admin/subscriptions/actions.ts b/app/admin/subscriptions/actions.ts
new file mode 100644
index 0000000..3dc840b
--- /dev/null
+++ b/app/admin/subscriptions/actions.ts
@@ -0,0 +1,59 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+
+import { createStableId } from "@/lib/password";
+import { requireAdminAccess } from "@/lib/session";
+import {
+ getSubscriptionSettings,
+ saveProxyGroups,
+ saveSubscriptionSettings,
+} from "@/lib/store";
+
+export async function saveSubscriptionSettingsAction(formData: FormData) {
+ await requireAdminAccess();
+
+ await saveSubscriptionSettings({
+ template: String(formData.get("template") ?? ""),
+ extraProxyGroups: String(formData.get("extra_proxy_groups") ?? ""),
+ profileName: String(formData.get("profile_name") ?? "").trim() || "FeieProxy",
+ });
+
+ revalidatePath("/admin/subscriptions");
+}
+
+export async function saveProxyGroupAction(formData: FormData) {
+ await requireAdminAccess();
+
+ const settings = await getSubscriptionSettings();
+ const id = String(formData.get("id") ?? "").trim() || createStableId();
+ const name = String(formData.get("name") ?? "").trim();
+ const memberSlugs = formData
+ .getAll("member_slugs")
+ .map((value) => String(value))
+ .filter(Boolean);
+
+ if (!name) {
+ return;
+ }
+
+ const proxyGroups = settings.proxyGroups.filter((group) => group.id !== id);
+ proxyGroups.push({
+ id,
+ name,
+ type: "select",
+ memberSlugs,
+ });
+
+ await saveProxyGroups(proxyGroups);
+ revalidatePath("/admin/subscriptions");
+}
+
+export async function deleteProxyGroupAction(formData: FormData) {
+ await requireAdminAccess();
+
+ const settings = await getSubscriptionSettings();
+ const id = String(formData.get("id") ?? "").trim();
+ await saveProxyGroups(settings.proxyGroups.filter((group) => group.id !== id));
+ revalidatePath("/admin/subscriptions");
+}
diff --git a/app/admin/subscriptions/page.tsx b/app/admin/subscriptions/page.tsx
new file mode 100644
index 0000000..204b539
--- /dev/null
+++ b/app/admin/subscriptions/page.tsx
@@ -0,0 +1,160 @@
+import { getRequestOrigin } from "@/lib/request-origin";
+import { buildSubscriptionUrl, getDefaultClashTemplate } from "@/lib/subscription";
+import {
+ getAdminMirrorUser,
+ getSubscriptionSettings,
+ listSubscriptionNodes,
+} from "@/lib/store";
+
+import {
+ deleteProxyGroupAction,
+ saveProxyGroupAction,
+ saveSubscriptionSettingsAction,
+} from "./actions";
+
+export default async function AdminSubscriptionsPage() {
+ const [settings, adminUser, nodes, origin] = await Promise.all([
+ getSubscriptionSettings(),
+ getAdminMirrorUser(),
+ listSubscriptionNodes(),
+ getRequestOrigin(),
+ ]);
+
+ return (
+
+
+
+
SUBSCRIPTIONS
+
订阅与模板
+
+ 管理 Clash 基础模板、订阅文件名、代理分组以及管理员默认不限量订阅链接。
+
+
+ 动态 proxies
+ 分组 {settings.proxyGroups.length}
+
+
+
+
+ 可用节点
+ {nodes.length}
+
+
+ 默认主组
+ {settings.profileName}
+
+
+
+
+
+
+
管理员订阅
+
+ {adminUser?.subscription_token ? (
+
+
+ {buildSubscriptionUrl(adminUser.subscription_token, origin)}
+
+
+ ) : (
+ 管理员订阅链接暂不可用。
+ )}
+
+
+
+
+
Proxy Groups
+
+
+ {settings.proxyGroups.map((group) => (
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/admin/users/actions.ts b/app/admin/users/actions.ts
new file mode 100644
index 0000000..faa319e
--- /dev/null
+++ b/app/admin/users/actions.ts
@@ -0,0 +1,71 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+
+import { requireAdminAccess } from "@/lib/session";
+import {
+ createUser,
+ deleteUser,
+ resetUserPassword,
+ toBytesFromGiB,
+ updateUser,
+} from "@/lib/store";
+
+function boolFromForm(value: FormDataEntryValue | null) {
+ return value === "on";
+}
+
+function optionalDateTime(value: FormDataEntryValue | null) {
+ const raw = String(value ?? "").trim();
+ return raw ? new Date(raw).toISOString() : null;
+}
+
+export async function createUserAction(formData: FormData) {
+ await requireAdminAccess();
+ await createUser({
+ username: String(formData.get("username") ?? "").trim(),
+ password: String(formData.get("password") ?? ""),
+ enabled: boolFromForm(formData.get("enabled")),
+ isAdmin: false,
+ expiresAt: optionalDateTime(formData.get("expires_at")),
+ trafficLimitBytes: toBytesFromGiB(String(formData.get("traffic_limit_gib") ?? "")),
+ notes: String(formData.get("notes") ?? "").trim(),
+ });
+
+ revalidatePath("/admin/users");
+ revalidatePath("/admin/dashboard");
+}
+
+export async function updateUserAction(formData: FormData) {
+ await requireAdminAccess();
+ await updateUser({
+ id: Number(formData.get("id")),
+ username: String(formData.get("username") ?? "").trim(),
+ enabled: boolFromForm(formData.get("enabled")),
+ subscriptionLabel:
+ String(formData.get("subscription_label") ?? "").trim() ||
+ String(formData.get("username") ?? "").trim(),
+ expiresAt: optionalDateTime(formData.get("expires_at")),
+ trafficLimitBytes: toBytesFromGiB(String(formData.get("traffic_limit_gib") ?? "")),
+ notes: String(formData.get("notes") ?? "").trim(),
+ });
+
+ revalidatePath("/admin/users");
+ revalidatePath("/admin/dashboard");
+}
+
+export async function deleteUserAction(formData: FormData) {
+ await requireAdminAccess();
+ await deleteUser(Number(formData.get("id")));
+ revalidatePath("/admin/users");
+ revalidatePath("/admin/dashboard");
+}
+
+export async function resetUserPasswordAction(formData: FormData) {
+ await requireAdminAccess();
+ const password = String(formData.get("password") ?? "");
+ if (!password) return;
+
+ await resetUserPassword(Number(formData.get("id")), password);
+ revalidatePath("/admin/users");
+}
diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx
new file mode 100644
index 0000000..acb934d
--- /dev/null
+++ b/app/admin/users/page.tsx
@@ -0,0 +1,220 @@
+import { getRequestOrigin } from "@/lib/request-origin";
+import {
+ buildSubscriptionUrl,
+ describeRemainingTraffic,
+} from "@/lib/subscription";
+import {
+ formatBytes,
+ listUsers,
+ toDatetimeLocalValue,
+} from "@/lib/store";
+
+import {
+ createUserAction,
+ deleteUserAction,
+ resetUserPasswordAction,
+ updateUserAction,
+} from "./actions";
+
+function getUserPills(user: {
+ enabled: number;
+ is_admin: number;
+ expires_at: string | null;
+}) {
+ return [
+ {
+ label: user.enabled ? "已启用" : "已禁用",
+ className: `status-pill ${user.enabled ? "status-pill--ok" : "status-pill--danger"}`,
+ },
+ user.is_admin
+ ? { label: "管理员", className: "status-pill status-pill--ink" }
+ : null,
+ user.expires_at
+ ? { label: user.expires_at, className: "status-pill status-pill--neutral" }
+ : { label: "长期有效", className: "status-pill status-pill--warm" },
+ ].filter((pill): pill is { label: string; className: string } => Boolean(pill));
+}
+
+export default async function UsersPage() {
+ const [users, origin] = await Promise.all([listUsers(), getRequestOrigin()]);
+
+ return (
+
+
+
+
USERS
+
用户管理
+
+ 管理用户名:密码、`auth_id`、到期时间、额度、订阅名和用户自助链接。
+
+
+ 自助登录
+ 订阅链接按用户独立生成
+
+
+
+
+ 用户数
+ {users.length}
+
+
+ 启用中
+ {users.filter((user) => user.enabled).length}
+
+
+
+
+
+
+
+ {users.map((user) => (
+
+
+
+
{user.username}
+
auth_id: {user.auth_id}
+
+
+
+ {getUserPills(user).map((pill) => (
+
+ {pill.label}
+
+ ))}
+
+
+ 累计 {formatBytes(user.used_tx_bytes + user.used_rx_bytes)} / 在线{" "}
+ {user.online_connections}
+
+
+
+
+
+ 总用量
+ {formatBytes(user.used_tx_bytes + user.used_rx_bytes)}
+
+
+ 剩余额度
+ {describeRemainingTraffic(user)}
+
+
+ 在线连接
+ {user.online_connections}
+
+
+
+
+
订阅链接
+
+ {user.subscription_token
+ ? buildSubscriptionUrl(user.subscription_token, origin)
+ : "暂不可用"}
+
+
+
+
+ {user.is_admin ? null : (
+
+ )}
+
+
+ ))}
+
+
+ );
+}
diff --git a/app/api/admin/nodes/[slug]/config/route.ts b/app/api/admin/nodes/[slug]/config/route.ts
new file mode 100644
index 0000000..66230a9
--- /dev/null
+++ b/app/api/admin/nodes/[slug]/config/route.ts
@@ -0,0 +1,28 @@
+import { NextResponse } from "next/server";
+
+import { getOriginFromRequest } from "@/lib/request-origin";
+import { requireAdminAccess } from "@/lib/session";
+import { buildNodeHy2Config } from "@/lib/subscription";
+import { getNodeBySlug } from "@/lib/store";
+
+export async function GET(
+ request: Request,
+ context: RouteContext<"/api/admin/nodes/[slug]/config">,
+) {
+ await requireAdminAccess();
+
+ const { slug } = await context.params;
+ const node = await getNodeBySlug(slug);
+
+ if (!node) {
+ return new NextResponse("node not found", { status: 404 });
+ }
+
+ return new NextResponse(buildNodeHy2Config(node, getOriginFromRequest(request)), {
+ status: 200,
+ headers: {
+ "Content-Type": "application/x-yaml; charset=utf-8",
+ "Content-Disposition": `attachment; filename="${slug}.config.yaml"`,
+ },
+ });
+}
diff --git a/app/api/hy2/nodes/[slug]/auth/route.ts b/app/api/hy2/nodes/[slug]/auth/route.ts
new file mode 100644
index 0000000..4ab126e
--- /dev/null
+++ b/app/api/hy2/nodes/[slug]/auth/route.ts
@@ -0,0 +1,134 @@
+import { getUserByUsername, writeAuthAudit } from "@/lib/store";
+import { parseHy2Userpass, verifyNodeToken, evaluateUserForAuth } from "@/lib/hy2";
+import { verifyPassword } from "@/lib/password";
+
+type AuthRequest = {
+ addr?: string;
+ auth?: string;
+ tx?: number;
+};
+
+function deny(id = "") {
+ return Response.json({ ok: false, id });
+}
+
+export async function POST(
+ request: Request,
+ context: RouteContext<"/api/hy2/nodes/[slug]/auth">,
+) {
+ const { slug } = await context.params;
+ const requestUrl = new URL(request.url);
+ const token =
+ requestUrl.searchParams.get("token") ||
+ request.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ||
+ request.headers.get("x-node-token");
+
+ const nodeResult = await verifyNodeToken(slug, token);
+ if (!nodeResult.ok) {
+ await writeAuthAudit({
+ nodeId: nodeResult.node?.id ?? null,
+ ok: false,
+ reason: nodeResult.reason,
+ });
+ return deny();
+ }
+
+ const node = nodeResult.node;
+ if (!node) {
+ return deny();
+ }
+
+ let payload: AuthRequest;
+
+ try {
+ payload = (await request.json()) as AuthRequest;
+ } catch {
+ await writeAuthAudit({
+ nodeId: node.id,
+ addr: null,
+ requestedTx: null,
+ ok: false,
+ reason: "bad_json",
+ });
+ return deny();
+ }
+
+ if (!payload.auth) {
+ await writeAuthAudit({
+ nodeId: node.id,
+ addr: payload.addr ?? null,
+ requestedTx: payload.tx ?? null,
+ ok: false,
+ reason: "missing_auth",
+ });
+ return deny();
+ }
+
+ const parsed = parseHy2Userpass(payload.auth);
+ if (!parsed) {
+ await writeAuthAudit({
+ nodeId: node.id,
+ addr: payload.addr ?? null,
+ requestedTx: payload.tx ?? null,
+ ok: false,
+ reason: "bad_auth_format",
+ });
+ return deny();
+ }
+
+ const user = await getUserByUsername(parsed.username);
+ if (!user) {
+ await writeAuthAudit({
+ nodeId: node.id,
+ username: parsed.username,
+ addr: payload.addr ?? null,
+ requestedTx: payload.tx ?? null,
+ ok: false,
+ reason: "user_not_found",
+ });
+ return deny();
+ }
+
+ const passwordOk = await verifyPassword(parsed.password, user.password_hash);
+ if (!passwordOk) {
+ await writeAuthAudit({
+ nodeId: node.id,
+ userId: user.id,
+ authId: user.auth_id,
+ username: user.username,
+ addr: payload.addr ?? null,
+ requestedTx: payload.tx ?? null,
+ ok: false,
+ reason: "password_invalid",
+ });
+ return deny();
+ }
+
+ const access = evaluateUserForAuth(user);
+ if (!access.ok) {
+ await writeAuthAudit({
+ nodeId: node.id,
+ userId: user.id,
+ authId: user.auth_id,
+ username: user.username,
+ addr: payload.addr ?? null,
+ requestedTx: payload.tx ?? null,
+ ok: false,
+ reason: access.reason,
+ });
+ return deny();
+ }
+
+ await writeAuthAudit({
+ nodeId: node.id,
+ userId: user.id,
+ authId: user.auth_id,
+ username: user.username,
+ addr: payload.addr ?? null,
+ requestedTx: payload.tx ?? null,
+ ok: true,
+ reason: "ok",
+ });
+
+ return Response.json({ ok: true, id: user.auth_id });
+}
diff --git a/app/api/subscription/[token]/clash/route.ts b/app/api/subscription/[token]/clash/route.ts
new file mode 100644
index 0000000..27b5734
--- /dev/null
+++ b/app/api/subscription/[token]/clash/route.ts
@@ -0,0 +1,38 @@
+import { buildSubscriptionConfig, buildSubscriptionHeaders } from "@/lib/subscription";
+import {
+ getSubscriptionSettings,
+ getUserBySubscriptionToken,
+ listSubscriptionNodes,
+} from "@/lib/store";
+
+export async function GET(
+ _request: Request,
+ context: RouteContext<"/api/subscription/[token]/clash">,
+) {
+ const { token } = await context.params;
+ const user = await getUserBySubscriptionToken(token);
+
+ if (!user || !user.enabled) {
+ return new Response("subscription not found", { status: 404 });
+ }
+
+ const [nodes, settings] = await Promise.all([
+ listSubscriptionNodes(),
+ getSubscriptionSettings(),
+ ]);
+
+ let body: string;
+
+ try {
+ body = buildSubscriptionConfig({ user, nodes, settings });
+ } catch {
+ return new Response("subscription secret unavailable", { status: 409 });
+ }
+
+ const profileName = user.subscription_label || settings.profileName || user.username;
+
+ return new Response(body, {
+ status: 200,
+ headers: buildSubscriptionHeaders(user, profileName),
+ });
+}
diff --git a/app/config/config.yaml b/app/config/config.yaml
new file mode 100644
index 0000000..7b37a1d
--- /dev/null
+++ b/app/config/config.yaml
@@ -0,0 +1,571 @@
+mixed-port: 7890
+allow-lan: true
+bind-address: '*'
+mode: rule
+log-level: info
+external-controller: '127.0.0.1:9090'
+dns:
+ enable: true
+ listen: '0.0.0.0:53'
+ ipv6: false
+ respect-rules: true
+ default-nameserver: [223.5.5.5, 223.6.6.6]
+ proxy-server-nameserver: ['https://223.5.5.5/dns-query', 'https://223.6.6.6/dns-query', 'tls://dns.alidns.com', 'tls://223.5.5.5:853', 'tls://223.6.6.6:853']
+ enhanced-mode: fake-ip
+ fake-ip-range: 198.18.0.1/16
+ fake-ip-filter: [+.lan, +.local, +.msftconnecttest.com, +.msftncsi.com, localhost.ptlogin2.qq.com, localhost.sec.qq.com, localhost.work.weixin.qq.com, 'geosite:private']
+ use-hosts: true
+ nameserver: ['https://223.5.5.5/dns-query', 'https://223.6.6.6/dns-query', 'tls://dns.alidns.com', 'tls://223.5.5.5:853', 'tls://223.6.6.6:853']
+ fallback: ['https://8.8.8.8/dns-query', 'https://8.8.4.4/dns-query', 'tls://dns.google', 'tls://8.8.8.8:853', 'tls://8.8.4.4:853']
+ fallback-filter: { geoip: true, geoip-code: CN, geosite: [gfw], ipcidr: [240.0.0.0/4, 0.0.0.0/32], domain: [+.google.com, +.facebook.com, +.youtube.com] }
+
+rules:
+ - 'DOMAIN-KEYWORD,anthropic,ChatGPT'
+ - 'DOMAIN-KEYWORD,chatgpt,ChatGPT'
+ - 'DOMAIN-KEYWORD,oaiusercontent,ChatGPT'
+ - 'DOMAIN-KEYWORD,oaistatic,ChatGPT'
+ - 'DOMAIN-KEYWORD,claude.ai,ChatGPT'
+ - 'DOMAIN-KEYWORD,openai,ChatGPT'
+ - 'DOMAIN-KEYWORD,gemini,ChatGPT'
+ - 'DOMAIN-KEYWORD,aistudio,ChatGPT'
+ - 'DOMAIN-KEYWORD,alkalimakersuite,ChatGPT'
+ - 'DOMAIN-KEYWORD,x.ai,ChatGPT'
+ - 'DOMAIN-KEYWORD,grok,ChatGPT'
+
+ - 'IP-CIDR,154.36.184.142/8,DIRECT'
+ - 'IP-CIDR,154.40.44.206/8,DIRECT'
+ - 'DOMAIN-KEYWORD,vrchat,FeieProxy'
+ - 'DOMAIN-KEYWORD,zdoc.app,FeieProxy'
+ - 'DOMAIN-KEYWORD,happymh,FeieProxy'
+ - 'DOMAIN-KEYWORD,bbae,FeieProxy'
+ - 'DOMAIN-KEYWORD,sketchfab,FeieProxy'
+ - 'DOMAIN-KEYWORD,tripo3d,FeieProxy'
+ - 'DOMAIN,www.vitejs.net,FeieProxy'
+
+ - 'DOMAIN,laowang.vip,FeieProxy'
+ - 'DOMAIN,hmziyuan.com,FeieProxy'
+ - 'DOMAIN-KEYWORD,maa.plus,FeieProxy'
+ - 'DOMAIN-KEYWORD,placeholder.com,FeieProxy'
+ - 'DOMAIN-KEYWORD,bangumi.moe,FeieProxy'
+ - 'DOMAIN-KEYWORD,linux.do,FeieProxy'
+ - 'DOMAIN-KEYWORD,openai,FeieProxy'
+ - 'DOMAIN-KEYWORD,tepis.me,FeieProxy'
+ - 'DOMAIN-KEYWORD,bangumi.moe,FeieProxy'
+ - 'DOMAIN-KEYWORD,tracker,FeieProxy'
+ - 'DOMAIN-KEYWORD,jiuse.me,FeieProxy'
+ - 'DOMAIN-KEYWORD,btc620,FeieProxy'
+ - 'DOMAIN-KEYWORD,killcovid2021,FeieProxy'
+ - 'DOMAIN-KEYWORD,knit.bid,FeieProxy'
+ - 'DOMAIN-KEYWORD,stripe,FeieProxy'
+ - 'DOMAIN-KEYWORD,intercomassets,FeieProxy'
+ - 'DOMAIN-KEYWORD,galleryepic,FeieProxy'
+ - 'DOMAIN-KEYWORD,adobe,REJECT'
+ - "DOMAIN-KEYWORD,fanbox.cc,FeieProxy"
+ - "DOMAIN-KEYWORD,fantia.jp,FeieProxy"
+
+ - "DOMAIN-SUFFIX,services.googleapis.cn,FeieProxy"
+ - "DOMAIN-SUFFIX,xn--ngstr-lra8j.com,FeieProxy"
+ - "DOMAIN,safebrowsing.urlsec.qq.com,DIRECT"
+ - "DOMAIN,safebrowsing.googleapis.com,DIRECT"
+ - "DOMAIN,developer.apple.com,FeieProxy"
+ - "DOMAIN-SUFFIX,digicert.com,FeieProxy"
+ - "DOMAIN,ocsp.comodoca.com,FeieProxy"
+ - "DOMAIN,ocsp.usertrust.com,FeieProxy"
+ - "DOMAIN,ocsp.sectigo.com,FeieProxy"
+ - "DOMAIN,ocsp.verisign.net,FeieProxy"
+
+ - "DOMAIN-SUFFIX,mzstatic.com,DIRECT"
+ - "DOMAIN-SUFFIX,itunes.apple.com,DIRECT"
+ - "DOMAIN-SUFFIX,icloud.com,DIRECT"
+ - "DOMAIN-SUFFIX,icloud-content.com,DIRECT"
+ - "DOMAIN-SUFFIX,me.com,DIRECT"
+ - "DOMAIN-SUFFIX,aaplimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,cdn20.com,DIRECT"
+ - "DOMAIN-SUFFIX,cdn-apple.com,DIRECT"
+ - "DOMAIN-SUFFIX,akadns.net,DIRECT"
+ - "DOMAIN-SUFFIX,akamaiedge.net,DIRECT"
+ - "DOMAIN-SUFFIX,edgekey.net,DIRECT"
+ - "DOMAIN-SUFFIX,mwcloudcdn.com,DIRECT"
+ - "DOMAIN-SUFFIX,mwcname.com,DIRECT"
+ - "DOMAIN-SUFFIX,apple.com,DIRECT"
+ - "DOMAIN-SUFFIX,apple-cloudkit.com,DIRECT"
+ - "DOMAIN-SUFFIX,apple-mapkit.com,DIRECT"
+ - "DOMAIN-SUFFIX,126.com,DIRECT"
+ - "DOMAIN-SUFFIX,126.net,DIRECT"
+ - "DOMAIN-SUFFIX,127.net,DIRECT"
+ - "DOMAIN-SUFFIX,163.com,DIRECT"
+ - "DOMAIN-SUFFIX,360buyimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,36kr.com,DIRECT"
+ - "DOMAIN-SUFFIX,acfun.tv,DIRECT"
+ - "DOMAIN-SUFFIX,air-matters.com,DIRECT"
+ - "DOMAIN-SUFFIX,aixifan.com,DIRECT"
+ - "DOMAIN-KEYWORD,alicdn,DIRECT"
+ - "DOMAIN-KEYWORD,alipay,DIRECT"
+ - "DOMAIN-KEYWORD,taobao,DIRECT"
+ - "DOMAIN-SUFFIX,amap.com,DIRECT"
+ - "DOMAIN-SUFFIX,autonavi.com,DIRECT"
+ - "DOMAIN-KEYWORD,baidu,DIRECT"
+ - "DOMAIN-SUFFIX,bdimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,bdstatic.com,DIRECT"
+ - "DOMAIN-SUFFIX,bilibili.com,DIRECT"
+ - "DOMAIN-SUFFIX,bilivideo.com,DIRECT"
+ - "DOMAIN-SUFFIX,caiyunapp.com,DIRECT"
+ - "DOMAIN-SUFFIX,clouddn.com,DIRECT"
+ - "DOMAIN-SUFFIX,cnbeta.com,DIRECT"
+ - "DOMAIN-SUFFIX,cnbetacdn.com,DIRECT"
+ - "DOMAIN-SUFFIX,cootekservice.com,DIRECT"
+ - "DOMAIN-SUFFIX,csdn.net,DIRECT"
+ - "DOMAIN-SUFFIX,ctrip.com,DIRECT"
+ - "DOMAIN-SUFFIX,dgtle.com,DIRECT"
+ - "DOMAIN-SUFFIX,dianping.com,DIRECT"
+ - "DOMAIN-SUFFIX,douban.com,DIRECT"
+ - "DOMAIN-SUFFIX,doubanio.com,DIRECT"
+ - "DOMAIN-SUFFIX,duokan.com,DIRECT"
+ - "DOMAIN-SUFFIX,easou.com,DIRECT"
+ - "DOMAIN-SUFFIX,ele.me,DIRECT"
+ - "DOMAIN-SUFFIX,feng.com,DIRECT"
+ - "DOMAIN-SUFFIX,fir.im,DIRECT"
+ - "DOMAIN-SUFFIX,frdic.com,DIRECT"
+ - "DOMAIN-SUFFIX,g-cores.com,DIRECT"
+ - "DOMAIN-SUFFIX,godic.net,DIRECT"
+ - "DOMAIN-SUFFIX,gtimg.com,DIRECT"
+ - "DOMAIN,cdn.hockeyapp.net,DIRECT"
+ - "DOMAIN-SUFFIX,hongxiu.com,DIRECT"
+ - "DOMAIN-SUFFIX,hxcdn.net,DIRECT"
+ - "DOMAIN-SUFFIX,iciba.com,DIRECT"
+ - "DOMAIN-SUFFIX,ifeng.com,DIRECT"
+ - "DOMAIN-SUFFIX,ifengimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,ipip.net,DIRECT"
+ - "DOMAIN-SUFFIX,iqiyi.com,DIRECT"
+ - "DOMAIN-SUFFIX,jd.com,DIRECT"
+ - "DOMAIN-SUFFIX,jianshu.com,DIRECT"
+ - "DOMAIN-SUFFIX,knewone.com,DIRECT"
+ - "DOMAIN-SUFFIX,le.com,DIRECT"
+ - "DOMAIN-SUFFIX,lecloud.com,DIRECT"
+ - "DOMAIN-SUFFIX,lemicp.com,DIRECT"
+ - "DOMAIN-SUFFIX,licdn.com,DIRECT"
+ - "DOMAIN-SUFFIX,luoo.net,DIRECT"
+ - "DOMAIN-SUFFIX,meituan.com,DIRECT"
+ - "DOMAIN-SUFFIX,meituan.net,DIRECT"
+ - "DOMAIN-SUFFIX,mi.com,DIRECT"
+ - "DOMAIN-SUFFIX,miaopai.com,DIRECT"
+ - "DOMAIN-SUFFIX,microsoft.com,DIRECT"
+ - "DOMAIN-SUFFIX,microsoftonline.com,DIRECT"
+ - "DOMAIN-SUFFIX,miui.com,DIRECT"
+ - "DOMAIN-SUFFIX,miwifi.com,DIRECT"
+ - "DOMAIN-SUFFIX,mob.com,DIRECT"
+ - "DOMAIN-SUFFIX,netease.com,DIRECT"
+ - "DOMAIN-SUFFIX,office.com,DIRECT"
+ - "DOMAIN-SUFFIX,office365.com,DIRECT"
+ - "DOMAIN-KEYWORD,officecdn,DIRECT"
+ - "DOMAIN-SUFFIX,oschina.net,DIRECT"
+ - "DOMAIN-SUFFIX,ppsimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,pstatp.com,DIRECT"
+ - "DOMAIN-SUFFIX,qcloud.com,DIRECT"
+ - "DOMAIN-SUFFIX,qdaily.com,DIRECT"
+ - "DOMAIN-SUFFIX,qdmm.com,DIRECT"
+ - "DOMAIN-SUFFIX,qhimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,qhres.com,DIRECT"
+ - "DOMAIN-SUFFIX,qidian.com,DIRECT"
+ - "DOMAIN-SUFFIX,qihucdn.com,DIRECT"
+ - "DOMAIN-SUFFIX,qiniu.com,DIRECT"
+ - "DOMAIN-SUFFIX,qiniucdn.com,DIRECT"
+ - "DOMAIN-SUFFIX,qiyipic.com,DIRECT"
+ - "DOMAIN-SUFFIX,qq.com,DIRECT"
+ - "DOMAIN-SUFFIX,qqurl.com,DIRECT"
+ - "DOMAIN-SUFFIX,rarbg.to,DIRECT"
+ - "DOMAIN-SUFFIX,ruguoapp.com,DIRECT"
+ - "DOMAIN-SUFFIX,segmentfault.com,DIRECT"
+ - "DOMAIN-SUFFIX,sinaapp.com,DIRECT"
+ - "DOMAIN-SUFFIX,smzdm.com,DIRECT"
+ - "DOMAIN-SUFFIX,snapdrop.net,DIRECT"
+ - "DOMAIN-SUFFIX,sogou.com,DIRECT"
+ - "DOMAIN-SUFFIX,sogoucdn.com,DIRECT"
+ - "DOMAIN-SUFFIX,sohu.com,DIRECT"
+ - "DOMAIN-SUFFIX,soku.com,DIRECT"
+ - "DOMAIN-SUFFIX,speedtest.net,DIRECT"
+ - "DOMAIN-SUFFIX,sspai.com,DIRECT"
+ - "DOMAIN-SUFFIX,suning.com,DIRECT"
+ - "DOMAIN-SUFFIX,taobao.com,DIRECT"
+ - "DOMAIN-SUFFIX,tencent.com,DIRECT"
+ - "DOMAIN-SUFFIX,tenpay.com,DIRECT"
+ - "DOMAIN-SUFFIX,tianyancha.com,DIRECT"
+ - "DOMAIN-SUFFIX,tmall.com,DIRECT"
+ - "DOMAIN-SUFFIX,tudou.com,DIRECT"
+ - "DOMAIN-SUFFIX,umetrip.com,DIRECT"
+ - "DOMAIN-SUFFIX,upaiyun.com,DIRECT"
+ - "DOMAIN-SUFFIX,upyun.com,DIRECT"
+ - "DOMAIN-SUFFIX,veryzhun.com,DIRECT"
+ - "DOMAIN-SUFFIX,weather.com,DIRECT"
+ - "DOMAIN-SUFFIX,weibo.com,DIRECT"
+ - "DOMAIN-SUFFIX,xiami.com,DIRECT"
+ - "DOMAIN-SUFFIX,xiami.net,DIRECT"
+ - "DOMAIN-SUFFIX,xiaomicp.com,DIRECT"
+ - "DOMAIN-SUFFIX,ximalaya.com,DIRECT"
+ - "DOMAIN-SUFFIX,xmcdn.com,DIRECT"
+ - "DOMAIN-SUFFIX,xunlei.com,DIRECT"
+ - "DOMAIN-SUFFIX,yhd.com,DIRECT"
+ - "DOMAIN-SUFFIX,yihaodianimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,yinxiang.com,DIRECT"
+ - "DOMAIN-SUFFIX,ykimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,youdao.com,DIRECT"
+ - "DOMAIN-SUFFIX,youku.com,DIRECT"
+ - "DOMAIN-SUFFIX,zealer.com,DIRECT"
+ - "DOMAIN-SUFFIX,zhihu.com,DIRECT"
+ - "DOMAIN-SUFFIX,zhimg.com,DIRECT"
+ - "DOMAIN-SUFFIX,zimuzu.tv,DIRECT"
+ - "DOMAIN-SUFFIX,zoho.com,DIRECT"
+ - "DOMAIN-KEYWORD,amazon,FeieProxy"
+ - "DOMAIN-KEYWORD,google,FeieProxy"
+ - "DOMAIN-KEYWORD,gmail,FeieProxy"
+ - "DOMAIN-KEYWORD,youtube,FeieProxy"
+ - "DOMAIN-KEYWORD,facebook,FeieProxy"
+ - "DOMAIN-SUFFIX,fb.me,FeieProxy"
+ - "DOMAIN-SUFFIX,fbcdn.net,FeieProxy"
+ - "DOMAIN-KEYWORD,twitter,FeieProxy"
+ - "DOMAIN-KEYWORD,instagram,FeieProxy"
+ - "DOMAIN-KEYWORD,dropbox,FeieProxy"
+ - "DOMAIN-SUFFIX,twimg.com,FeieProxy"
+ - "DOMAIN-KEYWORD,blogspot,FeieProxy"
+ - "DOMAIN-SUFFIX,youtu.be,FeieProxy"
+ - "DOMAIN-KEYWORD,whatsapp,FeieProxy"
+ - "DOMAIN-KEYWORD,admarvel,REJECT"
+ - "DOMAIN-KEYWORD,admaster,REJECT"
+ - "DOMAIN-KEYWORD,adsage,REJECT"
+ - "DOMAIN-KEYWORD,adsmogo,REJECT"
+ - "DOMAIN-KEYWORD,adsrvmedia,REJECT"
+ - "DOMAIN-KEYWORD,adwords,REJECT"
+ - "DOMAIN-KEYWORD,adservice,REJECT"
+ - "DOMAIN-SUFFIX,appsflyer.com,REJECT"
+ - "DOMAIN-KEYWORD,domob,REJECT"
+ - "DOMAIN-SUFFIX,doubleclick.net,REJECT"
+ - "DOMAIN-KEYWORD,duomeng,REJECT"
+ - "DOMAIN-KEYWORD,dwtrack,REJECT"
+ - "DOMAIN-KEYWORD,guanggao,REJECT"
+ - "DOMAIN-KEYWORD,lianmeng,REJECT"
+ - "DOMAIN-SUFFIX,mmstat.com,REJECT"
+ - "DOMAIN-KEYWORD,mopub,REJECT"
+ - "DOMAIN-KEYWORD,omgmta,REJECT"
+ - "DOMAIN-KEYWORD,openx,REJECT"
+ - "DOMAIN-KEYWORD,partnerad,REJECT"
+ - "DOMAIN-KEYWORD,pingfore,REJECT"
+ - "DOMAIN-KEYWORD,supersonicads,REJECT"
+ - "DOMAIN-KEYWORD,uedas,REJECT"
+ - "DOMAIN-KEYWORD,umeng,REJECT"
+ - "DOMAIN-KEYWORD,usage,REJECT"
+ - "DOMAIN-SUFFIX,vungle.com,REJECT"
+ - "DOMAIN-KEYWORD,wlmonitor,REJECT"
+ - "DOMAIN-KEYWORD,zjtoolbar,REJECT"
+ - "DOMAIN-SUFFIX,9to5mac.com,FeieProxy"
+ - "DOMAIN-SUFFIX,abpchina.org,FeieProxy"
+ - "DOMAIN-SUFFIX,adblockplus.org,FeieProxy"
+ - "DOMAIN-SUFFIX,adobe.com,FeieProxy"
+ - "DOMAIN-SUFFIX,akamaized.net,FeieProxy"
+ - "DOMAIN-SUFFIX,alfredapp.com,FeieProxy"
+ - "DOMAIN-SUFFIX,amplitude.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ampproject.org,FeieProxy"
+ - "DOMAIN-SUFFIX,android.com,FeieProxy"
+ - "DOMAIN-SUFFIX,angularjs.org,FeieProxy"
+ - "DOMAIN-SUFFIX,aolcdn.com,FeieProxy"
+ - "DOMAIN-SUFFIX,apkpure.com,FeieProxy"
+ - "DOMAIN-SUFFIX,appledaily.com,FeieProxy"
+ - "DOMAIN-SUFFIX,appshopper.com,FeieProxy"
+ - "DOMAIN-SUFFIX,appspot.com,FeieProxy"
+ - "DOMAIN-SUFFIX,arcgis.com,FeieProxy"
+ - "DOMAIN-SUFFIX,archive.org,FeieProxy"
+ - "DOMAIN-SUFFIX,armorgames.com,FeieProxy"
+ - "DOMAIN-SUFFIX,aspnetcdn.com,FeieProxy"
+ - "DOMAIN-SUFFIX,att.com,FeieProxy"
+ - "DOMAIN-SUFFIX,awsstatic.com,FeieProxy"
+ - "DOMAIN-SUFFIX,azureedge.net,FeieProxy"
+ - "DOMAIN-SUFFIX,azurewebsites.net,FeieProxy"
+ - "DOMAIN-SUFFIX,bing.com,FeieProxy"
+ - "DOMAIN-SUFFIX,bintray.com,FeieProxy"
+ - "DOMAIN-SUFFIX,bit.com,FeieProxy"
+ - "DOMAIN-SUFFIX,bit.ly,FeieProxy"
+ - "DOMAIN-SUFFIX,bitbucket.org,FeieProxy"
+ - "DOMAIN-SUFFIX,bjango.com,FeieProxy"
+ - "DOMAIN-SUFFIX,bkrtx.com,FeieProxy"
+ - "DOMAIN-SUFFIX,blog.com,FeieProxy"
+ - "DOMAIN-SUFFIX,blogcdn.com,FeieProxy"
+ - "DOMAIN-SUFFIX,blogger.com,FeieProxy"
+ - "DOMAIN-SUFFIX,blogsmithmedia.com,FeieProxy"
+ - "DOMAIN-SUFFIX,blogspot.com,FeieProxy"
+ - "DOMAIN-SUFFIX,blogspot.hk,FeieProxy"
+ - "DOMAIN-SUFFIX,bloomberg.com,FeieProxy"
+ - "DOMAIN-SUFFIX,box.com,FeieProxy"
+ - "DOMAIN-SUFFIX,box.net,FeieProxy"
+ - "DOMAIN-SUFFIX,cachefly.net,FeieProxy"
+ - "DOMAIN-SUFFIX,chromium.org,FeieProxy"
+ - "DOMAIN-SUFFIX,cl.ly,FeieProxy"
+ - "DOMAIN-SUFFIX,cloudflare.com,FeieProxy"
+ - "DOMAIN-SUFFIX,cloudfront.net,FeieProxy"
+ - "DOMAIN-SUFFIX,cloudmagic.com,FeieProxy"
+ - "DOMAIN-SUFFIX,cmail19.com,FeieProxy"
+ - "DOMAIN-SUFFIX,cnet.com,FeieProxy"
+ - "DOMAIN-SUFFIX,cocoapods.org,FeieProxy"
+ - "DOMAIN-SUFFIX,comodoca.com,FeieProxy"
+ - "DOMAIN-SUFFIX,crashlytics.com,FeieProxy"
+ - "DOMAIN-SUFFIX,culturedcode.com,FeieProxy"
+ - "DOMAIN-SUFFIX,d.pr,FeieProxy"
+ - "DOMAIN-SUFFIX,danilo.to,FeieProxy"
+ - "DOMAIN-SUFFIX,dayone.me,FeieProxy"
+ - "DOMAIN-SUFFIX,db.tt,FeieProxy"
+ - "DOMAIN-SUFFIX,deskconnect.com,FeieProxy"
+ - "DOMAIN-SUFFIX,disq.us,FeieProxy"
+ - "DOMAIN-SUFFIX,disqus.com,FeieProxy"
+ - "DOMAIN-SUFFIX,disquscdn.com,FeieProxy"
+ - "DOMAIN-SUFFIX,dnsimple.com,FeieProxy"
+ - "DOMAIN-SUFFIX,docker.com,FeieProxy"
+ - "DOMAIN-SUFFIX,dribbble.com,FeieProxy"
+ - "DOMAIN-SUFFIX,droplr.com,FeieProxy"
+ - "DOMAIN-SUFFIX,duckduckgo.com,FeieProxy"
+ - "DOMAIN-SUFFIX,dueapp.com,FeieProxy"
+ - "DOMAIN-SUFFIX,dytt8.net,FeieProxy"
+ - "DOMAIN-SUFFIX,edgecastcdn.net,FeieProxy"
+ - "DOMAIN-SUFFIX,edgekey.net,FeieProxy"
+ - "DOMAIN-SUFFIX,edgesuite.net,FeieProxy"
+ - "DOMAIN-SUFFIX,engadget.com,FeieProxy"
+ - "DOMAIN-SUFFIX,entrust.net,FeieProxy"
+ - "DOMAIN-SUFFIX,eurekavpt.com,FeieProxy"
+ - "DOMAIN-SUFFIX,evernote.com,FeieProxy"
+ - "DOMAIN-SUFFIX,fabric.io,FeieProxy"
+ - "DOMAIN-SUFFIX,fast.com,FeieProxy"
+ - "DOMAIN-SUFFIX,fastly.net,FeieProxy"
+ - "DOMAIN-SUFFIX,fc2.com,FeieProxy"
+ - "DOMAIN-SUFFIX,feedburner.com,FeieProxy"
+ - "DOMAIN-SUFFIX,feedly.com,FeieProxy"
+ - "DOMAIN-SUFFIX,feedsportal.com,FeieProxy"
+ - "DOMAIN-SUFFIX,fiftythree.com,FeieProxy"
+ - "DOMAIN-SUFFIX,firebaseio.com,FeieProxy"
+ - "DOMAIN-SUFFIX,flexibits.com,FeieProxy"
+ - "DOMAIN-SUFFIX,flickr.com,FeieProxy"
+ - "DOMAIN-SUFFIX,flipboard.com,FeieProxy"
+ - "DOMAIN-SUFFIX,g.co,FeieProxy"
+ - "DOMAIN-SUFFIX,gabia.net,FeieProxy"
+ - "DOMAIN-SUFFIX,geni.us,FeieProxy"
+ - "DOMAIN-SUFFIX,gfx.ms,FeieProxy"
+ - "DOMAIN-SUFFIX,ggpht.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ghostnoteapp.com,FeieProxy"
+ - "DOMAIN-SUFFIX,git.io,FeieProxy"
+ - "DOMAIN-KEYWORD,github,FeieProxy"
+ - "DOMAIN-SUFFIX,globalsign.com,FeieProxy"
+ - "DOMAIN-SUFFIX,gmodules.com,FeieProxy"
+ - "DOMAIN-SUFFIX,godaddy.com,FeieProxy"
+ - "DOMAIN-SUFFIX,golang.org,FeieProxy"
+ - "DOMAIN-SUFFIX,gongm.in,FeieProxy"
+ - "DOMAIN-SUFFIX,goo.gl,FeieProxy"
+ - "DOMAIN-SUFFIX,goodreaders.com,FeieProxy"
+ - "DOMAIN-SUFFIX,goodreads.com,FeieProxy"
+ - "DOMAIN-SUFFIX,gravatar.com,FeieProxy"
+ - "DOMAIN-SUFFIX,gstatic.com,FeieProxy"
+ - "DOMAIN-SUFFIX,gvt0.com,FeieProxy"
+ - "DOMAIN-SUFFIX,hockeyapp.net,FeieProxy"
+ - "DOMAIN-SUFFIX,hotmail.com,FeieProxy"
+ - "DOMAIN-SUFFIX,icons8.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ifixit.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ift.tt,FeieProxy"
+ - "DOMAIN-SUFFIX,ifttt.com,FeieProxy"
+ - "DOMAIN-SUFFIX,iherb.com,FeieProxy"
+ - "DOMAIN-SUFFIX,imageshack.us,FeieProxy"
+ - "DOMAIN-SUFFIX,img.ly,FeieProxy"
+ - "DOMAIN-SUFFIX,imgur.com,FeieProxy"
+ - "DOMAIN-SUFFIX,imore.com,FeieProxy"
+ - "DOMAIN-SUFFIX,instapaper.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ipn.li,FeieProxy"
+ - "DOMAIN-SUFFIX,is.gd,FeieProxy"
+ - "DOMAIN-SUFFIX,issuu.com,FeieProxy"
+ - "DOMAIN-SUFFIX,itgonglun.com,FeieProxy"
+ - "DOMAIN-SUFFIX,itun.es,FeieProxy"
+ - "DOMAIN-SUFFIX,ixquick.com,FeieProxy"
+ - "DOMAIN-SUFFIX,j.mp,FeieProxy"
+ - "DOMAIN-SUFFIX,js.revsci.net,FeieProxy"
+ - "DOMAIN-SUFFIX,jshint.com,FeieProxy"
+ - "DOMAIN-SUFFIX,jtvnw.net,FeieProxy"
+ - "DOMAIN-SUFFIX,justgetflux.com,FeieProxy"
+ - "DOMAIN-SUFFIX,kat.cr,FeieProxy"
+ - "DOMAIN-SUFFIX,klip.me,FeieProxy"
+ - "DOMAIN-SUFFIX,libsyn.com,FeieProxy"
+ - "DOMAIN-SUFFIX,linkedin.com,FeieProxy"
+ - "DOMAIN-SUFFIX,line-apps.com,FeieProxy"
+ - "DOMAIN-SUFFIX,linode.com,FeieProxy"
+ - "DOMAIN-SUFFIX,lithium.com,FeieProxy"
+ - "DOMAIN-SUFFIX,littlehj.com,FeieProxy"
+ - "DOMAIN-SUFFIX,live.com,FeieProxy"
+ - "DOMAIN-SUFFIX,live.net,FeieProxy"
+ - "DOMAIN-SUFFIX,livefilestore.com,FeieProxy"
+ - "DOMAIN-SUFFIX,llnwd.net,FeieProxy"
+ - "DOMAIN-SUFFIX,macid.co,FeieProxy"
+ - "DOMAIN-SUFFIX,macromedia.com,FeieProxy"
+ - "DOMAIN-SUFFIX,macrumors.com,FeieProxy"
+ - "DOMAIN-SUFFIX,mashable.com,FeieProxy"
+ - "DOMAIN-SUFFIX,mathjax.org,FeieProxy"
+ - "DOMAIN-SUFFIX,medium.com,FeieProxy"
+ - "DOMAIN-SUFFIX,mega.co.nz,FeieProxy"
+ - "DOMAIN-SUFFIX,mega.nz,FeieProxy"
+ - "DOMAIN-SUFFIX,megaupload.com,FeieProxy"
+ - "DOMAIN-SUFFIX,microsofttranslator.com,FeieProxy"
+ - "DOMAIN-SUFFIX,mindnode.com,FeieProxy"
+ - "DOMAIN-SUFFIX,mobile01.com,FeieProxy"
+ - "DOMAIN-SUFFIX,modmyi.com,FeieProxy"
+ - "DOMAIN-SUFFIX,msedge.net,FeieProxy"
+ - "DOMAIN-SUFFIX,myfontastic.com,FeieProxy"
+ - "DOMAIN-SUFFIX,name.com,FeieProxy"
+ - "DOMAIN-SUFFIX,nextmedia.com,FeieProxy"
+ - "DOMAIN-SUFFIX,nsstatic.net,FeieProxy"
+ - "DOMAIN-SUFFIX,nssurge.com,FeieProxy"
+ - "DOMAIN-SUFFIX,nyt.com,FeieProxy"
+ - "DOMAIN-SUFFIX,nytimes.com,FeieProxy"
+ - "DOMAIN-SUFFIX,omnigroup.com,FeieProxy"
+ - "DOMAIN-SUFFIX,onedrive.com,FeieProxy"
+ - "DOMAIN-SUFFIX,onenote.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ooyala.com,FeieProxy"
+ - "DOMAIN-SUFFIX,openvpn.net,FeieProxy"
+ - "DOMAIN-SUFFIX,openwrt.org,FeieProxy"
+ - "DOMAIN-SUFFIX,orkut.com,FeieProxy"
+ - "DOMAIN-SUFFIX,osxdaily.com,FeieProxy"
+ - "DOMAIN-SUFFIX,outlook.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ow.ly,FeieProxy"
+ - "DOMAIN-SUFFIX,paddleapi.com,FeieProxy"
+ - "DOMAIN-SUFFIX,parallels.com,FeieProxy"
+ - "DOMAIN-SUFFIX,parse.com,FeieProxy"
+ - "DOMAIN-SUFFIX,pdfexpert.com,FeieProxy"
+ - "DOMAIN-SUFFIX,periscope.tv,FeieProxy"
+ - "DOMAIN-SUFFIX,pinboard.in,FeieProxy"
+ - "DOMAIN-SUFFIX,pinterest.com,FeieProxy"
+ - "DOMAIN-SUFFIX,pixelmator.com,FeieProxy"
+ - "DOMAIN-SUFFIX,pixiv.net,FeieProxy"
+ - "DOMAIN-SUFFIX,playpcesor.com,FeieProxy"
+ - "DOMAIN-SUFFIX,playstation.com,FeieProxy"
+ - "DOMAIN-SUFFIX,playstation.com.hk,FeieProxy"
+ - "DOMAIN-SUFFIX,playstation.net,FeieProxy"
+ - "DOMAIN-SUFFIX,playstationnetwork.com,FeieProxy"
+ - "DOMAIN-SUFFIX,pushwoosh.com,FeieProxy"
+ - "DOMAIN-SUFFIX,rime.im,FeieProxy"
+ - "DOMAIN-SUFFIX,servebom.com,FeieProxy"
+ - "DOMAIN-SUFFIX,sfx.ms,FeieProxy"
+ - "DOMAIN-SUFFIX,shadowsocks.org,FeieProxy"
+ - "DOMAIN-SUFFIX,sharethis.com,FeieProxy"
+ - "DOMAIN-SUFFIX,shazam.com,FeieProxy"
+ - "DOMAIN-SUFFIX,skype.com,FeieProxy"
+ - "DOMAIN-SUFFIX,smartdnsFeieProxy.com,FeieProxy"
+ - "DOMAIN-SUFFIX,smartmailcloud.com,FeieProxy"
+ - "DOMAIN-SUFFIX,sndcdn.com,FeieProxy"
+ - "DOMAIN-SUFFIX,sony.com,FeieProxy"
+ - "DOMAIN-SUFFIX,soundcloud.com,FeieProxy"
+ - "DOMAIN-SUFFIX,sourceforge.net,FeieProxy"
+ - "DOMAIN-SUFFIX,spotify.com,FeieProxy"
+ - "DOMAIN-SUFFIX,squarespace.com,FeieProxy"
+ - "DOMAIN-SUFFIX,sstatic.net,FeieProxy"
+ - "DOMAIN-SUFFIX,st.luluku.pw,FeieProxy"
+ - "DOMAIN-SUFFIX,stackoverflow.com,FeieProxy"
+ - "DOMAIN-SUFFIX,startpage.com,FeieProxy"
+ - "DOMAIN-SUFFIX,staticflickr.com,FeieProxy"
+ - "DOMAIN-SUFFIX,steamcommunity.com,FeieProxy"
+ - "DOMAIN-SUFFIX,symauth.com,FeieProxy"
+ - "DOMAIN-SUFFIX,symcb.com,FeieProxy"
+ - "DOMAIN-SUFFIX,symcd.com,FeieProxy"
+ - "DOMAIN-SUFFIX,tapbots.com,FeieProxy"
+ - "DOMAIN-SUFFIX,tapbots.net,FeieProxy"
+ - "DOMAIN-SUFFIX,tdesktop.com,FeieProxy"
+ - "DOMAIN-SUFFIX,techcrunch.com,FeieProxy"
+ - "DOMAIN-SUFFIX,techsmith.com,FeieProxy"
+ - "DOMAIN-SUFFIX,thepiratebay.org,FeieProxy"
+ - "DOMAIN-SUFFIX,theverge.com,FeieProxy"
+ - "DOMAIN-SUFFIX,time.com,FeieProxy"
+ - "DOMAIN-SUFFIX,timeinc.net,FeieProxy"
+ - "DOMAIN-SUFFIX,tiny.cc,FeieProxy"
+ - "DOMAIN-SUFFIX,tinypic.com,FeieProxy"
+ - "DOMAIN-SUFFIX,tmblr.co,FeieProxy"
+ - "DOMAIN-SUFFIX,todoist.com,FeieProxy"
+ - "DOMAIN-SUFFIX,trello.com,FeieProxy"
+ - "DOMAIN-SUFFIX,trustasiassl.com,FeieProxy"
+ - "DOMAIN-SUFFIX,tumblr.co,FeieProxy"
+ - "DOMAIN-SUFFIX,tumblr.com,FeieProxy"
+ - "DOMAIN-SUFFIX,tweetdeck.com,FeieProxy"
+ - "DOMAIN-SUFFIX,tweetmarker.net,FeieProxy"
+ - "DOMAIN-SUFFIX,twitch.tv,FeieProxy"
+ - "DOMAIN-SUFFIX,txmblr.com,FeieProxy"
+ - "DOMAIN-SUFFIX,typekit.net,FeieProxy"
+ - "DOMAIN-SUFFIX,ubertags.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ublock.org,FeieProxy"
+ - "DOMAIN-SUFFIX,ubnt.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ulyssesapp.com,FeieProxy"
+ - "DOMAIN-SUFFIX,urchin.com,FeieProxy"
+ - "DOMAIN-SUFFIX,usertrust.com,FeieProxy"
+ - "DOMAIN-SUFFIX,v.gd,FeieProxy"
+ - "DOMAIN-SUFFIX,v2ex.com,FeieProxy"
+ - "DOMAIN-SUFFIX,vimeo.com,FeieProxy"
+ - "DOMAIN-SUFFIX,vimeocdn.com,FeieProxy"
+ - "DOMAIN-SUFFIX,vine.co,FeieProxy"
+ - "DOMAIN-SUFFIX,vivaldi.com,FeieProxy"
+ - "DOMAIN-SUFFIX,vox-cdn.com,FeieProxy"
+ - "DOMAIN-SUFFIX,vsco.co,FeieProxy"
+ - "DOMAIN-SUFFIX,vultr.com,FeieProxy"
+ - "DOMAIN-SUFFIX,w.org,FeieProxy"
+ - "DOMAIN-SUFFIX,w3schools.com,FeieProxy"
+ - "DOMAIN-SUFFIX,webtype.com,FeieProxy"
+ - "DOMAIN-SUFFIX,wikiwand.com,FeieProxy"
+ - "DOMAIN-SUFFIX,wikileaks.org,FeieProxy"
+ - "DOMAIN-SUFFIX,wikimedia.org,FeieProxy"
+ - "DOMAIN-SUFFIX,wikipedia.com,FeieProxy"
+ - "DOMAIN-SUFFIX,wikipedia.org,FeieProxy"
+ - "DOMAIN-SUFFIX,windows.com,FeieProxy"
+ - "DOMAIN-SUFFIX,windows.net,FeieProxy"
+ - "DOMAIN-SUFFIX,wire.com,FeieProxy"
+ - "DOMAIN-SUFFIX,wordpress.com,FeieProxy"
+ - "DOMAIN-SUFFIX,workflowy.com,FeieProxy"
+ - "DOMAIN-SUFFIX,wp.com,FeieProxy"
+ - "DOMAIN-SUFFIX,wsj.com,FeieProxy"
+ - "DOMAIN-SUFFIX,wsj.net,FeieProxy"
+ - "DOMAIN-SUFFIX,xda-developers.com,FeieProxy"
+ - "DOMAIN-SUFFIX,xeeno.com,FeieProxy"
+ - "DOMAIN-SUFFIX,xiti.com,FeieProxy"
+ - "DOMAIN-SUFFIX,yahoo.com,FeieProxy"
+ - "DOMAIN-SUFFIX,yimg.com,FeieProxy"
+ - "DOMAIN-SUFFIX,ying.com,FeieProxy"
+ - "DOMAIN-SUFFIX,yoyo.org,FeieProxy"
+ - "DOMAIN-SUFFIX,ytimg.com,FeieProxy"
+ - "DOMAIN-SUFFIX,telegra.ph,FeieProxy"
+ - "DOMAIN-SUFFIX,telegram.org,FeieProxy"
+ - "IP-CIDR,91.108.4.0/22,FeieProxy,no-resolve"
+ - "IP-CIDR,91.108.8.0/21,FeieProxy,no-resolve"
+ - "IP-CIDR,91.108.16.0/22,FeieProxy,no-resolve"
+ - "IP-CIDR,91.108.56.0/22,FeieProxy,no-resolve"
+ - "IP-CIDR,149.154.160.0/20,FeieProxy,no-resolve"
+ - "IP-CIDR6,2001:67c:4e8::/48,FeieProxy,no-resolve"
+ - "IP-CIDR6,2001:b28:f23d::/48,FeieProxy,no-resolve"
+ - "IP-CIDR6,2001:b28:f23f::/48,FeieProxy,no-resolve"
+ - "IP-CIDR,120.232.181.162/32,FeieProxy,no-resolve"
+ - "IP-CIDR,120.241.147.226/32,FeieProxy,no-resolve"
+ - "IP-CIDR,120.253.253.226/32,FeieProxy,no-resolve"
+ - "IP-CIDR,120.253.255.162/32,FeieProxy,no-resolve"
+ - "IP-CIDR,120.253.255.34/32,FeieProxy,no-resolve"
+ - "IP-CIDR,120.253.255.98/32,FeieProxy,no-resolve"
+ - "IP-CIDR,180.163.150.162/32,FeieProxy,no-resolve"
+ - "IP-CIDR,180.163.150.34/32,FeieProxy,no-resolve"
+ - "IP-CIDR,180.163.151.162/32,FeieProxy,no-resolve"
+ - "IP-CIDR,180.163.151.34/32,FeieProxy,no-resolve"
+ - "IP-CIDR,203.208.39.0/24,FeieProxy,no-resolve"
+ - "IP-CIDR,203.208.40.0/24,FeieProxy,no-resolve"
+ - "IP-CIDR,203.208.41.0/24,FeieProxy,no-resolve"
+ - "IP-CIDR,203.208.43.0/24,FeieProxy,no-resolve"
+ - "IP-CIDR,203.208.50.0/24,FeieProxy,no-resolve"
+ - "IP-CIDR,220.181.174.162/32,FeieProxy,no-resolve"
+ - "IP-CIDR,220.181.174.226/32,FeieProxy,no-resolve"
+ - "IP-CIDR,220.181.174.34/32,FeieProxy,no-resolve"
+ - "DOMAIN,injections.adguard.org,DIRECT"
+ - "DOMAIN,local.adguard.org,DIRECT"
+ - "DOMAIN-SUFFIX,local,DIRECT"
+ - "IP-CIDR,127.0.0.0/8,DIRECT"
+ - "IP-CIDR,172.16.0.0/12,DIRECT"
+ - "IP-CIDR,192.168.0.0/16,DIRECT"
+ - "IP-CIDR,10.0.0.0/8,DIRECT"
+ - "IP-CIDR,17.0.0.0/8,DIRECT"
+ - "IP-CIDR,100.64.0.0/10,DIRECT"
+ - "IP-CIDR,224.0.0.0/4,DIRECT"
+ - "IP-CIDR6,fe80::/10,DIRECT"
+ - "DOMAIN-SUFFIX,cn,DIRECT"
+ - "DOMAIN-KEYWORD,-cn,DIRECT"
+ - "GEOIP,CN,DIRECT"
+ - "MATCH,FeieProxy"
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
index a2dc41e..a3b8f3d 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,26 +1,887 @@
@import "tailwindcss";
:root {
- --background: #ffffff;
- --foreground: #171717;
+ --background: #efe5d4;
+ --foreground: #18222c;
+ --panel: rgba(255, 251, 245, 0.76);
+ --panel-strong: rgba(255, 251, 245, 0.92);
+ --panel-tint: rgba(255, 244, 230, 0.62);
+ --border: rgba(24, 34, 44, 0.12);
+ --border-strong: rgba(24, 34, 44, 0.22);
+ --accent: #c96233;
+ --accent-deep: #7a3116;
+ --accent-soft: rgba(201, 98, 51, 0.14);
+ --ink-soft: rgba(24, 34, 44, 0.08);
+ --success: #1f7a4f;
+ --danger: #af2d21;
+ --warn: #8e5c10;
+ --muted: #5e6670;
+ --shadow: 0 26px 90px rgba(51, 35, 18, 0.12);
+ --shadow-soft: 0 10px 30px rgba(51, 35, 18, 0.06);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+ --font-sans: "Avenir Next", "PingFang SC", "Segoe UI", sans-serif;
+ --font-mono: "IBM Plex Mono", "SFMono-Regular", monospace;
}
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ min-height: 100%;
}
body {
+ margin: 0;
background: var(--background);
color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+ font-family: var(--font-sans);
+ background-image:
+ radial-gradient(circle at top left, rgba(201, 98, 51, 0.2), transparent 24rem),
+ radial-gradient(circle at 88% 10%, rgba(39, 90, 114, 0.1), transparent 20rem),
+ radial-gradient(circle at bottom right, rgba(24, 34, 44, 0.08), transparent 28rem),
+ linear-gradient(180deg, #f8f0e5 0%, #ede2d3 100%);
+}
+
+body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ background-image:
+ linear-gradient(rgba(255, 255, 255, 0.18) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.18) 1px, transparent 1px);
+ background-size: 44px 44px;
+ mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.65), transparent 80%);
+}
+
+::selection {
+ background: rgba(201, 98, 51, 0.18);
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+input,
+textarea,
+button {
+ font: inherit;
+}
+
+input,
+textarea {
+ width: 100%;
+ border: 1px solid var(--border-strong);
+ border-radius: 18px;
+ padding: 0.85rem 1rem;
+ background: rgba(255, 255, 255, 0.84);
+ color: var(--foreground);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
+ transition: border-color 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
+}
+
+input:focus,
+textarea:focus {
+ outline: none;
+ border-color: rgba(201, 98, 51, 0.55);
+ box-shadow:
+ 0 0 0 4px rgba(201, 98, 51, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.72);
+ background: rgba(255, 255, 255, 0.96);
+}
+
+textarea {
+ resize: vertical;
+}
+
+button {
+ cursor: pointer;
+ border: 0;
+ transition:
+ transform 140ms ease,
+ opacity 140ms ease,
+ background-color 140ms ease,
+ box-shadow 140ms ease;
+}
+
+button:hover {
+ transform: translateY(-1px);
+}
+
+.login-shell {
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ padding: 2.5rem;
+}
+
+.login-shell--immersive {
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+ background:
+ radial-gradient(circle at 20% 18%, rgba(233, 128, 64, 0.3), transparent 18rem),
+ radial-gradient(circle at 78% 16%, rgba(38, 89, 111, 0.22), transparent 24rem),
+ radial-gradient(circle at 50% 100%, rgba(24, 34, 44, 0.14), transparent 24rem),
+ linear-gradient(135deg, #f7ecdd 0%, #efe0cb 44%, #e7d7c6 100%);
+}
+
+.login-shell--immersive::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background-image:
+ linear-gradient(rgba(255, 255, 255, 0.2) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.2) 1px, transparent 1px);
+ background-size: 72px 72px;
+ mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.85), transparent 88%);
+ opacity: 0.55;
+}
+
+.login-backdrop {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
+
+.login-orb {
+ position: absolute;
+ border-radius: 999px;
+ filter: blur(18px);
+ opacity: 0.8;
+ animation: login-float 14s ease-in-out infinite;
+}
+
+.login-orb--one {
+ top: 10%;
+ left: max(2rem, 8vw);
+ width: 18rem;
+ height: 18rem;
+ background: radial-gradient(circle, rgba(201, 98, 51, 0.36), rgba(201, 98, 51, 0.02));
+}
+
+.login-orb--two {
+ right: max(2rem, 10vw);
+ bottom: 12%;
+ width: 22rem;
+ height: 22rem;
+ background: radial-gradient(circle, rgba(33, 93, 118, 0.24), rgba(33, 93, 118, 0.02));
+ animation-duration: 18s;
+ animation-delay: -6s;
+}
+
+.login-orb--three {
+ top: 22%;
+ right: 18%;
+ width: 10rem;
+ height: 10rem;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.04));
+ animation-duration: 12s;
+ animation-delay: -3s;
+}
+
+.login-stage {
+ position: relative;
+ z-index: 1;
+ width: min(34rem, 100%);
+}
+
+.login-layout {
+ width: min(76rem, 100%);
+ display: grid;
+ grid-template-columns: minmax(0, 1.15fr) minmax(22rem, 28rem);
+ gap: 1.25rem;
+}
+
+.login-card,
+.panel-card,
+.metric-card,
+.sidebar {
+ position: relative;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ background: var(--panel);
+ backdrop-filter: blur(18px);
+ box-shadow: var(--shadow);
+}
+
+.login-card::before,
+.panel-card::before,
+.metric-card::before,
+.sidebar::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.35), transparent 38%);
+}
+
+.login-card {
+ width: 100%;
+ border-radius: 30px;
+ padding: 2rem 2.1rem;
+}
+
+.login-card--hero {
+ display: grid;
+ gap: 1.3rem;
+ align-content: space-between;
+ min-height: 34rem;
+}
+
+.login-card--form {
+ display: grid;
+ align-content: start;
+}
+
+.login-card--brand {
+ gap: 1.1rem;
+ padding: 2.2rem;
+ border-color: rgba(255, 255, 255, 0.34);
+ background:
+ linear-gradient(160deg, rgba(255, 252, 247, 0.84), rgba(255, 248, 241, 0.66)),
+ rgba(255, 251, 245, 0.72);
+ box-shadow:
+ 0 34px 90px rgba(60, 36, 18, 0.16),
+ inset 0 1px 0 rgba(255, 255, 255, 0.66);
+}
+
+.login-card--brand h1 {
+ font-size: clamp(2.1rem, 5vw, 3.2rem);
+ letter-spacing: -0.06em;
+}
+
+.login-form {
+ margin-top: 0.25rem;
+}
+
+.login-points {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.9rem;
+}
+
+.login-point {
+ display: grid;
+ gap: 0.35rem;
+ min-height: 6.6rem;
+ padding: 1rem;
+ border-radius: 22px;
+ border: 1px solid rgba(24, 34, 44, 0.1);
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.56), rgba(255, 255, 255, 0.26)),
+ rgba(255, 255, 255, 0.36);
+}
+
+.login-point strong {
+ font-size: 1rem;
+ line-height: 1.35;
+}
+
+.login-note {
+ margin: 0;
+ color: var(--muted);
+ font-size: 0.92rem;
+}
+
+.wide-card {
+ width: min(56rem, 100%);
+}
+
+.admin-shell {
+ min-height: 100vh;
+ display: grid;
+ grid-template-columns: 19rem minmax(0, 1fr);
+}
+
+.sidebar {
+ position: sticky;
+ top: 0;
+ min-height: 100vh;
+ padding: 1.5rem 1.25rem;
+ border-right: 1px solid rgba(24, 34, 44, 0.08);
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.content-shell {
+ padding: 1.35rem;
+}
+
+.content-frame {
+ min-height: calc(100vh - 2.7rem);
+ border: 1px solid rgba(24, 34, 44, 0.08);
+ border-radius: 32px;
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.34), transparent 20%),
+ rgba(255, 251, 245, 0.26);
+ box-shadow: var(--shadow-soft);
+ padding: 1.4rem;
+}
+
+.brand-block h1,
+.page-header h2,
+.section-title h3,
+.login-card h1,
+.login-card h2 {
+ margin: 0.2rem 0 0;
+ line-height: 1.1;
+}
+
+.eyebrow {
+ font-family: var(--font-mono);
+ font-size: 0.77rem;
+ letter-spacing: 0.18em;
+ color: var(--accent-deep);
+}
+
+.page-lede {
+ margin: 0;
+ color: var(--muted);
+ line-height: 1.65;
+ max-width: 46rem;
+}
+
+.muted {
+ color: var(--muted);
+}
+
+.mono {
+ font-family: var(--font-mono);
+}
+
+.sidebar-note {
+ border: 1px solid var(--border);
+ border-radius: 20px;
+ padding: 0.95rem 1rem;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.38), rgba(255, 255, 255, 0.18));
+}
+
+.sidebar-note strong {
+ display: block;
+ margin-bottom: 0.35rem;
+}
+
+.nav-list,
+.sidebar-stack,
+.page-stack,
+.stack-lg,
+.stack-md,
+.stack-tight,
+.hero-copy,
+.hero-aside {
+ display: grid;
+}
+
+.nav-list {
+ gap: 0.5rem;
+}
+
+.sidebar-stack {
+ gap: 0.6rem;
+}
+
+.sidebar-chip {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 0.8rem 0.95rem;
+ background: rgba(255, 255, 255, 0.38);
+}
+
+.nav-link,
+.ghost-button,
+.primary-button,
+.danger-button {
+ position: relative;
+ border-radius: 16px;
+ padding: 0.9rem 1rem;
+}
+
+.nav-link,
+.ghost-button {
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.42);
+}
+
+.nav-link:hover,
+.ghost-button:hover {
+ background: rgba(255, 255, 255, 0.72);
+ box-shadow: var(--shadow-soft);
+}
+
+.nav-link {
+ display: grid;
+ gap: 0.18rem;
+}
+
+.nav-link__label {
+ font-weight: 700;
+}
+
+.nav-link__meta {
+ color: var(--muted);
+ font-size: 0.83rem;
+}
+
+.nav-link.is-active {
+ background: linear-gradient(135deg, rgba(201, 98, 51, 0.18), rgba(255, 255, 255, 0.62));
+ border-color: rgba(201, 98, 51, 0.28);
+}
+
+.nav-link.is-active::after {
+ content: "";
+ position: absolute;
+ right: 0.8rem;
+ top: 50%;
+ width: 0.48rem;
+ height: 0.48rem;
+ margin-top: -0.24rem;
+ border-radius: 999px;
+ background: var(--accent);
+}
+
+.primary-button {
+ background: var(--accent);
+ color: #fff8f2;
+ box-shadow: 0 12px 24px rgba(201, 98, 51, 0.24);
+}
+
+.danger-button {
+ background: rgba(175, 45, 33, 0.14);
+ color: var(--danger);
+}
+
+.full-width {
+ width: 100%;
+}
+
+.page-stack {
+ gap: 1.25rem;
+}
+
+.page-stack > * {
+ animation: rise-in 360ms ease both;
+}
+
+.stack-lg {
+ gap: 1.25rem;
+}
+
+.stack-md {
+ gap: 0.9rem;
+}
+
+.stack-tight {
+ gap: 0.5rem;
+}
+
+.page-header,
+.section-title,
+.list-row,
+.action-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.section-title--top {
+ align-items: start;
+}
+
+.metrics-grid,
+.panel-grid,
+.form-grid,
+.summary-grid,
+.feature-grid,
+.checkbox-grid {
+ display: grid;
+ gap: 1rem;
+}
+
+.metrics-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.panel-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.form-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.summary-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.feature-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.checkbox-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.8rem;
+}
+
+.metric-card,
+.panel-card {
+ border-radius: 28px;
+ padding: 1.2rem 1.25rem;
+}
+
+.metric-card {
+ display: grid;
+ gap: 0.52rem;
+ min-height: 9rem;
+}
+
+.metric-card strong {
+ font-size: 1.9rem;
+ letter-spacing: -0.04em;
+}
+
+.metric-card small {
+ color: var(--muted);
+}
+
+.metric-card--warm {
+ background: linear-gradient(180deg, rgba(247, 225, 203, 0.92), rgba(255, 250, 242, 0.72));
+}
+
+.metric-card--ink {
+ background: linear-gradient(180deg, rgba(223, 231, 236, 0.9), rgba(255, 250, 242, 0.72));
+}
+
+.metric-card--sage {
+ background: linear-gradient(180deg, rgba(224, 236, 228, 0.9), rgba(255, 250, 242, 0.72));
+}
+
+.metric-card--paper {
+ background: linear-gradient(180deg, rgba(255, 252, 247, 0.95), rgba(255, 250, 242, 0.72));
+}
+
+.hero-panel {
+ display: grid;
+ grid-template-columns: minmax(0, 1.3fr) minmax(18rem, 0.9fr);
+ gap: 1rem;
+ padding: 1.35rem;
+ border: 1px solid var(--border);
+ border-radius: 30px;
+ background:
+ radial-gradient(circle at top right, rgba(201, 98, 51, 0.12), transparent 16rem),
+ linear-gradient(180deg, rgba(255, 255, 255, 0.46), rgba(255, 255, 255, 0.18));
+ box-shadow: var(--shadow-soft);
+}
+
+.hero-panel--compact {
+ grid-template-columns: minmax(0, 1.4fr) minmax(16rem, 0.75fr);
+}
+
+.hero-copy,
+.hero-aside {
+ align-content: start;
+ gap: 0.85rem;
+}
+
+.hero-aside {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.badge-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.55rem;
+}
+
+.badge-row--end {
+ justify-content: flex-end;
+}
+
+.status-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ border-radius: 999px;
+ padding: 0.4rem 0.75rem;
+ font-size: 0.82rem;
+ font-weight: 700;
+ border: 1px solid transparent;
+}
+
+.status-pill--ok {
+ background: rgba(31, 122, 79, 0.12);
+ color: var(--success);
+ border-color: rgba(31, 122, 79, 0.18);
+}
+
+.status-pill--danger {
+ background: rgba(175, 45, 33, 0.12);
+ color: var(--danger);
+ border-color: rgba(175, 45, 33, 0.16);
+}
+
+.status-pill--off {
+ background: rgba(24, 34, 44, 0.08);
+ color: var(--foreground);
+ border-color: rgba(24, 34, 44, 0.12);
+}
+
+.status-pill--neutral {
+ background: rgba(39, 90, 114, 0.1);
+ color: #26596f;
+ border-color: rgba(39, 90, 114, 0.14);
+}
+
+.status-pill--warm {
+ background: rgba(201, 98, 51, 0.1);
+ color: var(--accent-deep);
+ border-color: rgba(201, 98, 51, 0.14);
+}
+
+.status-pill--ink {
+ background: rgba(24, 34, 44, 0.1);
+ color: var(--foreground);
+ border-color: rgba(24, 34, 44, 0.14);
+}
+
+.status-pill--sage {
+ background: rgba(78, 113, 91, 0.1);
+ color: #456756;
+ border-color: rgba(78, 113, 91, 0.14);
+}
+
+.field {
+ display: grid;
+ gap: 0.4rem;
+}
+
+.field span {
+ font-size: 0.92rem;
+ color: var(--muted);
+}
+
+.form-grid.compact {
+ margin-top: 1rem;
+}
+
+.full-span {
+ grid-column: 1 / -1;
+}
+
+.checkbox {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.checkbox input {
+ width: auto;
+}
+
+.align-right {
+ text-align: right;
+}
+
+.inline-form {
+ display: flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+}
+
+.inline-button {
+ display: inline-flex;
+}
+
+.metric-inline {
+ display: grid;
+ gap: 0.3rem;
+ padding: 0.9rem 1rem;
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.52);
+ border: 1px solid var(--border);
+}
+
+.metric-inline span,
+.stat-tile span {
+ color: var(--muted);
+ font-size: 0.88rem;
+}
+
+.metric-inline strong,
+.stat-tile strong {
+ font-size: 1.02rem;
+}
+
+.stat-tile {
+ display: grid;
+ gap: 0.3rem;
+ border-radius: 22px;
+ border: 1px solid var(--border);
+ padding: 1rem;
+ background: rgba(255, 255, 255, 0.48);
+ min-height: 7rem;
+}
+
+.feature-card {
+ display: grid;
+ gap: 0.45rem;
+ padding: 1rem;
+ border-radius: 22px;
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.44);
+}
+
+.feature-kicker {
+ font-family: var(--font-mono);
+ color: var(--accent-deep);
+ font-size: 0.76rem;
+ letter-spacing: 0.16em;
+}
+
+.break-all {
+ word-break: break-all;
+}
+
+.instruction-block {
+ margin-top: 1rem;
+ padding: 1rem;
+ border: 1px solid var(--border);
+ border-radius: 20px;
+ background: rgba(255, 255, 255, 0.5);
+}
+
+.instruction-block.no-margin {
+ margin-top: 0;
+}
+
+.instruction-block pre {
+ overflow: auto;
+ margin: 0.75rem 0;
+ padding: 1rem 1.05rem;
+ border-radius: 18px;
+ background: #1a1f25;
+ color: #f7eadf;
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+.list-row--card {
+ padding: 0.9rem 1rem;
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.38);
+}
+
+.entity-card {
+ display: grid;
+ gap: 1rem;
+}
+
+.error-banner,
+.error-text {
+ color: var(--danger);
+ font-weight: 600;
+}
+
+@keyframes rise-in {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes login-float {
+ 0%,
+ 100% {
+ transform: translate3d(0, 0, 0) scale(1);
+ }
+ 50% {
+ transform: translate3d(0, -18px, 0) scale(1.04);
+ }
+}
+
+@media (max-width: 960px) {
+ .admin-shell,
+ .login-layout,
+ .hero-panel,
+ .hero-panel--compact,
+ .hero-aside,
+ .metrics-grid,
+ .panel-grid,
+ .form-grid,
+ .checkbox-grid,
+ .summary-grid,
+ .feature-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ position: static;
+ min-height: auto;
+ }
+
+ .page-header,
+ .section-title,
+ .list-row,
+ .action-row {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .align-right {
+ text-align: left;
+ }
+
+ .badge-row--end {
+ justify-content: flex-start;
+ }
+
+ .content-frame {
+ padding: 1rem;
+ border-radius: 24px;
+ }
+
+ .login-points {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 640px) {
+ .login-shell {
+ padding: 1.15rem;
+ }
+
+ .login-card--brand {
+ padding: 1.4rem;
+ border-radius: 24px;
+ }
+
+ .login-orb--one {
+ width: 13rem;
+ height: 13rem;
+ }
+
+ .login-orb--two {
+ width: 15rem;
+ height: 15rem;
+ }
}
diff --git a/app/layout.tsx b/app/layout.tsx
index 976eb90..b2ab713 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,20 +1,9 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
-
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "HY2 Panel",
+ description: "Self-hosted Hysteria 2 panel with auth, traffic sync, and node management",
};
export default function RootLayout({
@@ -23,11 +12,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
- {children}
+
+ {children}
);
}
diff --git a/app/login/actions.ts b/app/login/actions.ts
new file mode 100644
index 0000000..5e22ca7
--- /dev/null
+++ b/app/login/actions.ts
@@ -0,0 +1,34 @@
+"use server";
+
+import { redirect } from "next/navigation";
+
+import { getAdminByUsername, getUserByUsername } from "@/lib/store";
+import { verifyPassword } from "@/lib/password";
+import { createAdminSession, createUserSession } from "@/lib/session";
+
+export async function loginAction(formData: FormData) {
+ const username = String(formData.get("username") ?? "").trim();
+ const password = String(formData.get("password") ?? "");
+
+ const admin = await getAdminByUsername(username);
+ if (admin) {
+ const ok = await verifyPassword(password, admin.password_hash);
+ if (ok) {
+ await createAdminSession(admin.id);
+ redirect("/admin/dashboard");
+ }
+ }
+
+ const user = await getUserByUsername(username);
+ if (!user) {
+ redirect("/login?error=1");
+ }
+
+ const ok = await verifyPassword(password, user.password_hash);
+ if (!ok || !user.enabled) {
+ redirect("/login?error=1");
+ }
+
+ await createUserSession(user.id);
+ redirect(user.is_admin ? "/admin/dashboard" : "/me");
+}
diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 0000000..4692c09
--- /dev/null
+++ b/app/login/page.tsx
@@ -0,0 +1,70 @@
+import type { Metadata } from "next";
+import { redirect } from "next/navigation";
+
+import { getAdminSession, getUserSession } from "@/lib/session";
+
+import { loginAction } from "./actions";
+
+export const metadata: Metadata = {
+ title: "鹅梯 FeieProxy",
+};
+
+export default async function LoginPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ error?: string }>;
+}) {
+ const [adminSession, userSession] = await Promise.all([
+ getAdminSession(),
+ getUserSession(),
+ ]);
+ if (adminSession) {
+ redirect("/admin/dashboard");
+ }
+ if (userSession) {
+ redirect("/me");
+ }
+
+ const params = await searchParams;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ FEIEPROXY ACCESS
+
鹅梯 FeieProxy
+
+
家宽节点
+
+
+ 统一管理多节点鉴权、流量同步与 Clash 订阅。管理员进入控制台,普通用户进入自助页查看剩余流量并生成订阅链接。
+
+
+ 管理员与普通用户共用入口,系统会自动跳转到对应面板。
+
+
+
+ );
+}
diff --git a/app/logout/route.ts b/app/logout/route.ts
new file mode 100644
index 0000000..10c8101
--- /dev/null
+++ b/app/logout/route.ts
@@ -0,0 +1,8 @@
+import { NextResponse } from "next/server";
+
+import { destroyAdminSession, destroyUserSession } from "@/lib/session";
+
+export async function POST(request: Request) {
+ await Promise.all([destroyAdminSession(), destroyUserSession()]);
+ return NextResponse.redirect(new URL("/login", request.url));
+}
diff --git a/app/me/page.tsx b/app/me/page.tsx
new file mode 100644
index 0000000..91eb6f7
--- /dev/null
+++ b/app/me/page.tsx
@@ -0,0 +1,75 @@
+import { redirect } from "next/navigation";
+
+import { getRequestOrigin } from "@/lib/request-origin";
+import { buildSubscriptionUrl, describeRemainingTraffic } from "@/lib/subscription";
+import { requireUserSession } from "@/lib/session";
+import { formatBytes, getUserById } from "@/lib/store";
+
+export default async function MePage() {
+ const session = await requireUserSession();
+ const [user, origin] = await Promise.all([
+ getUserById(session.user_id),
+ getRequestOrigin(),
+ ]);
+
+ if (!user) {
+ redirect("/login");
+ }
+
+ return (
+
+
+
+
+
SELF SERVICE
+
{user.username}
+
+ 已用 {formatBytes(user.used_tx_bytes + user.used_rx_bytes)},{describeRemainingTraffic(user)}
+
+
+
+ {user.enabled ? "已启用" : "已禁用"}
+
+
+ {user.expires_at ?? "长期有效"}
+
+
+
+
+
+ auth_id
+ {user.auth_id}
+
+
+ 订阅链接
+
+ {user.subscription_token
+ ? buildSubscriptionUrl(user.subscription_token, origin)
+ : "暂不可用"}
+
+
+
+
+
+
+ 上传
+ {formatBytes(user.used_tx_bytes)}
+
+
+ 下载
+ {formatBytes(user.used_rx_bytes)}
+
+
+ 剩余额度
+ {describeRemainingTraffic(user)}
+
+
+
+
+
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
index 3f36f7c..621252b 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,65 +1,20 @@
-import Image from "next/image";
+import { redirect } from "next/navigation";
-export default function Home() {
- return (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
-
-
- );
+import { getAdminSession, getUserSession } from "@/lib/session";
+
+export default async function Home() {
+ const [adminSession, userSession] = await Promise.all([
+ getAdminSession(),
+ getUserSession(),
+ ]);
+
+ if (adminSession) {
+ redirect("/admin");
+ }
+
+ if (userSession) {
+ redirect("/me");
+ }
+
+ redirect("/login");
}
diff --git a/bun.lock b/bun.lock
index 556f6fb..fc06622 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,6 +26,7 @@
},
},
"trustedDependencies": [
+ "better-sqlite3",
"sharp",
"unrs-resolver",
],
diff --git a/deploy/hy2-panel.service b/deploy/hy2-panel.service
new file mode 100644
index 0000000..41cbd7f
--- /dev/null
+++ b/deploy/hy2-panel.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=HY2 Panel
+After=network.target
+
+[Service]
+Type=simple
+WorkingDirectory=/root/hy2-panel
+Environment=NODE_ENV=production
+Environment=PATH=/root/.bun/bin:/root/.nvm/versions/node/v22.22.2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+EnvironmentFile=/root/hy2-panel/.env
+ExecStart=/root/.bun/bin/bun run start
+Restart=always
+RestartSec=3
+User=root
+
+[Install]
+WantedBy=multi-user.target
diff --git a/instrumentation.ts b/instrumentation.ts
new file mode 100644
index 0000000..5d6612a
--- /dev/null
+++ b/instrumentation.ts
@@ -0,0 +1,11 @@
+export async function register() {
+ if (process.env.NEXT_RUNTIME === "nodejs") {
+ const [{ ensureDb }, { startPoller }] = await Promise.all([
+ import("@/lib/db"),
+ import("@/lib/poller"),
+ ]);
+
+ await ensureDb();
+ startPoller();
+ }
+}
diff --git a/lib/crypto.ts b/lib/crypto.ts
new file mode 100644
index 0000000..65cf6e7
--- /dev/null
+++ b/lib/crypto.ts
@@ -0,0 +1,38 @@
+import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
+
+import { getEnv } from "@/lib/env";
+
+function getKey() {
+ return createHash("sha256").update(getEnv().SESSION_SECRET).digest();
+}
+
+export function encryptText(value: string) {
+ const iv = randomBytes(12);
+ const cipher = createCipheriv("aes-256-gcm", getKey(), iv);
+ const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
+ const tag = cipher.getAuthTag();
+ return `${iv.toString("base64url")}.${tag.toString("base64url")}.${encrypted.toString("base64url")}`;
+}
+
+export function decryptText(payload: string | null | undefined) {
+ if (!payload) return null;
+
+ const [ivPart, tagPart, encryptedPart] = payload.split(".");
+ if (!ivPart || !tagPart || !encryptedPart) {
+ return null;
+ }
+
+ const decipher = createDecipheriv(
+ "aes-256-gcm",
+ getKey(),
+ Buffer.from(ivPart, "base64url"),
+ );
+ decipher.setAuthTag(Buffer.from(tagPart, "base64url"));
+
+ const decrypted = Buffer.concat([
+ decipher.update(Buffer.from(encryptedPart, "base64url")),
+ decipher.final(),
+ ]);
+
+ return decrypted.toString("utf8");
+}
diff --git a/lib/db.ts b/lib/db.ts
new file mode 100644
index 0000000..ad56a05
--- /dev/null
+++ b/lib/db.ts
@@ -0,0 +1,340 @@
+import Database from "better-sqlite3";
+import fs from "node:fs";
+import path from "node:path";
+
+import { encryptText } from "@/lib/crypto";
+import { getEnv } from "@/lib/env";
+import { createOpaqueToken, createStableId, hashPassword } from "@/lib/password";
+
+const MIGRATIONS = [
+ `
+ CREATE TABLE IF NOT EXISTS admins (
+ id INTEGER PRIMARY KEY CHECK (id = 1),
+ username TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS admin_sessions (
+ id TEXT PRIMARY KEY,
+ admin_id INTEGER NOT NULL,
+ session_hash TEXT NOT NULL UNIQUE,
+ expires_at TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ last_seen_at TEXT NOT NULL,
+ FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE
+ );
+
+ CREATE TABLE IF NOT EXISTS hy2_users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ auth_id TEXT NOT NULL UNIQUE,
+ username TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ enabled INTEGER NOT NULL DEFAULT 1,
+ expires_at TEXT,
+ traffic_limit_bytes INTEGER,
+ used_tx_bytes INTEGER NOT NULL DEFAULT 0,
+ used_rx_bytes INTEGER NOT NULL DEFAULT 0,
+ notes TEXT NOT NULL DEFAULT '',
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS hy2_nodes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ slug TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
+ auth_token TEXT NOT NULL,
+ traffic_stats_url TEXT NOT NULL,
+ traffic_stats_secret TEXT NOT NULL,
+ enabled INTEGER NOT NULL DEFAULT 1,
+ poll_interval_seconds INTEGER NOT NULL DEFAULT 15,
+ last_polled_at TEXT,
+ last_sync_ok_at TEXT,
+ last_error_at TEXT,
+ last_error_message TEXT,
+ last_online_users INTEGER NOT NULL DEFAULT 0,
+ last_stream_count INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS traffic_ledgers (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ node_id INTEGER NOT NULL,
+ user_id INTEGER,
+ auth_id TEXT NOT NULL,
+ tx_bytes INTEGER NOT NULL,
+ rx_bytes INTEGER NOT NULL,
+ window_started_at TEXT NOT NULL,
+ window_ended_at TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ FOREIGN KEY (node_id) REFERENCES hy2_nodes(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES hy2_users(id) ON DELETE SET NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS node_sync_records (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ node_id INTEGER NOT NULL,
+ started_at TEXT NOT NULL,
+ finished_at TEXT NOT NULL,
+ ok INTEGER NOT NULL,
+ traffic_entries INTEGER NOT NULL DEFAULT 0,
+ online_users INTEGER NOT NULL DEFAULT 0,
+ stream_count INTEGER NOT NULL DEFAULT 0,
+ kicked_auth_ids TEXT NOT NULL DEFAULT '',
+ error_message TEXT,
+ created_at TEXT NOT NULL,
+ FOREIGN KEY (node_id) REFERENCES hy2_nodes(id) ON DELETE CASCADE
+ );
+
+ CREATE TABLE IF NOT EXISTS auth_audits (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ node_id INTEGER,
+ user_id INTEGER,
+ auth_id TEXT,
+ username TEXT,
+ addr TEXT,
+ requested_tx INTEGER,
+ ok INTEGER NOT NULL,
+ reason TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ FOREIGN KEY (node_id) REFERENCES hy2_nodes(id) ON DELETE SET NULL,
+ FOREIGN KEY (user_id) REFERENCES hy2_users(id) ON DELETE SET NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS node_user_presence (
+ node_id INTEGER NOT NULL,
+ user_id INTEGER,
+ auth_id TEXT NOT NULL,
+ connections INTEGER NOT NULL DEFAULT 0,
+ updated_at TEXT NOT NULL,
+ PRIMARY KEY (node_id, auth_id),
+ FOREIGN KEY (node_id) REFERENCES hy2_nodes(id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES hy2_users(id) ON DELETE SET NULL
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_hy2_users_auth_id ON hy2_users(auth_id);
+ CREATE INDEX IF NOT EXISTS idx_hy2_users_username ON hy2_users(username);
+ CREATE INDEX IF NOT EXISTS idx_hy2_nodes_enabled ON hy2_nodes(enabled);
+ CREATE INDEX IF NOT EXISTS idx_traffic_ledgers_auth_id ON traffic_ledgers(auth_id);
+ CREATE INDEX IF NOT EXISTS idx_traffic_ledgers_created_at ON traffic_ledgers(created_at);
+ CREATE INDEX IF NOT EXISTS idx_node_sync_records_node_id ON node_sync_records(node_id, created_at DESC);
+ CREATE INDEX IF NOT EXISTS idx_auth_audits_created_at ON auth_audits(created_at DESC);
+ `,
+ `
+ ALTER TABLE hy2_users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0;
+ ALTER TABLE hy2_users ADD COLUMN auth_secret_encrypted TEXT;
+ ALTER TABLE hy2_users ADD COLUMN subscription_token TEXT;
+ ALTER TABLE hy2_users ADD COLUMN subscription_label TEXT NOT NULL DEFAULT '';
+
+ CREATE TABLE IF NOT EXISTS user_sessions (
+ id TEXT PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ session_hash TEXT NOT NULL UNIQUE,
+ expires_at TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ last_seen_at TEXT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES hy2_users(id) ON DELETE CASCADE
+ );
+
+ CREATE TABLE IF NOT EXISTS app_settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ );
+
+ ALTER TABLE hy2_nodes ADD COLUMN server_host TEXT NOT NULL DEFAULT '';
+ ALTER TABLE hy2_nodes ADD COLUMN server_port INTEGER NOT NULL DEFAULT 443;
+ ALTER TABLE hy2_nodes ADD COLUMN client_name TEXT NOT NULL DEFAULT '';
+ ALTER TABLE hy2_nodes ADD COLUMN bandwidth_up_mbps INTEGER NOT NULL DEFAULT 30;
+ ALTER TABLE hy2_nodes ADD COLUMN bandwidth_down_mbps INTEGER NOT NULL DEFAULT 30;
+ ALTER TABLE hy2_nodes ADD COLUMN skip_cert_verify INTEGER NOT NULL DEFAULT 0;
+ ALTER TABLE hy2_nodes ADD COLUMN sni TEXT;
+ ALTER TABLE hy2_nodes ADD COLUMN traffic_stats_listen TEXT NOT NULL DEFAULT ':9999';
+
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hy2_users_subscription_token ON hy2_users(subscription_token);
+ CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
+ `,
+ `
+ ALTER TABLE hy2_nodes ADD COLUMN hy2_listen TEXT NOT NULL DEFAULT ':443';
+ `,
+];
+
+declare global {
+ var __hy2PanelDb: Database.Database | undefined;
+ var __hy2PanelDbInit: Promise | undefined;
+}
+
+function createDbInstance() {
+ const env = getEnv();
+ const filePath = path.isAbsolute(env.DATABASE_PATH)
+ ? env.DATABASE_PATH
+ : path.join(/* turbopackIgnore: true */ process.cwd(), env.DATABASE_PATH);
+
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
+
+ const db = new Database(filePath);
+ db.pragma("journal_mode = WAL");
+ db.pragma("foreign_keys = ON");
+
+ return db;
+}
+
+async function initializeDb(db: Database.Database) {
+ const currentVersion = db.pragma("user_version", { simple: true }) as number;
+
+ for (let index = currentVersion; index < MIGRATIONS.length; index += 1) {
+ db.exec(MIGRATIONS[index]);
+ db.pragma(`user_version = ${index + 1}`);
+ }
+
+ const env = getEnv();
+ const now = new Date().toISOString();
+ const passwordHash = await hashPassword(env.ADMIN_PASSWORD);
+
+ db.prepare(
+ `
+ INSERT INTO admins (id, username, password_hash, created_at, updated_at)
+ VALUES (1, @username, @passwordHash, @now, @now)
+ ON CONFLICT(id) DO UPDATE SET
+ username = excluded.username,
+ password_hash = excluded.password_hash,
+ updated_at = excluded.updated_at
+ `,
+ ).run({
+ username: env.ADMIN_USERNAME,
+ passwordHash,
+ now,
+ });
+
+ const templatePath = path.join(process.cwd(), "app", "config", "config.yaml");
+ const defaultTemplate = fs.readFileSync(templatePath, "utf8");
+ const settings = db.prepare(
+ `
+ INSERT INTO app_settings (key, value, updated_at)
+ VALUES (?, ?, ?)
+ ON CONFLICT(key) DO NOTHING
+ `,
+ );
+ settings.run("clash_template", defaultTemplate, now);
+ settings.run("extra_proxy_groups", "", now);
+ settings.run("profile_name", "FeieProxy", now);
+ settings.run("proxy_groups_json", "[]", now);
+
+ const existingAdminUser = db
+ .prepare(
+ `
+ SELECT id, auth_id, subscription_token
+ FROM hy2_users
+ WHERE is_admin = 1 OR username = ?
+ ORDER BY is_admin DESC, id ASC
+ LIMIT 1
+ `,
+ )
+ .get(env.ADMIN_USERNAME) as
+ | { id: number; auth_id: string; subscription_token: string | null }
+ | undefined;
+
+ if (existingAdminUser) {
+ db.prepare(
+ `
+ UPDATE hy2_users
+ SET
+ username = @username,
+ password_hash = @passwordHash,
+ auth_secret_encrypted = @secret,
+ is_admin = 1,
+ enabled = 1,
+ expires_at = NULL,
+ traffic_limit_bytes = NULL,
+ subscription_token = COALESCE(subscription_token, @subscriptionToken),
+ subscription_label = CASE
+ WHEN subscription_label = '' THEN @username
+ ELSE subscription_label
+ END,
+ updated_at = @now
+ WHERE id = @id
+ `,
+ ).run({
+ id: existingAdminUser.id,
+ username: env.ADMIN_USERNAME,
+ passwordHash,
+ secret: encryptText(env.ADMIN_PASSWORD),
+ subscriptionToken: existingAdminUser.subscription_token ?? createOpaqueToken(),
+ now,
+ });
+ } else {
+ db.prepare(
+ `
+ INSERT INTO hy2_users (
+ auth_id, username, password_hash, enabled, expires_at, traffic_limit_bytes,
+ used_tx_bytes, used_rx_bytes, notes, is_admin, auth_secret_encrypted,
+ subscription_token, subscription_label, created_at, updated_at
+ )
+ VALUES (
+ @authId, @username, @passwordHash, 1, NULL, NULL,
+ 0, 0, 'System administrator account', 1, @secret,
+ @subscriptionToken, @username, @now, @now
+ )
+ `,
+ ).run({
+ authId: `admin-${createStableId()}`,
+ username: env.ADMIN_USERNAME,
+ passwordHash,
+ secret: encryptText(env.ADMIN_PASSWORD),
+ subscriptionToken: createOpaqueToken(),
+ now,
+ });
+ }
+
+ const usersMissingTokens = db
+ .prepare("SELECT id, username FROM hy2_users WHERE subscription_token IS NULL")
+ .all() as Array<{ id: number; username: string }>;
+
+ const updateUserToken = db.prepare(
+ `
+ UPDATE hy2_users
+ SET
+ subscription_token = ?,
+ subscription_label = CASE
+ WHEN subscription_label = '' THEN ?
+ ELSE subscription_label
+ END,
+ updated_at = ?
+ WHERE id = ?
+ `,
+ );
+
+ for (const user of usersMissingTokens) {
+ updateUserToken.run(createOpaqueToken(), user.username, now, user.id);
+ }
+
+ db.prepare(
+ `
+ UPDATE hy2_users
+ SET
+ subscription_label = username,
+ updated_at = ?
+ WHERE subscription_label = ''
+ `,
+ ).run(now);
+}
+
+export function getDb() {
+ if (!global.__hy2PanelDb) {
+ global.__hy2PanelDb = createDbInstance();
+ }
+
+ return global.__hy2PanelDb;
+}
+
+export async function ensureDb() {
+ if (!global.__hy2PanelDbInit) {
+ global.__hy2PanelDbInit = initializeDb(getDb());
+ }
+
+ await global.__hy2PanelDbInit;
+ return getDb();
+}
diff --git a/lib/env.ts b/lib/env.ts
new file mode 100644
index 0000000..176da5b
--- /dev/null
+++ b/lib/env.ts
@@ -0,0 +1,37 @@
+import { z } from "zod";
+
+const schema = z.object({
+ NODE_ENV: z
+ .enum(["development", "test", "production"])
+ .default("development"),
+ APP_URL: z.string().url().default("http://127.0.0.1:3000"),
+ DATABASE_PATH: z.string().default("./data/hy2-panel.db"),
+ SESSION_SECRET: z
+ .string()
+ .min(16)
+ .default("change-this-session-secret-before-production"),
+ SESSION_COOKIE_SECURE: z
+ .enum(["auto", "true", "false"])
+ .default("auto"),
+ SESSION_TTL_HOURS: z.coerce.number().int().min(1).max(24 * 365).default(720),
+ ADMIN_USERNAME: z.string().min(1).max(64).default("admin"),
+ ADMIN_PASSWORD: z.string().min(8).max(128).default("change-me-now"),
+ POLLER_ENABLED: z
+ .union([z.literal("true"), z.literal("false")])
+ .default("true")
+ .transform((value) => value === "true"),
+ POLLER_STARTUP_DELAY_MS: z.coerce.number().int().min(0).max(60_000).default(2500),
+});
+
+export type AppEnv = z.infer;
+
+let cachedEnv: AppEnv | null = null;
+
+export function getEnv(): AppEnv {
+ if (cachedEnv) {
+ return cachedEnv;
+ }
+
+ cachedEnv = schema.parse(process.env);
+ return cachedEnv;
+}
diff --git a/lib/hy2.ts b/lib/hy2.ts
new file mode 100644
index 0000000..dab766b
--- /dev/null
+++ b/lib/hy2.ts
@@ -0,0 +1,40 @@
+import { getNodeBySlug, getUserLifecycleState } from "@/lib/store";
+
+export function parseHy2Userpass(auth: string) {
+ const separator = auth.indexOf(":");
+ if (separator <= 0 || separator === auth.length - 1) {
+ return null;
+ }
+
+ return {
+ username: auth.slice(0, separator),
+ password: auth.slice(separator + 1),
+ };
+}
+
+export async function verifyNodeToken(slug: string, token: string | null) {
+ const node = await getNodeBySlug(slug);
+ if (!node) {
+ return { ok: false, reason: "node_not_found" as const, node: null };
+ }
+
+ if (!node.enabled) {
+ return { ok: false, reason: "node_disabled" as const, node };
+ }
+
+ if (!token || token !== node.auth_token) {
+ return { ok: false, reason: "node_token_invalid" as const, node };
+ }
+
+ return { ok: true, reason: "ok" as const, node };
+}
+
+export function evaluateUserForAuth(user: {
+ enabled: number;
+ expires_at: string | null;
+ traffic_limit_bytes: number | null;
+ used_tx_bytes: number;
+ used_rx_bytes: number;
+}) {
+ return getUserLifecycleState(user);
+}
diff --git a/lib/password.ts b/lib/password.ts
new file mode 100644
index 0000000..0e435f3
--- /dev/null
+++ b/lib/password.ts
@@ -0,0 +1,27 @@
+import bcrypt from "bcryptjs";
+import { createHash, randomBytes, randomUUID } from "node:crypto";
+
+const BCRYPT_ROUNDS = 12;
+
+export async function hashPassword(password: string): Promise {
+ return bcrypt.hash(password, BCRYPT_ROUNDS);
+}
+
+export async function verifyPassword(
+ password: string,
+ passwordHash: string,
+): Promise {
+ return bcrypt.compare(password, passwordHash);
+}
+
+export function createOpaqueToken(size = 24): string {
+ return randomBytes(size).toString("base64url");
+}
+
+export function createStableId(): string {
+ return randomUUID();
+}
+
+export function sha256(input: string): string {
+ return createHash("sha256").update(input).digest("hex");
+}
diff --git a/lib/poller.ts b/lib/poller.ts
new file mode 100644
index 0000000..bc469d6
--- /dev/null
+++ b/lib/poller.ts
@@ -0,0 +1,127 @@
+import { ensureDb, getDb } from "@/lib/db";
+import { getEnv } from "@/lib/env";
+import {
+ applyNodeSync,
+ getKickCandidates,
+ listEnabledNodesForPolling,
+ shouldPollNode,
+ writeNodeSyncError,
+} from "@/lib/store";
+
+declare global {
+ var __hy2PanelPollerStarted: boolean | undefined;
+ var __hy2PanelPollerRunning: boolean | undefined;
+}
+
+type TrafficMap = Record;
+
+async function fetchJson(url: string, init: RequestInit): Promise {
+ const response = await fetch(url, {
+ ...init,
+ signal: AbortSignal.timeout(10_000),
+ cache: "no-store",
+ });
+
+ if (!response.ok) {
+ throw new Error(`${response.status} ${response.statusText}`);
+ }
+
+ return (await response.json()) as T;
+}
+
+async function syncNode(node: Awaited>[number]) {
+ const startedAt = new Date().toISOString();
+
+ try {
+ const baseUrl = node.traffic_stats_url.replace(/\/$/, "");
+ const headers = {
+ Authorization: node.traffic_stats_secret,
+ "Content-Type": "application/json",
+ };
+
+ const [traffic, online, streams] = await Promise.all([
+ fetchJson(`${baseUrl}/traffic?clear=1`, { headers }),
+ fetchJson>(`${baseUrl}/online`, { headers }),
+ fetchJson<{ streams?: unknown[] }>(`${baseUrl}/dump/streams`, { headers }),
+ ]);
+
+ const kickAuthIds = await getKickCandidates(Object.keys(online));
+
+ if (kickAuthIds.length > 0) {
+ await fetch(`${baseUrl}/kick`, {
+ method: "POST",
+ headers,
+ body: JSON.stringify(kickAuthIds),
+ signal: AbortSignal.timeout(10_000),
+ }).then((response) => {
+ if (!response.ok) {
+ throw new Error(`kick failed: ${response.status} ${response.statusText}`);
+ }
+ });
+ }
+
+ await ensureDb();
+ const db = getDb();
+ const finishedAt = new Date().toISOString();
+ const transaction = db.transaction(() => {
+ applyNodeSync(db, {
+ node,
+ startedAt,
+ finishedAt,
+ traffic,
+ online,
+ streamCount: streams.streams?.length ?? 0,
+ kickedAuthIds: kickAuthIds,
+ });
+ });
+ transaction();
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "unknown_sync_error";
+ await writeNodeSyncError(node.id, startedAt, message);
+ }
+}
+
+async function runPollingCycle() {
+ if (global.__hy2PanelPollerRunning) {
+ return;
+ }
+
+ global.__hy2PanelPollerRunning = true;
+
+ try {
+ const nodes = await listEnabledNodesForPolling();
+
+ for (const node of nodes) {
+ if (await shouldPollNode(node.id, node.poll_interval_seconds)) {
+ await syncNode(node);
+ }
+ }
+ } finally {
+ global.__hy2PanelPollerRunning = false;
+ }
+}
+
+export function startPoller() {
+ const env = getEnv();
+ if (!env.POLLER_ENABLED || global.__hy2PanelPollerStarted) {
+ return;
+ }
+
+ global.__hy2PanelPollerStarted = true;
+
+ const start = () => {
+ runPollingCycle().catch(() => undefined);
+ const handle = setInterval(() => {
+ runPollingCycle().catch(() => undefined);
+ }, 5_000);
+ handle.unref();
+ };
+
+ if (env.POLLER_STARTUP_DELAY_MS > 0) {
+ const timeout = setTimeout(start, env.POLLER_STARTUP_DELAY_MS);
+ timeout.unref();
+ return;
+ }
+
+ start();
+}
diff --git a/lib/request-origin.ts b/lib/request-origin.ts
new file mode 100644
index 0000000..7ab4746
--- /dev/null
+++ b/lib/request-origin.ts
@@ -0,0 +1,24 @@
+import { headers } from "next/headers";
+
+import { getEnv } from "@/lib/env";
+
+export async function getRequestOrigin() {
+ const headerList = await headers();
+ const forwardedProto = headerList.get("x-forwarded-proto");
+ const forwardedHost = headerList.get("x-forwarded-host");
+ const host = forwardedHost || headerList.get("host");
+
+ if (!host) {
+ return getEnv().APP_URL;
+ }
+
+ const proto =
+ forwardedProto || (host.startsWith("localhost") || host.startsWith("127.0.0.1") ? "http" : "http");
+
+ return `${proto}://${host}`;
+}
+
+export function getOriginFromRequest(request: Request) {
+ const url = new URL(request.url);
+ return `${url.protocol}//${url.host}`;
+}
diff --git a/lib/session.ts b/lib/session.ts
new file mode 100644
index 0000000..3702981
--- /dev/null
+++ b/lib/session.ts
@@ -0,0 +1,188 @@
+import { cookies } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { getEnv } from "@/lib/env";
+import { createOpaqueToken, createStableId, sha256 } from "@/lib/password";
+import {
+ createAdminSessionRecord,
+ createUserSessionRecord,
+ deleteAdminSessionByHash,
+ deleteUserSessionByHash,
+ getAdminSessionByHash,
+ getUserSessionByHash,
+ touchAdminSession,
+ touchUserSession,
+} from "@/lib/store";
+
+const ADMIN_COOKIE_NAME = "hy2_panel_admin_session";
+const USER_COOKIE_NAME = "hy2_panel_user_session";
+
+function shouldUseSecureCookies() {
+ const env = getEnv();
+
+ if (env.SESSION_COOKIE_SECURE === "true") {
+ return true;
+ }
+
+ if (env.SESSION_COOKIE_SECURE === "false") {
+ return false;
+ }
+
+ return new URL(env.APP_URL).protocol === "https:";
+}
+
+export async function createAdminSession(adminId: number) {
+ const env = getEnv();
+ const token = createOpaqueToken(32);
+ const expiresAt = new Date(
+ Date.now() + env.SESSION_TTL_HOURS * 60 * 60 * 1000,
+ ).toISOString();
+
+ await createAdminSessionRecord({
+ id: createStableId(),
+ adminId,
+ sessionHash: sha256(`${env.SESSION_SECRET}:${token}`),
+ expiresAt,
+ });
+
+ const cookieStore = await cookies();
+ cookieStore.set(ADMIN_COOKIE_NAME, token, {
+ httpOnly: true,
+ sameSite: "lax",
+ secure: shouldUseSecureCookies(),
+ path: "/",
+ expires: new Date(expiresAt),
+ });
+}
+
+export async function createUserSession(userId: number) {
+ const env = getEnv();
+ const token = createOpaqueToken(32);
+ const expiresAt = new Date(
+ Date.now() + env.SESSION_TTL_HOURS * 60 * 60 * 1000,
+ ).toISOString();
+
+ await createUserSessionRecord({
+ id: createStableId(),
+ userId,
+ sessionHash: sha256(`${env.SESSION_SECRET}:${token}`),
+ expiresAt,
+ });
+
+ const cookieStore = await cookies();
+ cookieStore.set(USER_COOKIE_NAME, token, {
+ httpOnly: true,
+ sameSite: "lax",
+ secure: shouldUseSecureCookies(),
+ path: "/",
+ expires: new Date(expiresAt),
+ });
+}
+
+export async function destroyAdminSession() {
+ const cookieStore = await cookies();
+ const raw = cookieStore.get(ADMIN_COOKIE_NAME)?.value;
+
+ if (raw) {
+ await deleteAdminSessionByHash(sha256(`${getEnv().SESSION_SECRET}:${raw}`));
+ }
+
+ cookieStore.delete(ADMIN_COOKIE_NAME);
+}
+
+export async function destroyUserSession() {
+ const cookieStore = await cookies();
+ const raw = cookieStore.get(USER_COOKIE_NAME)?.value;
+
+ if (raw) {
+ await deleteUserSessionByHash(sha256(`${getEnv().SESSION_SECRET}:${raw}`));
+ }
+
+ cookieStore.delete(USER_COOKIE_NAME);
+}
+
+export async function getAdminSession() {
+ const raw = (await cookies()).get(ADMIN_COOKIE_NAME)?.value;
+ if (!raw) return null;
+
+ const session = await getAdminSessionByHash(
+ sha256(`${getEnv().SESSION_SECRET}:${raw}`),
+ );
+
+ if (!session) {
+ return null;
+ }
+
+ if (new Date(session.expires_at).getTime() <= Date.now()) {
+ await deleteAdminSessionByHash(sha256(`${getEnv().SESSION_SECRET}:${raw}`));
+ return null;
+ }
+
+ if (Date.now() - new Date(session.last_seen_at).getTime() > 5 * 60 * 1000) {
+ await touchAdminSession(session.id);
+ }
+
+ return session;
+}
+
+export async function getUserSession() {
+ const raw = (await cookies()).get(USER_COOKIE_NAME)?.value;
+ if (!raw) return null;
+
+ const session = await getUserSessionByHash(
+ sha256(`${getEnv().SESSION_SECRET}:${raw}`),
+ );
+
+ if (!session) {
+ return null;
+ }
+
+ if (new Date(session.expires_at).getTime() <= Date.now()) {
+ await deleteUserSessionByHash(sha256(`${getEnv().SESSION_SECRET}:${raw}`));
+ return null;
+ }
+
+ if (Date.now() - new Date(session.last_seen_at).getTime() > 5 * 60 * 1000) {
+ await touchUserSession(session.id);
+ }
+
+ return session;
+}
+
+export async function requireAdminSession() {
+ const session = await getAdminSession();
+ if (!session) {
+ redirect("/login");
+ }
+
+ return session;
+}
+
+export async function requireAdminAccess() {
+ const adminSession = await getAdminSession();
+ if (adminSession) {
+ return {
+ username: adminSession.username,
+ source: "admin" as const,
+ };
+ }
+
+ const userSession = await getUserSession();
+ if (userSession?.is_admin) {
+ return {
+ username: userSession.username,
+ source: "user" as const,
+ };
+ }
+
+ redirect("/login");
+}
+
+export async function requireUserSession() {
+ const session = await getUserSession();
+ if (!session) {
+ redirect("/login");
+ }
+
+ return session;
+}
diff --git a/lib/store.ts b/lib/store.ts
new file mode 100644
index 0000000..78de92c
--- /dev/null
+++ b/lib/store.ts
@@ -0,0 +1,1062 @@
+import type Database from "better-sqlite3";
+
+import { encryptText } from "@/lib/crypto";
+import { ensureDb, getDb } from "@/lib/db";
+import { getEnv } from "@/lib/env";
+import { createOpaqueToken, createStableId, hashPassword } from "@/lib/password";
+
+export type Hy2UserRow = {
+ id: number;
+ auth_id: string;
+ username: string;
+ password_hash: string;
+ auth_secret_encrypted: string | null;
+ enabled: number;
+ is_admin: number;
+ expires_at: string | null;
+ traffic_limit_bytes: number | null;
+ used_tx_bytes: number;
+ used_rx_bytes: number;
+ notes: string;
+ subscription_token: string | null;
+ subscription_label: string;
+ created_at: string;
+ updated_at: string;
+};
+
+export type Hy2NodeRow = {
+ id: number;
+ slug: string;
+ name: string;
+ auth_token: string;
+ hy2_listen: string;
+ traffic_stats_url: string;
+ traffic_stats_secret: string;
+ traffic_stats_listen: string;
+ server_host: string;
+ server_port: number;
+ client_name: string;
+ bandwidth_up_mbps: number;
+ bandwidth_down_mbps: number;
+ skip_cert_verify: number;
+ sni: string | null;
+ enabled: number;
+ poll_interval_seconds: number;
+ last_polled_at: string | null;
+ last_sync_ok_at: string | null;
+ last_error_at: string | null;
+ last_error_message: string | null;
+ last_online_users: number;
+ last_stream_count: number;
+ created_at: string;
+ updated_at: string;
+};
+
+export type ProxyGroupConfig = {
+ id: string;
+ name: string;
+ type: "select";
+ memberSlugs: string[];
+};
+
+type AuditRecordInput = {
+ nodeId?: number | null;
+ userId?: number | null;
+ authId?: string | null;
+ username?: string | null;
+ addr?: string | null;
+ requestedTx?: number | null;
+ ok: boolean;
+ reason: string;
+};
+
+function dbNow() {
+ return new Date().toISOString();
+}
+
+function asBoolean(value: number) {
+ return value === 1;
+}
+
+export function formatBytes(bytes: number | null | undefined) {
+ if (bytes == null) return "0 B";
+ if (bytes < 1024) return `${bytes} B`;
+
+ const units = ["KB", "MB", "GB", "TB", "PB"];
+ let value = bytes / 1024;
+ let unit = units[0];
+
+ for (let index = 1; index < units.length && value >= 1024; index += 1) {
+ value /= 1024;
+ unit = units[index];
+ }
+
+ return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${unit}`;
+}
+
+export function toBytesFromGiB(input: string | null | undefined) {
+ if (!input) return null;
+ const value = Number(input);
+ if (!Number.isFinite(value) || value <= 0) return null;
+ return Math.round(value * 1024 * 1024 * 1024);
+}
+
+export function toDatetimeLocalValue(value: string | null) {
+ if (!value) return "";
+ return value.slice(0, 16);
+}
+
+export function buildNodeAuthUrl(slug: string, token: string, origin = getEnv().APP_URL) {
+ return `${origin}/api/hy2/nodes/${encodeURIComponent(slug)}/auth?token=${encodeURIComponent(token)}`;
+}
+
+export function buildTrafficStatsUrl(serverHost: string, listen: string) {
+ const portMatch = listen.match(/:?(\d{2,5})$/);
+ const port = portMatch?.[1] ?? "9999";
+ return `http://${serverHost}:${port}`;
+}
+
+export function getUserLifecycleState(user: {
+ enabled: number;
+ expires_at: string | null;
+ traffic_limit_bytes: number | null;
+ used_tx_bytes: number;
+ used_rx_bytes: number;
+}) {
+ if (!asBoolean(user.enabled)) {
+ return { ok: false, reason: "user_disabled" };
+ }
+
+ if (user.expires_at && new Date(user.expires_at).getTime() <= Date.now()) {
+ return { ok: false, reason: "user_expired" };
+ }
+
+ const total = user.used_tx_bytes + user.used_rx_bytes;
+ if (
+ user.traffic_limit_bytes != null &&
+ Number.isFinite(user.traffic_limit_bytes) &&
+ total >= user.traffic_limit_bytes
+ ) {
+ return { ok: false, reason: "user_quota_exceeded" };
+ }
+
+ return { ok: true, reason: "ok" };
+}
+
+export async function getAdminByUsername(username: string) {
+ const db = await ensureDb();
+ return db
+ .prepare("SELECT * FROM admins WHERE username = ?")
+ .get(username) as
+ | {
+ id: number;
+ username: string;
+ password_hash: string;
+ }
+ | undefined;
+}
+
+export async function listUsers() {
+ const db = await ensureDb();
+ return db
+ .prepare(
+ `
+ SELECT
+ u.*,
+ COALESCE(SUM(p.connections), 0) AS online_connections
+ FROM hy2_users u
+ LEFT JOIN node_user_presence p ON p.user_id = u.id
+ GROUP BY u.id
+ ORDER BY u.created_at DESC
+ `,
+ )
+ .all() as Array;
+}
+
+export async function getUserByUsername(username: string) {
+ const db = await ensureDb();
+ return db
+ .prepare("SELECT * FROM hy2_users WHERE username = ?")
+ .get(username) as Hy2UserRow | undefined;
+}
+
+export async function getUserById(id: number) {
+ const db = await ensureDb();
+ return db
+ .prepare("SELECT * FROM hy2_users WHERE id = ?")
+ .get(id) as Hy2UserRow | undefined;
+}
+
+export async function getUserByAuthId(authId: string) {
+ const db = await ensureDb();
+ return db
+ .prepare("SELECT * FROM hy2_users WHERE auth_id = ?")
+ .get(authId) as Hy2UserRow | undefined;
+}
+
+export async function getUserBySubscriptionToken(token: string) {
+ const db = await ensureDb();
+ return db
+ .prepare("SELECT * FROM hy2_users WHERE subscription_token = ?")
+ .get(token) as Hy2UserRow | undefined;
+}
+
+export async function getAdminMirrorUser() {
+ const db = await ensureDb();
+ return db
+ .prepare(
+ `
+ SELECT *
+ FROM hy2_users
+ WHERE is_admin = 1 OR username = ?
+ ORDER BY is_admin DESC, id ASC
+ LIMIT 1
+ `,
+ )
+ .get(getEnv().ADMIN_USERNAME) as Hy2UserRow | undefined;
+}
+
+export async function createUser(input: {
+ username: string;
+ password: string;
+ enabled: boolean;
+ isAdmin?: boolean;
+ expiresAt: string | null;
+ trafficLimitBytes: number | null;
+ notes: string;
+}) {
+ const db = await ensureDb();
+ const now = dbNow();
+ const passwordHash = await hashPassword(input.password);
+ const authId = createStableId();
+
+ db.prepare(
+ `
+ INSERT INTO hy2_users (
+ auth_id, username, password_hash, auth_secret_encrypted, enabled, is_admin,
+ expires_at, traffic_limit_bytes, notes, subscription_token, subscription_label,
+ created_at, updated_at
+ )
+ VALUES (
+ @authId, @username, @passwordHash, @authSecretEncrypted, @enabled, @isAdmin,
+ @expiresAt, @trafficLimitBytes, @notes, @subscriptionToken, @subscriptionLabel,
+ @now, @now
+ )
+ `,
+ ).run({
+ authId,
+ username: input.username,
+ passwordHash,
+ authSecretEncrypted: encryptText(input.password),
+ enabled: input.enabled ? 1 : 0,
+ isAdmin: input.isAdmin ? 1 : 0,
+ expiresAt: input.expiresAt,
+ trafficLimitBytes: input.trafficLimitBytes,
+ notes: input.notes,
+ subscriptionToken: createOpaqueToken(),
+ subscriptionLabel: input.username,
+ now,
+ });
+}
+
+export async function updateUser(input: {
+ id: number;
+ username: string;
+ enabled: boolean;
+ subscriptionLabel: string;
+ expiresAt: string | null;
+ trafficLimitBytes: number | null;
+ notes: string;
+}) {
+ const db = await ensureDb();
+ db.prepare(
+ `
+ UPDATE hy2_users
+ SET
+ username = @username,
+ enabled = @enabled,
+ subscription_label = @subscriptionLabel,
+ expires_at = @expiresAt,
+ traffic_limit_bytes = @trafficLimitBytes,
+ notes = @notes,
+ updated_at = @updatedAt
+ WHERE id = @id
+ `,
+ ).run({
+ id: input.id,
+ username: input.username,
+ enabled: input.enabled ? 1 : 0,
+ subscriptionLabel: input.subscriptionLabel,
+ expiresAt: input.expiresAt,
+ trafficLimitBytes: input.trafficLimitBytes,
+ notes: input.notes,
+ updatedAt: dbNow(),
+ });
+}
+
+export async function deleteUser(id: number) {
+ const db = await ensureDb();
+ db.prepare("DELETE FROM hy2_users WHERE id = ?").run(id);
+}
+
+export async function resetUserPassword(id: number, password: string) {
+ const db = await ensureDb();
+ const passwordHash = await hashPassword(password);
+ db.prepare(
+ `
+ UPDATE hy2_users
+ SET password_hash = ?, auth_secret_encrypted = ?, updated_at = ?
+ WHERE id = ?
+ `,
+ ).run(passwordHash, encryptText(password), dbNow(), id);
+}
+
+export async function listNodes() {
+ const db = await ensureDb();
+ return db
+ .prepare(
+ `
+ SELECT *
+ FROM hy2_nodes
+ ORDER BY created_at DESC
+ `,
+ )
+ .all() as Hy2NodeRow[];
+}
+
+export async function getNodeBySlug(slug: string) {
+ const db = await ensureDb();
+ return db
+ .prepare("SELECT * FROM hy2_nodes WHERE slug = ?")
+ .get(slug) as Hy2NodeRow | undefined;
+}
+
+export async function createNode(input: {
+ slug: string;
+ name: string;
+ authToken: string | null;
+ hy2Listen: string;
+ serverHost: string;
+ serverPort: number;
+ clientName: string;
+ bandwidthUpMbps: number;
+ bandwidthDownMbps: number;
+ skipCertVerify: boolean;
+ sni: string | null;
+ trafficStatsUrl: string;
+ trafficStatsListen: string;
+ trafficStatsSecret: string;
+ enabled: boolean;
+ pollIntervalSeconds: number;
+}) {
+ const db = await ensureDb();
+ const now = dbNow();
+
+ db.prepare(
+ `
+ INSERT INTO hy2_nodes (
+ slug, name, auth_token, hy2_listen, server_host, server_port, client_name,
+ bandwidth_up_mbps, bandwidth_down_mbps, skip_cert_verify, sni,
+ traffic_stats_url, traffic_stats_listen, traffic_stats_secret, enabled,
+ poll_interval_seconds, created_at, updated_at
+ )
+ VALUES (
+ @slug, @name, @authToken, @hy2Listen, @serverHost, @serverPort, @clientName,
+ @bandwidthUpMbps, @bandwidthDownMbps, @skipCertVerify, @sni,
+ @trafficStatsUrl, @trafficStatsListen, @trafficStatsSecret, @enabled,
+ @pollIntervalSeconds, @now, @now
+ )
+ `,
+ ).run({
+ slug: input.slug,
+ name: input.name,
+ authToken: input.authToken || createOpaqueToken(),
+ hy2Listen: input.hy2Listen || `:${input.serverPort}`,
+ serverHost: input.serverHost,
+ serverPort: input.serverPort,
+ clientName: input.clientName,
+ bandwidthUpMbps: input.bandwidthUpMbps,
+ bandwidthDownMbps: input.bandwidthDownMbps,
+ skipCertVerify: input.skipCertVerify ? 1 : 0,
+ sni: input.sni,
+ trafficStatsUrl:
+ input.trafficStatsUrl || buildTrafficStatsUrl(input.serverHost, input.trafficStatsListen),
+ trafficStatsListen: input.trafficStatsListen,
+ trafficStatsSecret: input.trafficStatsSecret || createOpaqueToken(),
+ enabled: input.enabled ? 1 : 0,
+ pollIntervalSeconds: input.pollIntervalSeconds,
+ now,
+ });
+}
+
+export async function updateNode(input: {
+ id: number;
+ slug: string;
+ name: string;
+ authToken: string;
+ hy2Listen: string;
+ serverHost: string;
+ serverPort: number;
+ clientName: string;
+ bandwidthUpMbps: number;
+ bandwidthDownMbps: number;
+ skipCertVerify: boolean;
+ sni: string | null;
+ trafficStatsUrl: string;
+ trafficStatsListen: string;
+ trafficStatsSecret: string;
+ enabled: boolean;
+ pollIntervalSeconds: number;
+}) {
+ const db = await ensureDb();
+ db.prepare(
+ `
+ UPDATE hy2_nodes
+ SET
+ slug = @slug,
+ name = @name,
+ auth_token = @authToken,
+ hy2_listen = @hy2Listen,
+ server_host = @serverHost,
+ server_port = @serverPort,
+ client_name = @clientName,
+ bandwidth_up_mbps = @bandwidthUpMbps,
+ bandwidth_down_mbps = @bandwidthDownMbps,
+ skip_cert_verify = @skipCertVerify,
+ sni = @sni,
+ traffic_stats_url = @trafficStatsUrl,
+ traffic_stats_listen = @trafficStatsListen,
+ traffic_stats_secret = CASE
+ WHEN @trafficStatsSecret = '' THEN traffic_stats_secret
+ ELSE @trafficStatsSecret
+ END,
+ enabled = @enabled,
+ poll_interval_seconds = @pollIntervalSeconds,
+ updated_at = @updatedAt
+ WHERE id = @id
+ `,
+ ).run({
+ ...input,
+ hy2Listen: input.hy2Listen || `:${input.serverPort}`,
+ trafficStatsUrl:
+ input.trafficStatsUrl || buildTrafficStatsUrl(input.serverHost, input.trafficStatsListen),
+ enabled: input.enabled ? 1 : 0,
+ skipCertVerify: input.skipCertVerify ? 1 : 0,
+ updatedAt: dbNow(),
+ });
+}
+
+export async function deleteNode(id: number) {
+ const db = await ensureDb();
+ db.prepare("DELETE FROM hy2_nodes WHERE id = ?").run(id);
+}
+
+export async function listAudits(limit = 100) {
+ const db = await ensureDb();
+ return db
+ .prepare(
+ `
+ SELECT
+ a.*,
+ n.name AS node_name
+ FROM auth_audits a
+ LEFT JOIN hy2_nodes n ON n.id = a.node_id
+ ORDER BY a.created_at DESC
+ LIMIT ?
+ `,
+ )
+ .all(limit) as Array<{
+ id: number;
+ node_name: string | null;
+ auth_id: string | null;
+ username: string | null;
+ addr: string | null;
+ requested_tx: number | null;
+ ok: number;
+ reason: string;
+ created_at: string;
+ }>;
+}
+
+export async function getDashboardMetrics() {
+ const db = await ensureDb();
+
+ const totals = db
+ .prepare(
+ `
+ SELECT
+ (SELECT COUNT(*) FROM hy2_users) AS total_users,
+ (SELECT COUNT(*) FROM hy2_users WHERE enabled = 1) AS enabled_users,
+ (SELECT COUNT(*) FROM hy2_users WHERE is_admin = 1) AS admin_users,
+ (SELECT COUNT(*) FROM hy2_nodes WHERE enabled = 1) AS enabled_nodes,
+ (SELECT COUNT(*) FROM node_user_presence WHERE connections > 0) AS active_presence_rows,
+ (SELECT COALESCE(SUM(connections), 0) FROM node_user_presence WHERE connections > 0) AS total_online_connections,
+ (SELECT COALESCE(SUM(used_tx_bytes + used_rx_bytes), 0) FROM hy2_users) AS total_traffic
+ `,
+ )
+ .get() as {
+ total_users: number;
+ enabled_users: number;
+ admin_users: number;
+ enabled_nodes: number;
+ active_presence_rows: number;
+ total_online_connections: number;
+ total_traffic: number;
+ };
+
+ const topUsers = db
+ .prepare(
+ `
+ SELECT
+ username,
+ auth_id,
+ used_tx_bytes,
+ used_rx_bytes,
+ (used_tx_bytes + used_rx_bytes) AS total_bytes
+ FROM hy2_users
+ ORDER BY total_bytes DESC
+ LIMIT 8
+ `,
+ )
+ .all() as Array<{
+ username: string;
+ auth_id: string;
+ used_tx_bytes: number;
+ used_rx_bytes: number;
+ total_bytes: number;
+ }>;
+
+ const nodeHealth = db
+ .prepare(
+ `
+ SELECT
+ id,
+ name,
+ slug,
+ enabled,
+ last_sync_ok_at,
+ last_error_at,
+ last_error_message,
+ last_online_users,
+ last_stream_count
+ FROM hy2_nodes
+ ORDER BY name ASC
+ `,
+ )
+ .all() as Array<{
+ id: number;
+ name: string;
+ slug: string;
+ enabled: number;
+ last_sync_ok_at: string | null;
+ last_error_at: string | null;
+ last_error_message: string | null;
+ last_online_users: number;
+ last_stream_count: number;
+ }>;
+
+ const recentFailures = db
+ .prepare(
+ `
+ SELECT
+ a.created_at,
+ a.reason,
+ a.username,
+ a.addr,
+ n.name AS node_name
+ FROM auth_audits a
+ LEFT JOIN hy2_nodes n ON n.id = a.node_id
+ WHERE a.ok = 0
+ ORDER BY a.created_at DESC
+ LIMIT 12
+ `,
+ )
+ .all() as Array<{
+ created_at: string;
+ reason: string;
+ username: string | null;
+ addr: string | null;
+ node_name: string | null;
+ }>;
+
+ const recentUsage = db
+ .prepare(
+ `
+ SELECT
+ l.created_at,
+ l.auth_id,
+ l.tx_bytes,
+ l.rx_bytes,
+ u.username,
+ n.name AS node_name
+ FROM traffic_ledgers l
+ LEFT JOIN hy2_users u ON u.id = l.user_id
+ LEFT JOIN hy2_nodes n ON n.id = l.node_id
+ ORDER BY l.created_at DESC
+ LIMIT 20
+ `,
+ )
+ .all() as Array<{
+ created_at: string;
+ auth_id: string;
+ tx_bytes: number;
+ rx_bytes: number;
+ username: string | null;
+ node_name: string | null;
+ }>;
+
+ const syncErrors = db
+ .prepare(
+ `
+ SELECT
+ r.created_at,
+ r.error_message,
+ n.name AS node_name
+ FROM node_sync_records r
+ INNER JOIN hy2_nodes n ON n.id = r.node_id
+ WHERE r.ok = 0
+ ORDER BY r.created_at DESC
+ LIMIT 12
+ `,
+ )
+ .all() as Array<{
+ created_at: string;
+ error_message: string | null;
+ node_name: string;
+ }>;
+
+ return {
+ totals,
+ topUsers,
+ nodeHealth,
+ recentFailures,
+ recentUsage,
+ syncErrors,
+ };
+}
+
+export async function createAdminSessionRecord(input: {
+ id: string;
+ adminId: number;
+ sessionHash: string;
+ expiresAt: string;
+}) {
+ const db = await ensureDb();
+ const now = dbNow();
+ db.prepare(
+ `
+ INSERT INTO admin_sessions (id, admin_id, session_hash, expires_at, created_at, last_seen_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ `,
+ ).run(input.id, input.adminId, input.sessionHash, input.expiresAt, now, now);
+}
+
+export async function getAdminSessionByHash(sessionHash: string) {
+ const db = await ensureDb();
+ return db
+ .prepare(
+ `
+ SELECT
+ s.id,
+ s.admin_id,
+ s.expires_at,
+ s.last_seen_at,
+ a.username
+ FROM admin_sessions s
+ INNER JOIN admins a ON a.id = s.admin_id
+ WHERE s.session_hash = ?
+ `,
+ )
+ .get(sessionHash) as
+ | {
+ id: string;
+ admin_id: number;
+ username: string;
+ expires_at: string;
+ last_seen_at: string;
+ }
+ | undefined;
+}
+
+export async function touchAdminSession(sessionId: string) {
+ const db = await ensureDb();
+ db.prepare("UPDATE admin_sessions SET last_seen_at = ? WHERE id = ?").run(
+ dbNow(),
+ sessionId,
+ );
+}
+
+export async function createUserSessionRecord(input: {
+ id: string;
+ userId: number;
+ sessionHash: string;
+ expiresAt: string;
+}) {
+ const db = await ensureDb();
+ const now = dbNow();
+ db.prepare(
+ `
+ INSERT INTO user_sessions (id, user_id, session_hash, expires_at, created_at, last_seen_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ `,
+ ).run(input.id, input.userId, input.sessionHash, input.expiresAt, now, now);
+}
+
+export async function getUserSessionByHash(sessionHash: string) {
+ const db = await ensureDb();
+ return db
+ .prepare(
+ `
+ SELECT
+ s.id,
+ s.user_id,
+ s.expires_at,
+ s.last_seen_at,
+ u.username,
+ u.is_admin
+ FROM user_sessions s
+ INNER JOIN hy2_users u ON u.id = s.user_id
+ WHERE s.session_hash = ?
+ `,
+ )
+ .get(sessionHash) as
+ | {
+ id: string;
+ user_id: number;
+ username: string;
+ is_admin: number;
+ expires_at: string;
+ last_seen_at: string;
+ }
+ | undefined;
+}
+
+export async function touchUserSession(sessionId: string) {
+ const db = await ensureDb();
+ db.prepare("UPDATE user_sessions SET last_seen_at = ? WHERE id = ?").run(
+ dbNow(),
+ sessionId,
+ );
+}
+
+export async function deleteUserSessionByHash(sessionHash: string) {
+ const db = await ensureDb();
+ db.prepare("DELETE FROM user_sessions WHERE session_hash = ?").run(sessionHash);
+}
+
+export async function deleteAdminSessionByHash(sessionHash: string) {
+ const db = await ensureDb();
+ db.prepare("DELETE FROM admin_sessions WHERE session_hash = ?").run(sessionHash);
+}
+
+export async function getSubscriptionSettings() {
+ const db = await ensureDb();
+ const rows = db
+ .prepare("SELECT key, value FROM app_settings")
+ .all() as Array<{ key: string; value: string }>;
+ const map = new Map(rows.map((row) => [row.key, row.value]));
+
+ return {
+ template: map.get("clash_template") ?? "",
+ extraProxyGroups: map.get("extra_proxy_groups") ?? "",
+ profileName: map.get("profile_name") ?? "FeieProxy",
+ proxyGroups: JSON.parse(map.get("proxy_groups_json") ?? "[]") as ProxyGroupConfig[],
+ };
+}
+
+export async function saveSubscriptionSettings(input: {
+ template: string;
+ extraProxyGroups: string;
+ profileName: string;
+}) {
+ const db = await ensureDb();
+ const upsert = db.prepare(
+ `
+ INSERT INTO app_settings (key, value, updated_at)
+ VALUES (?, ?, ?)
+ ON CONFLICT(key) DO UPDATE SET
+ value = excluded.value,
+ updated_at = excluded.updated_at
+ `,
+ );
+ const now = dbNow();
+ upsert.run("clash_template", input.template, now);
+ upsert.run("extra_proxy_groups", input.extraProxyGroups, now);
+ upsert.run("profile_name", input.profileName, now);
+}
+
+export async function saveProxyGroups(proxyGroups: ProxyGroupConfig[]) {
+ const db = await ensureDb();
+ const now = dbNow();
+ db.prepare(
+ `
+ INSERT INTO app_settings (key, value, updated_at)
+ VALUES (?, ?, ?)
+ ON CONFLICT(key) DO UPDATE SET
+ value = excluded.value,
+ updated_at = excluded.updated_at
+ `,
+ ).run("proxy_groups_json", JSON.stringify(proxyGroups), now);
+}
+
+export async function listSubscriptionNodes() {
+ const db = await ensureDb();
+ return db
+ .prepare(
+ `
+ SELECT *
+ FROM hy2_nodes
+ WHERE enabled = 1
+ ORDER BY created_at DESC
+ `,
+ )
+ .all() as Hy2NodeRow[];
+}
+
+export async function writeAuthAudit(input: AuditRecordInput) {
+ const db = await ensureDb();
+ db.prepare(
+ `
+ INSERT INTO auth_audits (
+ node_id, user_id, auth_id, username, addr, requested_tx, ok, reason, created_at
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `,
+ ).run(
+ input.nodeId ?? null,
+ input.userId ?? null,
+ input.authId ?? null,
+ input.username ?? null,
+ input.addr ?? null,
+ input.requestedTx ?? null,
+ input.ok ? 1 : 0,
+ input.reason,
+ dbNow(),
+ );
+}
+
+export async function listEnabledNodesForPolling() {
+ const db = await ensureDb();
+ return db
+ .prepare(
+ `
+ SELECT *
+ FROM hy2_nodes
+ WHERE enabled = 1
+ ORDER BY id ASC
+ `,
+ )
+ .all() as Hy2NodeRow[];
+}
+
+export async function shouldPollNode(nodeId: number, intervalSeconds: number) {
+ const db = await ensureDb();
+ const row = db
+ .prepare("SELECT last_polled_at FROM hy2_nodes WHERE id = ?")
+ .get(nodeId) as { last_polled_at: string | null } | undefined;
+
+ if (!row?.last_polled_at) {
+ return true;
+ }
+
+ return Date.now() - new Date(row.last_polled_at).getTime() >= intervalSeconds * 1000;
+}
+
+export function applyNodeSync(
+ db: Database.Database,
+ input: {
+ node: Hy2NodeRow;
+ startedAt: string;
+ finishedAt: string;
+ traffic: Record;
+ online: Record;
+ streamCount: number;
+ kickedAuthIds: string[];
+ },
+) {
+ const authIds = new Set([
+ ...Object.keys(input.traffic),
+ ...Object.keys(input.online),
+ ...input.kickedAuthIds,
+ ]);
+
+ const lookupUsers = db.prepare(
+ `
+ SELECT id, auth_id
+ FROM hy2_users
+ WHERE auth_id IN (${Array.from(authIds)
+ .map(() => "?")
+ .join(",") || "''"})
+ `,
+ );
+
+ const users = authIds.size
+ ? (lookupUsers.all(...Array.from(authIds)) as Array<{ id: number; auth_id: string }>)
+ : [];
+
+ const userMap = new Map(users.map((user) => [user.auth_id, user.id]));
+ const now = input.finishedAt;
+
+ const insertLedger = db.prepare(
+ `
+ INSERT INTO traffic_ledgers (
+ node_id, user_id, auth_id, tx_bytes, rx_bytes, window_started_at, window_ended_at, created_at
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ `,
+ );
+
+ const updateUserTraffic = db.prepare(
+ `
+ UPDATE hy2_users
+ SET
+ used_tx_bytes = used_tx_bytes + ?,
+ used_rx_bytes = used_rx_bytes + ?,
+ updated_at = ?
+ WHERE auth_id = ?
+ `,
+ );
+
+ for (const [authId, counters] of Object.entries(input.traffic)) {
+ insertLedger.run(
+ input.node.id,
+ userMap.get(authId) ?? null,
+ authId,
+ counters.tx,
+ counters.rx,
+ input.startedAt,
+ input.finishedAt,
+ now,
+ );
+ updateUserTraffic.run(counters.tx, counters.rx, now, authId);
+ }
+
+ db.prepare(
+ `
+ UPDATE node_user_presence
+ SET connections = 0, updated_at = ?
+ WHERE node_id = ?
+ `,
+ ).run(now, input.node.id);
+
+ const upsertPresence = db.prepare(
+ `
+ INSERT INTO node_user_presence (node_id, user_id, auth_id, connections, updated_at)
+ VALUES (?, ?, ?, ?, ?)
+ ON CONFLICT(node_id, auth_id) DO UPDATE SET
+ user_id = excluded.user_id,
+ connections = excluded.connections,
+ updated_at = excluded.updated_at
+ `,
+ );
+
+ for (const [authId, connections] of Object.entries(input.online)) {
+ upsertPresence.run(
+ input.node.id,
+ userMap.get(authId) ?? null,
+ authId,
+ connections,
+ now,
+ );
+ }
+
+ db.prepare(
+ `
+ UPDATE hy2_nodes
+ SET
+ last_polled_at = ?,
+ last_sync_ok_at = ?,
+ last_error_at = NULL,
+ last_error_message = NULL,
+ last_online_users = ?,
+ last_stream_count = ?,
+ updated_at = ?
+ WHERE id = ?
+ `,
+ ).run(
+ now,
+ now,
+ Object.keys(input.online).length,
+ input.streamCount,
+ now,
+ input.node.id,
+ );
+
+ db.prepare(
+ `
+ INSERT INTO node_sync_records (
+ node_id, started_at, finished_at, ok, traffic_entries, online_users, stream_count,
+ kicked_auth_ids, error_message, created_at
+ )
+ VALUES (?, ?, ?, 1, ?, ?, ?, ?, NULL, ?)
+ `,
+ ).run(
+ input.node.id,
+ input.startedAt,
+ input.finishedAt,
+ Object.keys(input.traffic).length,
+ Object.keys(input.online).length,
+ input.streamCount,
+ input.kickedAuthIds.join(","),
+ now,
+ );
+}
+
+export async function writeNodeSyncError(nodeId: number, startedAt: string, errorMessage: string) {
+ const db = await ensureDb();
+ const now = dbNow();
+
+ db.prepare(
+ `
+ UPDATE hy2_nodes
+ SET
+ last_polled_at = ?,
+ last_error_at = ?,
+ last_error_message = ?,
+ updated_at = ?
+ WHERE id = ?
+ `,
+ ).run(now, now, errorMessage, now, nodeId);
+
+ db.prepare(
+ `
+ INSERT INTO node_sync_records (
+ node_id, started_at, finished_at, ok, traffic_entries, online_users, stream_count,
+ kicked_auth_ids, error_message, created_at
+ )
+ VALUES (?, ?, ?, 0, 0, 0, 0, '', ?, ?)
+ `,
+ ).run(nodeId, startedAt, now, errorMessage, now);
+}
+
+export async function getKickCandidates(authIds: string[]) {
+ if (authIds.length === 0) {
+ return [];
+ }
+
+ const db = await ensureDb();
+ const placeholders = authIds.map(() => "?").join(",");
+ const rows = db
+ .prepare(
+ `
+ SELECT *
+ FROM hy2_users
+ WHERE auth_id IN (${placeholders})
+ `,
+ )
+ .all(...authIds) as Hy2UserRow[];
+
+ const map = new Map(rows.map((row) => [row.auth_id, row]));
+
+ return authIds.filter((authId) => {
+ const user = map.get(authId);
+ if (!user) return true;
+ return !getUserLifecycleState(user).ok;
+ });
+}
+
+export async function runInTransaction(callback: (db: Database.Database) => T) {
+ const db = await ensureDb();
+ const transaction = db.transaction(callback);
+ return transaction(getDb());
+}
diff --git a/lib/subscription.ts b/lib/subscription.ts
new file mode 100644
index 0000000..848c40a
--- /dev/null
+++ b/lib/subscription.ts
@@ -0,0 +1,196 @@
+import fs from "node:fs";
+import path from "node:path";
+
+import { decryptText } from "@/lib/crypto";
+import { getEnv } from "@/lib/env";
+import type {
+ Hy2NodeRow,
+ Hy2UserRow,
+ ProxyGroupConfig,
+} from "@/lib/store";
+import { buildNodeAuthUrl, formatBytes } from "@/lib/store";
+
+type ClashSettings = {
+ template: string;
+ extraProxyGroups: string;
+ profileName: string;
+ proxyGroups: ProxyGroupConfig[];
+};
+
+function readDefaultTemplate() {
+ const filePath = path.join(process.cwd(), "app", "config", "config.yaml");
+ return fs.readFileSync(filePath, "utf8").trimEnd();
+}
+
+function indent(text: string, spaces: number) {
+ const prefix = " ".repeat(spaces);
+ return text
+ .split("\n")
+ .map((line) => (line ? `${prefix}${line}` : line))
+ .join("\n");
+}
+
+function renderProxy(node: Hy2NodeRow, authString: string) {
+ const lines = [
+ `- name: "${node.client_name || node.name}"`,
+ " type: hysteria2",
+ ` server: ${node.server_host}`,
+ ` port: ${node.server_port}`,
+ ` password: "${authString}"`,
+ ];
+
+ if (node.sni) {
+ lines.push(` sni: ${node.sni}`);
+ }
+
+ if (node.skip_cert_verify) {
+ lines.push(" skip-cert-verify: true");
+ }
+
+ lines.push(" bandwidth:");
+ lines.push(` up: ${node.bandwidth_up_mbps} mbps`);
+ lines.push(` down: ${node.bandwidth_down_mbps} mbps`);
+
+ return lines.join("\n");
+}
+
+function renderGroupEntry(name: string, proxyNames: string[]) {
+ const quoted = proxyNames.map((name) => `"${name}"`).join(", ");
+ return [
+ ` - name: ${name}`,
+ " type: select",
+ ` proxies: [${quoted}]`,
+ ].join("\n");
+}
+
+export function encodeRFC5987ValueChars(value: string) {
+ return encodeURIComponent(value)
+ .replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
+ .replace(/\*/g, "%2A")
+ .replace(/%(7C|60|5E)/g, (_, code) =>
+ String.fromCharCode(Number.parseInt(code, 16)),
+ );
+}
+
+function buildAsciiFilenameFallback(value: string) {
+ const ascii = value
+ .normalize("NFKD")
+ .replace(/[^\x20-\x7E]+/g, " ")
+ .replace(/["\\]/g, "_")
+ .replace(/\s+/g, " ")
+ .trim();
+
+ return ascii || "subscription";
+}
+
+export function buildSubscriptionHeaders(user: Hy2UserRow, profileName: string) {
+ const expire = user.expires_at
+ ? Math.round(new Date(user.expires_at).getTime() / 1000)
+ : user.is_admin
+ ? 4102444800
+ : Math.round(Date.now() / 1000) + 3600 * 24 * 365 * 10;
+
+ const total =
+ user.is_admin || user.traffic_limit_bytes == null
+ ? "9223372036854775807"
+ : String(user.traffic_limit_bytes);
+ const fallbackFilename = buildAsciiFilenameFallback(profileName);
+
+ return {
+ "Content-Disposition": `attachment; filename="${fallbackFilename}"; filename*=UTF-8''${encodeRFC5987ValueChars(profileName)}`,
+ "Content-Type": "text/yaml; charset=utf-8",
+ "Profile-Update-Interval": "12",
+ "subscription-userinfo": `upload=${user.used_tx_bytes}; download=${user.used_rx_bytes}; total=${total}; expire=${expire}`,
+ };
+}
+
+export function buildSubscriptionUrl(token: string, origin = getEnv().APP_URL) {
+ return `${origin}/api/subscription/${token}/clash`;
+}
+
+export function buildNodeHy2Config(node: Hy2NodeRow, origin = getEnv().APP_URL) {
+ const authUrl = buildNodeAuthUrl(node.slug, node.auth_token, origin);
+
+ return [
+ `listen: ${node.hy2_listen || `:${node.server_port}`}`,
+ "tls:",
+ " cert: /etc/hysteria/server.crt",
+ " key: /etc/hysteria/server.key",
+ "auth:",
+ " type: http",
+ " http:",
+ ` url: ${authUrl}`,
+ " insecure: false",
+ "trafficStats:",
+ ` listen: ${node.traffic_stats_listen}`,
+ ` secret: ${node.traffic_stats_secret}`,
+ ].join("\n");
+}
+
+export function buildSubscriptionConfig(input: {
+ user: Hy2UserRow;
+ nodes: Hy2NodeRow[];
+ settings: ClashSettings;
+}) {
+ const authPassword = decryptText(input.user.auth_secret_encrypted);
+ if (!authPassword) {
+ throw new Error("user_secret_unavailable");
+ }
+
+ const authString = `${input.user.username}:${authPassword}`;
+ const eligibleNodes = input.nodes.filter(
+ (node) => node.enabled && node.server_host && node.server_port > 0,
+ );
+ const nodeNameBySlug = new Map(
+ eligibleNodes.map((node) => [node.slug, node.client_name || node.name]),
+ );
+ const proxyNames = eligibleNodes.map((node) => node.client_name || node.name);
+ const proxiesBlock = eligibleNodes.map((node) => renderProxy(node, authString)).join("\n");
+ const defaultTemplate = input.settings.template.trimEnd() || readDefaultTemplate();
+ const renderedGroups = [
+ renderGroupEntry(input.settings.profileName || "FeieProxy", proxyNames),
+ ...input.settings.proxyGroups
+ .map((group) => {
+ const members = group.memberSlugs
+ .map((slug) => nodeNameBySlug.get(slug))
+ .filter((name): name is string => Boolean(name));
+ if (members.length === 0) {
+ return null;
+ }
+
+ return renderGroupEntry(group.name, members);
+ })
+ .filter((group): group is string => Boolean(group)),
+ ].join("\n");
+ const extraGroups = input.settings.extraProxyGroups
+ .trim()
+ .replace(/^proxy-groups:\s*/i, "");
+
+ return [
+ defaultTemplate,
+ "",
+ "proxies:",
+ indent(proxiesBlock, 2),
+ "",
+ "proxy-groups:",
+ renderedGroups,
+ extraGroups ? `\n${extraGroups}` : "",
+ "",
+ ].join("\n");
+}
+
+export function getDefaultClashTemplate() {
+ return readDefaultTemplate();
+}
+
+export function describeRemainingTraffic(user: Hy2UserRow) {
+ if (user.is_admin || user.traffic_limit_bytes == null) {
+ return "不限流量";
+ }
+
+ const remaining = Math.max(
+ user.traffic_limit_bytes - user.used_tx_bytes - user.used_rx_bytes,
+ 0,
+ );
+ return `${formatBytes(remaining)} 剩余`;
+}
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..1ff136e 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ serverExternalPackages: ["better-sqlite3"],
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..3dd8158
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,5172 @@
+{
+ "name": "hy2-panel",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "hy2-panel",
+ "version": "0.1.0",
+ "dependencies": {
+ "bcryptjs": "^3.0.3",
+ "better-sqlite3": "^12.9.0",
+ "next": "16.2.3",
+ "react": "19.2.4",
+ "react-dom": "19.2.4",
+ "zod": "^4.3.6"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@types/better-sqlite3": "^7.6.13",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "16.2.3",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.2.4",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.2.4",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.5",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.5",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "16.2.3",
+ "license": "MIT"
+ },
+ "node_modules/@next/eslint-plugin-next": {
+ "version": "16.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "3.3.1"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "16.2.3",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "16.2.3",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nolyfill/is-core-module": {
+ "version": "1.0.39",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.4.0"
+ }
+ },
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.15",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.19.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.2.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.2.2",
+ "@tailwindcss/oxide-darwin-arm64": "4.2.2",
+ "@tailwindcss/oxide-darwin-x64": "4.2.2",
+ "@tailwindcss/oxide-freebsd-x64": "4.2.2",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.2",
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.2",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.2.2",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.2.2",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.2.2",
+ "@tailwindcss/oxide": "4.2.2",
+ "postcss": "^8.5.6",
+ "tailwindcss": "4.2.2"
+ }
+ },
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.39",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/type-utils": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.58.2",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.58.2",
+ "@typescript-eslint/types": "^8.58.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.58.2",
+ "@typescript-eslint/tsconfig-utils": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+ "version": "1.11.1",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+ "version": "1.11.1",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.11.3",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.18",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bcryptjs": {
+ "version": "3.0.3",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
+ "node_modules/better-sqlite3": {
+ "version": "12.9.0",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ },
+ "engines": {
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001787",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "license": "ISC"
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "license": "MIT"
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.336",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.20.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.1.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.3.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.5",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-next": {
+ "version": "16.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@next/eslint-plugin-next": "16.2.3",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-import-resolver-typescript": "^3.5.2",
+ "eslint-plugin-import": "^2.32.0",
+ "eslint-plugin-jsx-a11y": "^6.10.0",
+ "eslint-plugin-react": "^7.37.0",
+ "eslint-plugin-react-hooks": "^7.0.0",
+ "globals": "16.4.0",
+ "typescript-eslint": "^8.46.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=9.0.0",
+ "typescript": ">=3.3.1"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.16.1",
+ "resolve": "^2.0.0-next.6"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.10.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@nolyfill/is-core-module": "1.0.39",
+ "debug": "^4.4.0",
+ "get-tsconfig": "^4.10.0",
+ "is-bun-module": "^2.0.0",
+ "stable-hash": "^0.0.5",
+ "tinyglobby": "^0.2.13",
+ "unrs-resolver": "^1.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-import-resolver-typescript"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*",
+ "eslint-plugin-import-x": "*"
+ },
+ "peerDependenciesMeta": {
+ "eslint-plugin-import": {
+ "optional": true
+ },
+ "eslint-plugin-import-x": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.12.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.32.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.10.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "aria-query": "^5.3.2",
+ "array-includes": "^3.1.8",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "^4.10.0",
+ "axobject-query": "^4.1.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.includes": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "license": "MIT"
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "license": "MIT"
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "license": "MIT"
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.4.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "license": "ISC"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bun-module": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.7.1"
+ }
+ },
+ "node_modules/is-bun-module/node_modules/semver": {
+ "version": "7.7.4",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.23",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "license": "MIT"
+ },
+ "node_modules/napi-postinstall": {
+ "version": "0.3.4",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "napi-postinstall": "lib/cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/napi-postinstall"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/next": {
+ "version": "16.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "@next/env": "16.2.3",
+ "@swc/helpers": "0.5.15",
+ "baseline-browser-mapping": "^2.9.19",
+ "caniuse-lite": "^1.0.30001579",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.6"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "16.2.3",
+ "@next/swc-darwin-x64": "16.2.3",
+ "@next/swc-linux-arm64-gnu": "16.2.3",
+ "@next/swc-linux-arm64-musl": "16.2.3",
+ "@next/swc-linux-x64-gnu": "16.2.3",
+ "@next/swc-linux-x64-musl": "16.2.3",
+ "@next/swc-win32-arm64-msvc": "16.2.3",
+ "@next/swc-win32-x64-msvc": "16.2.3",
+ "sharp": "^0.34.5"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.51.1",
+ "babel-plugin-react-compiler": "*",
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@playwright/test": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/next/node_modules/postcss": {
+ "version": "8.4.31",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.89.0",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.7.4",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-exports-info": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array.prototype.flatmap": "^1.3.3",
+ "es-errors": "^1.3.0",
+ "object.entries": "^1.1.9",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.9",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "2.0.0-next.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "node-exports-info": "^1.6.0",
+ "object-keys": "^1.1.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.34.5",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.7.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.5",
+ "@img/sharp-darwin-x64": "0.34.5",
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
+ "@img/sharp-libvips-linux-arm": "1.2.4",
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
+ "@img/sharp-libvips-linux-x64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+ "@img/sharp-linux-arm": "0.34.5",
+ "@img/sharp-linux-arm64": "0.34.5",
+ "@img/sharp-linux-ppc64": "0.34.5",
+ "@img/sharp-linux-riscv64": "0.34.5",
+ "@img/sharp-linux-s390x": "0.34.5",
+ "@img/sharp-linux-x64": "0.34.5",
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
+ "@img/sharp-linuxmusl-x64": "0.34.5",
+ "@img/sharp-wasm32": "0.34.5",
+ "@img/sharp-win32-arm64": "0.34.5",
+ "@img/sharp-win32-ia32": "0.34.5",
+ "@img/sharp-win32-x64": "0.34.5"
+ }
+ },
+ "node_modules/sharp/node_modules/semver": {
+ "version": "7.7.4",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stable-hash": {
+ "version": "0.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string.prototype.includes": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.6",
+ "license": "MIT",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "license": "0BSD"
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.58.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unrs-resolver": {
+ "version": "1.11.1",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "napi-postinstall": "^0.3.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unrs-resolver"
+ },
+ "optionalDependencies": {
+ "@unrs/resolver-binding-android-arm-eabi": "1.11.1",
+ "@unrs/resolver-binding-android-arm64": "1.11.1",
+ "@unrs/resolver-binding-darwin-arm64": "1.11.1",
+ "@unrs/resolver-binding-darwin-x64": "1.11.1",
+ "@unrs/resolver-binding-freebsd-x64": "1.11.1",
+ "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1",
+ "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1",
+ "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-arm64-musl": "1.11.1",
+ "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1",
+ "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-x64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-x64-musl": "1.11.1",
+ "@unrs/resolver-binding-wasm32-wasi": "1.11.1",
+ "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1",
+ "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1",
+ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.20",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz",
+ "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz",
+ "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz",
+ "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz",
+ "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz",
+ "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz",
+ "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 4622d4d..4e50605 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"unrs-resolver"
],
"trustedDependencies": [
+ "better-sqlite3",
"sharp",
"unrs-resolver"
]