diff --git a/.env.test b/.env.test
new file mode 100644
index 00000000..2c87534c
--- /dev/null
+++ b/.env.test
@@ -0,0 +1 @@
+NODE_ENV=test
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..8dfeb6a4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,162 @@
+name: Bug Report / Bug 反馈
+description: Report a bug to help us improve / 报告 bug 帮助我们改进
+title: "[Bug]: "
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this bug report. Even a brief description helps us understand what went wrong. If you can share the details below, it usually helps us reproduce the issue without asking you to repeat yourself.
+
+ 感谢您愿意告诉我们这个问题。哪怕只是简要描述,也能帮助我们理解发生了什么。如果方便的话,补充下面这些信息,通常能帮助我们更快复现问题,也尽量减少反复向您确认。
+
+ - type: checkboxes
+ id: pre-submission
+ attributes:
+ label: Pre-submission Checklist / 提交前检查清单
+ description: Please confirm the following before submitting / 提交前请确认以下内容
+ options:
+ - label: I searched existing issues to avoid creating a duplicate / 我已检索现有 issue,确认这不是重复问题
+ required: true
+ - label: I tested on the latest release or current main branch / 我已在最新 release 或 main 分支版本中复现
+ required: true
+
+ - type: dropdown
+ id: deployment
+ attributes:
+ label: Deployment Type / 部署方式
+ description: Where did you reproduce the problem? / 您是在什么运行方式下复现这个问题的?
+ options:
+ - Docker Compose / Docker Compose 部署
+ - Docker run / Docker 命令部署
+ - Desktop app / 桌面端
+ - Local development / 本地源码开发
+ - Zeabur deployment / Zeabur 部署
+ - Render deployment / Render 部署
+ - Other / 其他
+ validations:
+ required: true
+
+ - type: dropdown
+ id: problem-area
+ attributes:
+ label: Problem Area / 问题模块
+ description: Which part of Metapi seems related? Optional, but helpful. / 您觉得问题和 Metapi 的哪个模块有关?选填,但有助于排查。
+ options:
+ - Site / Account Management / 站点与账号管理
+ - Token Management / 令牌管理
+ - Routing / 路由
+ - Proxy API (/v1/*) / 代理接口
+ - Model Marketplace / 模型广场
+ - Check-in / 签到
+ - Notifications / 通知
+ - Desktop App / 桌面端
+ - Deployment / Database / 部署或数据库
+ - Other / 其他
+ validations:
+ required: false
+
+ - type: textarea
+ id: app-version
+ attributes:
+ label: Metapi Version / Metapi 版本
+ description: Tell us the version, image tag, or commit if you know it. / 如果您知道,请填写版本号、镜像标签或 commit。
+ placeholder: |
+ e.g. v1.2.2 / latest / commit SHA
+ 例如 v1.2.2 / latest / commit SHA
+ validations:
+ required: true
+
+ - type: dropdown
+ id: database
+ attributes:
+ label: Database Type / 数据库类型
+ description: Which runtime database are you using? / 您当前运行时使用的是哪种数据库?
+ options:
+ - SQLite
+ - MySQL
+ - PostgreSQL
+ - Not sure / 不确定
+ validations:
+ required: false
+
+ - type: textarea
+ id: bug-description
+ attributes:
+ label: Bug Description / Bug 描述
+ description: A clear and concise description of what the bug is / 清晰简洁地描述这个 bug 是什么
+ placeholder: |
+ Describe the problem you observed.
+ 请描述您实际遇到的问题。
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label: Steps To Reproduce / 复现步骤
+ description: Steps to reproduce the behavior / 请尽量提供稳定复现的步骤
+ placeholder: |
+ 1. Go to ... / 打开 ...
+ 2. Configure ... / 配置 ...
+ 3. Click or send request ... / 点击或发送请求 ...
+ 4. Observe the result ... / 观察结果 ...
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected-behavior
+ attributes:
+ label: Expected Behavior / 预期行为
+ description: What did you expect to happen? / 您原本期望发生什么?
+ placeholder: |
+ Describe the expected behavior.
+ 请描述您期望的正确行为。
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual-behavior
+ attributes:
+ label: Actual Behavior / 实际行为
+ description: What actually happened? / 实际发生了什么?
+ placeholder: |
+ Describe the actual result.
+ 请描述实际发生的结果。
+ validations:
+ required: true
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Logs Or Error Output / 日志或错误输出
+ description: Paste relevant logs, stack traces, request/response snippets, or screenshots of error output. / 请粘贴相关日志、报错堆栈、请求/响应片段,或错误输出截图。
+ render: shell
+
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots / 截图
+ description: If applicable, add screenshots to help explain the problem. / 如果方便,请附上截图帮助说明问题。
+
+ - type: textarea
+ id: environment
+ attributes:
+ label: Environment Details / 环境信息
+ description: Include OS, host platform, reverse proxy, client tool, browser version if relevant, and anything else that might matter. / 请补充系统、宿主环境、反向代理、客户端工具,以及在相关时补充浏览器版本等信息。
+ placeholder: |
+ OS:
+ Deployment:
+ Host platform:
+ Reverse proxy:
+ Client tool:
+ Metapi version:
+ Database:
+
+ 操作系统:
+ 部署方式:
+ 宿主环境:
+ 反向代理:
+ 客户端工具:
+ Metapi 版本:
+ 数据库:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..0de19be2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Metapi README
+ url: https://github.com/cita-777/metapi#readme
+ about: Start with the README for setup, usage, and deployment details.
diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml
new file mode 100644
index 00000000..b4b8852a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/docs.yml
@@ -0,0 +1,65 @@
+name: Documentation Update / 文档反馈
+description: Report missing, incorrect, or unclear documentation / 反馈文档缺失、错误或不清晰的问题
+title: "[Docs]: "
+labels: ["docs"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Use this template when documentation is missing, incorrect, outdated, or hard to follow.
+
+ 如果文档存在缺失、错误、过时或难以理解的问题,请使用这个模板。
+
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: Before submitting / 提交前确认
+ options:
+ - label: I searched existing issues to avoid creating a duplicate / 我已搜索现有 issue,确认这不是重复反馈
+ required: true
+
+ - type: input
+ id: location
+ attributes:
+ label: Documentation Location / 文档位置
+ description: Link the page or file that needs attention. / 请填写需要修改的页面、章节或文件路径。
+ placeholder: e.g. README.md or docs/getting-started.md / 例如 README.md 或 docs/getting-started.md
+ validations:
+ required: true
+
+ - type: dropdown
+ id: doc-area
+ attributes:
+ label: Documentation Area / 文档类型
+ description: Which documentation area is affected? / 受影响的是哪一类文档?
+ options:
+ - README / 项目首页说明
+ - Getting Started / 快速开始
+ - Deployment / 部署
+ - Configuration / 配置
+ - Client Integration / 客户端接入
+ - FAQ / 常见问题
+ - Operations / 运维
+ - Other / 其他
+ validations:
+ required: false
+
+ - type: textarea
+ id: issue
+ attributes:
+ label: What Is Wrong Or Missing? / 哪里有问题或缺失?
+ description: Describe the missing, inaccurate, outdated, or confusing part. / 请描述缺失、不准确、过时或不清晰的内容。
+ placeholder: |
+ Describe the missing, inaccurate, or confusing part.
+ 请说明哪一部分有问题,以及为什么会造成困惑。
+ validations:
+ required: true
+
+ - type: textarea
+ id: fix
+ attributes:
+ label: Suggested Improvement / 建议修改方式
+ description: Describe how the documentation could be improved. / 请描述您建议的修改方式或理想写法。
+ placeholder: |
+ Describe how the documentation could be improved.
+ 请尽量给出建议内容、结构或示例。
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 00000000..8e978c65
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,78 @@
+name: Feature Request / 功能请求
+description: Suggest a new feature or enhancement / 建议新功能或改进
+title: "[Feature]: "
+labels: ["enhancement"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to suggest a new feature. A focused request with enough context helps us evaluate the idea much faster.
+
+ 感谢您提出功能建议。一个聚焦、信息完整的功能请求,通常能让我们更快判断需求价值和实现方向。
+
+ - type: checkboxes
+ id: pre-submission
+ attributes:
+ label: Pre-submission Checklist / 提交前检查清单
+ description: Please confirm the following before submitting / 提交前请确认以下内容
+ options:
+ - label: I searched existing issues to avoid creating a duplicate / 我已检索现有 issue,确认这不是重复需求
+ required: true
+ - label: I confirmed this feature is not already available in the latest release or main branch / 我已确认该功能尚未在最新 release 或 main 分支中提供
+ required: true
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem Or Use Case / 使用场景或问题背景
+ description: What problem are you trying to solve? What is inconvenient today? / 您想解决什么问题?当前使用上哪里不方便?
+ placeholder: |
+ Explain the workflow or pain point behind this request.
+ 请描述当前流程、痛点或为什么需要这个功能。
+ validations:
+ required: true
+
+ - type: dropdown
+ id: area
+ attributes:
+ label: Related Area / 相关模块
+ description: Which part of Metapi is this feature mainly about? / 这个功能主要涉及 Metapi 的哪个模块?
+ options:
+ - Site / Account Management / 站点与账号管理
+ - Token Management / 令牌管理
+ - Routing / 路由
+ - Proxy API (/v1/*) / 代理接口
+ - Model Marketplace / 模型广场
+ - Check-in / 签到
+ - Notifications / 通知
+ - Desktop App / 桌面端
+ - Deployment / Database / 部署或数据库
+ - Other / 其他
+ validations:
+ required: false
+
+ - type: textarea
+ id: proposal
+ attributes:
+ label: Proposed Solution / 建议的解决方案
+ description: Describe the change you want to see. / 请描述您希望增加的功能或行为变化。
+ placeholder: |
+ Explain the feature or behavior you want added.
+ 请尽量具体描述您希望如何实现。
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives Considered / 考虑过的替代方案
+ description: Describe any workaround or alternative approach you considered. / 请描述您考虑过的替代方案、当前 workaround,或为什么现有方式不够好。
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional Context / 其他信息
+ description: Add screenshots, examples, related issues, or links if they help explain the request. / 如果截图、示例、关联 issue 或参考链接有帮助,请补充在这里。
+ placeholder: |
+ You can include route examples, API paths, screenshots, upstream platform names, or migration concerns.
+ 可以补充路由示例、API 路径、截图、上游平台名称,或兼容性/迁移方面的考虑。
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
new file mode 100644
index 00000000..f1b84d4c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -0,0 +1,72 @@
+name: Question / 使用问题
+description: Ask for clarification or usage guidance / 询问使用方法、配置方式或行为解释
+title: "[Question]: "
+labels: ["question"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Use this template for support or clarification questions that are not bug reports or feature requests. The more context you share, the easier it is for us to give a direct answer.
+
+ 如果这是使用方式、配置方法或行为解释相关的问题,请使用这个模板。您提供的上下文越完整,我们越容易直接给出准确答案。
+
+ - type: checkboxes
+ id: checks
+ attributes:
+ label: Before submitting / 提交前确认
+ options:
+ - label: I searched existing issues and the README for an answer / 我已搜索现有 issue 和 README,但还没有找到答案
+ required: true
+
+ - type: textarea
+ id: question
+ attributes:
+ label: Question / 问题描述
+ description: What do you need help understanding? / 您具体想确认什么问题?
+ placeholder: |
+ Ask one clear question and include the relevant context.
+ 请尽量用一句话先说清楚您想问什么。
+ validations:
+ required: true
+
+ - type: dropdown
+ id: scenario
+ attributes:
+ label: Question Type / 问题类型
+ description: What kind of question is this? / 这是哪一类使用问题?
+ options:
+ - Deployment / 部署
+ - Configuration / 配置
+ - Client Integration / 客户端接入
+ - Routing / 路由
+ - Site / Account / Token / 站点、账号或令牌
+ - Desktop App / 桌面端
+ - Proxy API (/v1/*) / 代理接口
+ - Other / 其他
+ validations:
+ required: false
+
+ - type: textarea
+ id: tried
+ attributes:
+ label: What Have You Tried? / 您已经尝试过什么?
+ description: Include commands, config, screenshots, or investigation you already attempted. / 请补充您已经尝试过的命令、配置、截图或排查过程。
+
+ - type: textarea
+ id: environment
+ attributes:
+ label: Relevant Environment Or Config / 相关环境或配置
+ description: Paste config snippets, request examples, deployment details, or environment information if needed. / 如有需要,请补充配置片段、请求示例、部署方式或环境信息。
+ placeholder: |
+ Deployment:
+ Base URL:
+ Client:
+ Database:
+ Related config:
+
+ 部署方式:
+ Base URL:
+ 客户端:
+ 数据库:
+ 相关配置:
+ render: shell
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..95814580
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,130 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ time: "03:00"
+ timezone: Asia/Shanghai
+ open-pull-requests-limit: 6
+ ignore:
+ - dependency-name: "vite"
+ update-types:
+ - "version-update:semver-major"
+ - dependency-name: "@vitejs/plugin-react"
+ update-types:
+ - "version-update:semver-major"
+ - dependency-name: "vitest"
+ update-types:
+ - "version-update:semver-major"
+ - dependency-name: "react-router-dom"
+ update-types:
+ - "version-update:semver-major"
+ - dependency-name: "undici"
+ update-types:
+ - "version-update:semver-major"
+ - dependency-name: "cross-env"
+ update-types:
+ - "version-update:semver-major"
+ - dependency-name: "drizzle-orm"
+ update-types:
+ - "version-update:semver-major"
+ - dependency-name: "drizzle-kit"
+ update-types:
+ - "version-update:semver-major"
+ groups:
+ drizzle-risk:
+ patterns:
+ - "drizzle-orm"
+ - "drizzle-kit"
+ update-types:
+ - minor
+ - patch
+ build-tooling:
+ patterns:
+ - "vite"
+ - "@vitejs/plugin-react"
+ - "vitest"
+ - "tsx"
+ - "typescript"
+ update-types:
+ - minor
+ - patch
+ routing:
+ patterns:
+ - "react-router-dom"
+ update-types:
+ - minor
+ - patch
+ runtime-http:
+ patterns:
+ - "undici"
+ update-types:
+ - minor
+ - patch
+ node-runtime-sensitive:
+ patterns:
+ - "cross-env"
+ update-types:
+ - minor
+ - patch
+ minor-and-patch:
+ patterns:
+ - "*"
+ exclude-patterns:
+ - "drizzle-orm"
+ - "drizzle-kit"
+ - "vite"
+ - "@vitejs/plugin-react"
+ - "vitest"
+ - "tsx"
+ - "typescript"
+ - "react-router-dom"
+ - "undici"
+ - "cross-env"
+ update-types:
+ - minor
+ - patch
+ dev-dependencies:
+ dependency-type: development
+ patterns:
+ - "*"
+ update-types:
+ - minor
+ - patch
+ exclude-patterns:
+ - "drizzle-kit"
+ - "vite"
+ - "@vitejs/plugin-react"
+ - "vitest"
+ - "tsx"
+ - "typescript"
+ - "react-router-dom"
+ - "cross-env"
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ time: "03:00"
+ timezone: Asia/Shanghai
+ open-pull-requests-limit: 5
+ groups:
+ github-actions:
+ patterns:
+ - "*"
+
+ - package-ecosystem: docker
+ directory: /docker
+ schedule:
+ interval: weekly
+ day: monday
+ time: "03:00"
+ timezone: Asia/Shanghai
+ open-pull-requests-limit: 5
+ groups:
+ docker:
+ patterns:
+ - "*"
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 00000000..cb17b5c3
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,45 @@
+"area: db":
+ - changed-files:
+ - any-glob-to-any-file:
+ - "src/server/db/**"
+ - "drizzle/**"
+
+"area: server":
+ - all:
+ - changed-files:
+ - any-glob-to-any-file:
+ - "src/server/**"
+ - all-globs-to-all-files:
+ - "!src/server/db/**"
+
+"area: web":
+ - changed-files:
+ - any-glob-to-any-file:
+ - "src/web/**"
+
+"area: desktop":
+ - changed-files:
+ - any-glob-to-any-file:
+ - "src/desktop/**"
+ - "electron-builder.yml"
+ - "tsconfig.desktop.json"
+
+"area: docs":
+ - changed-files:
+ - any-glob-to-any-file:
+ - "docs/**"
+ - "README.md"
+ - "README_EN.md"
+
+"area: scripts":
+ - changed-files:
+ - any-glob-to-any-file:
+ - "scripts/**"
+
+"area: ci":
+ - changed-files:
+ - any-glob-to-any-file:
+ - ".github/**"
+ - "docker/**"
+ - "render.yaml"
+ - "zeabur-template.yaml"
diff --git a/.github/workflows/backfill-pr-labels.yml b/.github/workflows/backfill-pr-labels.yml
new file mode 100644
index 00000000..f8e1a800
--- /dev/null
+++ b/.github/workflows/backfill-pr-labels.yml
@@ -0,0 +1,176 @@
+name: Backfill PR Labels
+
+on:
+ workflow_dispatch:
+ inputs:
+ state:
+ description: Pull request state to scan
+ required: true
+ default: all
+ type: choice
+ options:
+ - open
+ - closed
+ - all
+ limit:
+ description: Maximum number of pull requests to inspect
+ required: true
+ default: "100"
+ type: string
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ backfill:
+ name: Backfill missing PR labels
+ runs-on: ubuntu-latest
+ steps:
+ - name: Backfill PR labels
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const inputs = context.payload.inputs || {};
+ const requestedState = inputs.state || 'all';
+ const limit = Number.parseInt(inputs.limit || '100', 10);
+ const sizeLabels = ['size: XS', 'size: S', 'size: M', 'size: L', 'size: XL', 'size: XXL'];
+ const areaRules = [
+ { label: 'area: db', include: ['src/server/db/', 'drizzle/'] },
+ { label: 'area: server', include: ['src/server/'], exclude: ['src/server/db/'] },
+ { label: 'area: web', include: ['src/web/'] },
+ { label: 'area: desktop', include: ['src/desktop/', 'electron-builder.yml', 'tsconfig.desktop.json'] },
+ { label: 'area: docs', include: ['docs/', 'README.md', 'README_EN.md'] },
+ { label: 'area: scripts', include: ['scripts/'] },
+ { label: 'area: ci', include: ['.github/', 'docker/', 'render.yaml', 'zeabur-template.yaml'] },
+ ];
+
+ if (!Number.isFinite(limit) || limit <= 0) {
+ core.setFailed('The limit input must be a positive integer.');
+ return;
+ }
+
+ function matchesRule(filename, rule) {
+ const included = rule.include.some((prefix) => filename === prefix || filename.startsWith(prefix));
+ if (!included) return false;
+ if (!rule.exclude) return true;
+ return !rule.exclude.some((prefix) => filename === prefix || filename.startsWith(prefix));
+ }
+
+ function computeAreaLabels(files) {
+ return areaRules
+ .filter((rule) => files.some((file) => matchesRule(file.filename, rule)))
+ .map((rule) => rule.label);
+ }
+
+ function computeSizeLabel(totalChanges) {
+ if (totalChanges < 50) return 'size: XS';
+ if (totalChanges < 200) return 'size: S';
+ if (totalChanges < 500) return 'size: M';
+ if (totalChanges < 1000) return 'size: L';
+ if (totalChanges < 2000) return 'size: XL';
+ return 'size: XXL';
+ }
+
+ const state = requestedState === 'all' ? 'all' : requestedState;
+ const pulls = await github.paginate(github.rest.pulls.list, {
+ owner,
+ repo,
+ state,
+ per_page: 100,
+ });
+
+ let inspected = 0;
+ let updated = 0;
+ let skipped = 0;
+ let warned = 0;
+
+ for (const pull of pulls) {
+ if (inspected >= limit) {
+ break;
+ }
+
+ inspected += 1;
+
+ const { data: fullPull } = await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number: pull.number,
+ });
+
+ const files = await github.paginate(github.rest.pulls.listFiles, {
+ owner,
+ repo,
+ pull_number: pull.number,
+ per_page: 100,
+ });
+
+ const desiredAreaLabels = computeAreaLabels(files);
+ const totalChanges = fullPull.additions + fullPull.deletions;
+ const hasValidSizeData = Number.isFinite(totalChanges) && totalChanges >= 0;
+ const currentLabels = pull.labels.map((label) => label.name);
+ const missingAreaLabels = desiredAreaLabels.filter((label) => !currentLabels.includes(label));
+ const currentSizeLabels = currentLabels.filter((label) => sizeLabels.includes(label));
+ const desiredSizeLabel = hasValidSizeData ? computeSizeLabel(totalChanges) : null;
+ const sizeMatches =
+ hasValidSizeData &&
+ currentSizeLabels.length === 1 &&
+ currentSizeLabels[0] === desiredSizeLabel;
+
+ if (!hasValidSizeData) {
+ warned += 1;
+ core.warning(`Skipping size label for PR #${pull.number}: invalid additions/deletions values.`);
+ }
+
+ if (missingAreaLabels.length === 0 && (hasValidSizeData ? sizeMatches : currentSizeLabels.length === 0)) {
+ skipped += 1;
+ continue;
+ }
+
+ if (hasValidSizeData) {
+ for (const label of currentSizeLabels) {
+ if (label === desiredSizeLabel) {
+ continue;
+ }
+
+ try {
+ await github.rest.issues.removeLabel({
+ owner,
+ repo,
+ issue_number: pull.number,
+ name: label,
+ });
+ } catch (error) {
+ if (error.status !== 404) {
+ throw error;
+ }
+ }
+ }
+ }
+
+ const labelsToAdd = [...missingAreaLabels];
+ if (hasValidSizeData && !sizeMatches) {
+ labelsToAdd.push(desiredSizeLabel);
+ }
+
+ if (labelsToAdd.length > 0) {
+ await github.rest.issues.addLabels({
+ owner,
+ repo,
+ issue_number: pull.number,
+ labels: labelsToAdd,
+ });
+ }
+
+ updated += 1;
+ }
+
+ core.summary
+ .addHeading('Backfill summary')
+ .addRaw(`Inspected PRs: ${inspected}\n`)
+ .addRaw(`Updated PRs: ${updated}\n`)
+ .addRaw(`Skipped PRs: ${skipped}\n`)
+ .addRaw(`Warnings: ${warned}\n`)
+ .write();
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5593d601..2139321d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,41 +3,340 @@ name: CI
on:
push:
branches: [main, master]
- tags: ['v*']
+ paths-ignore:
+ - 'docs/**'
+ - 'README.md'
+ - 'README_EN.md'
+ - 'CONTRIBUTING.md'
+ - 'SECURITY.md'
+ - 'CODE_OF_CONDUCT.md'
pull_request:
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+env:
+ NODE_VERSION: 22
+
jobs:
- test-and-build:
+ changes:
+ name: Detect Docs Changes
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ outputs:
+ docs: ${{ steps.filter.outputs.docs }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Detect docs-related changes
+ id: filter
+ uses: dorny/paths-filter@v4
+ with:
+ filters: |
+ docs:
+ - 'docs/**'
+ - 'README.md'
+ - 'README_EN.md'
+ - 'CONTRIBUTING.md'
+ - 'SECURITY.md'
+ - 'CODE_OF_CONDUCT.md'
+ - '.github/workflows/docs-pages.yml'
+
+ test-core:
+ name: Test Core
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
- node-version: 22
+ node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
- run: npm ci
+ run: npm ci --prefer-offline --no-audit --no-fund
- name: Run tests
+ env:
+ DB_PARITY_SKIP_LIVE_SCHEMA: 'true'
run: npm test
- - name: Build
- run: npm run build
+ build-web:
+ name: Build Web
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Build web
+ run: npm run build:web
+
+ build-server:
+ name: Build Server
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Build server
+ run: npm run build:server
+
+ build-desktop:
+ name: Build Desktop
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Build desktop
+ run: npm run build:desktop
+
+ typecheck:
+ name: Typecheck
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Run typecheck
+ run: npm run typecheck
+
+ repo-drift:
+ name: Repo Drift Check
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Run repo drift check
+ run: npm run repo:drift-check
+
+ schema-sqlite:
+ name: Schema Check (SQLite)
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Run sqlite schema parity
+ run: npm run test:schema:parity
+
+ - name: Run sqlite schema upgrade
+ run: npm run test:schema:upgrade
+
+ schema-mysql:
+ name: Schema Check (MySQL)
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ services:
+ mysql:
+ image: mysql:8.4
+ env:
+ MYSQL_DATABASE: metapi
+ MYSQL_ROOT_PASSWORD: root
+ ports:
+ - 3306:3306
+ options: >-
+ --health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=10
+ env:
+ DB_PARITY_MYSQL_URL: mysql://root:root@127.0.0.1:3306/metapi
+ DB_PARITY_SQLITE: 'false'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Run mysql schema parity
+ run: npm run test:schema:parity
+
+ - name: Run mysql schema upgrade
+ run: npm run test:schema:upgrade
+
+ - name: Run mysql runtime schema bootstrap
+ run: npm run test:schema:runtime
+
+ schema-postgres:
+ name: Schema Check (Postgres)
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ services:
+ postgres:
+ image: postgres:16
+ env:
+ POSTGRES_DB: metapi
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_USER: postgres
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd="pg_isready -U postgres -d metapi"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=10
+ env:
+ DB_PARITY_POSTGRES_URL: postgres://postgres:postgres@127.0.0.1:5432/metapi
+ DB_PARITY_SQLITE: 'false'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Run postgres schema parity
+ run: npm run test:schema:parity
+
+ - name: Run postgres schema upgrade
+ run: npm run test:schema:upgrade
+
+ - name: Run postgres runtime schema bootstrap
+ run: npm run test:schema:runtime
+
+ docs-build:
+ name: Build Docs
+ if: github.event_name == 'pull_request' && needs.changes.outputs.docs == 'true'
+ needs: changes
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Build docs
+ run: npm run docs:build
+
+ audit:
+ name: Audit Production Dependencies
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ # Keep PR audit informational so build/test/schema remain the merge gate.
+ continue-on-error: true
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
- name: Audit production dependencies
run: npm audit --omit=dev --audit-level=high
publish-docker-arch:
name: Publish Docker Image (${{ matrix.arch }})
+ if: github.event_name == 'push'
runs-on: ${{ matrix.runner }}
- needs: test-and-build
- if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')
timeout-minutes: 45
strategy:
fail-fast: false
@@ -49,10 +348,13 @@ jobs:
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
+ - arch: armv7
+ platform: linux/arm/v7
+ runner: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Ensure Docker Hub secrets are configured
run: |
@@ -62,23 +364,26 @@ jobs:
fi
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
+
+ - name: Set up QEMU
+ if: matrix.arch == 'armv7'
+ uses: docker/setup-qemu-action@v4
- name: Log in to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: 1467078763/metapi
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
- type=ref,event=tag
type=sha,format=short
- name: Add architecture suffix to tags
@@ -98,7 +403,7 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: Build and push ${{ matrix.platform }} image
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
@@ -111,14 +416,14 @@ jobs:
publish-docker:
name: Publish Docker Manifest
+ if: github.event_name == 'push'
runs-on: ubuntu-latest
needs: publish-docker-arch
- if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')
timeout-minutes: 15
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Ensure Docker Hub secrets are configured
run: |
@@ -128,23 +433,22 @@ jobs:
fi
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: 1467078763/metapi
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
- type=ref,event=tag
type=sha,format=short
- name: Create multi-arch manifests
@@ -156,5 +460,6 @@ jobs:
docker buildx imagetools create \
--tag "$tag" \
"${tag}-amd64" \
- "${tag}-arm64"
+ "${tag}-arm64" \
+ "${tag}-armv7"
done <<< "${{ steps.meta.outputs.tags }}"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 00000000..fba8f387
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,56 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [main, master]
+ paths-ignore:
+ - 'docs/**'
+ - 'README.md'
+ - 'README_EN.md'
+ - 'CONTRIBUTING.md'
+ - 'SECURITY.md'
+ - 'CODE_OF_CONDUCT.md'
+ pull_request:
+ paths-ignore:
+ - 'docs/**'
+ - 'README.md'
+ - 'README_EN.md'
+ - 'CONTRIBUTING.md'
+ - 'SECURITY.md'
+ - 'CODE_OF_CONDUCT.md'
+ schedule:
+ - cron: '19 18 * * 0'
+
+permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ analyze:
+ name: Analyze (JavaScript/TypeScript)
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['javascript-typescript']
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: none
+
+ - name: Analyze
+ uses: github/codeql-action/analyze@v4
+ with:
+ category: /language:${{ matrix.language }}
diff --git a/.github/workflows/docs-pages.yml b/.github/workflows/docs-pages.yml
index c3e881e4..712f2744 100644
--- a/.github/workflows/docs-pages.yml
+++ b/.github/workflows/docs-pages.yml
@@ -3,6 +3,11 @@ name: Docs Pages
on:
push:
branches: [main]
+ paths:
+ - 'docs/**'
+ - '.github/workflows/docs-pages.yml'
+ - 'package.json'
+ - 'package-lock.json'
workflow_dispatch:
permissions:
@@ -11,7 +16,7 @@ permissions:
id-token: write
concurrency:
- group: docs-pages
+ group: docs-pages-${{ github.ref }}
cancel-in-progress: true
jobs:
@@ -22,10 +27,10 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
@@ -40,7 +45,7 @@ jobs:
uses: actions/configure-pages@v5
- name: Upload pages artifact
- uses: actions/upload-pages-artifact@v3
+ uses: actions/upload-pages-artifact@v4
with:
path: docs/.vitepress/dist
diff --git a/.github/workflows/harness-drift-report.yml b/.github/workflows/harness-drift-report.yml
new file mode 100644
index 00000000..846ec1af
--- /dev/null
+++ b/.github/workflows/harness-drift-report.yml
@@ -0,0 +1,44 @@
+name: Harness Drift Report
+
+on:
+ schedule:
+ - cron: '37 17 * * 1'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+env:
+ NODE_VERSION: 22
+
+jobs:
+ drift-report:
+ name: Generate Drift Report
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --prefer-offline --no-audit --no-fund
+
+ - name: Generate repo drift report
+ run: npm run repo:drift-check -- --format markdown --output tmp/repo-drift-report.md --report-only
+
+ - name: Publish report to step summary
+ run: cat tmp/repo-drift-report.md >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Upload report artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: repo-drift-report
+ path: tmp/repo-drift-report.md
+ retention-days: 30
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 00000000..caeb4b0d
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,71 @@
+name: Label Pull Requests
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened, ready_for_review]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ apply-labels:
+ name: Apply area and size labels
+ runs-on: ubuntu-latest
+ steps:
+ - name: Apply area labels
+ uses: actions/labeler@v6
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ sync-labels: false
+
+ - name: Apply size label
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const pull_number = context.payload.pull_request.number;
+ const sizeLabels = ['size: XS', 'size: S', 'size: M', 'size: L', 'size: XL', 'size: XXL'];
+
+ const { data: pull } = await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number,
+ });
+
+ const totalChanges = pull.additions + pull.deletions;
+ let nextSize = 'size: XXL';
+ if (totalChanges < 50) nextSize = 'size: XS';
+ else if (totalChanges < 200) nextSize = 'size: S';
+ else if (totalChanges < 500) nextSize = 'size: M';
+ else if (totalChanges < 1000) nextSize = 'size: L';
+ else if (totalChanges < 2000) nextSize = 'size: XL';
+
+ const currentLabels = pull.labels.map((label) => label.name);
+ const existingSizeLabels = currentLabels.filter((label) => sizeLabels.includes(label));
+ const labelsToRemove = existingSizeLabels.filter((label) => label !== nextSize);
+
+ for (const label of labelsToRemove) {
+ try {
+ await github.rest.issues.removeLabel({
+ owner,
+ repo,
+ issue_number: pull_number,
+ name: label,
+ });
+ } catch (error) {
+ if (error.status !== 404) {
+ throw error;
+ }
+ }
+ }
+
+ if (!currentLabels.includes(nextSize)) {
+ await github.rest.issues.addLabels({
+ owner,
+ repo,
+ issue_number: pull_number,
+ labels: [nextSize],
+ });
+ }
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d5afa11a..08720704 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,264 +1,289 @@
-name: Release
-
-on:
- push:
- tags: ['v*']
- workflow_dispatch:
-
-permissions:
- contents: write
- packages: write
-
-concurrency:
- group: release-${{ github.ref }}
- cancel-in-progress: false
-
-jobs:
- verify:
- name: Verify
- runs-on: ubuntu-latest
- timeout-minutes: 20
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: npm
-
- - name: Install dependencies
- run: npm ci
-
- - name: Run tests
- run: npm test
-
- - name: Build
- run: npm run build
-
- - name: Upload built dist
- uses: actions/upload-artifact@v4
- with:
- name: metapi-dist-${{ github.sha }}
- path: dist
- if-no-files-found: error
-
- build-packages:
- name: Build Desktop Package (${{ matrix.os }})
- needs: verify
- runs-on: ${{ matrix.os }}
- timeout-minutes: 60
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: npm
-
- - name: Restore Electron cache (Windows)
- if: runner.os == 'Windows'
- uses: actions/cache@v4
- with:
- path: |
- ${{ runner.temp }}/.cache/electron
- ${{ runner.temp }}/.cache/electron-builder
- ${{ env.LOCALAPPDATA }}\electron\Cache
- ${{ env.LOCALAPPDATA }}\electron-builder\Cache
- key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
- restore-keys: |
- electron-cache-${{ runner.os }}-
-
- - name: Restore Electron cache (Linux)
- if: runner.os == 'Linux'
- uses: actions/cache@v4
- with:
- path: |
- ${{ runner.temp }}/.cache/electron
- ${{ runner.temp }}/.cache/electron-builder
- ${{ runner.temp }}/.cache/appimage-tools
- ~/.cache/electron
- ~/.cache/electron-builder
- key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
- restore-keys: |
- electron-cache-${{ runner.os }}-
-
- - name: Restore Electron cache (macOS)
- if: runner.os == 'macOS'
- uses: actions/cache@v4
- with:
- path: |
- ${{ runner.temp }}/.cache/electron
- ${{ runner.temp }}/.cache/electron-builder
- ~/Library/Caches/electron
- ~/Library/Caches/electron-builder
- key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
- restore-keys: |
- electron-cache-${{ runner.os }}-
-
- - name: Install dependencies
- run: npm ci
-
- - name: Download built dist
- uses: actions/download-artifact@v4
- with:
- name: metapi-dist-${{ github.sha }}
- path: dist
-
- - name: Build desktop artifacts (Windows)
- if: runner.os == 'Windows'
- shell: pwsh
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- npm_config_cache: ${{ runner.temp }}/.npm
- ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
- ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
- ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
- npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
- WIN_CSC_LINK_INPUT: ${{ secrets.CSC_LINK }}
- WIN_CSC_KEY_PASSWORD_INPUT: ${{ secrets.CSC_KEY_PASSWORD }}
- run: |
- if ($env:WIN_CSC_LINK_INPUT) { $env:WIN_CSC_LINK = $env:WIN_CSC_LINK_INPUT }
- if ($env:WIN_CSC_KEY_PASSWORD_INPUT) { $env:WIN_CSC_KEY_PASSWORD = $env:WIN_CSC_KEY_PASSWORD_INPUT }
- for ($attempt = 1; $attempt -le 2; $attempt++) {
- Write-Host "Desktop build attempt $attempt/2"
- npm run package:desktop
- if ($LASTEXITCODE -eq 0) {
- break
- }
- if ($attempt -eq 2) {
- exit $LASTEXITCODE
- }
- Start-Sleep -Seconds 90
- }
-
- - name: Smoke test packaged native modules (Windows)
- if: runner.os == 'Windows'
- shell: pwsh
- env:
- ELECTRON_RUN_AS_NODE: 1
- run: |
- $appRoot = Join-Path $PWD 'release\win-unpacked\resources\app'
- $exePath = Join-Path $PWD 'release\win-unpacked\Metapi.exe'
- Push-Location $appRoot
- try {
- & $exePath -e "require('./node_modules/better-sqlite3'); console.log('better-sqlite3 ok')"
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
- } finally {
- Pop-Location
- }
-
- - name: Build desktop artifacts (Linux)
- if: runner.os == 'Linux'
- shell: bash
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- npm_config_cache: ${{ runner.temp }}/.npm
- ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
- ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
- ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
- npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
- run: |
- set -euo pipefail
- for attempt in 1 2; do
- echo "Desktop build attempt ${attempt}/2"
- if npm run package:desktop; then
- break
- fi
- if [ "$attempt" -eq 2 ]; then
- exit 1
- fi
- sleep 90
- done
-
- - name: Build desktop artifacts (macOS)
- if: runner.os == 'macOS'
- shell: bash
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- npm_config_cache: ${{ runner.temp }}/.npm
- ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
- ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
- ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
- npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
- APPLE_ID_INPUT: ${{ secrets.APPLE_ID }}
- APPLE_APP_SPECIFIC_PASSWORD_INPUT: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
- APPLE_TEAM_ID_INPUT: ${{ secrets.APPLE_TEAM_ID }}
- run: |
- set -euo pipefail
- if [ -n "${APPLE_ID_INPUT:-}" ]; then export APPLE_ID="$APPLE_ID_INPUT"; fi
- if [ -n "${APPLE_APP_SPECIFIC_PASSWORD_INPUT:-}" ]; then export APPLE_APP_SPECIFIC_PASSWORD="$APPLE_APP_SPECIFIC_PASSWORD_INPUT"; fi
- if [ -n "${APPLE_TEAM_ID_INPUT:-}" ]; then export APPLE_TEAM_ID="$APPLE_TEAM_ID_INPUT"; fi
- for attempt in 1 2; do
- echo "Desktop build attempt ${attempt}/2"
- if npm run package:desktop; then
- break
- fi
- if [ "$attempt" -eq 2 ]; then
- exit 1
- fi
- sleep 90
- done
-
- - name: Upload desktop package
- uses: actions/upload-artifact@v4
- with:
- name: metapi-desktop-${{ github.ref_name }}-${{ runner.os }}-${{ runner.arch }}
- path: |
- release/*.exe
- release/*.dmg
- release/*.zip
- release/*.AppImage
- release/*.deb
- release/*.blockmap
- release/latest*.yml
- release/latest*.json
- release/builder-debug.yml
- if-no-files-found: error
-
- publish-release:
- name: Publish GitHub Release
- needs: build-packages
- runs-on: ubuntu-latest
- timeout-minutes: 15
- if: startsWith(github.ref, 'refs/tags/')
-
- steps:
- - name: Download package artifacts
- uses: actions/download-artifact@v4
- with:
- path: release-assets
- merge-multiple: true
-
- - name: Show release files
- run: ls -la release-assets
-
- - name: Upload Release Assets
- uses: softprops/action-gh-release@v2
- with:
- tag_name: ${{ github.ref_name }}
- generate_release_notes: true
- files: release-assets/*
-
- publish-docker-arch:
- name: Publish Docker Image (${{ matrix.arch }})
- needs: build-packages
- runs-on: ${{ matrix.runner }}
- timeout-minutes: 45
- if: startsWith(github.ref, 'refs/tags/')
- strategy:
- fail-fast: false
+name: Release
+
+on:
+ push:
+ tags: ['v*']
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ packages: write
+
+concurrency:
+ group: release-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ verify:
+ name: Verify
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests
+ run: npm test
+
+ - name: Build
+ run: npm run build
+
+ - name: Upload built dist
+ uses: actions/upload-artifact@v7
+ with:
+ name: metapi-dist-${{ github.sha }}
+ path: dist
+ if-no-files-found: error
+
+ build-packages:
+ name: Build Desktop Package (${{ matrix.platform }})
+ needs: verify
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 60
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux
+ runner: ubuntu-latest
+ artifactLabel: linux-x64
+ - platform: windows
+ runner: windows-latest
+ artifactLabel: windows-x64
+ - platform: mac-arm64
+ runner: macos-15
+ artifactLabel: mac-arm64
+ packageCommand: npm run package:desktop -- --mac --arm64
+ expectedMacArch: arm64
+ - platform: mac-x64
+ runner: macos-15-intel
+ artifactLabel: mac-x64
+ packageCommand: npm run package:desktop:mac:intel
+ expectedMacArch: x64
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Restore Electron cache (Windows)
+ if: runner.os == 'Windows'
+ uses: actions/cache@v5
+ with:
+ path: |
+ ${{ runner.temp }}/.cache/electron
+ ${{ runner.temp }}/.cache/electron-builder
+ ${{ env.LOCALAPPDATA }}\electron\Cache
+ ${{ env.LOCALAPPDATA }}\electron-builder\Cache
+ key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
+ restore-keys: |
+ electron-cache-${{ runner.os }}-
+
+ - name: Restore Electron cache (Linux)
+ if: runner.os == 'Linux'
+ uses: actions/cache@v5
+ with:
+ path: |
+ ${{ runner.temp }}/.cache/electron
+ ${{ runner.temp }}/.cache/electron-builder
+ ${{ runner.temp }}/.cache/appimage-tools
+ ~/.cache/electron
+ ~/.cache/electron-builder
+ key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
+ restore-keys: |
+ electron-cache-${{ runner.os }}-
+
+ - name: Restore Electron cache (macOS)
+ if: runner.os == 'macOS'
+ uses: actions/cache@v5
+ with:
+ path: |
+ ${{ runner.temp }}/.cache/electron
+ ${{ runner.temp }}/.cache/electron-builder
+ ~/Library/Caches/electron
+ ~/Library/Caches/electron-builder
+ key: electron-cache-${{ runner.os }}-${{ hashFiles('package-lock.json', 'package.json', 'electron-builder.yml') }}
+ restore-keys: |
+ electron-cache-${{ runner.os }}-
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Download built dist
+ uses: actions/download-artifact@v8
+ with:
+ name: metapi-dist-${{ github.sha }}
+ path: dist
+
+ - name: Build desktop artifacts (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ npm_config_cache: ${{ runner.temp }}/.npm
+ ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
+ ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
+ ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
+ npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
+ WIN_CSC_LINK_INPUT: ${{ secrets.CSC_LINK }}
+ WIN_CSC_KEY_PASSWORD_INPUT: ${{ secrets.CSC_KEY_PASSWORD }}
+ run: |
+ if ($env:WIN_CSC_LINK_INPUT) { $env:WIN_CSC_LINK = $env:WIN_CSC_LINK_INPUT }
+ if ($env:WIN_CSC_KEY_PASSWORD_INPUT) { $env:WIN_CSC_KEY_PASSWORD = $env:WIN_CSC_KEY_PASSWORD_INPUT }
+ for ($attempt = 1; $attempt -le 2; $attempt++) {
+ Write-Host "Desktop build attempt $attempt/2"
+ npm run package:desktop
+ if ($LASTEXITCODE -eq 0) {
+ break
+ }
+ if ($attempt -eq 2) {
+ exit $LASTEXITCODE
+ }
+ Start-Sleep -Seconds 90
+ }
+
+ - name: Smoke test packaged native modules (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ env:
+ ELECTRON_RUN_AS_NODE: 1
+ run: |
+ $appRoot = Join-Path $PWD 'release\win-unpacked\resources\app'
+ $exePath = Join-Path $PWD 'release\win-unpacked\Metapi.exe'
+ Push-Location $appRoot
+ try {
+ & $exePath -e "require('./node_modules/better-sqlite3'); console.log('better-sqlite3 ok')"
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ } finally {
+ Pop-Location
+ }
+
+ - name: Build desktop artifacts (Linux)
+ if: runner.os == 'Linux'
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ npm_config_cache: ${{ runner.temp }}/.npm
+ ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
+ ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
+ ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
+ npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
+ run: |
+ set -euo pipefail
+ for attempt in 1 2; do
+ echo "Desktop build attempt ${attempt}/2"
+ if npm run package:desktop; then
+ break
+ fi
+ if [ "$attempt" -eq 2 ]; then
+ exit 1
+ fi
+ sleep 90
+ done
+
+ - name: Build desktop artifacts (macOS)
+ if: startsWith(matrix.platform, 'mac-')
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ npm_config_cache: ${{ runner.temp }}/.npm
+ ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
+ ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
+ ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
+ npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
+ APPLE_ID_INPUT: ${{ secrets.APPLE_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD_INPUT: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ APPLE_TEAM_ID_INPUT: ${{ secrets.APPLE_TEAM_ID }}
+ PACKAGE_COMMAND: ${{ matrix.packageCommand }}
+ run: |
+ set -euo pipefail
+ if [ -n "${APPLE_ID_INPUT:-}" ]; then export APPLE_ID="$APPLE_ID_INPUT"; fi
+ if [ -n "${APPLE_APP_SPECIFIC_PASSWORD_INPUT:-}" ]; then export APPLE_APP_SPECIFIC_PASSWORD="$APPLE_APP_SPECIFIC_PASSWORD_INPUT"; fi
+ if [ -n "${APPLE_TEAM_ID_INPUT:-}" ]; then export APPLE_TEAM_ID="$APPLE_TEAM_ID_INPUT"; fi
+ for attempt in 1 2; do
+ echo "Desktop build (${PACKAGE_COMMAND}) attempt ${attempt}/2"
+ if eval "${PACKAGE_COMMAND}"; then
+ break
+ fi
+ if [ "$attempt" -eq 2 ]; then
+ exit 1
+ fi
+ sleep 90
+ done
+
+ - name: Verify packaged macOS architecture
+ if: startsWith(matrix.platform, 'mac-')
+ shell: bash
+ env:
+ expectedMacArch: ${{ matrix.expectedMacArch }}
+ run: |
+ node scripts/desktop/verifyMacArchitecture.mjs --release-dir release --expected-arch "$expectedMacArch"
+
+ - name: Upload desktop package
+ uses: actions/upload-artifact@v7
+ with:
+ name: metapi-desktop-${{ github.run_id }}-${{ matrix.artifactLabel }}
+ path: |
+ release/*.exe
+ release/*.dmg
+ release/*.zip
+ release/*.AppImage
+ release/*.deb
+ release/*.blockmap
+ release/latest*.yml
+ release/latest*.json
+ release/builder-debug.yml
+ if-no-files-found: error
+
+ publish-release:
+ name: Publish GitHub Release
+ needs: build-packages
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ if: startsWith(github.ref, 'refs/tags/')
+
+ steps:
+ - name: Download package artifacts
+ uses: actions/download-artifact@v8
+ with:
+ path: release-assets
+ merge-multiple: true
+
+ - name: Show release files
+ run: ls -la release-assets
+
+ - name: Upload Release Assets
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ github.ref_name }}
+ generate_release_notes: true
+ files: release-assets/*
+
+ publish-docker-arch:
+ name: Publish Docker Image (${{ matrix.arch }})
+ needs: build-packages
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 45
+ if: startsWith(github.ref, 'refs/tags/')
+ strategy:
+ fail-fast: false
matrix:
include:
- arch: amd64
@@ -267,126 +292,134 @@ jobs:
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Ensure Docker Hub secrets are configured
- run: |
- if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then
- echo "Missing Docker Hub secrets: DOCKERHUB_USERNAME / DOCKERHUB_TOKEN"
- exit 1
- fi
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- - name: Docker metadata
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: |
- 1467078763/metapi
- ghcr.io/${{ github.repository }}
- tags: |
- type=ref,event=tag
- type=raw,value=latest
-
- - name: Add architecture suffix to tags
- id: arch-tags
- shell: bash
- run: |
- set -euo pipefail
- rm -f tags.txt
- while IFS= read -r tag; do
- [ -n "$tag" ] || continue
- echo "${tag}-${{ matrix.arch }}" >> tags.txt
- done <<< "${{ steps.meta.outputs.tags }}"
- {
- echo 'tags<> "$GITHUB_OUTPUT"
-
- - name: Build and push ${{ matrix.platform }} image
- uses: docker/build-push-action@v6
- with:
- context: .
- file: docker/Dockerfile
- platforms: ${{ matrix.platform }}
- push: true
- tags: ${{ steps.arch-tags.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=gha,scope=release-${{ matrix.arch }}
- cache-to: type=gha,mode=max,scope=release-${{ matrix.arch }}
-
- publish-docker:
- name: Publish Docker Manifests
- needs: publish-docker-arch
- runs-on: ubuntu-latest
- timeout-minutes: 15
- if: startsWith(github.ref, 'refs/tags/')
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Ensure Docker Hub secrets are configured
- run: |
- if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then
- echo "Missing Docker Hub secrets: DOCKERHUB_USERNAME / DOCKERHUB_TOKEN"
- exit 1
- fi
-
+ - arch: armv7
+ platform: linux/arm/v7
+ runner: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Ensure Docker Hub secrets are configured
+ run: |
+ if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then
+ echo "Missing Docker Hub secrets: DOCKERHUB_USERNAME / DOCKERHUB_TOKEN"
+ exit 1
+ fi
+
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- - name: Docker metadata
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: |
- 1467078763/metapi
- ghcr.io/${{ github.repository }}
- tags: |
- type=ref,event=tag
- type=raw,value=latest
-
- - name: Create multi-arch manifests
- shell: bash
- run: |
- set -euo pipefail
- while IFS= read -r tag; do
- [ -n "$tag" ] || continue
+ uses: docker/setup-buildx-action@v4
+
+ - name: Set up QEMU
+ if: matrix.arch == 'armv7'
+ uses: docker/setup-qemu-action@v4
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v4
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Docker metadata
+ id: meta
+ uses: docker/metadata-action@v6
+ with:
+ images: |
+ 1467078763/metapi
+ ghcr.io/${{ github.repository }}
+ tags: |
+ type=ref,event=tag
+ type=raw,value=latest
+
+ - name: Add architecture suffix to tags
+ id: arch-tags
+ shell: bash
+ run: |
+ set -euo pipefail
+ rm -f tags.txt
+ while IFS= read -r tag; do
+ [ -n "$tag" ] || continue
+ echo "${tag}-${{ matrix.arch }}" >> tags.txt
+ done <<< "${{ steps.meta.outputs.tags }}"
+ {
+ echo 'tags<> "$GITHUB_OUTPUT"
+
+ - name: Build and push ${{ matrix.platform }} image
+ uses: docker/build-push-action@v7
+ with:
+ context: .
+ file: docker/Dockerfile
+ platforms: ${{ matrix.platform }}
+ push: true
+ tags: ${{ steps.arch-tags.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha,scope=release-${{ matrix.arch }}
+ cache-to: type=gha,mode=max,scope=release-${{ matrix.arch }}
+
+ publish-docker:
+ name: Publish Docker Manifests
+ needs: publish-docker-arch
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ if: startsWith(github.ref, 'refs/tags/')
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Ensure Docker Hub secrets are configured
+ run: |
+ if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then
+ echo "Missing Docker Hub secrets: DOCKERHUB_USERNAME / DOCKERHUB_TOKEN"
+ exit 1
+ fi
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v4
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Docker metadata
+ id: meta
+ uses: docker/metadata-action@v6
+ with:
+ images: |
+ 1467078763/metapi
+ ghcr.io/${{ github.repository }}
+ tags: |
+ type=ref,event=tag
+ type=raw,value=latest
+
+ - name: Create multi-arch manifests
+ shell: bash
+ run: |
+ set -euo pipefail
+ while IFS= read -r tag; do
+ [ -n "$tag" ] || continue
docker buildx imagetools create \
--tag "$tag" \
"${tag}-amd64" \
- "${tag}-arm64"
+ "${tag}-arm64" \
+ "${tag}-armv7"
done <<< "${{ steps.meta.outputs.tags }}"
diff --git a/.gitignore b/.gitignore
index 32353900..d789c71e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,11 +9,13 @@ data/*
logs/
tmp/*
!tmp/.gitkeep
+.worktrees/
# Local desktop/debug artifacts
/.tmp-*
/main.js
/NUL
+/.playwright-cli/
.DS_Store
Thumbs.db
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 00000000..5bd68117
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+20.19.0
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..1a2ee166
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,74 @@
+# Metapi Engineering Rules
+
+These rules apply to the whole repository unless a deeper `AGENTS.md` overrides
+them. They are intentionally opinionated and mechanical so humans and agents can
+make small, consistent changes without re-learning the codebase each time.
+
+## Golden Principles
+
+- Prefer one source of truth. If a helper, contract, or workflow already owns
+ an invariant, extend it instead of creating a parallel implementation.
+- Fix the family, not just the symptom. When a bug comes from a repeated
+ pattern, sweep adjacent paths in the same subsystem before calling the work
+ done.
+- Keep changes narrow and reviewable. Land one coherent slice at a time and
+ avoid bundling unrelated cleanup into the same patch.
+
+## Server Layers
+
+- `src/server/routes/**` are adapters, not owners. Route files may register
+ Fastify endpoints, parse request context, and delegate. They must not own
+ protocol conversion, retry policy, stream lifecycle, billing, or
+ persistence.
+- If a helper is imported by anything outside one route file, it does not
+ belong under `src/server/routes/proxy/`.
+- `src/server/proxy-core/**` owns proxy orchestration. Endpoint fallback should
+ flow through `executeEndpointFlow()`. Channel/session bookkeeping should flow
+ through `sharedSurface.ts`.
+- `src/server/transformers/**` are protocol-pure. Do not import from
+ `src/server/routes/**`, Fastify, OAuth services, token router, or runtime
+ dispatch modules. If a transformer needs a shared contract, move it to a
+ neutral module first.
+- Whole-body upstream reads in proxy orchestration should use
+ `readRuntimeResponseText()` instead of direct `.text()` reads.
+
+## Platform And Routing Rules
+
+- Platform behavior must be explicit. Detection, endpoint preference, discovery
+ transport, and management capability should come from one declared capability
+ story, not scattered `if platform === ...` branches.
+- Thin adapters must stay honest. Do not let a platform look feature-complete
+ through inherited defaults if the underlying upstream does not support the
+ feature.
+- Retry classification and routing health classification should share the same
+ failure vocabulary whenever possible.
+
+## Database Rules
+
+- One schema change requires three synchronized outputs: update the Drizzle
+ schema, update SQLite migration history, and regenerate checked-in schema
+ artifacts together.
+- Cross-dialect bootstrap and upgrade SQL must be generated from the schema
+ contract. Do not hand-write new MySQL/Postgres schema patches in feature
+ code.
+- Legacy schema compatibility is temporary and spec-owned. Additive startup
+ shims should stay narrow and trace back to a feature compatibility spec.
+
+## Web Rules
+
+- Pages are orchestration surfaces, not shared utility libraries. Do not import
+ one top-level page from another top-level page.
+- Mobile behavior should reuse existing shared primitives first:
+ `ResponsiveFilterPanel`, `ResponsiveBatchActionBar`, `MobileCard`,
+ `useIsMobile`, and `mobileLayout.ts`.
+- When a page grows a second complex modal, drawer, or panel family, extract it
+ into a domain subfolder before adding more inline state and rendering logic.
+
+## Guardrails
+
+- Run `npm run repo:drift-check` before finishing changes that touch shared
+ architecture boundaries.
+- If you add a new boundary-heavy module, add or extend an architecture test in
+ the same area so the rule becomes executable.
+- Keep local planning files under `docs/plans/`. They are intentionally ignored
+ by git and should not be treated as published documentation.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index a88b8157..166d5ab5 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,44 +1,106 @@
-# Code of Conduct
+# Code of Conduct / 行为准则
-## Our Commitment
+## Our Pledge / 我们的承诺
-We are committed to providing a welcoming, harassment-free community for everyone.
+We as members, contributors, and maintainers pledge to make participation in the Metapi community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
-## Expected Behavior
+作为 Metapi 社区的成员、贡献者和维护者,我们承诺让每个人都能在无骚扰的环境中参与,无论年龄、体型、残疾、种族、性别认同和表达、经验水平、国籍、个人外貌、种族、宗教或性取向如何。
-- Be respectful and constructive.
-- Focus on technical discussion, not personal attacks.
-- Assume good intent and ask clarifying questions before escalating.
+## Our Standards / 我们的标准
-## Unacceptable Behavior
+### Expected Behavior / 期望的行为
-- Harassment, discrimination, or abusive language
-- Personal attacks or intimidation
-- Publishing others' private information without consent
-- Deliberate disruption, trolling, or bad-faith escalation
-- Repeatedly ignoring maintainer moderation requests
+Examples of behavior that contributes to a positive environment / 有助于营造积极环境的行为示例:
-## Reporting
+- Using welcoming and inclusive language / 使用友好和包容的语言
+- Being respectful of differing viewpoints and experiences / 尊重不同的观点和经验
+- Gracefully accepting constructive criticism / 优雅地接受建设性批评
+- Focusing on what is best for the community / 关注对社区最有利的事情
+- Showing empathy towards other community members / 对其他社区成员表现出同理心
+- Providing helpful and constructive feedback / 提供有益和建设性的反馈
+- Assuming good intent and asking clarifying questions / 假定善意并提出澄清性问题
-- Email `cita-777@users.noreply.github.com` with subject prefix `[metapi conduct]`.
-- Include links, screenshots, timestamps, and any context that helps reconstruct the incident.
-- If the incident is happening directly on GitHub and needs urgent platform action, also use GitHub's built-in report tools.
-- We will keep reporter details as private as practical and do not allow retaliation for good-faith reports.
+### Unacceptable Behavior / 不可接受的行为
-## Enforcement
+Examples of unacceptable behavior / 不可接受行为的示例:
-Project maintainers are responsible for clarifying and enforcing this code of conduct.
+- Harassment, discrimination, or abusive language / 骚扰、歧视或辱骂性语言
+- Trolling, insulting/derogatory comments, and personal or political attacks / 挑衅、侮辱性/贬损性评论以及人身或政治攻击
+- Public or private harassment or intimidation / 公开或私下的骚扰或恐吓
+- Publishing others' private information (e.g., physical or email address) without explicit permission / 未经明确许可发布他人的私人信息(例如,实际地址或电子邮件地址)
+- Deliberate disruption of discussions or project activities / 故意破坏讨论或项目活动
+- Repeatedly ignoring maintainer moderation requests / 反复忽视维护者的管理要求
+- Other conduct which could reasonably be considered inappropriate in a professional setting / 在专业环境中可能被合理认为不当的其他行为
-Possible actions include:
+## Reporting / 举报
-- editing or removing comments, issues, discussions, or PR content
-- warning the contributor
-- locking threads or limiting participation in project spaces
-- rejecting contributions or blocking a contributor from repository spaces
-- escalating serious cases to GitHub or another hosting provider
+If you experience or witness unacceptable behavior, or have any other concerns, please report it by:
-We aim to acknowledge conduct reports within 7 days when possible and will review the available evidence before taking action.
+如果您遇到或目睹不可接受的行为,或有任何其他疑虑,请通过以下方式举报:
-## Scope
+1. **Email** / **邮件**: `cita-777@users.noreply.github.com` with subject prefix `[Metapi Conduct]` / 主题前缀为 `[Metapi Conduct]`
+2. **GitHub**: Use [GitHub's built-in reporting tools](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam) for urgent platform-level issues / 对于紧急的平台级问题,使用 [GitHub 的内置举报工具](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam)
-This policy applies to project spaces and public/private interactions where an individual is representing the project.
+### What to Include / 应包含的内容
+
+When reporting, please include / 举报时请包含:
+
+- Your contact information / 您的联系信息
+- Names (usernames, real names) of individuals involved / 涉及人员的姓名(用户名、真实姓名)
+- Description of the incident / 事件描述
+- Links to relevant issues, PRs, or discussions / 相关 issue、PR 或讨论的链接
+- Screenshots or logs (with sensitive information redacted) / 截图或日志(删除敏感信息)
+- Timestamps and context / 时间戳和上下文
+- Any other information that would be helpful / 任何其他有用的信息
+
+### Confidentiality / 保密性
+
+All reports will be handled with discretion. We will keep reporter details as private as practical and do not allow retaliation for good-faith reports.
+
+所有举报都将谨慎处理。我们将尽可能保持举报者信息的私密性,并且不允许对善意举报进行报复。
+
+## Enforcement / 执行
+
+Project maintainers are responsible for clarifying and enforcing this code of conduct. They have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct.
+
+项目维护者负责阐明和执行本行为准则。他们有权利和责任删除、编辑或拒绝不符合本行为准则的评论、提交、代码、wiki 编辑、issue 和其他贡献。
+
+### Enforcement Actions / 执行措施
+
+Depending on the severity and context, maintainers may take actions including / 根据严重程度和上下文,维护者可能采取的措施包括:
+
+1. **Warning** / **警告**: A private or public warning about the behavior / 对行为进行私下或公开警告
+2. **Temporary Ban** / **临时禁止**: Temporary restriction from interacting in project spaces / 暂时限制在项目空间中的互动
+3. **Permanent Ban** / **永久禁止**: Permanent removal from all project spaces / 永久从所有项目空间中移除
+4. **Content Removal** / **内容删除**: Editing or removing comments, issues, discussions, or PR content / 编辑或删除评论、issue、讨论或 PR 内容
+5. **Thread Locking** / **话题锁定**: Locking threads to prevent further discussion / 锁定话题以防止进一步讨论
+6. **Platform Escalation** / **平台升级**: Escalating serious cases to GitHub or other hosting providers / 将严重案例上报给 GitHub 或其他托管平台
+
+### Response Timeline / 响应时间
+
+- We aim to acknowledge conduct reports within **7 days** when possible. / 我们力求在 **7 天**内确认行为举报(如有可能)。
+- We will review the available evidence before taking action. / 我们将在采取行动之前审查可用证据。
+- Complex cases may require additional time for investigation. / 复杂案例可能需要额外时间进行调查。
+
+## Scope / 适用范围
+
+This Code of Conduct applies to all project spaces, including / 本行为准则适用于所有项目空间,包括:
+
+- GitHub repository (issues, PRs, discussions, code reviews) / GitHub 仓库(issue、PR、讨论、代码审查)
+- Documentation and wiki / 文档和 wiki
+- Community forums and chat channels / 社区论坛和聊天频道
+- Official social media accounts / 官方社交媒体账号
+- Project events (online or offline) / 项目活动(线上或线下)
+- Any other spaces where an individual is representing the Metapi project / 个人代表 Metapi 项目的任何其他空间
+
+## Attribution / 归属
+
+This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
+
+本行为准则改编自 [Contributor Covenant](https://www.contributor-covenant.org/) 2.1 版本。
+
+## Questions / 问题
+
+If you have questions about this Code of Conduct, please open an issue or contact the maintainers at `cita-777@users.noreply.github.com`.
+
+如果您对本行为准则有疑问,请开启 issue 或通过 `cita-777@users.noreply.github.com` 联系维护者。
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 465e9d0b..87f7ed87 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,99 +1,224 @@
-# Contributing
+# Contributing / 贡献指南
-Thanks for contributing to Metapi.
+Thank you for your interest in contributing to Metapi! / 感谢您对 Metapi 项目的贡献!
-## Local Setup
+Metapi is a meta-aggregation layer for AI API platforms (New API, One API, OneHub, etc.), providing unified proxy, intelligent routing, and centralized management.
-1. Install dependencies:
+Metapi 是 AI API 聚合平台(New API、One API、OneHub 等)的元聚合层,提供统一代理、智能路由和集中管理。
+
+## Before You Start / 开始之前
+
+- Check existing [Issues](https://github.com/cita-777/metapi/issues) and [Pull Requests](https://github.com/cita-777/metapi/pulls) to avoid duplicates. / 检查现有的 [Issues](https://github.com/cita-777/metapi/issues) 和 [Pull Requests](https://github.com/cita-777/metapi/pulls) 以避免重复。
+- For major changes, open an issue first to discuss your proposal. / 对于重大更改,请先开启 issue 讨论您的提案。
+- Read our [Code of Conduct](CODE_OF_CONDUCT.md). / 阅读我们的[行为准则](CODE_OF_CONDUCT.md)。
+
+## Local Development Setup / 本地开发环境设置
+
+### Prerequisites / 前置要求
+
+- Node.js 20+ / Node.js 20 或更高版本
+- npm or compatible package manager / npm 或兼容的包管理器
+
+### Setup Steps / 设置步骤
+
+1. **Fork and clone the repository** / **Fork 并克隆仓库**
```bash
-npm install
+git clone https://github.com/YOUR_USERNAME/metapi.git
+cd metapi
```
-2. Create a local environment file:
+2. **Install dependencies** / **安装依赖**
-```powershell
-Copy-Item .env.example .env
+```bash
+npm install
```
+3. **Create environment file** / **创建环境文件**
+
```bash
+# Windows PowerShell
+Copy-Item .env.example .env
+
+# Linux/macOS/Git Bash
cp .env.example .env
```
-3. Initialize the default SQLite database:
+Edit `.env` and set your tokens / 编辑 `.env` 并设置您的令牌:
+
+```env
+AUTH_TOKEN=your-dev-admin-token
+PROXY_TOKEN=your-dev-proxy-token
+```
+
+4. **Initialize database** / **初始化数据库**
```bash
npm run db:migrate
```
-## Common Commands
-
-### App development
+5. **Start development server** / **启动开发服务器**
```bash
npm run dev
-npm run dev:server
-restart.bat
```
-- `npm run dev` starts the Fastify server and Vite together.
-- `npm run dev:server` runs only the backend watcher.
-- `restart.bat` is the Windows-friendly restart entrypoint; it forwards to `scripts\dev\restart.bat`, clears stale listeners, and starts `npm run dev`.
+The app will be available at `http://localhost:4000` (backend) and `http://localhost:5173` (frontend).
-### Docs
+应用将在 `http://localhost:4000`(后端)和 `http://localhost:5173`(前端)可用。
+
+## Development Commands / 开发命令
+
+### Web Application / Web 应用
```bash
-npm run docs:dev
-npm run docs:build
-npm run docs:preview
+npm run dev # Start backend + frontend with hot reload / 启动后端 + 前端热更新
+npm run dev:server # Start backend only / 仅启动后端
+npm run build # Build all (web + server + desktop) / 构建全部
+npm run build:web # Build frontend only / 仅构建前端
+npm run build:server # Build backend only / 仅构建后端
```
-### Desktop
+### Desktop Application / 桌面应用
```bash
-npm run dev:desktop
-npm run build:desktop
-npm run dist:desktop
-npm run package:desktop
+npm run dev:desktop # Start desktop app in dev mode / 开发模式启动桌面应用
+npm run build:desktop # Build desktop app / 构建桌面应用
+npm run dist:desktop # Package desktop app / 打包桌面应用
+npm run dist:desktop:mac:intel # Package for macOS Intel / 打包 macOS Intel 版本
```
-`npm run dev:desktop` expects the backend on `http://127.0.0.1:4000` and the Vite frontend on `http://127.0.0.1:5173`.
+### Documentation / 文档
+
+```bash
+npm run docs:dev # Start VitePress dev server / 启动 VitePress 开发服务器
+npm run docs:build # Build documentation / 构建文档
+npm run docs:preview # Preview built docs / 预览构建的文档
+```
+
+### Testing / 测试
+
+```bash
+npm test # Run all tests / 运行所有测试
+npm run test:watch # Run tests in watch mode / 监听模式运行测试
+npm run smoke:db # Database smoke test (SQLite) / 数据库冒烟测试(SQLite)
+npm run smoke:db:mysql # MySQL smoke test / MySQL 冒烟测试
+npm run smoke:db:postgres # PostgreSQL smoke test / PostgreSQL 冒烟测试
+```
-### Test, build, and smoke checks
+### Database / 数据库
```bash
-npm test
-npm run test:watch
-npm run build
-npm run build:web
-npm run build:server
-npm run smoke:db
-npm run smoke:db:sqlite
-npm run smoke:db:mysql -- --db-url mysql://user:pass@host:3306/db
-npm run smoke:db:postgres -- --db-url postgres://user:pass@host:5432/db
+npm run db:generate # Generate Drizzle migration files / 生成 Drizzle 迁移文件
+npm run db:migrate # Run database migrations / 运行数据库迁移
+npm run schema:generate # Generate schema artifacts / 生成 schema 构件
```
-## Windows Notes
+## Project Structure / 项目结构
+
+```
+metapi/
+├── src/
+│ ├── server/ # Backend (Fastify) / 后端(Fastify)
+│ │ ├── routes/ # API routes / API 路由
+│ │ ├── services/ # Business logic / 业务逻辑
+│ │ ├── db/ # Database & ORM / 数据库与 ORM
+│ │ └── middleware/ # Middleware / 中间件
+│ ├── web/ # Frontend (React + Vite) / 前端(React + Vite)
+│ └── desktop/ # Electron desktop app / Electron 桌面应用
+├── docs/ # VitePress documentation / VitePress 文档
+├── drizzle/ # Database migrations / 数据库迁移
+└── scripts/ # Build & dev scripts / 构建与开发脚本
+```
+
+## Pull Request Guidelines / Pull Request 指南
+
+### Before Submitting / 提交之前
+
+1. **Keep PRs focused and small** / **保持 PR 专注且小巧**
+ - One feature or fix per PR / 每个 PR 一个功能或修复
+ - Split large changes into multiple PRs / 将大型更改拆分为多个 PR
+
+2. **Write tests** / **编写测试**
+ - Add tests for new features / 为新功能添加测试
+ - Update tests for behavior changes / 为行为变更更新测试
+ - Ensure all tests pass: `npm test` / 确保所有测试通过:`npm test`
+
+3. **Update documentation** / **更新文档**
+ - Update README if adding user-facing features / 如果添加面向用户的功能,请更新 README
+ - Update docs/ for configuration or API changes / 配置或 API 更改请更新 docs/
+ - Add JSDoc comments for new functions / 为新函数添加 JSDoc 注释
+
+4. **Run checks** / **运行检查**
+ - Documentation changes: `npm run docs:build` / 文档更改:`npm run docs:build`
+ - Code changes: `npm test && npm run build` / 代码更改:`npm test && npm run build`
+ - Database changes: `npm run smoke:db` / 数据库更改:`npm run smoke:db`
+ - Architecture / repo drift changes: `npm run repo:drift-check` / 架构与仓库漂移检查:`npm run repo:drift-check`
+
+5. **Follow code style** / **遵循代码风格**
+ - Use TypeScript for type safety / 使用 TypeScript 确保类型安全
+ - Follow existing code patterns / 遵循现有代码模式
+ - Follow repo-level engineering rules in `AGENTS.md` / 遵循仓库根目录 `AGENTS.md` 中的工程规则
+ - Keep functions small and focused / 保持函数小而专注
+
+### Commit Messages / 提交信息
+
+Use conventional commit format / 使用约定式提交格式:
+
+```
+:
+
+[optional body]
+```
+
+Types / 类型:
+- `feat`: New feature / 新功能
+- `fix`: Bug fix / 错误修复
+- `docs`: Documentation / 文档
+- `refactor`: Code refactoring / 代码重构
+- `test`: Tests / 测试
+- `chore`: Build/tooling / 构建/工具
+
+Examples / 示例:
+```
+feat: add AnyRouter platform adapter
+fix: handle empty model list in dashboard
+docs: update Docker deployment guide
+refactor: extract route selection logic
+test: add tests for checkin reward parser
+chore: upgrade Vite to 6.0
+```
+
+### What Not to Commit / 不要提交的内容
+
+- Runtime data: `data/`, `tmp/` / 运行时数据:`data/`、`tmp/`
+- Environment files: `.env` (only `.env.example` is tracked) / 环境文件:`.env`(仅跟踪 `.env.example`)
+- Build artifacts: `dist/`, `node_modules/` / 构建产物:`dist/`、`node_modules/`
+- IDE-specific files (unless beneficial to all contributors) / IDE 特定文件(除非对所有贡献者有益)
+
+## Platform Adapters / 平台适配器
+
+If you're adding support for a new AI API platform / 如果您要添加对新 AI API 平台的支持:
+
+1. Create adapter in `src/server/services/platforms/` / 在 `src/server/services/platforms/` 中创建适配器
+2. Implement required interfaces: login, balance, models, proxy / 实现必需接口:登录、余额、模型、代理
+3. Add platform tests / 添加平台测试
+4. Update documentation with platform details / 更新文档说明平台详情
+
+## Windows Development Notes / Windows 开发注意事项
-- Prefer `Copy-Item` or Explorer copy/paste over `cp` if you are working in PowerShell or `cmd.exe`.
-- If a previous dev process keeps ports busy, use `restart.bat` instead of manually hunting PIDs.
-- If dependencies or `.cmd` shims look broken after a Node.js upgrade, rerun `npm install` before assuming the scripts are wrong.
+- Use `restart.bat` to restart dev server (clears port locks) / 使用 `restart.bat` 重启开发服务器(清除端口锁定)
+- Use PowerShell `Copy-Item` instead of `cp` / 使用 PowerShell 的 `Copy-Item` 而不是 `cp`
+- If Node.js upgrade breaks scripts, run `npm install` again / 如果 Node.js 升级导致脚本损坏,请重新运行 `npm install`
-## Pull Request Guidelines
+## Getting Help / 获取帮助
-- Keep PRs focused and small.
-- Add or update tests for behavior changes.
-- Update docs when user-facing behavior, commands, ports, or configuration change.
-- Run the checks that match your change set before opening a PR:
- - docs only: `npm run docs:build`
- - app code: `npm test` and the relevant `npm run build:*`
- - runtime DB work: one of the `npm run smoke:db*` commands
-- Avoid committing runtime data (`data/`) or temporary files (`tmp/`).
+- 📖 [Documentation](https://metapi.cita777.me) / [文档](https://metapi.cita777.me)
+- 💬 [GitHub Discussions](https://github.com/cita-777/metapi/discussions) / [GitHub 讨论区](https://github.com/cita-777/metapi/discussions)
+- 🐛 [Issue Tracker](https://github.com/cita-777/metapi/issues) / [Issue 跟踪](https://github.com/cita-777/metapi/issues)
-## Commit Messages
+## License / 许可证
-Use concise messages with clear scope, for example:
+By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
-- `feat: add token route health guard`
-- `fix: handle empty model list in dashboard`
-- `docs: clarify docker env setup`
+通过贡献,您同意您的贡献将根据 [MIT 许可证](LICENSE) 授权。
diff --git a/PR_PREPARATION.md b/PR_PREPARATION.md
new file mode 100644
index 00000000..0b7098ab
--- /dev/null
+++ b/PR_PREPARATION.md
@@ -0,0 +1,253 @@
+# 模型白名单功能 - PR 准备文档
+
+## 功能概述
+
+实现全局模型白名单功能,允许管理员配置只启用特定模型的路由。当白名单配置后,不在白名单中的模型路由会被自动移除,实现精细化的模型访问控制。
+
+## 业务场景
+
+- **成本控制**: 只允许使用价格合理的模型
+- **合规要求**: 限制只使用审批通过的模型
+- **简化管理**: 隐藏不需要的模型,减少用户困惑
+- **测试验证**: 在特定场景下只开放测试模型
+
+## 技术实现
+
+### 1. 配置层 (src/server/config.ts)
+
+```typescript
+globalAllowedModels: [] as string[]
+```
+
+- 默认为空数组(向后兼容)
+- 存储在内存中,启动时从数据库加载
+
+### 2. 数据持久化 (src/server/routes/api/settings.ts)
+
+**数据库存储:**
+- Key: `global_allowed_models`
+- Value: JSON 字符串数组
+
+**API 接口:**
+
+#### GET /api/settings/runtime
+返回当前白名单配置
+
+#### PUT /api/settings/runtime
+```json
+{
+ "globalAllowedModels": ["gpt-4", "gpt-4o", "claude-3-sonnet"]
+}
+```
+
+**处理流程:**
+1. 验证输入(必须是字符串数组)
+2. 去重、trim、过滤空值
+3. 比较新旧配置
+4. 更新数据库和内存配置
+5. 如果配置变更,自动触发路由重建
+
+### 3. 路由重建 (src/server/services/modelService.ts)
+
+**核心函数:** `rebuildTokenRoutesFromAvailability()`
+
+```typescript
+// 加载白名单
+const globalAllowedModels = new Set(
+ config.globalAllowedModels.map(m => m.toLowerCase().trim()).filter(Boolean)
+);
+
+// 判断模型是否允许
+function isModelAllowedByWhitelist(modelName: string): boolean {
+ if (globalAllowedModels.size === 0) return true; // 向后兼容
+ return globalAllowedModels.has(modelName.toLowerCase().trim());
+}
+
+// 添加模型候选时过滤
+const addModelCandidate = (modelNameRaw, accountId, tokenId, siteId) => {
+ const modelName = (modelNameRaw || '').trim();
+ if (!modelName) return;
+ if (!isModelAllowedByWhitelist(modelName)) return; // 白名单过滤
+ // ... 其他过滤逻辑
+};
+```
+
+**处理逻辑:**
+1. 从 `token_model_availability` 加载所有可用模型
+2. 通过 `addModelCandidate()` 过滤,只保留白名单中的模型
+3. 创建/更新路由和渠道
+4. 删除不在白名单中的旧路由
+
+### 4. 模型候选API (src/server/routes/api/stats.ts)
+
+**接口:** GET /api/models/token-candidates
+
+**返回字段:**
+- `models`: 有token的模型
+- `modelsWithoutToken`: 无token的模型
+- `modelsMissingTokenGroups`: 缺少token分组的模型
+
+**过滤逻辑:**
+```typescript
+if (globalAllowedModels.size > 0) {
+ // 白名单模式:只返回白名单中的模型
+ for (const [modelName, candidates] of Object.entries(result)) {
+ if (globalAllowedModels.has(modelName.toLowerCase().trim())) {
+ filteredResult[modelName] = candidates;
+ }
+ }
+} else {
+ // 向后兼容:返回所有模型
+ Object.assign(filteredResult, result);
+}
+```
+
+### 5. 前端UI (src/web/pages/Settings.tsx)
+
+**新增卡片:** "全局模型白名单"
+
+**功能特性:**
+- 输入框手动添加模型名称(支持回车)
+- 可用模型列表展示(点击快速添加)
+- 已选模型徽章显示(绿色徽章)
+- 点击 × 删除模型
+- "保存模型白名单"按钮
+- 保存成功后显示提示并自动刷新页面
+
+**交互流程:**
+1. 加载当前白名单配置
+2. 加载可用模型列表
+3. 用户添加/删除模型
+4. 点击保存触发API调用
+5. 后端自动重建路由
+6. 前端显示成功提示
+
+## 数据流程图
+
+```
+用户输入 → Settings API
+ ↓
+ 数据库存储 (global_allowed_models)
+ ↓
+ 内存配置更新 (config.globalAllowedModels)
+ ↓
+ 触发路由重建 (异步后台任务)
+ ↓
+ 加载 token_model_availability
+ ↓
+ 白名单过滤 (isModelAllowedByWhitelist)
+ ↓
+ 创建/更新/删除路由
+ ↓
+ 候选API过滤 (token-candidates)
+ ↓
+ 前端显示过滤后的模型列表
+```
+
+## 向后兼容性
+
+| 场景 | 行为 |
+|------|------|
+| 白名单为空 | 允许所有模型(原有行为) |
+| 白名单有值 | 只允许白名单中的模型 |
+| 数据库无配置 | 默认为空数组 |
+| API未传该字段 | 不修改现有配置 |
+
+## 测试验证
+
+### 功能测试清单
+
+- [x] 白名单为空时,所有模型可见
+- [x] 设置白名单后,只显示白名单模型
+- [x] 大小写不敏感匹配(GPT-4 = gpt-4)
+- [x] 自动trim空格
+- [x] 保存后自动触发路由重建
+- [x] 路由重建成功后,路由正确过滤
+- [x] 候选API正确过滤三个返回字段
+- [ ] 前端UI交互正常
+- [ ] 输入框支持回车添加
+- [ ] 可用模型列表正确显示
+- [ ] 点击模型快速添加
+- [ ] 删除模型功能正常
+- [ ] 保存成功提示显示
+
+### API 测试
+
+```bash
+# 查看白名单
+curl -H "Authorization: Bearer test-admin-token" \
+ http://localhost:4000/api/settings/runtime
+
+# 设置白名单
+curl -X PUT \
+ -H "Authorization: Bearer test-admin-token" \
+ -H "Content-Type: application/json" \
+ -d '{"globalAllowedModels": ["gpt-4", "gpt-4o"]}' \
+ http://localhost:4000/api/settings/runtime
+
+# 查看模型候选
+curl -H "Authorization: Bearer test-admin-token" \
+ http://localhost:4000/api/models/token-candidates
+```
+
+## 文件修改列表
+
+| 文件 | 修改内容 | 行数 |
+|------|---------|-----|
+| src/server/config.ts | 新增 globalAllowedModels 配置 | 1 |
+| src/server/routes/api/settings.ts | 类型定义、加载、保存、触发重建 | ~80 |
+| src/server/services/modelService.ts | 白名单过滤逻辑 | ~20 |
+| src/server/routes/api/stats.ts | 候选API过滤 | ~30 |
+| src/web/pages/Settings.tsx | 前端UI组件 | ~150 |
+
+**总计:** 约 280 行代码新增/修改
+
+## 性能影响
+
+- **内存:** 新增一个 Set 数据结构,大小为白名单模型数量
+- **路由重建:** 无额外开销(仅增加一个 O(1) 的 Set 查找)
+- **候选API:** 增加过滤循环,复杂度 O(n),n为模型数量
+
+**结论:** 性能影响可忽略不计
+
+## 安全考虑
+
+- ✅ 仅管理员可配置(需要 admin token)
+- ✅ 输入验证(类型检查、trim、去重)
+- ✅ 无SQL注入风险(使用 ORM)
+- ✅ 配置变更自动记录日志
+
+## 后续优化建议
+
+1. **批量导入**: 支持从文件批量导入白名单
+2. **预设模板**: 提供常用模型组合模板
+3. **分组管理**: 支持多个白名单分组
+4. **权限控制**: 不同用户看到不同白名单
+5. **审计日志**: 记录白名单变更历史
+
+## 已知限制
+
+1. 白名单只支持精确匹配,不支持通配符或正则
+2. 模型名称大小写不敏感,但保留原始大小写显示
+3. 白名单全局生效,不支持按站点或账号单独配置
+
+## 相关 Issue
+
+- Issue #XXX: 模型访问控制需求
+- Issue #XXX: 简化模型列表显示
+
+## 测试环境
+
+- Node.js: v20.x
+- pnpm: 8.x
+- SQLite: 3.x
+- 测试服务器: http://localhost:4000
+- 真实模型数据已验证
+
+## 下一步计划
+
+1. ✅ 完成功能开发和测试
+2. ✅ 编写PR文档
+3. ⏳ 等待用户前端测试验证
+4. ⏳ 合并到主仓库
+5. ⏳ 开始第二个功能:新站点跳转选择
\ No newline at end of file
diff --git a/README.md b/README.md
index 1cbd83cb..8a4308d9 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,12 @@
汇聚成 一个 API Key、一个入口 ,自动发现模型、智能路由、成本最优。
+
+
+
+
+
+
@@ -17,6 +23,9 @@
-->
+
+
+
+
@@ -51,6 +63,21 @@
---
+## 🌐 在线体验
+
+> 无需部署,直接体验 Metapi 的完整功能:
+
+| | |
+| ---------------------- | ---------------------------------------------------------- |
+| 🔗**体验地址** | [metapi-t9od.onrender.com](https://metapi-t9od.onrender.com/) |
+| 🔑**管理员令牌** | `123456` |
+
+> **⚠️ 安全提示**:体验站为公共环境,**请勿填入你的 API Key、账号密码或站点信息**。数据随时可能被清空。
+
+> **ℹ️ 说明**:体验站使用 Render 免费方案 + OpenRouter 免费模型(仅 `:free` 后缀的模型可用)。
+
+---
+
## 📖 介绍
现在 AI 生态里有越来越多基于 New API / One API 系列的聚合中转站,要管理多个站点的余额、模型列表和 API 密钥,往往既分散又费时。
@@ -65,14 +92,14 @@
- [AnyRouter](https://anyrouter.top) — 通用路由平台
- [Sub2API](https://github.com/Wei-Shaw/sub2api) — 订阅制中转
-| 痛点 | Metapi 怎么解决 |
-| ------------------------------------- | --------------------------------------------------------------- |
+| 痛点 | Metapi 怎么解决 |
+| ------------------------------------- | ---------------------------------------------------------------------- |
| 🔑 每个站点一个 Key,下游工具配置一堆 | **统一代理入口 + 可选多下游 Key 策略**,模型自动聚合到 `/v1/*` |
-| 💸 不知道哪个站点用某个模型最便宜 | **智能路由** 自动按成本、余额、使用率选最优通道 |
-| 🔄 某个站点挂了,手动切换好麻烦 | **自动故障转移**,一个通道失败自动冷却并切到下一个 |
-| 📊 余额分散在各处,不知道还剩多少 | **集中看板** 一目了然,余额不足自动告警 |
-| ✅ 每天得去各站签到领额度 | **自动签到** 定时执行,奖励自动追踪 |
-| 🤷 不知道哪个站有什么模型 | **自动模型发现**,上游新增模型零配置出现在你的模型列表里 |
+| 💸 不知道哪个站点用某个模型最便宜 | **智能路由** 自动按成本、余额、使用率选最优通道 |
+| 🔄 某个站点挂了,手动切换好麻烦 | **自动故障转移**,一个通道失败自动冷却并切到下一个 |
+| 📊 余额分散在各处,不知道还剩多少 | **集中看板** 一目了然,余额不足自动告警 |
+| ✅ 每天得去各站签到领额度 | **自动签到** 定时执行,奖励自动追踪 |
+| 🤷 不知道哪个站有什么模型 | **自动模型发现**,上游新增模型零配置出现在你的模型列表里 |
---
@@ -141,7 +168,6 @@
-
---
## 🏛️ 架构概览
@@ -208,22 +234,61 @@
模型广场 — 一站式浏览所有可用模型的覆盖率、定价和性能指标
-### ✅ 自动签到 · 💰 余额管理 · 🔔 告警通知 · 📊 数据看板
+### ✅ 自动签到
+
+- Cron 定时执行(默认每日 08:00)
+- 智能解析奖励金额,签到失败自动通知
+- 按账号启用/禁用控制
+- 完整签到日志与历史查询
+- 并发锁防止重复签到
+
+### 💰 余额管理
+
+- 定时余额刷新(默认每小时),批量更新所有活跃账号
+- 收入追踪:每日/累计收入与消费趋势分析
+- 余额兜底估算:API 不可用时通过代理日志推算余额变动
+- 凭证过期自动重新登录
+
+### 🔔 告警通知
-- **自动签到**:Cron 定时执行,智能解析奖励金额,签到失败自动通知
-- **余额管理**:定时刷新,收入追踪,余额兜底估算,凭证过期自动重登录
-- **告警通知**:支持 Webhook / Bark / Server酱 / Telegram Bot / SMTP 邮件,覆盖余额不足、站点异常、签到失败等场景
-- **数据看板**:余额分布饼图、消费趋势图表、全局搜索、代理请求日志
+支持五种通知渠道:
+
+| 渠道 | 说明 |
+| ---------------------- | ----------------- |
+| **Webhook** | 自定义 HTTP 推送 |
+| **Bark** | iOS 推送通知 |
+| **Server酱** | 微信通知 |
+| **Telegram Bot** | Telegram 消息通知 |
+| **SMTP 邮件** | 标准邮件通知 |
+
+告警场景:余额不足预警、站点/账号异常、签到失败、代理请求失败、Token 过期提醒、每日摘要报告。告警冷却机制(默认 300 秒)防止重复通知。
+
+### 📊 数据看板
+
+- 站点余额饼图、每日消费趋势图
+- 全局搜索(站点、账号、模型)
+- 系统事件日志、代理请求日志(模型、状态、延迟、Token 用量、成本估算)
数据看板 — 余额分布、消费趋势、系统健康状态一目了然
+### 🎮 模型操练场
+
+- 交互式聊天测试,即时验证模型可用性与响应质量
+- 选择任意路由模型,对比不同通道输出
+- 流式 / 非流式双模式测试
+
+
+
+
模型操练场 — 在线交互测试,验证模型可用性与响应质量
+
+
### 📦 轻量部署
-- **单 Docker 容器**,默认本地数据目录部署,开箱即用
-- Alpine 基础镜像,体积精简
+- **单 Docker 容器**,默认本地数据目录部署,支持外接 MySQL / PostgreSQL 运行时数据库
+- Docker 镜像支持 `amd64`、`arm64` 和 `armv7l`(`linux/arm/v7`)服务端部署
- 数据完整导入导出,迁移无忧
---
@@ -233,41 +298,93 @@
+
+
+
+
+### Docker Compose(推荐)
+
+```bash
+mkdir metapi && cd metapi
+
+cat > docker-compose.yml << 'EOF'
+services:
+ metapi:
+ image: 1467078763/metapi:latest
+ ports:
+ - "4000:4000"
+ volumes:
+ - ./data:/app/data
+ environment:
+ AUTH_TOKEN: ${AUTH_TOKEN:?AUTH_TOKEN is required}
+ PROXY_TOKEN: ${PROXY_TOKEN:?PROXY_TOKEN is required}
+ CHECKIN_CRON: "0 8 * * *"
+ BALANCE_REFRESH_CRON: "0 * * * *"
+ PORT: ${PORT:-4000}
+ DATA_DIR: /app/data
+ TZ: ${TZ:-Asia/Shanghai}
+ restart: unless-stopped
+EOF
+
+# 设置 Token 并启动
+# AUTH_TOKEN = 管理后台登录令牌(登录时输入此值)
+export AUTH_TOKEN=your-admin-token
+# PROXY_TOKEN = 下游客户端调用 /v1/* 的 Token
+export PROXY_TOKEN=your-proxy-sk-token
+docker compose up -d
+```
+
+
+一行 Docker 命令
```bash
-docker run -d --name metapi -p 4000:4000 \
+docker run -d --name metapi \
+ -p 4000:4000 \
-e AUTH_TOKEN=your-admin-token \
-e PROXY_TOKEN=your-proxy-sk-token \
- -v ./data:/app/data --restart unless-stopped \
+ -e TZ=Asia/Shanghai \
+ -v ./data:/app/data \
+ --restart unless-stopped \
1467078763/metapi:latest
```
+
+
启动后访问 `http://localhost:4000`,用 `AUTH_TOKEN` 登录即可。
+> [!NOTE]
+> Docker 镜像支持 `amd64`、`arm64` 和 `armv7l`(`linux/arm/v7`)服务端部署。
+> 当前 `armv7l` 支持范围仅限服务端 / Docker 运行,不包含桌面安装包。
+
+
> [!IMPORTANT]
-> 请务必修改 `AUTH_TOKEN` 和 `PROXY_TOKEN`,不要使用默认值。
+> 请务必修改 `AUTH_TOKEN` 和 `PROXY_TOKEN`,不要使用默认值。数据存储在 `./data` 目录,升级不会丢失。
-📖 **[完整部署文档](https://metapi.cita777.me/deployment)** — Zeabur 一键部署 / Docker Compose / 桌面版安装包 / 反向代理 / 升级回滚
+> [!TIP]
+> 初始管理员令牌即启动时配置的 `AUTH_TOKEN`。
+> 若在 Compose 外运行且未显式设置 `AUTH_TOKEN`,默认为 `change-me-admin-token`(仅用于本地调试)。
+> 桌面安装包首次启动也属于这类场景:如果你没有额外注入 `AUTH_TOKEN`,默认管理员令牌同样是 `change-me-admin-token`。
+> 如果在「设置」面板中修改了管理员令牌,后续登录请使用新令牌。
-📖 **[环境变量与配置](https://metapi.cita777.me/configuration)** — 全部环境变量、路由参数、通知渠道
+Docker Compose、桌面安装包、反向代理、升级与数据库选项等详见 [部署指南](https://metapi.cita777.me/deployment)。
-📖 **[客户端接入指南](https://metapi.cita777.me/client-integration)** — Cursor / Claude Code / Codex / Open WebUI 等配置方法
+📖 **[环境变量与配置](https://metapi.cita777.me/configuration)** · **[客户端接入指南](https://metapi.cita777.me/client-integration)** · **[常见问题](https://metapi.cita777.me/faq)**
---
## 🏗️ 技术栈
-| 层 | 技术 |
-| -------------------- | --------------------------------------------------------------------------------------------------------- |
-| **后端框架** | [Fastify](https://fastify.dev) — 高性能 Node.js 后端框架 |
-| **前端框架** | [React 18](https://react.dev) + [Vite](https://vitejs.dev) |
-| **语言** | [TypeScript](https://www.typescriptlang.org) — 端到端类型安全 |
-| **样式** | [Tailwind CSS v4](https://tailwindcss.com) — 原子化样式框架 |
-| **数据库** | SQLite / MySQL / PostgreSQL + [Drizzle ORM](https://orm.drizzle.team) |
-| **数据可视化** | [VChart](https://visactor.io/vchart) (@visactor/react-vchart) |
-| **定时任务** | [node-cron](https://github.com/node-cron/node-cron) |
-| **容器化** | Docker (Alpine) + Docker Compose |
-| **测试** | [Vitest](https://vitest.dev) |
+| 层 | 技术 |
+| -------------------- | ----------------------------------------------------------------- |
+| **后端框架** | [Fastify](https://fastify.dev) — 高性能 Node.js 后端框架 |
+| **前端框架** | [React 18](https://react.dev) + [Vite](https://vitejs.dev) |
+| **语言** | [TypeScript](https://www.typescriptlang.org) — 端到端类型安全 |
+| **样式** | [Tailwind CSS v4](https://tailwindcss.com) — 原子化样式框架 |
+| **数据库** | SQLite / MySQL / PostgreSQL +[Drizzle ORM](https://orm.drizzle.team) |
+| **数据可视化** | [VChart](https://visactor.io/vchart) (@visactor/react-vchart) |
+| **定时任务** | [node-cron](https://github.com/node-cron/node-cron) |
+| **容器化** | Docker (Debian slim) + Docker Compose |
+| **测试** | [Vitest](https://vitest.dev) |
---
@@ -288,6 +405,7 @@ npm run dev
npm run build # 构建前端 + 后端
npm run build:web # 仅构建前端(Vite)
npm run build:server # 仅构建后端(TypeScript)
+npm run dist:desktop:mac:intel # 构建 mac Intel (x64) 桌面安装包
npm test # 运行全部测试
npm run test:watch # 监听模式
npm run db:generate # 生成 Drizzle 迁移文件
@@ -349,7 +467,7 @@ Metapi 完全自托管,所有数据(账号、令牌、路由、日志)均
## ⭐ Star History
-[](https://www.star-history.com/?repos=cita-777%2Fmetapi&type=timeline&legend=top-left)
+[](https://www.star-history.com/#cita-777/metapi&type=date&legend=top-left)
---
@@ -357,6 +475,6 @@ Metapi 完全自托管,所有数据(账号、令牌、路由、日志)均
**⭐ 如果 Metapi 对你有帮助,给个 Star 就是最大的支持!**
-Built with ❤️ by the AI community
+``Built with ❤️ by the AI community` `
diff --git a/README_EN.md b/README_EN.md
index 702e136d..34c78341 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -10,6 +10,12 @@ Bring together all your New API / One API / OneHub / DoneHub / Veloera / AnyRout
into one API Key, one endpoint , with automatic model discovery, smart routing, and cost optimization.
+
+
+
+
+
+
@@ -17,6 +23,9 @@ into one API Key, one endpoint , with automatic model discovery,
-->
+
+
+-->
+
+
+
+
@@ -36,11 +51,12 @@ into one API Key, one endpoint , with automatic model discovery,
- Docs ·
- Quick Start ·
- Deployment ·
- Configuration ·
- FAQ ·
+ Docs ·
+ Quick Start ·
+ Deployment ·
+ Configuration ·
+ Client Integration ·
+ FAQ ·
Contributing
@@ -48,6 +64,21 @@ into one API Key, one endpoint , with automatic model discovery,
---
+## 🌐 Live Demo
+
+> Try Metapi without deploying — full-featured demo instance:
+
+| | |
+|---|---|
+| 🔗 **Demo URL** | [metapi-t9od.onrender.com](https://metapi-t9od.onrender.com/) |
+| 🔑 **Admin Token** | `123456` |
+
+> **⚠️ Security Notice**: This is a public demo. **Do NOT enter any real API keys, credentials, or site information.** Data may be reset at any time.
+
+> **ℹ️ Note**: Demo runs on Render free tier + OpenRouter free models (only `:free` suffixed models available). First visit may take 30-60s to wake up.
+
+---
+
## About
The AI ecosystem is seeing a growing number of aggregation relay stations based on New API / One API and similar projects. Managing balances, model lists, and API keys across multiple sites is scattered and time-consuming.
@@ -265,7 +296,7 @@ Alert scenarios: low balance warning, site/account anomalies, check-in failures,
### Lightweight Deployment
- **Single Docker container** with a default local data directory, plus optional external MySQL / PostgreSQL runtime DB
-- Alpine base image, minimal footprint
+- Docker images support `amd64`, `arm64`, and `armv7l` (`linux/arm/v7`) server deployments
- Full data import/export for worry-free migration
---
@@ -322,12 +353,18 @@ docker run -d --name metapi \
After starting, visit `http://localhost:4000` and log in with your `AUTH_TOKEN`!
+> [!NOTE]
+> Docker images support `amd64`, `arm64`, and `armv7l` (`linux/arm/v7`) server deployments.
+> Current `armv7l` support is limited to server / Docker usage and does not include Electron desktop packaging support.
+
+
> [!IMPORTANT]
> Make sure to change `AUTH_TOKEN` and `PROXY_TOKEN` — do not use default values. Data is stored in the `./data` directory and persists across upgrades.
> [!TIP]
> The initial admin token is the `AUTH_TOKEN` configured at startup.
> If running outside Compose without explicitly setting `AUTH_TOKEN`, the default is `change-me-admin-token` (for local debugging only).
+> The desktop installer falls into this category on first launch too: if you do not inject `AUTH_TOKEN`, the default admin token is also `change-me-admin-token`.
> If you change the admin token in the Settings panel, use the new token for subsequent logins.
For Docker Compose, desktop installers, reverse proxy, upgrades, and database options, see [Deployment Guide](docs/deployment.md).
@@ -387,46 +424,9 @@ For Docker Compose, desktop installers, reverse proxy, upgrades, and database op
| `BALANCE_REFRESH_CRON` | Balance refresh cron expression | `0 * * * *` |
-Smart Routing Parameters
-
-| Variable | Description | Default |
-| --- | --- | --- |
-| `ROUTING_FALLBACK_UNIT_COST` | Default unit price when no cost signal | `1` |
-| `BASE_WEIGHT_FACTOR` | Base weight factor | `0.5` |
-| `VALUE_SCORE_FACTOR` | Value score factor | `0.5` |
-| `COST_WEIGHT` | Cost weight in routing selection | `0.4` |
-| `BALANCE_WEIGHT` | Balance weight in routing selection | `0.3` |
-| `USAGE_WEIGHT` | Usage weight in routing selection | `0.3` |
-
-
-
-
-Notification Channel Configuration
+Smart Routing, Notification & Security Configuration
-| Variable | Description | Default |
-| --- | --- | --- |
-| `WEBHOOK_ENABLED` | Enable Webhook notifications | `true` |
-| `WEBHOOK_URL` | Webhook push URL | empty |
-| `BARK_ENABLED` | Enable Bark push | `true` |
-| `BARK_URL` | Bark push URL | empty |
-| `SERVERCHAN_ENABLED` | Enable ServerChan notifications | `true` |
-| `SERVERCHAN_KEY` | ServerChan SendKey | empty |
-| `SMTP_ENABLED` | Enable email notifications | `false` |
-| `SMTP_HOST` | SMTP server address | empty |
-| `SMTP_PORT` | SMTP port | `587` |
-| `SMTP_SECURE` | Use SSL/TLS | `false` |
-| `SMTP_USER` / `SMTP_PASS` | SMTP authentication | empty |
-| `SMTP_FROM` / `SMTP_TO` | Sender / recipient | empty |
-| `NOTIFY_COOLDOWN_SEC` | Same-alert cooldown (seconds) | `300` |
-
-
-
-
-Security Configuration
-
-| Variable | Description | Default |
-| --- | --- | --- |
-| `ADMIN_IP_ALLOWLIST` | Admin API IP allowlist (comma-separated) | empty (no restriction) |
+See [docs/configuration.md](docs/configuration.md) for full details on smart routing parameters, notification channels (Webhook / Bark / ServerChan / Telegram / SMTP), and security settings (IP allowlist).
@@ -456,7 +456,7 @@ Include `Authorization: Bearer ` in request headers.
The global `PROXY_TOKEN` works by default.
From `System Settings -> Downstream API Key Strategy` you can create multiple project-level downstream keys with individual configuration:
-- Expiration time (ExpireAt)
+- Expiration time (expiresAt)
- Cost and request limits (MaxCost / MaxRequests)
- Model allowlist (SupportedModels, supports exact/glob/re:regex)
- Route allowlist (AllowedRouteIds)
@@ -491,7 +491,7 @@ For detailed per-client setup, examples, and troubleshooting, see [docs/client-i
| **Database** | SQLite / MySQL / PostgreSQL + [Drizzle ORM](https://orm.drizzle.team) |
| **Charts** | [VChart](https://visactor.io/vchart) (@visactor/react-vchart) |
| **Scheduling** | [node-cron](https://github.com/node-cron/node-cron) |
-| **Containerization** | Docker (Alpine) + Docker Compose |
+| **Containerization** | Docker (Debian slim) + Docker Compose |
| **Testing** | [Vitest](https://vitest.dev) |
---
@@ -513,6 +513,7 @@ npm run dev
npm run build # Build frontend + backend
npm run build:web # Build frontend only (Vite)
npm run build:server # Build backend only (TypeScript)
+npm run dist:desktop:mac:intel # Build mac Intel (x64) desktop installer
npm test # Run all tests
npm run test:watch # Watch mode
npm run db:generate # Generate Drizzle migration files
@@ -575,6 +576,7 @@ If you discover a security issue, please refer to [SECURITY.md](SECURITY.md) and
## Star History
[](https://www.star-history.com/#cita-777/metapi&type=date&legend=top-left)
+
---
diff --git a/SECURITY.md b/SECURITY.md
index b623c683..5aa2edb3 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,37 +1,178 @@
-# Security Policy
+# Security Policy / 安全政策
-## Supported Versions
+## Overview / 概述
-- The latest release on GitHub.
-- The current `main` branch.
+The security of Metapi is important to us. This document outlines our security policy and how to report vulnerabilities.
-Older tags and ad-hoc forks may receive guidance, but they are not guaranteed security backports.
+Metapi 的安全对我们很重要。本文档概述了我们的安全政策以及如何报告漏洞。
-## Reporting a Vulnerability
+Since Metapi is a self-hosted meta-aggregation layer that manages sensitive credentials (API keys, account passwords) and proxies AI API requests, security is a critical concern.
-Please do not report security issues in public issues, discussions, or pull requests.
+由于 Metapi 是一个自托管的元聚合层,管理敏感凭证(API 密钥、账号密码)并代理 AI API 请求,因此安全性至关重要。
-Use one of these channels:
+## Supported Versions / 支持的版本
-1. GitHub Security Advisory private report: `https://github.com/cita-777/metapi/security/advisories/new`
-2. Email: `cita-777@users.noreply.github.com` with subject prefix `[metapi security]`
+We provide security updates for the following versions / 我们为以下版本提供安全更新:
-When reporting, include:
+| Version / 版本 | Supported / 支持状态 |
+| -------------- | -------------------- |
+| Latest release / 最新版本 | ✅ Fully supported / 完全支持 |
+| `main` branch / `main` 分支 | ✅ Supported / 支持 |
+| Older releases / 旧版本 | ⚠️ Best effort / 尽力而为 |
+| Forks / 分支 | ❌ Not supported / 不支持 |
-- Affected version, commit, or deployment mode
-- Affected endpoint, module, or configuration surface
-- Reproduction steps or proof-of-concept
-- Expected impact and attack preconditions
-- Any logs, screenshots, or packets with secrets redacted
-- Suggested mitigation or patch ideas, if you already have them
+**Recommendation** / **建议**: Always use the latest stable release from [GitHub Releases](https://github.com/cita-777/metapi/releases) or the `main` branch for the most up-to-date security patches.
-## Response Process
+始终使用 [GitHub Releases](https://github.com/cita-777/metapi/releases) 的最新稳定版本或 `main` 分支以获得最新的安全补丁。
-- We aim to acknowledge private reports within 3 business days.
-- We aim to provide an initial triage or follow-up questions within 7 business days.
-- We may ask for extra reproduction details, redacted logs, or environment information before confirming severity.
-- We coordinate disclosure with the reporter when possible. Please keep the report private until a fix or mitigation is ready.
+## Security Considerations / 安全注意事项
-## Handling Public Reports
+When deploying Metapi, please consider / 部署 Metapi 时,请考虑:
-If a vulnerability is posted publicly by mistake, maintainers may redact sensitive details, convert the report to a private channel, or close the public thread after redirecting the reporter to the private process above.
+### Credential Storage / 凭证存储
+
+- All sensitive credentials (API keys, passwords) are encrypted at rest in the database / 所有敏感凭证(API 密钥、密码)在数据库中静态加密存储
+- Use strong `AUTH_TOKEN` and `PROXY_TOKEN` values / 使用强 `AUTH_TOKEN` 和 `PROXY_TOKEN` 值
+- Never commit `.env` files or expose tokens in logs / 切勿提交 `.env` 文件或在日志中暴露令牌
+
+### Network Security / 网络安全
+
+- Deploy behind HTTPS/TLS in production / 在生产环境中部署在 HTTPS/TLS 后面
+- Use `ADMIN_IP_ALLOWLIST` to restrict admin access / 使用 `ADMIN_IP_ALLOWLIST` 限制管理员访问
+- Consider firewall rules to limit access to port 4000 / 考虑使用防火墙规则限制对端口 4000 的访问
+
+### Database Security / 数据库安全
+
+- Secure your database with strong credentials / 使用强凭证保护您的数据库
+- Regularly backup the `data/` directory / 定期备份 `data/` 目录
+- For production, consider using MySQL/PostgreSQL instead of SQLite / 对于生产环境,考虑使用 MySQL/PostgreSQL 而不是 SQLite
+
+### Docker Security / Docker 安全
+
+- Keep Docker images up to date / 保持 Docker 镜像最新
+- Use volume mounts carefully to avoid exposing sensitive data / 谨慎使用卷挂载以避免暴露敏感数据
+- Run containers with minimal privileges / 以最小权限运行容器
+
+## Reporting a Vulnerability / 报告漏洞
+
+**⚠️ Please do NOT report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
+
+**⚠️ 请勿通过公开的 GitHub issue、讨论或 pull request 报告安全漏洞。**
+
+### Reporting Channels / 报告渠道
+
+Use one of these private channels / 使用以下私密渠道之一:
+
+1. **GitHub Security Advisory** (Preferred) / **GitHub 安全公告**(首选)
+ - Go to: https://github.com/cita-777/metapi/security/advisories/new
+ - This allows for coordinated disclosure and CVE assignment / 这允许协调披露和 CVE 分配
+
+2. **Email** / **邮件**
+ - Send to: `cita-777@users.noreply.github.com`
+ - Subject: `[Metapi Security]
` / 主题:`[Metapi Security] <简要描述>`
+
+### What to Include / 应包含的内容
+
+To help us understand and address the issue quickly, please include / 为了帮助我们快速理解和解决问题,请包含:
+
+- **Description** / **描述**: Clear description of the vulnerability / 漏洞的清晰描述
+- **Impact** / **影响**: What an attacker could achieve / 攻击者可能实现的目标
+- **Affected versions** / **受影响的版本**: Version, commit hash, or deployment mode / 版本、提交哈希或部署模式
+- **Affected components** / **受影响的组件**: Specific endpoints, modules, or configuration / 特定端点、模块或配置
+- **Reproduction steps** / **复现步骤**: Step-by-step instructions to reproduce / 逐步复现说明
+- **Proof of concept** / **概念验证**: Code, screenshots, or logs (with secrets redacted) / 代码、截图或日志(删除敏感信息)
+- **Attack preconditions** / **攻击前提条件**: Required access level, network position, etc. / 所需访问级别、网络位置等
+- **Suggested fix** / **建议修复**: If you have ideas for mitigation or patches / 如果您有缓解措施或补丁的想法
+
+### Example Report / 报告示例
+
+```
+Subject: [Metapi Security] SQL Injection in account search
+
+Description:
+The account search endpoint is vulnerable to SQL injection through the
+'name' parameter, allowing unauthorized database access.
+
+Impact:
+An authenticated attacker could extract all account credentials from
+the database.
+
+Affected Version:
+v1.2.2 and earlier
+
+Reproduction:
+1. Login to Metapi admin panel
+2. Navigate to /api/accounts/search?name=' OR '1'='1
+3. Observe all accounts returned
+
+Proof of Concept:
+[Screenshot attached with sensitive data redacted]
+
+Suggested Fix:
+Use parameterized queries instead of string concatenation in
+src/server/routes/api/accounts.ts:45
+```
+
+## Response Process / 响应流程
+
+Our security response process / 我们的安全响应流程:
+
+1. **Acknowledgment** / **确认**: We aim to acknowledge your report within **3 business days** / 我们力求在 **3 个工作日**内确认您的报告
+
+2. **Initial Triage** / **初步分类**: We will provide initial assessment or follow-up questions within **7 business days** / 我们将在 **7 个工作日**内提供初步评估或后续问题
+
+3. **Investigation** / **调查**: We may request additional details, logs, or reproduction steps / 我们可能会要求额外的细节、日志或复现步骤
+
+4. **Fix Development** / **修复开发**: We will develop and test a fix / 我们将开发并测试修复方案
+
+5. **Coordinated Disclosure** / **协调披露**: We will coordinate disclosure timing with you / 我们将与您协调披露时间
+ - Please keep the vulnerability confidential until we release a fix / 请在我们发布修复之前对漏洞保密
+ - We will credit you in the security advisory (unless you prefer to remain anonymous) / 我们将在安全公告中致谢您(除非您希望保持匿名)
+
+6. **Release** / **发布**: We will release a patched version and publish a security advisory / 我们将发布修补版本并发布安全公告
+
+### Severity Assessment / 严重性评估
+
+We use the following severity levels / 我们使用以下严重性级别:
+
+- **Critical** / **严重**: Remote code execution, credential theft, data breach / 远程代码执行、凭证盗窃、数据泄露
+- **High** / **高**: Authentication bypass, privilege escalation / 身份验证绕过、权限提升
+- **Medium** / **中**: Information disclosure, denial of service / 信息泄露、拒绝服务
+- **Low** / **低**: Minor information leaks, configuration issues / 轻微信息泄露、配置问题
+
+## Public Disclosure / 公开披露
+
+If a vulnerability is accidentally posted publicly / 如果漏洞被意外公开发布:
+
+1. We will immediately assess the risk / 我们将立即评估风险
+2. We may redact sensitive details from the public post / 我们可能会从公开帖子中删除敏感细节
+3. We will redirect the reporter to this private process / 我们将把报告者重定向到此私密流程
+4. We will expedite the fix and release process / 我们将加快修复和发布流程
+
+## Security Updates / 安全更新
+
+Security updates will be announced through / 安全更新将通过以下方式公布:
+
+- [GitHub Security Advisories](https://github.com/cita-777/metapi/security/advisories)
+- [GitHub Releases](https://github.com/cita-777/metapi/releases) with `[SECURITY]` tag / 带有 `[SECURITY]` 标签
+- Project README and documentation / 项目 README 和文档
+
+Subscribe to repository notifications to stay informed / 订阅仓库通知以保持了解。
+
+## Bug Bounty / 漏洞赏金
+
+We currently do not offer a paid bug bounty program. However, we deeply appreciate security researchers who responsibly disclose vulnerabilities and will publicly acknowledge your contribution (with your permission).
+
+我们目前不提供付费漏洞赏金计划。但是,我们非常感谢负责任地披露漏洞的安全研究人员,并将公开致谢您的贡献(经您许可)。
+
+## Questions / 问题
+
+If you have questions about this security policy, please contact `cita-777@users.noreply.github.com`.
+
+如果您对本安全政策有疑问,请联系 `cita-777@users.noreply.github.com`。
+
+---
+
+**Thank you for helping keep Metapi and its users safe!**
+
+**感谢您帮助保护 Metapi 及其用户的安全!**
diff --git a/TEST_ENVIRONMENT_SETUP.md b/TEST_ENVIRONMENT_SETUP.md
new file mode 100644
index 00000000..e544f5f5
--- /dev/null
+++ b/TEST_ENVIRONMENT_SETUP.md
@@ -0,0 +1,208 @@
+# 本地测试环境搭建指南
+
+## 目录结构
+
+```
+D:\Code\Projects\Metapi\
+├── metapi # 主仓库(upstream)
+├── metapi-routing-ux-optimization # 开发 worktree (模型白名单功能)
+├── metapi-upstream-latest # 其他 worktree
+└── ...
+```
+
+## 快速启动测试服务器
+
+### 1. 进入开发 worktree
+
+```bash
+cd D:/Code/Projects/Metapi/metapi-routing-ux-optimization
+```
+
+### 2. 安装依赖(首次运行)
+
+```bash
+pnpm install
+```
+
+### 3. 构建项目
+
+```bash
+pnpm build
+```
+
+### 4. 启动测试服务器
+
+```bash
+DATA_DIR="./tmp/test-db" node dist/server/index.js
+```
+
+服务器将在 http://localhost:4000 启动
+
+**测试Token:** `test-admin-token`
+
+### 5. 访问前端
+
+打开浏览器访问: http://localhost:4000
+
+登录使用 Token: `test-admin-token`
+
+## 测试数据准备
+
+### 方式1: 导入真实站点
+
+在前端"站点管理"中添加真实的 new-api 站点:
+- 站点URL: 真实的 new-api 地址
+- 平台: new-api
+- 添加账号和token
+
+然后触发模型发现:
+```bash
+curl -X POST -H "Authorization: Bearer test-admin-token" \
+ -H "Content-Type: application/json" \
+ -d '{"refreshModels": true, "wait": true}' \
+ http://localhost:4000/api/routes/rebuild
+```
+
+### 方式2: 使用测试脚本(可选)
+
+```bash
+DATA_DIR="./tmp/test-db" node scripts/seed-test-data.js
+```
+
+## 测试模型白名单功能
+
+### 1. 进入设置页面
+
+点击左侧导航"设置",滚动到"全局模型白名单"卡片
+
+### 2. 测试功能
+
+#### 添加模型到白名单
+- **方式1**: 在输入框输入模型名称,按回车
+- **方式2**: 点击"可用模型列表"中的模型
+
+#### 删除模型
+- 点击已选模型徽章上的 × 按钮
+
+#### 保存配置
+- 点击"保存模型白名单"按钮
+- 系统自动触发路由重建
+
+### 3. 验证效果
+
+#### 查看路由
+进入"令牌路由"页面,确认只有白名单中的模型有路由
+
+#### 查看模型候选
+```bash
+curl -H "Authorization: Bearer test-admin-token" \
+ http://localhost:4000/api/models/token-candidates | python -m json.tool
+```
+
+应该只返回白名单中的模型
+
+### 4. API 测试命令
+
+#### 查看当前白名单
+```bash
+curl -H "Authorization: Bearer test-admin-token" \
+ http://localhost:4000/api/settings/runtime | python -m json.tool | grep -A 5 "globalAllowedModels"
+```
+
+#### 设置白名单
+```bash
+curl -X PUT \
+ -H "Authorization: Bearer test-admin-token" \
+ -H "Content-Type: application/json" \
+ -d '{"globalAllowedModels": ["gpt-4", "gpt-4o"]}' \
+ http://localhost:4000/api/settings/runtime | python -m json.tool
+```
+
+#### 清空白名单(允许所有模型)
+```bash
+curl -X PUT \
+ -H "Authorization: Bearer test-admin-token" \
+ -H "Content-Type: application/json" \
+ -d '{"globalAllowedModels": []}' \
+ http://localhost:4000/api/settings/runtime | python -m json.tool
+```
+
+## 停止测试服务器
+
+### 查找进程
+```bash
+# Windows
+netstat -ano | grep ":4000" | grep "LISTENING"
+
+# Linux/Mac
+lsof -i :4000
+```
+
+### 停止进程
+```bash
+# Windows (替换 PID)
+taskkill //F //PID
+
+# Linux/Mac
+kill
+```
+
+## 开发工作流
+
+### 修改代码后重新构建
+
+```bash
+pnpm build
+```
+
+### 运行测试
+
+```bash
+pnpm test
+```
+
+### 提交代码
+
+```bash
+git add .
+git commit -m "feat: 实现模型白名单功能"
+git push origin routing-ux-optimization
+```
+
+## 常见问题
+
+### Q: 路由列表为空?
+A: 检查 token_model_availability 表是否有数据,触发模型发现重新加载
+
+### Q: 白名单保存后路由消失?
+A: 正常现象,白名单会过滤掉不在列表中的模型路由
+
+### Q: 如何恢复所有模型?
+A: 清空白名单(设置为空数组 [])即可
+
+## 数据库位置
+
+测试数据库存储在: `./tmp/test-db/hub.db`
+
+查看数据库:
+```bash
+sqlite3 ./tmp/test-db/hub.db
+```
+
+常用查询:
+```sql
+-- 查看所有站点
+SELECT id, name, url, platform FROM sites WHERE status = 'active';
+
+-- 查看所有账号
+SELECT id, site_id, username, status FROM accounts;
+
+-- 查看所有token
+SELECT id, account_id, name, value_status FROM account_tokens;
+
+-- 查看模型可用性
+SELECT COUNT(*) FROM token_model_availability;
+
+-- 查看路由
+SELECT COUNT(*) FROM token_routes;
+```
\ No newline at end of file
diff --git a/build/desktop-icon.png b/build/desktop-icon.png
new file mode 100644
index 00000000..00a365e7
Binary files /dev/null and b/build/desktop-icon.png differ
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 692340a4..9350db66 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1.7
-FROM node:20-bookworm-slim AS builder
+FROM node:22-bookworm-slim AS builder
WORKDIR /app
@@ -10,13 +10,14 @@ RUN apt-get update \
ENV PYTHON=/usr/bin/python3
COPY package.json package-lock.json ./
-RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund
+RUN --mount=type=cache,target=/root/.npm npm ci --ignore-scripts --no-audit --no-fund
+RUN npm rebuild esbuild sharp better-sqlite3 --no-audit --no-fund
COPY . .
-RUN npm run build
+RUN npm run build:web && npm run build:server
RUN --mount=type=cache,target=/root/.npm npm prune --omit=dev --no-audit --no-fund
-FROM node:20-bookworm-slim
+FROM node:22-bookworm-slim
WORKDIR /app
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 360a8f46..99d4a6aa 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -2,7 +2,7 @@ services:
metapi:
image: 1467078763/metapi:latest
ports:
- - "4000:4000"
+ - "127.0.0.1:4000:4000"
volumes:
- ./data:/app/data
environment:
diff --git a/docs/.vitepress/config.test.ts b/docs/.vitepress/config.test.ts
new file mode 100644
index 00000000..0e60588f
--- /dev/null
+++ b/docs/.vitepress/config.test.ts
@@ -0,0 +1,96 @@
+import { existsSync, globSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import config from './config';
+
+const getAliasEntry = (aliasConfig: unknown, specifier: string) => {
+ if (!aliasConfig) return undefined;
+
+ if (Array.isArray(aliasConfig)) {
+ return aliasConfig.find(
+ (entry) =>
+ entry &&
+ typeof entry === 'object' &&
+ 'find' in entry &&
+ ((typeof entry.find === 'string' && entry.find === specifier) ||
+ (entry.find instanceof RegExp && entry.find.test(specifier))),
+ );
+ }
+
+ if (typeof aliasConfig === 'object' && aliasConfig !== null && specifier in aliasConfig) {
+ return {
+ find: specifier,
+ replacement: String((aliasConfig as Record)[specifier]),
+ };
+ }
+
+ return undefined;
+};
+
+const getAlias = (aliasConfig: unknown, specifier: string): string | undefined => {
+ const aliasEntry = getAliasEntry(aliasConfig, specifier);
+ return aliasEntry && typeof aliasEntry === 'object' && 'replacement' in aliasEntry ? String(aliasEntry.replacement) : undefined;
+};
+
+const repoRoot = fileURLToPath(new URL('../..', import.meta.url));
+
+const getExpectedEntry = (hoistedRelativePath: string, pnpmPattern: string) => {
+ const hoistedEntry = resolve(repoRoot, hoistedRelativePath);
+ if (existsSync(hoistedEntry)) return hoistedEntry;
+
+ const [pnpmEntry] = globSync(resolve(repoRoot, pnpmPattern));
+ return pnpmEntry;
+};
+
+describe('docs vitepress config', () => {
+ it('ships copied main-app favicon assets for docs', () => {
+ expect(existsSync(resolve(repoRoot, 'docs/public/favicon.png'))).toBe(true);
+ expect(existsSync(resolve(repoRoot, 'docs/public/favicon-64.png'))).toBe(true);
+ expect(existsSync(resolve(repoRoot, 'docs/public/favicon.ico'))).toBe(true);
+ });
+
+ it('ships a fallback favicon.ico for browsers that probe the default path', () => {
+ expect(existsSync(resolve(repoRoot, 'docs/public/favicon.ico'))).toBe(true);
+ });
+
+ it('declares the main-app favicon assets in docs head tags', () => {
+ const iconLinks =
+ config.head?.filter(
+ (entry) =>
+ entry[0] === 'link' &&
+ typeof entry[1] === 'object' &&
+ entry[1] !== null &&
+ 'rel' in entry[1] &&
+ (entry[1].rel === 'icon' || entry[1].rel === 'shortcut icon'),
+ ) ?? [];
+
+ expect(iconLinks.some((entry) => typeof entry[1] === 'object' && entry[1] !== null && 'href' in entry[1] && entry[1].href === '/favicon.png')).toBe(true);
+ expect(iconLinks.some((entry) => typeof entry[1] === 'object' && entry[1] !== null && 'href' in entry[1] && entry[1].href === '/favicon-64.png')).toBe(true);
+ expect(iconLinks.some((entry) => typeof entry[1] === 'object' && entry[1] !== null && 'href' in entry[1] && entry[1].href === '/favicon.ico')).toBe(true);
+ });
+
+ it('aliases dayjs to the ESM entry for mermaid browser compatibility', () => {
+ const aliasConfig = config.vite?.resolve?.alias;
+ const alias = getAlias(aliasConfig, 'dayjs');
+ const aliasEntry = getAliasEntry(aliasConfig, 'dayjs');
+
+ expect(alias).toBe(getExpectedEntry('node_modules/dayjs/esm/index.js', 'node_modules/.pnpm/dayjs@*/node_modules/dayjs/esm/index.js'));
+ expect(alias && existsSync(alias)).toBe(true);
+ expect(Array.isArray(aliasConfig)).toBe(true);
+ expect(aliasEntry && typeof aliasEntry === 'object' && 'find' in aliasEntry && aliasEntry.find instanceof RegExp).toBe(true);
+ expect(aliasEntry && typeof aliasEntry === 'object' && 'find' in aliasEntry && aliasEntry.find instanceof RegExp && aliasEntry.find.test('dayjs/plugin/duration.js')).toBe(false);
+ });
+
+ it('aliases sanitize-url to a browser-loadable source entry', () => {
+ const alias = getAlias(config.vite?.resolve?.alias, '@braintree/sanitize-url');
+
+ expect(alias).toBe(
+ getExpectedEntry(
+ 'node_modules/@braintree/sanitize-url/src/index.ts',
+ 'node_modules/.pnpm/@braintree+sanitize-url@*/node_modules/@braintree/sanitize-url/src/index.ts',
+ ),
+ );
+ expect(alias && existsSync(alias)).toBe(true);
+ });
+});
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 502bb1dc..e79909ce 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,20 +1,63 @@
+import { existsSync, globSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitepress';
import { withMermaid } from 'vitepress-plugin-mermaid';
+const repoRoot = fileURLToPath(new URL('../..', import.meta.url));
+const resolveDependencyEntry = (hoistedRelativePath: string, pnpmPattern: string) => {
+ const hoistedEntry = resolve(repoRoot, hoistedRelativePath);
+ if (existsSync(hoistedEntry)) return hoistedEntry;
+
+ const [pnpmEntry] = globSync(resolve(repoRoot, pnpmPattern));
+ return pnpmEntry;
+};
+
+const dayjsEsmEntry = resolveDependencyEntry(
+ 'node_modules/dayjs/esm/index.js',
+ 'node_modules/.pnpm/dayjs@*/node_modules/dayjs/esm/index.js',
+);
+const sanitizeUrlSourceEntry = resolveDependencyEntry(
+ 'node_modules/@braintree/sanitize-url/src/index.ts',
+ 'node_modules/.pnpm/@braintree+sanitize-url@*/node_modules/@braintree/sanitize-url/src/index.ts',
+);
+
+if (!dayjsEsmEntry) {
+ throw new Error('Unable to resolve the dayjs ESM entry required by vitepress-plugin-mermaid.');
+}
+
+if (!sanitizeUrlSourceEntry) {
+ throw new Error('Unable to resolve the sanitize-url source entry required by vitepress-plugin-mermaid.');
+}
+
export default withMermaid(
defineConfig({
lang: 'zh-CN',
title: 'Metapi 文档',
description: 'Metapi 使用文档、FAQ 与维护协作指南',
+ head: [
+ ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon.png' }],
+ ['link', { rel: 'icon', type: 'image/png', sizes: '64x64', href: '/favicon-64.png' }],
+ ['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
+ ],
cleanUrls: true,
lastUpdated: true,
ignoreDeadLinks: true,
+ vite: {
+ resolve: {
+ alias: [
+ { find: /^dayjs$/, replacement: dayjsEsmEntry },
+ { find: /^@braintree\/sanitize-url$/, replacement: sanitizeUrlSourceEntry },
+ ],
+ },
+ },
themeConfig: {
siteTitle: 'Metapi Docs',
logo: '/logos/logo-icon-512.png',
nav: [
{ text: '首页', link: '/' },
{ text: '快速上手', link: '/getting-started' },
+ { text: '上游接入', link: '/upstream-integration' },
{ text: 'FAQ', link: '/faq' },
{ text: '文档维护', link: '/README' },
{ text: '项目主页', link: 'https://github.com/cita-777/metapi' },
@@ -31,6 +74,7 @@ export default withMermaid(
{
text: '使用与运维',
items: [
+ { text: '上游接入', link: '/upstream-integration' },
{ text: '配置说明', link: '/configuration' },
{ text: '客户端接入', link: '/client-integration' },
{ text: '运维手册', link: '/operations' },
diff --git a/docs/README.md b/docs/README.md
index baaad0d1..8af72617 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -28,12 +28,14 @@ npm run docs:build
|------|--------|------------|
| 对外第一印象、产品定位、核心入口 | [文档首页](/) | 需要调整公开落地页信息架构、首页 CTA 或首屏导航时 |
| 新用户部署与首条请求 | [快速上手](./getting-started.md) | 新安装流程、默认端口、首次调用步骤变化时 |
+| 上游平台选择与接法 | [上游接入](./upstream-integration.md) | 平台支持范围、默认连接分段、自动识别规则变化时 |
| 生产部署与回滚 | [部署指南](./deployment.md) | Docker Compose、反向代理、升级回滚策略变更时 |
| 环境变量、参数和配置项 | [配置说明](./configuration.md) | 新增配置、默认值变化、兼容行为变化时 |
| 客户端与工具接入 | [客户端接入](./client-integration.md) | Open WebUI、Cherry Studio、Cursor 等接入方式变化时 |
| 运维排障与日常维护 | [运维手册](./operations.md) / [常见问题](./faq.md) | 新排障案例、备份恢复、健康检查、典型报错变化时 |
| FAQ / 教程协作沉淀 | [FAQ/教程贡献规范](./community/faq-tutorial-guidelines.md) | 需要新增教程、FAQ 模板、内容提交流程时 |
| 仓库目录与组织约定 | [目录规范](./project-structure.md) | 目录结构、归档策略或命名约定变化时 |
+| 工程守则与漂移治理 | [Harness Engineering](./engineering/harness-engineering.md) | 需要更新仓库级黄金原则、自动巡检范围或垃圾回收流程时 |
## 维护约定
diff --git a/docs/client-integration.md b/docs/client-integration.md
index 4e28ba96..28fcc385 100644
--- a/docs/client-integration.md
+++ b/docs/client-integration.md
@@ -13,10 +13,13 @@ Metapi 暴露标准 OpenAI / Claude 兼容接口,下游客户端通常只需
| 配置项 | 值 |
|--------|-----|
| **Base URL** | 按客户端字段行为填写:会自动补 `/v1` 的字段填 `https://your-domain.com`;要求完整 API URL 的字段填 `https://your-domain.com/v1` |
-| **API Key** | 你设置的 `PROXY_TOKEN` 值,或创建的下游 API Key |
+| **API Key** | 你设置的 `PROXY_TOKEN` 值,或创建的[下游 API Key](./configuration.md#下游-api-key-策略) |
模型列表自动从 `GET /v1/models` 获取,无需手动配置。
+> [!TIP]
+> 如果不确定客户端是否自动补 `/v1`,先试根域名,报 404 再改成带 `/v1` 的完整路径。
+
## 支持的接口
| 接口 | 方法 | 说明 |
@@ -82,9 +85,19 @@ Metapi 暴露标准 OpenAI / Claude 兼容接口,下游客户端通常只需
}
```
-> 说明:`ANTHROPIC_BASE_URL` 这里填根域名,不要手动拼接 `/v1`;不同版本客户端可能读取 `ANTHROPIC_API_KEY` 或 `ANTHROPIC_AUTH_TOKEN`,建议同时设置。
-> 上述变量由 Claude Code 客户端读取,属于客户端行为开关;
-> `metapi` 服务端无法替代客户端自行设置这些变量,只能处理已经发来的请求。
+**环境变量说明:**
+
+| 变量 | 作用 |
+|------|------|
+| `ANTHROPIC_BASE_URL` | 指向 Metapi 的根域名,Claude Code 会自动拼接 `/v1/messages` |
+| `ANTHROPIC_API_KEY` | 认证令牌,填 `PROXY_TOKEN` 值 |
+| `ANTHROPIC_AUTH_TOKEN` | 部分版本读取此变量而非 `ANTHROPIC_API_KEY`,建议两个都设 |
+| `CLAUDE_CODE_ATTRIBUTION_HEADER` | 设为 `0` 禁用 Attribution 头,避免部分上游不支持该头导致报错 |
+| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | 设为 `1` 减少非必要请求(遥测等),降低 Token 消耗 |
+
+> [!IMPORTANT]
+> `ANTHROPIC_BASE_URL` 填根域名即可,**不要**手动拼接 `/v1`。
+> 上述变量由 Claude Code 客户端读取,属于客户端行为开关;Metapi 服务端只能处理已经发来的请求。
### Codex CLI
@@ -115,22 +128,23 @@ base_url = "https://your-domain.com/v1"
### 其他客户端
-所有支持 OpenAI API 格式的客户端均可接入,只需找到 Base URL 和 API Key 的配置位置即可:如果客户端会自动补 `/v1`,填根域名;如果字段要求完整 OpenAI API URL,就填 `https://your-domain.com/v1`。
+所有支持 OpenAI API 格式的客户端均可接入,只需找到 Base URL 和 API Key 的配置位置即可:
-## 下游 API Key 策略
+| 你的客户端... | 填法 |
+|--------------|------|
+| 有 "Base URL" 字段,会自动补 `/v1` | 填 `https://your-domain.com` |
+| 有 "OpenAI API URL" 字段,要求完整路径 | 填 `https://your-domain.com/v1` |
+| 有 "Anthropic" 选项 | 填 `https://your-domain.com`,Metapi 自动处理 `/v1/messages` |
+| 只能填 API Key | 填 `PROXY_TOKEN` 值即可 |
-除了全局 `PROXY_TOKEN`,你还可以在 **系统设置 → 下游 API Key 策略** 中创建多个项目级下游 Key,每个 Key 可独立配置:
+> [!TIP]
+> 不确定选哪种?先用 `/v1/models` 测试连通性,再配置模型。
-| 配置项 | 说明 |
-|--------|------|
-| **过期时间**(ExpiresAt) | Key 到期后自动失效 |
-| **费用上限**(MaxCost) | 累计消费超限后拒绝请求 |
-| **请求上限**(MaxRequests) | 累计请求数超限后拒绝请求 |
-| **模型白名单**(SupportedModels) | 限制可用模型,支持 exact / glob / `re:regex` |
-| **路由白名单**(AllowedRouteIds) | 限制可走的路由规则 |
-| **站点倍率**(SiteWeightMultipliers) | 按站点调整路由权重,控制上游偏好 |
+## 下游 API Key 策略
+
+除了全局 `PROXY_TOKEN`,你还可以创建多个项目级下游 Key,每个 Key 可独立配置过期时间、费用/请求上限、模型白名单、路由白名单和站点倍率,适用于多团队/多项目共用一个 Metapi 实例的场景。
-适用于多团队/多项目共用一个 Metapi 实例但需要独立计量和权限控制的场景。
+详细字段说明见 [配置说明 → 下游 API Key 策略](./configuration.md#下游-api-key-策略)。
---
@@ -164,29 +178,11 @@ curl -sS https://your-domain.com/v1/chat/completions \
## 常见问题
-### 流式响应异常
-
-如果非流式正常但流式异常,原因几乎都是反向代理配置问题:
-
-1. Nginx 未设置 `proxy_buffering off`
-2. CDN 或中间层缓存了 SSE 响应
-3. 中间层改写了 `text/event-stream` Content-Type
-
-参考 [部署指南 → Nginx 配置](./deployment.md#nginx) 解决。
-
-### 模型列表为空
-
-- 检查是否已添加站点和账号
-- 检查账号是否处于 `healthy` 状态
-- 检查是否已同步 Token
-- 在管理后台手动触发「刷新模型」
-
-### 客户端提示 401 / 403
-
-- 确认使用的是 `PROXY_TOKEN` 而非 `AUTH_TOKEN`
-- 确认反向代理透传了 `Authorization` 请求头
+遇到流式响应异常、模型列表为空、客户端提示 401/403 等问题,请参阅 [常见问题 FAQ](./faq.md)。
## 下一步
-- [配置说明](./configuration.md) — 环境变量详解
+- [配置说明](./configuration.md) — 环境变量详解与下游 API Key 策略
+- [上游接入](./upstream-integration.md) — 添加和管理上游平台
+- [运维手册](./operations.md) — 日志排查与健康检查
- [常见问题](./faq.md) — 更多故障排查
diff --git a/docs/configuration.md b/docs/configuration.md
index 15d2fee6..2019fa8f 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -87,12 +87,19 @@ Metapi 的路由引擎按多因子加权选择最优通道。
### 路由预设建议
-| 场景 | COST_WEIGHT | BALANCE_WEIGHT | USAGE_WEIGHT |
-|------|:-----------:|:--------------:|:------------:|
-| **成本优先** | 0.7 | 0.2 | 0.1 |
-| **均衡(默认)** | 0.4 | 0.3 | 0.3 |
-| **稳定优先** | 0.2 | 0.5 | 0.3 |
-| **轮转均匀** | 0.1 | 0.1 | 0.8 |
+| 场景 | COST_WEIGHT | BALANCE_WEIGHT | USAGE_WEIGHT | 说明 |
+|------|:-----------:|:--------------:|:------------:|------|
+| **成本优先** | 0.7 | 0.2 | 0.1 | 适合预算敏感场景,总是选最便宜的通道 |
+| **均衡(默认)** | 0.4 | 0.3 | 0.3 | 兼顾成本、余额和负载均衡,推荐大多数用户 |
+| **稳定优先** | 0.2 | 0.5 | 0.3 | 适合生产环境,优先选余额充足的通道避免中断 |
+| **轮转均匀** | 0.1 | 0.1 | 0.8 | 适合测试场景,尽量让每个通道都被使用到 |
+
+**调参建议:**
+
+- 如果某个站点总是被选中导致余额很快耗尽 → 调高 `BALANCE_WEIGHT`
+- 如果你有几个很便宜的站点想优先用 → 调高 `COST_WEIGHT`
+- 如果你想让请求均匀分散到所有站点 → 调高 `USAGE_WEIGHT`
+- 如果你需要对特定项目偏好某个站点 → 使用[下游 API Key 的 `siteWeightMultipliers`](#下游-api-key-策略) 而非调全局权重
## 代理日志保留
@@ -169,8 +176,9 @@ Metapi 的路由引擎按多因子加权选择最优通道。
- **个人聊天**:向你的 Bot 发送任意消息,然后访问 `https://api.telegram.org/bot<你的Token>/getUpdates`,在返回的 JSON 中找到 `chat.id`,或者在 Telegram 搜索 @userinfobot 或是 @getmyid_bot。点击 Start,它会立刻回复一串数字(通常是 9 到 10 位)。
- **群组**:将 Bot 邀请进群组,在群内发送消息后同样通过 `getUpdates` 接口获取群组 Chat ID(通常为负数,如 `-1001234567890`)
- **频道**:使用频道用户名,如 `@your_channel`(需先将 Bot 添加为频道管理员)
-3. **填入配置**:将获取的 Token 和 Chat ID 填入环境变量或在管理后台「通知设置」页面中配置
-4. **测试**:保存后点击页面上的「测试通知」按钮验证是否收到消息
+3. **填入配置**:将获取的 Token 和 Chat ID 填入管理后台「通知设置」页面
+4. **大陆服务器反代**:如果服务器无法直连 Telegram,可在「通知设置」里填写 `Telegram API Base URL`,例如 `https://your-proxy.example.com`,Metapi 将请求 `https://your-proxy.example.com/bot/sendMessage`
+5. **测试**:保存后点击页面上的「测试通知」按钮验证是否收到消息
### SMTP 邮件
@@ -205,7 +213,18 @@ Metapi 的路由引擎按多因子加权选择最优通道。
> `TOKEN_ROUTER_CACHE_TTL_MS`、`PROXY_LOG_RETENTION_DAYS`、`PROXY_LOG_RETENTION_PRUNE_INTERVAL_MINUTES` 当前仍属于部署级环境变量,不在后台运行时设置里单独维护。
+## 站点公告
+
+管理后台新增了「站点公告」页面,用于保存和浏览 Metapi 已同步到本地的上游公告记录。
+
+- 首次发现的上游公告会写入站内通知,并按现有通知渠道外发一次
+- 后续重复同步只更新本地公告记录,不会重复外发同一条公告
+- 当前支持的上游公告来源包括 `new-api`、`done-hub` 与 `sub2api`
+- 「清空公告」只删除 Metapi 本地保存的公告记录,不会修改上游站点数据
+
## 下一步
- [部署指南](./deployment.md) — Docker Compose 与反向代理
- [客户端接入](./client-integration.md) — 对接下游应用
+- [上游接入](./upstream-integration.md) — 添加和管理上游平台
+- [运维手册](./operations.md) — 备份、日志与健康检查
diff --git a/docs/deployment.md b/docs/deployment.md
index d3ba7542..9e11788c 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -9,7 +9,8 @@
| 场景 | 推荐方式 | 对外访问方式 | 数据位置 |
|------|----------|--------------|----------|
| 云服务器 / NAS / 家用主机长期运行 | Docker / Docker Compose / Zeabur | 固定服务地址,例如 `http://your-host:4000` 或反向代理域名 | 你挂载的 `DATA_DIR` / 持久化卷 |
-| 个人电脑本地使用 | 桌面版安装包 | 桌面窗口;如需本机客户端直连,使用日志中打印的 `http://127.0.0.1:` | Electron `app.getPath('userData')/data` |
+| 免费云部署(24h 在线) | Render + TiDB + UptimeRobot | Render 分配的 `.onrender.com` 域名或自定义域名 | TiDB Serverless(外部 MySQL 数据库) |
+| 个人电脑本地使用 | 桌面版安装包 | 桌面窗口;默认本机客户端直连使用 `http://127.0.0.1:4000`,局域网客户端可使用当前机器 IP + `4000`;如有覆盖则按 `METAPI_DESKTOP_SERVER_PORT` 的实际端口访问 | Electron `app.getPath('userData')/data` |
| 二次开发 / 调试 | 本地开发 | 前端 `http://localhost:5173`,后端默认 `http://localhost:4000` | 仓库内 `./data` 或自定义 `DATA_DIR` |
> [!NOTE]
@@ -44,6 +45,89 @@
---
+## Render 一键部署(免费 24h 运行)
+
+
+
+
+
+通过 **Render + TiDB + UptimeRobot** 组合,可以实现 **完全免费的 24 小时持续运行**:
+
+| 组件 | 作用 | 免费额度 |
+|------|------|----------|
+| [Render](https://render.com) | 运行 Metapi 容器 | Free Web Service(750 小时/月,闲置 15 分钟自动休眠) |
+| [TiDB Serverless](https://tidbcloud.com) | MySQL 兼容数据库,替代 SQLite 实现数据持久化 | 5 GiB 存储 + 5000 万 Request Units/月 |
+| [UptimeRobot](https://uptimerobot.com) | 每 5 分钟 ping 一次,防止 Render 免费实例休眠 | 50 个免费监控 |
+
+> [!IMPORTANT]
+> Render 免费版 **不支持持久化磁盘**,容器重启后本地文件会丢失。因此 **必须使用外部数据库**(推荐 TiDB Serverless),不能使用默认的 SQLite。
+
+### 步骤 1:注册 TiDB Serverless 并获取连接串
+
+1. 前往 [TiDB Cloud](https://tidbcloud.com) 注册账号
+2. 创建一个 **Serverless** 集群(免费)
+3. 在集群概览页点击 **Connect**,在弹出的面板中:
+ - **Connection Type**: Public
+ - **Database**: ⚠️ **必须改为 `test`**(默认是 `sys`,这是系统库,不允许建表!)
+ - 点击 **Generate Password** 生成密码并妥善保存
+4. 使用面板中显示的参数拼接 `DB_URL`:
+
+ ```
+ mysql://:@:4000/test?ssl={"rejectUnauthorized":true}
+ ```
+
+ > ⚠️ 注意:``、``、`` 从 Connect 面板中获取,**数据库名必须用 `test` 而非默认的 `sys`**。
+
+> [!TIP]
+> 这里只是以TiDB作为示例,你也可以使用其他提供免费额度的云数据库方案(如 Neon、Supabase 等),只需将 `DB_TYPE` 设为对应的 `mysql` 或 `postgres`,并填入正确的连接串即可。什么?你不会其他的?把步骤复制给Gemini问他怎么改。
+
+### 步骤 2:部署到 Render
+
+**方式一:一键部署(推荐)**
+
+1. 点击上方 **Deploy to Render** 按钮
+2. 如果你 Fork 了仓库,也可以使用你自己的仓库地址
+3. 在 Render 界面中填写环境变量(见下表)
+
+**方式二:手动创建**
+
+1. 在 [Render Dashboard](https://dashboard.render.com) 点击 **New → Web Service**
+2. 连接你的 GitHub 仓库(或使用公开仓库地址 `https://github.com/cita-777/metapi`)
+3. 配置:
+ - **Environment**: Docker
+ - **Dockerfile Path**: `./docker/Dockerfile`
+ - **Docker Build Context**: `.`(仓库根目录)
+ - **Instance Type**: Free
+4. 添加环境变量(见下表)
+
+### 环境变量配置
+
+| 变量 | 说明 | 示例值 |
+|------|------|--------|
+| `AUTH_TOKEN` | 管理后台登录令牌(**必填**) | 你的强密码 |
+| `PROXY_TOKEN` | 代理接口 Bearer Token(**必填**) | 你的代理密钥 |
+| `DB_TYPE` | 数据库类型(**必填**) | `mysql` |
+| `DB_URL` | TiDB 连接串(**必填**) | `mysql://user:pass@host:4000/db?ssl=...` |
+| `DB_SSL` | 启用 SSL 连接 | `true` |
+| `TZ` | 时区 | `Asia/Shanghai` |
+| `PORT` | 服务端口(默认即可) | `4000` |
+
+### 步骤 3:配置 UptimeRobot 防休眠
+
+Render 免费实例在 15 分钟无流量后会自动休眠。使用 UptimeRobot 定时 ping 可以保持实例 24h 在线:
+
+1. 前往 [UptimeRobot](https://uptimerobot.com) 注册免费账号
+2. 添加新监控:
+ - **Monitor Type**: HTTP(s)
+ - **URL**: `https://your-app.onrender.com`(替换为 Render 分配的域名)
+ - **Monitoring Interval**: 5 minutes
+3. 保存即可,UptimeRobot 会每 5 分钟访问一次你的服务,防止休眠
+
+> [!TIP]
+> 部署完成后,通过 Render 分配的 `.onrender.com` 域名访问后台,使用 `AUTH_TOKEN` 登录即可。也可以在 Render 设置中绑定自定义域名。
+
+---
+
## Docker Compose 部署(推荐)
### 标准步骤
@@ -97,50 +181,23 @@ docker run -d --name metapi \
## 桌面版部署(Windows / macOS / Linux)
-个人电脑本地部署请直接使用 [Releases](https://github.com/cita-777/metapi/releases) 中的 Electron 安装包:
-
-1. 下载与你系统匹配的桌面安装包
-2. 安装并启动 Metapi Desktop
-3. 桌面壳会自动启动本地服务并将数据保存到应用数据目录
+桌面版面向个人电脑本地使用,基本安装与配置流程见 [快速上手 → 桌面版启动](./getting-started.md#方式二-桌面版启动-windows-macos-linux)。
-| 项目 | 说明 |
-|------|------|
-| 适用场景 | 单机本地使用、个人电脑常驻、需要免 Docker 的桌面体验 |
-| 后端监听 | 绑定 `127.0.0.1`,默认会在 `4310..4399` 中选择空闲端口;只有设置 `METAPI_DESKTOP_SERVER_PORT` 时才固定 |
-| 管理界面 | 直接在桌面窗口打开,不建议把桌面版当成固定 `localhost:4000` 的服务来写文档或脚本 |
-| 数据目录 | `app.getPath('userData')/data` |
-| 日志目录 | `app.getPath('userData')/logs`,托盘菜单提供 `Open Logs Folder` |
-
-> [!TIP]
-> - Windows 下常见路径是 `%APPDATA%\Metapi\data` 和 `%APPDATA%\Metapi\logs`。
-> - 如果你想让本机其他客户端调用桌面版内置 `/v1/*`,先从日志确认当前端口。
+以下是部署相关的补充说明。
-桌面版特性:
+### 桌面版特性
- 内置本地 Metapi 服务,无需手动准备 Node.js 运行环境
- 托盘菜单支持重新打开窗口、重启后端、开机自启
- 支持基于 GitHub Releases 的应用内更新检查
+> [!IMPORTANT]
+> 桌面版首次启动时,如果没有显式注入 `AUTH_TOKEN`,管理员登录令牌默认是 `change-me-admin-token`。
+> 这只适合本机初始调试使用,首次登录后应立即修改。
+
> [!NOTE]
> 服务器部署不再提供裸 Node.js Release 压缩包,统一推荐 Docker / Docker Compose。
-### 桌面版如何找到当前本地地址
-
-桌面版启动后,后端会把当前地址打印到日志中。定位方式:
-
-1. 打开托盘菜单,点击 `Open Logs Folder`
-2. 打开最新日志文件
-3. 搜索 `Dashboard:` 或 `Proxy API:` 行
-
-常见日志内容如下:
-
-```text
-Dashboard: http://127.0.0.1:4312
-Proxy API: http://127.0.0.1:4312/v1/chat/completions
-```
-
-如果你只是通过桌面窗口使用管理后台,可以完全忽略这个端口;只有在本机其他客户端需要直连桌面版内置后端时,才需要读取这里的地址。
-
### 桌面版升级
1. 通过应用内更新提示安装新版本,或从 Releases 下载最新安装包覆盖安装
@@ -149,22 +206,7 @@ Proxy API: http://127.0.0.1:4312/v1/chat/completions
## 本地开发运行(源码调试)
-如果你的目标是开发、调试或提交 PR,请直接跑源码:
-
-```bash
-git clone https://github.com/cita-777/metapi.git
-cd metapi
-npm install
-npm run db:migrate
-npm run dev
-```
-
-默认访问地址:
-
-| 服务 | 地址 |
-|------|------|
-| 前端(Vite) | `http://localhost:5173` |
-| 后端 API | `http://localhost:4000` |
+开发、调试或提交 PR 的完整流程见 [快速上手 → 本地开发启动](./getting-started.md#方式三-本地开发启动) 和 [CONTRIBUTING.md](../CONTRIBUTING.md)。
> [!NOTE]
> 这条路径是开发流程,不是下载 `Release` 包后再手动跑 Node.js 的替代说法。
@@ -173,7 +215,7 @@ npm run dev
## 反向代理
-以下反向代理配置面向 Docker / 服务器模式。桌面版内置后端默认只绑定本机 `127.0.0.1`,通常不作为公网服务直接暴露。
+以下反向代理配置面向 Docker / 服务器模式。桌面版内置后端默认监听 `0.0.0.0:4000`,但通常仍作为单机桌面应用使用;如果要给局域网或公网客户端访问,请自行配置防火墙、反向代理和认证边界。若你显式设置了 `METAPI_DESKTOP_SERVER_PORT`,请把示例里的 `4000` 改成对应端口。
### Nginx
@@ -235,27 +277,9 @@ docker image prune -f
## 回滚
-如果升级后出现问题:
-
-1. **升级前备份**(建议每次升级前执行):
+如果升级后出现问题,请参考 [运维手册 → 数据备份与恢复](./operations.md#数据备份) 进行回滚。
-```bash
-cp -r data/ data-backup-$(date +%Y%m%d)/
-```
-
-2. **回滚到指定版本**:
-
-```bash
-# 修改 docker-compose.yml 中的 image tag
-# 例如:image: 1467078763/metapi:v1.0.0
-
-# 恢复数据
-rm -rf data/
-cp -r data-backup-20260228/ data/
-
-# 重启
-docker compose up -d
-```
+核心思路:升级前备份数据目录(或数据库),出问题时停止服务、还原数据、指定旧版镜像重启。
## 数据持久化
@@ -264,6 +288,7 @@ docker compose up -d
| 运行方式 | 数据目录 | 说明 |
|----------|----------|------|
| Docker / Docker Compose / Zeabur | 容器内 `DATA_DIR`(常见为 `/app/data`) | 需要映射到宿主机目录或平台持久化卷 |
+| Render + TiDB | TiDB Serverless(外部 MySQL) | 无本地持久化,数据全部存储在 TiDB 云端数据库 |
| 本地开发 | `DATA_DIR`,默认 `./data` | 位于当前仓库工作目录 |
| 桌面版 | `app.getPath('userData')/data` | 不在仓库目录里,升级桌面应用时会保留 |
@@ -271,39 +296,7 @@ docker compose up -d
只要备份了对应的数据目录,升级、重启通常都不会丢失现有配置和 SQLite 数据。
-### 备份策略建议
-
-- 每日自动备份 `data/` 目录
-- 保留最近 7~30 天的备份
-- 重要操作前手动快照
-
-## 文档站部署
-
-Metapi 使用 [VitePress](https://vitepress.dev) 构建文档站,支持本地预览和 GitHub Pages 自动部署。
-
-### 本地预览
-
-```bash
-npm run docs:dev
-```
-
-访问 `http://localhost:4173` 查看文档站。
-
-### 构建静态站点
-
-```bash
-npm run docs:build
-```
-
-构建产物位于 `docs/.vitepress/dist/`,可部署到任意静态站点托管服务。
-
-### GitHub Pages 自动部署
-
-推送到 `main` 分支后,`.github/workflows/docs-pages.yml` 会自动构建并部署到 GitHub Pages。
-
-首次使用需在仓库设置中开启:
-
-`Settings → Pages → Build and deployment → Source: GitHub Actions`
+完整备份策略见 [运维手册 → 数据备份](./operations.md#数据备份)。
---
diff --git a/docs/engineering/harness-engineering.md b/docs/engineering/harness-engineering.md
new file mode 100644
index 00000000..2f48fd4a
--- /dev/null
+++ b/docs/engineering/harness-engineering.md
@@ -0,0 +1,140 @@
+# Metapi Harness Engineering
+
+This document captures the repo-level engineering taste that should remain true
+even when future work is performed by autonomous agents. The goal is not to
+replace feature tests. The goal is to keep the repository readable,
+review-friendly, and hard to drift.
+
+## Why This Exists
+
+`metapi` already has strong local discipline in several areas:
+
+- architecture tests that pin important boundaries
+- schema parity and runtime bootstrap verification across databases
+- shared protocol helpers that deliberately replace parallel implementations
+
+What was still missing was one place that states the mechanical rules behind
+those choices and a lightweight loop that continuously checks for drift. This
+document is that shared contract.
+
+## Golden Principles
+
+### 1. One Invariant, One Source Of Truth
+
+If a helper, contract, or workflow already owns an invariant, new work should
+reuse that home instead of creating a second implementation.
+
+Examples:
+
+- protocol shaping and normalization belong in `src/server/transformers/**`
+- endpoint fallback belongs in `src/server/routes/proxy/endpointFlow.ts`
+- proxy success/failure bookkeeping belongs in
+ `src/server/proxy-core/surfaces/sharedSurface.ts`
+- schema heuristics belong in `src/server/db/schemaMetadata.ts`
+
+### 2. Routes Are Adapters, Not Owners
+
+`src/server/routes/**` should remain thin. Route files may register endpoints,
+read Fastify request state, and delegate to shared orchestration. They should
+not become the place where retry loops, protocol conversion, stream lifecycle,
+or billing logic lives.
+
+If a helper is imported outside a single route file, that helper should move to
+`proxy-core`, `services`, `transformers`, or another neutral home.
+
+### 3. Transformers Must Stay Protocol-Pure
+
+Transformers are the protocol boundary. They may depend on canonical/shared
+contracts, but they should not reach back into route files, Fastify handlers,
+OAuth services, token routing, or runtime dispatch details.
+
+In practice this means:
+
+- `canonical` is the request truth source
+- `shared/normalized` is the response truth source
+- compatibility layers may orchestrate retries or fallback bodies, but they do
+ not redefine protocol semantics
+
+### 4. Proxy-Core Follows The Golden Path
+
+Proxy orchestration should prefer shared, tested paths instead of bespoke
+surface-local logic.
+
+Current preferred path:
+
+- endpoint ranking and request shaping via `upstreamEndpoint.ts`
+- endpoint-attempt loops via `executeEndpointFlow()`
+- whole-body response decoding via `readRuntimeResponseText()`
+- OAuth refresh, sticky/lease behavior, billing, and proxy logging via
+ `sharedSurface.ts`
+- Codex header/session semantics via provider profiles and header utils
+- Codex websocket transport via the dedicated websocket runtime, not generic
+ fetch executors
+
+### 5. Platform Capability Must Be Explicit
+
+Platform behavior is easy to let drift because it spans adapters, discovery,
+endpoint preference, and routing. Any platform-specific behavior that matters
+at runtime should be stated once and reused, not re-inferred in multiple
+subsystems.
+
+Thin adapters should stay honest. Feature-complete adapters should be tested as
+feature-complete. Do not silently “upgrade” support through generic defaults.
+
+### 6. Database Changes Must Stay Contract-Driven
+
+Schema work already has one of the strongest harnesses in the repo. Keep it
+that way.
+
+- update Drizzle schema and SQLite migration history together
+- regenerate checked-in contract/artifacts together
+- keep cross-dialect bootstrap/upgrade generation contract-driven
+- keep legacy startup compatibility narrow and spec-owned
+
+### 7. Web Pages Are Orchestration Surfaces
+
+Top-level pages should not be reused as shared component libraries.
+
+- no page-to-page imports for reusable UI
+- mobile behavior should reuse shared primitives first
+- extract repeated modals, drawers, and panels into domain subfolders early
+
+## First-Wave Drift Checks
+
+The first repo-level drift loop is intentionally small and high-signal. It
+checks for:
+
+- transformer imports from `routes/proxy`
+- new proxy-core surface body reads that bypass `readRuntimeResponseText()`
+- new imports from `routes/proxy` inside `proxy-core` beyond the current debt
+ baseline
+- new top-level page-to-page imports in the admin UI beyond the current debt
+ baseline
+
+These checks live in `scripts/dev/repo-drift-check.ts` and are wired into CI.
+
+## Tracked Debt Vs New Violations
+
+The repository already contains some known architectural debt, especially where
+`proxy-core` still imports helpers from `routes/proxy` and where one admin page
+reuses another page's export.
+
+The harness uses a ratchet:
+
+- tracked debt is reported so it stays visible
+- new violations fail CI
+
+This keeps the repo moving forward without forcing a risky “rewrite
+everything first” migration.
+
+## Garbage Collection Loop
+
+The harness loop is intentionally conservative:
+
+1. rules are encoded in repo docs and executable checks
+2. CI blocks new drift
+3. a scheduled workflow generates a drift report artifact
+4. targeted cleanup PRs can remove tracked debt in small slices
+
+The goal is steady principal payments on technical debt, not occasional heroic
+cleanup sprints.
diff --git a/docs/faq.md b/docs/faq.md
index 97e6bb06..a738e1ce 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -47,11 +47,13 @@
**A:** Sub2API 常见 JWT 短期会话机制,和传统 NewAPI 站点差异较大。当前建议:
-1. 在「凭证模式」里选择 Session 模式,粘贴 JWT / Bearer Token 进行验证
+1. 在「凭证模式」里选择 Session 模式,分别粘贴 F12 界面中的 `auth_token`、`refresh_token`、`token_expires_at` 字段进行验证,无需配置用户 ID
2. 不要使用账号密码登录,Metapi 不支持代替 Sub2API 做登录
-3. Sub2API 不支持签到;如果你只关心代理调用,也可以直接改用 API Key 模式
+3. Sub2API 通常为订阅制使用,不支持签到;如果你只关心代理调用,也可以直接改用 API Key 模式
4. 若 `GET /v1/models` 为空,先确认该账号下已有可用用户 API Key,Metapi 会再尝试用它发现模型
+详细操作截图见 [上游接入 → Sub2API](./upstream-integration.md#sub2api)。
+
## 部署相关
### Q: 启动后无法访问管理后台
diff --git a/docs/getting-started.md b/docs/getting-started.md
index a71ff4bf..8a341448 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -13,6 +13,7 @@
| 场景 | 推荐方式 | 需要准备 |
|------|----------|----------|
| 云服务器 / NAS / 家用主机长期运行 | Docker / Docker Compose | Docker 与 Docker Compose |
+| 免费云部署(24h 在线) | Render + TiDB + UptimeRobot | 注册 Render / TiDB Cloud / UptimeRobot 免费账号,详见 [Render 部署指南](./deployment.md#render-一键部署免费-24h-运行) |
| 个人电脑本地使用 | 桌面版安装包 | 从 [Releases](https://github.com/cita-777/metapi/releases) 下载对应系统的桌面安装包 |
| 二次开发 / 调试 | 本地开发 | Node.js 20+ 与 npm |
@@ -79,13 +80,22 @@ docker compose up -d
| 项目 | 说明 |
|------|------|
| 管理界面 | 应用启动后会直接打开桌面窗口,不需要假设固定的 `http://localhost:4000` |
-| 本地后端地址 | 桌面版把内置服务绑定到 `127.0.0.1`,默认会在 `4310..4399` 中挑选空闲端口;只有显式设置 `METAPI_DESKTOP_SERVER_PORT` 时才会固定 |
+| 本地后端地址 | 桌面版内置服务默认监听 `0.0.0.0:4000`;桌面窗口和本机 curl 可继续使用 `http://127.0.0.1:4000`,局域网其他设备请使用当前机器的实际 IP + `4000`;如需改端口,可显式设置 `METAPI_DESKTOP_SERVER_PORT` |
| 数据目录 | 保存在 `app.getPath('userData')/data`,不是仓库里的 `./data` |
| 日志目录 | 保存在 `app.getPath('userData')/logs`;托盘菜单提供 `Open Logs Folder` |
+> [!IMPORTANT]
+> 桌面版首次启动时,如果你没有额外注入 `AUTH_TOKEN`,默认管理员令牌就是 `change-me-admin-token`。
+> 首次登录后建议立即到「设置」里改成你自己的强密码令牌。
+
> [!TIP]
> - Windows 下常见路径是 `%APPDATA%\Metapi\data` 和 `%APPDATA%\Metapi\logs`。
-> - 如果你要把本机其他客户端接到桌面版内置后端,先到日志里查当前端口,不要写死 `4000`。
+> - 如果没有额外覆盖端口,本机其他客户端可以直接连接 `http://127.0.0.1:4000`。
+
+> [!WARNING]
+> **端口冲突排障:** 桌面版默认使用 `4000` 端口;如果该端口被其他应用占用:
+> - 设置环境变量 `METAPI_DESKTOP_SERVER_PORT=<指定端口>` 改到一个空闲端口
+> - 或关闭占用 `4000` 的应用后重启 Metapi Desktop
> [!NOTE]
> 服务器部署统一推荐 Docker / Docker Compose,不再提供裸 Node.js 的 Release 压缩包。
@@ -111,6 +121,8 @@ npm run dev
> [!TIP] 从 ALL-API-Hub 迁移(可选)
> 如果你使用过 ALL-API-Hub,Metapi 兼容其导出的备份设置,可直接导入,无需手动逐项配置。
>
+> 导入后刷新账号状态可能出现个别账号令牌过期,点击重新绑定按钮按照下面步骤2的方法获取Access Token或者Cookie等即可。
+>
> 
### 步骤 1:添加站点
@@ -118,59 +130,77 @@ npm run dev
进入 **站点管理**,添加你使用的上游中转站:
- 填写站点名称(自己想怎么取就怎么取)和 URL
-- 选择平台类型(New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API),通常可自动检测
-- 填写站点的管理员 API Key(可选,部分功能需要)
+- 选择平台类型(`new-api` / `one-api` / `one-hub` / `done-hub` / `veloera` / `anyrouter` / `sub2api` / `openai` / `claude` / `gemini` / `cliproxyapi`),通常可自动检测,检测有误或者因为防护页导致检测失败可以手动选择。
+- 可选是否开启系统代理,方便国内机器访问国外中转站。
+- 可选站点权重,站点权重越大,路由将更加频繁使用这个站点的模型。
+
+如果你不确定该选哪个平台,先看 [上游接入](./upstream-integration.md)。

-### 步骤 2:添加账号
+### 步骤 2:添加账号(可签到、查询余额等)
-首先前往你想添加的公益站,进入下图界面:
+进入 **连接管理中的账号管理**,为每个站点添加已注册的账号:

-进入 **账号管理**,为每个站点添加已注册的账号:
-
-
-
-
-
- 填入用户名和访问凭证

- 系统会自动登录并获取余额信息
-
+ 
- 启用自动签到(如站点支持)
-### 步骤 3:同步 Token
+### 步骤 3:添加 API Key(Base URL+Key模式,只可获取模型和使用模型)
+
+首先你需要在步骤1中,确保添加了(`new-api` / `one-api` / `one-hub` / `done-hub` / `veloera` / `anyrouter` / `sub2api` / `openai` / `claude` / `gemini` / `cliproxyapi`)的类型的Base URL。
-进入 **Token 管理**:
+- 进入 **连接管理中的API Key管理**,为每个站点添加你的API Key:
-- 点击「同步」从上游账号拉取 API Key
+
-- 或手动添加已有的 API Key,如下图所示。
+### 步骤 4:同步账号令牌
+
+进入 **连接管理中的账号令牌管理**:
+
+- 点击「同步」从上游账号拉取 账号令牌
+
+- 或手动添加已有的账号令牌,添加后上游站点的令牌管理页面会同步出现令牌,如下图所示。

-### 步骤 4:检查路由
+### 步骤 5:路由管理
进入 **路由管理**:
- 系统会自动发现模型并生成路由规则
+- 点击右上角的刷新选中概率可以显示并将概率载入缓存中
- 可以手动调整通道的优先级和权重
+- 关于路由权重参数调优,参考 [配置说明 → 智能路由](./configuration.md#智能路由)
+- 左侧可以进行品牌、站点、接口等的筛选,如下图所示:
+
+
+
+- **可以通过创建群组,从而对上游模型进行匹配和重定向,如果建立下图群组,下游访问Metapi时获取的claude-opus-4-6模型将在命中样本中智能选取,日志中可以看见映射。** 
+
+- **可以在使用日志中看见下游的请求模型和实际分配给下游使用的模型**
+
+ 
### 步骤 5:验证代理
+**Metapi还有更多功能,可以在设置中寻找,请尽情探索,有建议可以提出Issue改进。**
+
按运行方式选择验证入口:
| 运行方式 | 管理界面 | 代理接口基地址 |
|----------|----------|----------------|
| Docker / Docker Compose | `http://localhost:4000` | `http://localhost:4000` |
| 本地开发 | `http://localhost:5173` | `http://localhost:4000` |
-| 桌面版 | 直接使用桌面窗口 | 先从日志里的 `Proxy API:` 行确认当前 `http://127.0.0.1:` |
+| 桌面版 | 直接使用桌面窗口 | 默认 `http://127.0.0.1:4000`;如果设置了 `METAPI_DESKTOP_SERVER_PORT`,则按日志里的实际端口访问,局域网其他设备改用当前机器 IP + 同一端口 |
### Docker / 本地开发:直接用 curl 验证
@@ -186,26 +216,29 @@ curl -sS http://localhost:4000/v1/chat/completions \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
```
-### 桌面版:先确认当前端口再验证
+### 桌面版:默认直接用 4000 验证
打开托盘菜单的 `Open Logs Folder`,在最新日志里查找类似下面的启动信息:
```text
-Dashboard: http://127.0.0.1:4312
-Proxy API: http://127.0.0.1:4312/v1/chat/completions
+Dashboard: http://127.0.0.1:4000
+Proxy API: http://127.0.0.1:4000/v1/chat/completions
```
-然后把日志里的实际端口替换进 curl:
+如果你没有覆盖端口,可直接执行:
```bash
-curl -sS http://127.0.0.1:4312/v1/models \
+curl -sS http://127.0.0.1:4000/v1/models \
-H "Authorization: Bearer your-proxy-sk-token"
```
-返回正常响应,说明代理链路已经可用。
+如果你显式设置了 `METAPI_DESKTOP_SERVER_PORT`,再把上面的 `4000` 替换成日志里的实际端口。返回正常响应,说明代理链路已经可用。
+
+如果你要从同一局域网的其他设备访问桌面版,把上面的 `127.0.0.1` 替换成这台电脑的实际局域网 IP,并确认系统防火墙已放行对应端口。
## 下一步
+- [上游接入](./upstream-integration.md) — 当前代码支持哪些上游、默认该走哪个连接分段
- [部署指南](./deployment.md) — 反向代理、HTTPS、升级策略
- [配置说明](./configuration.md) — 详细环境变量与路由参数
- [客户端接入](./client-integration.md) — 对接 Open WebUI、Cherry Studio 等
diff --git a/docs/index.md b/docs/index.md
index f115e010..da881172 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -12,23 +12,23 @@ hero:
- theme: brand
text: 10 分钟快速上手
link: /getting-started
+ - theme: alt
+ text: 上游接入
+ link: /upstream-integration
- theme: alt
text: 常见问题 FAQ
link: /faq
- - theme: alt
- text: 文档维护与贡献
- link: /README
features:
- title: 快速上手
details: 从部署到第一条请求,按步骤完成最小可用环境搭建。
link: /getting-started
+ - title: 上游接入
+ details: 按当前代码支持的平台类型,快速判断该选什么平台、先走哪个连接分段。
+ link: /upstream-integration
- title: 问题排查
details: 汇总高频报错、根因定位和标准修复路径,降低重复沟通成本。
link: /faq
- - title: 运维与配置
- details: 覆盖部署、配置、监控、备份、升级回滚等生产场景。
- link: /operations
---
## 项目架构
@@ -289,6 +289,7 @@ onBeforeUnmount(() => {
## 从这里开始
- 初次部署或首次接入:从 [快速上手](/getting-started) 开始,先跑通最小可用链路。
+- 不确定上游平台该怎么选:先看 [上游接入](/upstream-integration),再决定走 `账号管理` 还是 `API Key管理`。
- 准备上线或升级回滚:查看 [部署指南](/deployment) 与 [运维手册](/operations)。
- 需要补齐环境变量或路由参数:直接查 [配置说明](/configuration)。
- 正在处理客户端或第三方工具接入:优先看 [客户端接入](/client-integration)。
diff --git a/docs/operations.md b/docs/operations.md
index 4b0ce06d..a3cc0f1b 100644
--- a/docs/operations.md
+++ b/docs/operations.md
@@ -30,12 +30,35 @@ cp -r data/ data-backup-$(date +%Y%m%d)/
### 方式二:数据库原生备份(MySQL / PostgreSQL)
-如果当前运行库已经切到 MySQL / Postgres,请使用数据库自己的备份工具或云快照,例如 `mysqldump`、`pg_dump`、RDS 快照等。
+如果当前运行库已经切到 MySQL / Postgres,请使用数据库自己的备份工具或云快照。
+
+**MySQL 备份示例:**
+
+```bash
+# 全量导出(替换为你的实际连接信息)
+mysqldump -h -u -p metapi > metapi-backup-$(date +%Y%m%d).sql
+
+# 自动备份(crontab,每天凌晨 3 点)
+0 3 * * * mysqldump -h -u -p metapi | gzip > /path/to/backups/metapi-$(date +\%Y\%m\%d).sql.gz
+```
+
+**PostgreSQL 备份示例:**
+
+```bash
+# 全量导出
+pg_dump -h -U -d metapi -F c -f metapi-backup-$(date +%Y%m%d).dump
+
+# 自动备份(crontab,每天凌晨 3 点,使用 .pgpass 免交互密码)
+0 3 * * * pg_dump -h -U -d metapi -F c -f /path/to/backups/metapi-$(date +\%Y\%m\%d).dump
+```
+
+**云托管数据库:** RDS、PlanetScale、Neon 等可直接使用平台的自动快照功能。
建议:
- 升级、迁移、执行「重新初始化系统」前先做一次库级备份
- 备份对象是当前 metapi 正在使用的运行库,而不只是本地 `data/`
- 恢复后重启 Metapi 一次,确认它重新连接到了正确的运行库
+- 建议保留最近 7~30 天的备份,定期清理过期文件
### 方式三:应用内导出
@@ -104,11 +127,16 @@ npm run dev
| 关键词 | 含义 | 处理方式 |
|--------|------|----------|
-| `auth failed` | 上游站点鉴权失败 | 检查账号凭证是否过期 |
-| `no available channel` | 路由无可用通道 | 检查 Token 是否同步、通道是否被冷却 |
-| `notify failed` | 通知发送失败 | 检查通知渠道配置 |
+| `auth failed` | 上游站点鉴权失败 | 检查账号凭证是否过期,系统会自动尝试重登录 |
+| `no available channel` | 路由无可用通道 | 检查 Token 是否同步、通道是否被冷却;可在路由页查看冷却状态 |
+| `channel cooling` | 通道进入冷却期 | 通道在请求失败后自动冷却 10 分钟,期间不会被路由选中;无需干预,会自动恢复 |
+| `upstream 429` | 上游限流 | 该上游站点触发了速率限制;路由引擎会自动切换其他通道,冷却期后重试 |
+| `upstream 5xx` | 上游服务器错误 | 上游站点临时不可用,路由引擎会自动故障转移到其他通道 |
+| `notify failed` | 通知发送失败 | 检查 [通知渠道配置](./configuration.md#通知渠道) |
| `checkin failed` | 签到失败 | 检查账号状态和站点连通性 |
-| `balance refresh failed` | 余额刷新失败 | 检查账号凭证 |
+| `balance refresh failed` | 余额刷新失败 | 检查账号凭证,可能需要重新登录 |
+| `proxy timeout` | 代理请求超时 | 上游响应过慢;检查网络延迟或考虑切换其他通道 |
+| `token expired` | Token 过期 | 系统会自动尝试续签;若反复出现,手动刷新 Token |
## 健康检查
@@ -126,7 +154,7 @@ curl -sS http://localhost:4000/v1/chat/completions \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}'
```
-以上示例默认服务端部署监听 `localhost:4000`。Desktop 内置后端运行在 `127.0.0.1` 的动态端口,优先通过应用界面、托盘日志或健康检查日志排查,不要假定固定端口。
+以上示例默认服务端部署监听 `localhost:4000`。Desktop 内置后端默认监听 `0.0.0.0:4000`;本机排查通常可直接使用 `127.0.0.1:4000`,如果显式设置了 `METAPI_DESKTOP_SERVER_PORT`,则按日志里的实际端口访问;局域网排查改用当前机器的实际 IP。
### 自动化监控建议
@@ -168,9 +196,14 @@ curl -sS http://localhost:4000/v1/chat/completions \
### 清理缓存并重建路由
-- 「设置 → 清除缓存并重建路由」会清空模型发现缓存、自动路由和自动通道,再后台触发一次模型刷新与路由重建
-- 「TokenRoutes → 重建路由」适合在调整账号、Token、路由规则后手动重新生成自动路由
-- 如果 `GET /v1/models`、路由列表或选择概率明显滞后,优先执行这一操作
+当模型列表(`GET /v1/models`)、路由列表或选择概率明显滞后时,使用以下操作:
+
+| 操作 | 位置 | 适用场景 |
+|------|------|----------|
+| **清除缓存并重建路由** | 设置 → 清除缓存并重建路由 | 全局刷新:清空模型发现缓存、自动路由和自动通道,后台触发模型刷新与路由重建 |
+| **重建路由** | TokenRoutes → 重建路由 | 局部刷新:调整账号、Token、路由规则后手动重新生成自动路由 |
+
+优先使用「重建路由」做局部刷新,问题持续时再用全局清除。
### 批量操作
@@ -193,16 +226,9 @@ curl -sS http://localhost:4000/v1/chat/completions \
执行前建议先做一次导出或数据库备份。
-## 发布前检查清单
-
-如果你在本地开发并准备发布:
-
-- [ ] `npm test` 通过
-- [ ] `npm run build` 通过
-- [ ] `.env`、`data/`、`tmp/` 未提交到 Git
-- [ ] 敏感凭证已从代码中移除
-
## 下一步
- [常见问题](./faq.md) — 常见报错与修复
- [配置说明](./configuration.md) — 环境变量详解
+- [上游接入](./upstream-integration.md) — 平台特定的连接与排障
+- [客户端接入](./client-integration.md) — 下游客户端对接
diff --git a/docs/plans/2026-03-10-model-refresh-health-design.md b/docs/plans/2026-03-10-model-refresh-health-design.md
new file mode 100644
index 00000000..d4df8e77
--- /dev/null
+++ b/docs/plans/2026-03-10-model-refresh-health-design.md
@@ -0,0 +1,49 @@
+---
+title: Model Refresh Health + Toast Feedback
+date: 2026-03-10
+owner: codex
+status: approved
+---
+
+# Goal
+When users click the "模型" refresh action, failed model discovery should mark the account runtime health as `unhealthy` and surface a clear failure toast. Success should show the discovered model names and count.
+
+# Scope
+- Applies to all account types (Session + API Key).
+- Server determines success/failure and error category; UI uses returned payload for toast text.
+
+# Non-goals
+- Changing upstream adapters' auth logic.
+- Altering auto-refresh schedules or background tasks.
+
+# Architecture
+- Update `refreshModelsForAccount(...)` to return structured outcome:
+ - `status`: `success` | `failed`
+ - `modelCount`
+ - `modelsPreview` (first N names)
+ - `errorCode` (e.g. `timeout`, `unauthorized`, `empty_models`, `unknown`)
+ - `errorMessage` (human-readable)
+- On failure: write `runtimeHealth = unhealthy` with a reason derived from `errorCode`.
+- `/api/models/check/:accountId` should return the above outcome.
+- UI toast reads outcome and displays:
+ - Success: `已获取到模型:A、B、C(共 N 个)`
+ - Failure: `模型获取失败(请求超时)` / `模型获取失败,API Key 已无效` / `模型获取失败:`
+
+# Data Flow
+1. User clicks "模型".
+2. UI calls `/api/models/check/:accountId`.
+3. Server refreshes models and returns structured outcome.
+4. UI shows toast based on outcome and reloads list.
+
+# Error Classification
+- `timeout`: error message contains `timeout` / `timed out` / `请求超时`.
+- `unauthorized`: HTTP 401/403 or message contains `invalid`, `unauthorized`, `无权`, `未提供令牌`.
+- `empty_models`: request succeeded but model list empty.
+- `unknown`: fallback for other errors.
+
+# Testing
+- Unit tests for `refreshModelsForAccount`:
+ - Failure updates runtime health to `unhealthy`.
+ - Failure classification maps to the right `errorCode`.
+ - Success returns `modelsPreview` and `modelCount`.
+- UI behavior: verify toast text for success vs. each failure category.
diff --git a/docs/plans/2026-03-16-downstream-key-ui-and-site-links.md b/docs/plans/2026-03-16-downstream-key-ui-and-site-links.md
new file mode 100644
index 00000000..902228e5
--- /dev/null
+++ b/docs/plans/2026-03-16-downstream-key-ui-and-site-links.md
@@ -0,0 +1,160 @@
+# Downstream Key UI And Site Links Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Restyle the downstream key page to match the restrained management-console pages and standardize site buttons across pages using the accounts page interaction.
+
+**Architecture:** Introduce one shared site link component based on the accounts page behavior, then update each affected page to consume it. Refactor downstream key layout styling toward existing `card`, `badge`, and `data-table` patterns without changing backend behavior.
+
+**Tech Stack:** React, TypeScript, React Router, Vitest, React Test Renderer
+
+---
+
+### Task 1: Extract the shared site button
+
+**Files:**
+- Create: `src/web/components/SiteBadgeLink.tsx`
+- Test: `src/web/components/site-badge-link.test.tsx`
+- Modify: `src/web/pages/Accounts.tsx`
+- Modify: `src/web/index.css`
+
+**Step 1: Write the failing test**
+
+Add a component test that renders a site badge link and asserts:
+- it renders the site name,
+- it has the shared interactive class,
+- clicking navigates to `/sites?focusSiteId=`.
+
+**Step 2: Run test to verify it fails**
+
+Run: `npm test -- src/web/components/site-badge-link.test.tsx`
+
+Expected: FAIL because the component does not exist yet.
+
+**Step 3: Write minimal implementation**
+
+- Create a small component that wraps `Link` from `react-router-dom`.
+- Accept `siteId`, `siteName`, and optional badge class props.
+- Reproduce the accounts page badge-link styling and fallback behavior.
+- Move the accounts page site badge rendering to this component.
+- Add any missing shared CSS class if accounts currently relies on page-local styling.
+
+**Step 4: Run test to verify it passes**
+
+Run: `npm test -- src/web/components/site-badge-link.test.tsx src/web/pages/accounts.segmented-connections.test.tsx`
+
+Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add src/web/components/SiteBadgeLink.tsx src/web/components/site-badge-link.test.tsx src/web/pages/Accounts.tsx src/web/index.css
+git commit -m "refactor: extract shared site badge link"
+```
+
+### Task 2: Apply the shared site button across affected pages
+
+**Files:**
+- Modify: `src/web/pages/Models.tsx`
+- Modify: `src/web/pages/ProxyLogs.tsx`
+- Modify: `src/web/pages/TokenRoutes.tsx`
+- Modify: `src/web/pages/DownstreamKeys.tsx`
+- Test: `src/web/pages/Models.marketplace-text.test.tsx`
+- Test: `src/web/pages/tokenRoutes.group-collapse.test.tsx`
+- Test: `src/web/pages/DownstreamKeys.test.tsx`
+
+**Step 1: Write failing assertions**
+
+Add or update tests to assert the affected pages render the shared site button class and, where practical, point to the focused site management route.
+
+**Step 2: Run test to verify it fails**
+
+Run: `npm test -- src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx`
+
+Expected: FAIL because the pages still use inconsistent site rendering.
+
+**Step 3: Write minimal implementation**
+
+- Replace page-local site label rendering with `SiteBadgeLink`.
+- Only convert labels that represent navigable management-site references.
+- Preserve non-site filters, external URLs, and unrelated badges.
+
+**Step 4: Run test to verify it passes**
+
+Run: `npm test -- src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx src/web/pages/ProxyLogs.server-driven.test.tsx`
+
+Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add src/web/pages/Models.tsx src/web/pages/ProxyLogs.tsx src/web/pages/TokenRoutes.tsx src/web/pages/DownstreamKeys.tsx src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx
+git commit -m "feat: unify site badge navigation across pages"
+```
+
+### Task 3: Restyle downstream keys page
+
+**Files:**
+- Modify: `src/web/pages/DownstreamKeys.tsx`
+- Test: `src/web/pages/DownstreamKeys.test.tsx`
+- Modify: `src/web/index.css`
+
+**Step 1: Write failing assertions**
+
+Add focused assertions for the downstream keys page structure where needed, such as shared management-page card classes, reduced bespoke styling hooks, or the new site badge placement.
+
+**Step 2: Run test to verify it fails**
+
+Run: `npm test -- src/web/pages/DownstreamKeys.test.tsx`
+
+Expected: FAIL after adding assertions that reflect the updated restrained layout.
+
+**Step 3: Write minimal implementation**
+
+- Reduce the page’s dashboard-like presentation.
+- Align summary, filters, and batch action sections with the same management-console card language used in sites/accounts.
+- Reuse shared badge/link patterns instead of one-off visual treatments where practical.
+- Keep operations, data fetching, and drawer behavior unchanged.
+
+**Step 4: Run test to verify it passes**
+
+Run: `npm test -- src/web/pages/DownstreamKeys.test.tsx`
+
+Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add src/web/pages/DownstreamKeys.tsx src/web/pages/DownstreamKeys.test.tsx src/web/index.css
+git commit -m "style: align downstream key page with management UI"
+```
+
+### Task 4: Final verification
+
+**Files:**
+- Modify: none
+
+**Step 1: Run targeted verification**
+
+Run:
+- `npm test -- src/web/components/site-badge-link.test.tsx`
+- `npm test -- src/web/pages/DownstreamKeys.test.tsx`
+- `npm test -- src/web/pages/Models.marketplace-text.test.tsx`
+- `npm test -- src/web/pages/tokenRoutes.group-collapse.test.tsx`
+- `npm test -- src/web/pages/ProxyLogs.server-driven.test.tsx`
+- `npm test -- src/web/pages/accounts.segmented-connections.test.tsx`
+
+Expected: PASS
+
+**Step 2: Run broader sanity verification if time permits**
+
+Run: `npm test -- src/web/App.sidebar.test.ts src/web/App.sidebar-mobile.test.tsx`
+
+Expected: PASS
+
+**Step 3: Commit final polish**
+
+```bash
+git add -A
+git commit -m "test: verify downstream key UI and site link consistency"
+```
diff --git a/docs/plans/2026-03-22-single-source-consolidation-pr1.md b/docs/plans/2026-03-22-single-source-consolidation-pr1.md
new file mode 100644
index 00000000..14a101be
--- /dev/null
+++ b/docs/plans/2026-03-22-single-source-consolidation-pr1.md
@@ -0,0 +1,254 @@
+# Single Source Consolidation PR1 Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Land PR1 for the highest-value duplication and double-source issues around responses normalization, shared proxy surface orchestration, provider runtime headers, token-route contract sharing, account mutation workflow, and standard API platform helpers.
+
+**Architecture:** Keep PR1 focused on extracting shared single-source modules and rewiring existing callers to consume them. Do not expand this PR into schema metamodel rewrites, `chatFormatsCore` breakup, or mobile page scaffold work; those remain follow-up PR scope once the highest-risk protocol and workflow duplications are removed.
+
+**Tech Stack:** TypeScript, Fastify, Vitest, shared server/web modules under `src/shared`, server proxy-core surfaces/providers/services, React admin pages.
+
+---
+
+## Task 1: Savepoint Commit
+
+**Files:**
+- Commit: `src/server/config.ts`
+- Commit: `src/server/config.test.ts`
+- Commit: `src/server/db/proxyFileSchemaCompatibility.ts`
+- Commit: `src/server/db/proxyFileSchemaCompatibility.test.ts`
+
+**Step 1: Verify the savepoint changes**
+
+Run: `npm test -- src/server/config.test.ts src/server/db/proxyFileSchemaCompatibility.test.ts`
+Expected: PASS
+
+**Step 2: Commit the savepoint**
+
+Run:
+
+```bash
+git add src/server/config.ts src/server/config.test.ts src/server/db/proxyFileSchemaCompatibility.ts src/server/db/proxyFileSchemaCompatibility.test.ts
+git commit -m "fix: trust proxy headers and align proxy file mysql indexes"
+```
+
+**Step 3: Cherry-pick into PR1 branch**
+
+Run:
+
+```bash
+git cherry-pick
+```
+
+Expected: first commit preserved inside PR1 branch before the larger consolidation commit.
+
+## Task 2: Responses Single Source
+
+**Files:**
+- Create: `src/server/transformers/openai/responses/normalization.ts`
+- Modify: `src/server/transformers/openai/responses/conversion.ts`
+- Modify: `src/server/transformers/openai/responses/compatibility.ts`
+- Test: `src/server/transformers/openai/responses/conversion.test.ts`
+
+**Step 1: Move shared normalization helpers into one source**
+
+Keep `normalizeResponsesInputForCompatibility`, `normalizeResponsesMessageContent`, and related block normalization in `normalization.ts`.
+
+**Step 2: Rewire both callers**
+
+Make `conversion.ts` and `compatibility.ts` import the shared helpers instead of maintaining their own independent copies.
+
+**Step 3: Verify parity**
+
+Run: `npm test -- src/server/transformers/openai/responses/conversion.test.ts`
+Expected: PASS, including assertions that compatibility exports point at the shared implementation.
+
+## Task 3: Shared Surface Orchestration
+
+**Files:**
+- Create: `src/server/proxy-core/surfaces/sharedSurface.ts`
+- Modify: `src/server/proxy-core/surfaces/chatSurface.ts`
+- Modify: `src/server/proxy-core/surfaces/openAiResponsesSurface.ts`
+- Modify: `src/server/proxy-core/surfaces/geminiSurface.ts`
+- Test: `src/server/proxy-core/surfaces/sharedSurface.test.ts`
+
+**Step 1: Extract common orchestration primitives**
+
+Centralize channel selection, dispatch, proxy logging, retry/failure handling, and common failure response shaping in `sharedSurface.ts`.
+
+**Step 2: Rewire surface callers**
+
+Make chat and responses surfaces consume the shared orchestration helpers instead of each keeping a parallel flow.
+
+**Step 3: Verify behavior**
+
+Run: `npm test -- src/server/proxy-core/surfaces/sharedSurface.test.ts`
+Expected: PASS
+
+## Task 4: Provider Header Builders
+
+**Files:**
+- Create: `src/server/proxy-core/providers/headerUtils.ts`
+- Modify: `src/server/proxy-core/providers/codexProviderProfile.ts`
+- Modify: `src/server/proxy-core/providers/claudeProviderProfile.ts`
+- Modify: `src/server/proxy-core/providers/geminiCliProviderProfile.ts`
+- Modify: `src/server/routes/proxy/upstreamEndpoint.ts`
+- Test: `src/server/proxy-core/providers/headerUtils.test.ts`
+
+**Step 1: Extract header parsing and runtime header construction**
+
+Keep Codex, Claude, and Gemini CLI runtime header shaping in `headerUtils.ts`.
+
+**Step 2: Remove duplicate builders**
+
+Make both provider profiles and `upstreamEndpoint.ts` call the shared header builders.
+
+**Step 3: Verify behavior**
+
+Run: `npm test -- src/server/proxy-core/providers/headerUtils.test.ts`
+Expected: PASS
+
+## Task 5: Cross-Layer Token Route Contract
+
+**Files:**
+- Create: `src/shared/tokenRouteContract.js`
+- Create: `src/shared/tokenRouteContract.d.ts`
+- Create: `src/shared/tokenRoutePatterns.js`
+- Create: `src/shared/tokenRoutePatterns.d.ts`
+- Modify: `src/server/services/tokenRouter.ts`
+- Modify: `src/server/routes/api/tokens.ts`
+- Modify: `src/web/pages/token-routes/utils.ts`
+- Modify: `src/web/pages/token-routes/types.ts`
+- Modify: `src/web/pages/helpers/routeListVisibility.ts`
+- Modify: `src/web/pages/Settings.tsx`
+- Test: `src/shared/tokenRouteContract.test.ts`
+- Test: `src/shared/tokenRoutePatterns.test.ts`
+
+**Step 1: Extract shared route mode and pattern matching**
+
+Make route mode normalization and model pattern matching authoritative in `src/shared`.
+
+**Step 2: Rewire server and web**
+
+Make token router, route APIs, token-routes helpers, and settings consume the shared contract instead of maintaining separate copies.
+
+**Step 3: Verify behavior**
+
+Run: `npm test -- src/shared/tokenRouteContract.test.ts src/shared/tokenRoutePatterns.test.ts`
+Expected: PASS
+
+## Task 6: Account Mutation Workflow
+
+**Files:**
+- Create: `src/server/services/accountMutationWorkflow.ts`
+- Modify: `src/server/routes/api/accounts.ts`
+- Modify: `src/server/routes/api/accountTokens.ts`
+- Test: `src/server/services/accountMutationWorkflow.test.ts`
+
+**Step 1: Extract convergence workflow**
+
+Move token sync, default token ensure, balance refresh, model refresh, and route rebuild sequencing into `accountMutationWorkflow.ts`.
+
+**Step 2: Rewire controllers**
+
+Make account and account-token APIs call the workflow service instead of each owning private orchestration logic.
+
+**Step 3: Verify behavior**
+
+Run: `npm test -- src/server/services/accountMutationWorkflow.test.ts`
+Expected: PASS
+
+## Task 7: Standard API Provider Base
+
+**Files:**
+- Create: `src/server/services/platforms/standardApiProvider.ts`
+- Modify: `src/server/services/platforms/openai.ts`
+- Modify: `src/server/services/platforms/claude.ts`
+- Modify: `src/server/services/platforms/gemini.ts`
+- Modify: `src/server/services/platforms/cliproxyapi.ts`
+- Test: `src/server/services/platforms/standardApiProvider.test.ts`
+- Test: `src/server/services/platforms/llmUpstream.test.ts`
+
+**Step 1: Extract shared standard API adapter logic**
+
+Centralize base URL normalization, `/v1/models` resolution, unsupported login/checkin/balance defaults, and common model fetch behavior.
+
+**Step 2: Rewire adapters**
+
+Make OpenAI, Claude, Gemini, and CLIProxyAPI adapters extend the shared base instead of repeating the same template logic.
+
+**Step 3: Verify behavior**
+
+Run: `npm test -- src/server/services/platforms/standardApiProvider.test.ts src/server/services/platforms/llmUpstream.test.ts`
+Expected: PASS
+
+## Task 8: Shared Input File Resolution And Web Helpers
+
+**Files:**
+- Modify: `src/server/services/proxyInputFileResolver.ts`
+- Modify: `src/server/services/proxyInputFileResolver.test.ts`
+- Modify: `src/server/proxy-core/surfaces/inputFilesSurface.ts`
+- Create: `src/web/pages/helpers/accountConnection.ts`
+- Create: `src/web/pages/helpers/accountConnection.test.ts`
+- Modify: `src/web/pages/Accounts.tsx`
+- Modify: `src/web/pages/Tokens.tsx`
+- Modify: `src/web/api.ts`
+- Modify: `src/web/api.test.ts`
+
+**Step 1: Make route-level file inlining reuse the service implementation**
+
+Replace the duplicate `inputFilesSurface.ts` implementation with a re-export of the shared resolver service.
+
+**Step 2: Extract shared web account helpers**
+
+Move `resolveAccountCredentialMode`, `parsePositiveInt`, and `isTruthyFlag` into a single helper consumed by both Accounts and Tokens.
+
+**Step 3: De-duplicate proxy test aliases**
+
+Make `testProxy` / `proxyTest` and stream variants reuse the same implementation function.
+
+**Step 4: Verify behavior**
+
+Run: `npm test -- src/server/services/proxyInputFileResolver.test.ts src/web/pages/helpers/accountConnection.test.ts src/web/api.test.ts`
+Expected: PASS
+
+## Task 9: PR1 Verification And Commit
+
+**Files:**
+- Verify: `scripts/dev/copy-runtime-db-generated.ts`
+- Verify: `scripts/dev/copy-runtime-db-generated.test.ts`
+- Verify: all files touched in Tasks 2-8
+
+**Step 1: Run focused regression suite**
+
+Run:
+
+```bash
+npm test -- scripts/dev/copy-runtime-db-generated.test.ts src/server/proxy-core/cliProfiles/codexProfile.test.ts src/server/proxy-core/providers/headerUtils.test.ts src/server/proxy-core/surfaces/sharedSurface.test.ts src/server/routes/api/oauth.test.ts src/server/services/accountMutationWorkflow.test.ts src/server/services/modelService.discovery.test.ts src/server/services/oauth/oauthAccount.test.ts src/server/services/platforms/standardApiProvider.test.ts src/server/services/platforms/llmUpstream.test.ts src/server/services/proxyInputFileResolver.test.ts src/server/transformers/openai/responses/conversion.test.ts src/shared/tokenRouteContract.test.ts src/shared/tokenRoutePatterns.test.ts src/web/api.test.ts src/web/pages/helpers/accountConnection.test.ts
+```
+
+Expected: PASS
+
+**Step 2: Run server build**
+
+Run: `npm run build:server`
+Expected: PASS
+
+**Step 3: Commit PR1 consolidation work**
+
+Run:
+
+```bash
+git add docs/plans/2026-03-22-single-source-consolidation-pr1.md scripts/dev/copy-runtime-db-generated.test.ts scripts/dev/copy-runtime-db-generated.ts src/server/proxy-core/cliProfiles/codexProfile.ts src/server/proxy-core/cliProfiles/codexProfile.test.ts src/server/proxy-core/providers/headerUtils.ts src/server/proxy-core/providers/headerUtils.test.ts src/server/proxy-core/providers/claudeProviderProfile.ts src/server/proxy-core/providers/codexProviderProfile.ts src/server/proxy-core/providers/geminiCliProviderProfile.ts src/server/proxy-core/surfaces/chatSurface.ts src/server/proxy-core/surfaces/geminiSurface.ts src/server/proxy-core/surfaces/inputFilesSurface.ts src/server/proxy-core/surfaces/openAiResponsesSurface.ts src/server/proxy-core/surfaces/sharedSurface.ts src/server/proxy-core/surfaces/sharedSurface.test.ts src/server/routes/api/accountTokens.ts src/server/routes/api/accounts.ts src/server/routes/api/oauth.test.ts src/server/routes/api/tokens.ts src/server/routes/proxy/downstreamClientContext.ts src/server/routes/proxy/responsesWebsocket.ts src/server/routes/proxy/upstreamEndpoint.ts src/server/services/accountMutationWorkflow.ts src/server/services/accountMutationWorkflow.test.ts src/server/services/backupService.ts src/server/services/modelService.discovery.test.ts src/server/services/modelService.ts src/server/services/oauth/oauthAccount.ts src/server/services/oauth/oauthAccount.test.ts src/server/services/oauth/quota.ts src/server/services/oauth/service.ts src/server/services/platforms/claude.ts src/server/services/platforms/cliproxyapi.ts src/server/services/platforms/gemini.ts src/server/services/platforms/llmUpstream.test.ts src/server/services/platforms/openai.ts src/server/services/platforms/standardApiProvider.ts src/server/services/platforms/standardApiProvider.test.ts src/server/services/proxyInputFileResolver.ts src/server/services/proxyInputFileResolver.test.ts src/server/services/siteProxy.ts src/server/services/tokenRouter.ts src/server/transformers/anthropic/messages/inbound.ts src/server/transformers/openai/responses/compatibility.ts src/server/transformers/openai/responses/conversion.test.ts src/server/transformers/openai/responses/conversion.ts src/server/transformers/openai/responses/normalization.ts src/shared/tokenRouteContract.d.ts src/shared/tokenRouteContract.js src/shared/tokenRouteContract.test.ts src/shared/tokenRoutePatterns.d.ts src/shared/tokenRoutePatterns.js src/shared/tokenRoutePatterns.test.ts src/web/api.ts src/web/api.test.ts src/web/pages/Accounts.tsx src/web/pages/Settings.tsx src/web/pages/Tokens.tsx src/web/pages/helpers/accountConnection.ts src/web/pages/helpers/accountConnection.test.ts src/web/pages/helpers/routeListVisibility.ts src/web/pages/token-routes/types.ts src/web/pages/token-routes/utils.ts
+git commit -m "refactor: consolidate single-source routing and protocol helpers"
+```
+
+### Deferred After PR1
+
+**Keep out of this PR:**
+- `shared/chatFormatsCore.ts` breakup
+- Gemini `generate-content` module boundary cleanup
+- schema contract/introspection meta-rule unification
+- `legacySchemaCompat.ts` feature compatibility dedupe
+- mobile page scaffold extraction
+- large-file decomposition for `Settings.tsx`, `ModelTester.tsx`, `tokenRouter.ts`, `modelService.ts`
diff --git a/docs/plans/2026-03-23-single-source-consolidation-mega-plan.md b/docs/plans/2026-03-23-single-source-consolidation-mega-plan.md
new file mode 100644
index 00000000..b14bce5e
--- /dev/null
+++ b/docs/plans/2026-03-23-single-source-consolidation-mega-plan.md
@@ -0,0 +1,96 @@
+# Single Source Consolidation Full-Scope Follow-Through
+
+## Context
+
+This document supersedes the scoped execution boundary of
+[`2026-03-22-single-source-consolidation-pr1.md`](./2026-03-22-single-source-consolidation-pr1.md)
+for PR #244. The PR1 document remains a historical snapshot of the initial
+landing slice. This document captures the expanded branch-wide finish that
+continued until the single-source, workflow, schema, and web admin duplication
+themes were fully closed for this PR.
+
+## Goals
+
+- Eliminate active double-source implementations for proxy protocols and runtime headers.
+- Centralize account, OAuth, route refresh, and platform discovery workflows.
+- Collapse schema metadata normalization and feature compatibility drift points.
+- Remove repeated web admin page scaffolding and large inline modal/panel implementations.
+- Finish the branch in a merge-ready state with focused regressions and formal builds.
+
+## Scope Delivered In This Branch
+
+### Server and Shared Runtime
+
+- OpenAI Responses normalization and conversion now flow through one shared implementation boundary.
+- Shared proxy surface orchestration owns retry recovery, success bookkeeping, and surface-level failure handling.
+- Provider header/body runtime shaping is centralized behind provider profiles and shared header utilities.
+- OAuth identity now treats structured columns as the canonical source, with compatibility/backfill isolated to a removable layer.
+- Route refresh and rebuild sequencing flows through `routeRefreshWorkflow`, not controller-local orchestration.
+- Platform discovery moved out of `modelService` into dedicated registry-backed discovery modules.
+- Schema metadata normalization is shared by contract and introspection code paths.
+- Legacy schema compatibility derives feature-owned allowances from feature specs instead of maintaining a second whitelist.
+
+### Web Admin
+
+- Token-route, brand, and downstream client display rules now rely on shared helpers and registries instead of page-local duplicates.
+- `ResponsiveBatchActionBar` centralizes repeated batch action shells across admin list pages.
+- `ResponsiveFilterPanel` centralizes the responsive "desktop controls vs mobile sheet" scaffold across admin pages.
+- `Settings`, `Accounts`, `ModelTester`, and `DownstreamKeys` no longer keep their heaviest modal or drawer UIs inline in the page file.
+- Brand matching metadata is owned by `brandRegistry`, with `BrandIcon` reduced to rendering concerns.
+
+## Commit Groups
+
+### Protocol and Workflow Consolidation
+
+- review-driven cleanup commits
+- OAuth identity and route refresh workflow commits
+- downstream client/runtime header/profile consolidation commits
+- platform discovery extraction commits
+
+### Schema and Shared Contract Consolidation
+
+- shared schema metadata normalization
+- legacy schema compatibility derivation from feature specs
+- shared token-route contract and pattern ownership
+
+### Web Admin Decomposition
+
+- responsive batch action scaffold
+- responsive filter scaffold
+- settings modal extraction
+- accounts model management modal extraction
+- model tester panel extraction
+- downstream key drawer extraction
+
+## Final Verification Matrix
+
+Run these groups before calling the branch complete.
+
+### Protocol and Surface
+
+- `npm test -- src/server/proxy-core/surfaces/sharedSurface.test.ts src/server/routes/proxy src/server/transformers/openai/responses src/server/transformers/anthropic/messages src/server/services/proxyInputFileResolver.test.ts`
+
+### OAuth, Workflow, and Platforms
+
+- `npm test -- src/server/routes/api src/server/services/accountMutationWorkflow.test.ts src/server/services/modelService.discovery.test.ts src/server/services/oauth/oauthAccount.test.ts src/server/services/platforms/standardApiProvider.test.ts src/server/services/platforms/llmUpstream.test.ts`
+
+### Schema
+
+- `npm run test:schema:unit`
+- `npm run schema:contract`
+- `npm run smoke:db:sqlite`
+
+### Web and Build
+
+- `npm test -- src/web/pages/responsiveFilterPanel.architecture.test.ts src/web/pages/checkin.mobile.test.tsx src/web/pages/logs.mobile.test.tsx src/web/pages/programLogs.mobile-layout.test.tsx src/web/pages/accounts.batch-actions.test.tsx src/web/pages/sites.mobile-actions.test.tsx src/web/pages/tokens.batch-actions.test.tsx src/web/pages/DownstreamKeys.test.tsx src/web/pages/DownstreamKeys.mobile.test.tsx src/web/pages/downstreamKeys.mobile-layout.test.tsx src/web/pages/tokenRoutes.mobile-layout.test.tsx src/web/pages/tokenRoutes.mobile.test.tsx src/web/pages/models.mobile-layout.test.tsx`
+- `npm run build`
+
+## Done Definition
+
+The branch is complete when:
+
+- no page under `src/web/pages` imports `MobileFilterSheet` directly;
+- no active protocol path keeps parallel single-source conversions alive;
+- controller layers do not own route rebuild choreography;
+- schema metadata and compatibility rules do not keep a second handwritten truth source;
+- focused regressions and formal builds pass from the commands above.
diff --git a/docs/project-structure.md b/docs/project-structure.md
index 5e4470b0..1e0550a5 100644
--- a/docs/project-structure.md
+++ b/docs/project-structure.md
@@ -90,6 +90,7 @@ scripts/
docs/
├── .vitepress/ # 文档站导航与主题配置
├── community/ # 社区贡献规范
+├── engineering/ # 仓库级工程规则、harness 与漂移治理说明
├── public/ # 文档站公开静态资源
├── logos/ # 可编辑 Logo 源文件与草稿
├── screenshots/ # 文档截图
diff --git a/docs/public/favicon-64.png b/docs/public/favicon-64.png
new file mode 100644
index 00000000..85eb0598
Binary files /dev/null and b/docs/public/favicon-64.png differ
diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico
new file mode 100644
index 00000000..85eb0598
Binary files /dev/null and b/docs/public/favicon.ico differ
diff --git a/docs/public/favicon.png b/docs/public/favicon.png
new file mode 100644
index 00000000..101e6f33
Binary files /dev/null and b/docs/public/favicon.png differ
diff --git a/docs/screenshots/api-key-management.png b/docs/screenshots/api-key-management.png
new file mode 100644
index 00000000..d214de78
Binary files /dev/null and b/docs/screenshots/api-key-management.png differ
diff --git a/docs/screenshots/donehub-token.png b/docs/screenshots/donehub-token.png
new file mode 100644
index 00000000..37c83a45
Binary files /dev/null and b/docs/screenshots/donehub-token.png differ
diff --git a/docs/screenshots/proxy-logs-mapping.png b/docs/screenshots/proxy-logs-mapping.png
new file mode 100644
index 00000000..74c88996
Binary files /dev/null and b/docs/screenshots/proxy-logs-mapping.png differ
diff --git a/docs/screenshots/route-group.png b/docs/screenshots/route-group.png
new file mode 100644
index 00000000..4d1feeb7
Binary files /dev/null and b/docs/screenshots/route-group.png differ
diff --git a/docs/screenshots/routes-filter.png b/docs/screenshots/routes-filter.png
new file mode 100644
index 00000000..50fefa24
Binary files /dev/null and b/docs/screenshots/routes-filter.png differ
diff --git a/docs/screenshots/session-cookie-f12.png b/docs/screenshots/session-cookie-f12.png
new file mode 100644
index 00000000..547265e9
Binary files /dev/null and b/docs/screenshots/session-cookie-f12.png differ
diff --git a/docs/screenshots/sub2api-auth-f12.png b/docs/screenshots/sub2api-auth-f12.png
new file mode 100644
index 00000000..60cd0650
Binary files /dev/null and b/docs/screenshots/sub2api-auth-f12.png differ
diff --git a/docs/screenshots/sub2api-session-config.png b/docs/screenshots/sub2api-session-config.png
new file mode 100644
index 00000000..688fd509
Binary files /dev/null and b/docs/screenshots/sub2api-session-config.png differ
diff --git a/docs/upstream-integration.md b/docs/upstream-integration.md
new file mode 100644
index 00000000..6afdc9ad
--- /dev/null
+++ b/docs/upstream-integration.md
@@ -0,0 +1,500 @@
+# 🔌 上游接入指南
+
+本文档详细说明如何将不同类型的 AI 中转站接入 Metapi。
+
+[返回文档中心](./README.md)
+
+---
+
+## 概述
+
+Metapi 支持两大类上游站点:
+
+1. **中转聚合平台** — New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API 等
+2. **官方 API 端点** — OpenAI / Claude (Anthropic) / Gemini (Google) 直连
+
+每种站点类型的接入方式略有不同,本文档按站点类型分别说明。
+
+---
+
+## 🎯 快速接入流程
+
+### 通用步骤
+
+1. **登录管理后台** — 访问 `http://your-metapi-host:4000`,使用 `AUTH_TOKEN` 登录
+2. **进入站点管理** — 点击左侧菜单「站点管理」
+3. **添加站点** — 点击「添加站点」按钮
+4. **填写站点信息** — 根据站点类型填写对应字段(见下文详细说明)
+5. **添加账号** — 站点创建后,在站点详情页添加账号凭证
+6. **验证连接** — 系统自动验证账号可用性并获取模型列表
+
+---
+
+## 📦 中转聚合平台接入
+
+### New API
+
+**适用平台:** New API 及其衍生版本(AnyRouter、VO-API、Super-API、RIX-API、Neo-API 等)
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称,便于识别 | `我的 New API` |
+| **站点 URL** | New API 部署地址(**不含** `/v1` 后缀) | `https://api.example.com` |
+| **平台类型** | 选择 `new-api` | - |
+| **代理 URL** | (可选)该站点专用代理地址 | `http://proxy.example.com:7890` |
+| **使用系统代理** | 是否使用全局 `SYSTEM_PROXY_URL` | 默认关闭 |
+
+#### 账号凭证类型
+
+New API 支持三种凭证类型:
+
+##### 1. 用户名密码登录(推荐AnyRouter使用)
+
+- **适用场景:** 有完整账号权限,需要自动签到、余额查询、账号令牌管理
+- **填写方式:**
+ - 用户名:`your-username`
+ - 密码:`your-password`
+- **自动获取:** 系统自动登录并获取 Access Token 和账号令牌
+
+##### 2. Access Token / Session Cookie
+
+- **适用场景:** 已有登录凭证,无需密码
+- **填写方式:**
+ - 在「Access Token」字段填入以下任一格式:
+ - Session Cookie:**(最不推荐)**
+ - 可通过浏览器F12获取 
+ - 一般为如下格式:`session=MTczNjQxMjM0NXxEdi1CQUFFQ180SUFBUkFCRUFBQVB2LUNBQUVHYzNSeWFXNW5EQThBRFhObGMzTnBiMjVmZEdGaWJHVUdjM1J5YVc1bkRBSUFBQT09fGRlYWRiZWVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWY=`
+ - 系统访问令牌和用户ID**(推荐非Anyrouter的其他New API站点使用)**
+ - 
+- **自动解析:** 系统自动识别凭证类型并提取用户信息
+
+##### 3. API Key(仅代理)
+
+- **适用场景:** 仅用于模型调用,不需要余额管理、自动签到等功能
+- **填写方式:**
+ - 在「API Token」字段填入:`sk-xxxxxxxxxxxxxx`
+- **限制:** 无法使用签到、余额刷新、账号令牌管理等功能
+
+#### 特殊说明
+
+**User ID 自动探测:** New API 通常需要在请求头中携带 `New-API-User` / `Veloera-User` / `voapi-user` 等字段。Metapi 会自动:
+
+1. 从 JWT Token 中解码 User ID
+2. 从 Session Cookie 中提取 User ID(支持 Gob 编码解析)
+3. 通过探测常见 ID 范围验证可用性
+
+如果以上方法都不能获取到ID,则需要用户手动获取。
+
+**防护盾穿透:** 自动处理阿里云盾 / Cloudflare 等 JS 挑战(`acw_sc__v2` / `cdn_sec_tc`),无需手动配置。
+
+---
+
+### One API
+
+**适用平台:** One API 原版及兼容分支
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称 | `One API 主站` |
+| **站点 URL** | One API 部署地址 | `https://oneapi.example.com` |
+| **平台类型** | 选择 `one-api` | - |
+
+#### 账号凭证
+
+One API 支持与 New API 相同的三种凭证类型(用户名密码 / Access Token / API Key),配置方式相同。
+
+---
+
+### OneHub
+
+**适用平台:** OneHub(One API 增强分支)
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称 | `OneHub 站点` |
+| **站点 URL** | OneHub 部署地址 | `https://onehub.example.com` |
+| **平台类型** | 选择 `one-hub` | - |
+
+#### 账号凭证
+
+OneHub 继承 One API 的凭证体系,支持用户名密码、Access Token、API Key 三种方式。
+
+**额外功能:** OneHub 支持 Token 分组(`token_group`),Metapi 会自动识别并保留分组信息。
+
+---
+
+### DoneHub
+
+**适用平台:** DoneHub(OneHub 增强分支)
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称 | `DoneHub 站点` |
+| **站点 URL** | DoneHub 部署地址 | `https://donehub.example.com` |
+| **平台类型** | 选择 `done-hub` | - |
+
+#### 账号凭证
+
+DoneHub 完全兼容 OneHub 的凭证体系,配置方式相同,DoneHub获取令牌位置如下图所示:
+
+---
+
+### Veloera
+
+**适用平台:** Veloera API 网关
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称 | `Veloera 网关` |
+| **站点 URL** | Veloera 部署地址 | `https://veloera.example.com` |
+| **平台类型** | 选择 `veloera` | - |
+
+#### 账号凭证
+
+Veloera 基于 New API 架构,支持相同的凭证类型。特别注意:
+- Veloera 需要 `Veloera-User` 请求头,Metapi 会自动添加
+
+---
+
+### Sub2API
+
+**适用平台:** Sub2API 订阅制中转平台
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称 | `Sub2API 订阅站` |
+| **站点 URL** | Sub2API 部署地址 | `https://sub2api.example.com` |
+| **平台类型** | 选择 `sub2api` | - |
+
+#### 账号凭证
+
+Sub2API 常见 JWT 短期会话机制,和传统 NewAPI 站点差异较大。按下面步骤进行添加:
+
+首先去中转站点F12打开如下界面:
+
+
+
+然后回到Metapi账号添加处:
+
+
+
+1. 在「凭证模式」里选择 Session 模式,分别粘贴F12界面中的auth_token、refresh_token、token_expires_at字段进行验证,无需配置用户ID。
+2. 不要使用账号密码登录,Metapi 不支持代替 Sub2API 做登录
+3. Sub2API 通常为订阅制使用,不支持签到;如果你只关心代理调用,也可以直接改用 API Key 模式
+4. 若 `GET /v1/models` 为空,先确认该账号下已有可用用户 API Key,Metapi 会再尝试用它发现模型
+
+---
+
+## 🌐 官方 API 端点接入
+
+### OpenAI
+
+**适用场景:** 直连 OpenAI 官方 API 或 OpenAI 兼容端点
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称 | `OpenAI 官方` |
+| **站点 URL** | OpenAI API 端点(**不含** `/v1` 后缀) | `https://api.openai.com` |
+| **平台类型** | 选择 `openai` | - |
+| **代理 URL** | (推荐)配置代理以访问 OpenAI | `http://proxy.example.com:7890` |
+
+#### 账号凭证
+
+**仅支持 API Key:**
+- 在「API Token」字段填入:`sk-proj-xxxxxxxxxxxxxx`
+- 不支持用户名密码登录(OpenAI 无此接口)
+
+#### 功能限制
+
+| 功能 | 支持情况 |
+|------|----------|
+| 模型列表获取 | ✅ 支持(`/v1/models`) |
+| 代理调用 | ✅ 支持 |
+| 余额查询 | ❌ 不支持(OpenAI 无公开接口) |
+| 自动签到 | ❌ 不适用 |
+| 账号令牌管理 | ❌ 不适用 |
+
+---
+
+### Claude (Anthropic)
+
+**适用场景:** 直连 Anthropic Claude API
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称 | `Claude 官方` |
+| **站点 URL** | Anthropic API 端点 | `https://api.anthropic.com` |
+| **平台类型** | 选择 `claude` | - |
+
+#### 账号凭证
+
+**仅支持 API Key:**
+- 在「API Token」字段填入:`sk-ant-api03-xxxxxxxxxxxxxx`
+
+#### 功能限制
+
+| 功能 | 支持情况 |
+|------|----------|
+| 模型列表获取 | ✅ 支持(内置模型目录) |
+| 代理调用 | ✅ 支持(自动转换 OpenAI ⇄ Claude 格式) |
+| 余额查询 | ❌ 不支持 |
+| 自动签到 | ❌ 不适用 |
+| 账号令牌管理 | ❌ 不适用 |
+
+**协议转换:** Metapi 自动处理 OpenAI 格式与 Claude Messages API 格式的双向转换,下游客户端可使用 OpenAI SDK 调用 Claude 模型。
+
+---
+
+### Gemini (Google)
+
+**适用场景:** 直连 Google Gemini API
+
+#### 站点配置
+
+| 字段 | 说明 | 示例 |
+|------|------|------|
+| **站点名称** | 自定义名称 | `Gemini 官方` |
+| **站点 URL** | Gemini API 端点 | `https://generativelanguage.googleapis.com` |
+| **平台类型** | 选择 `gemini` | - |
+
+#### 账号凭证
+
+**仅支持 API Key:**
+- 在「API Token」字段填入:`AIzaSyxxxxxxxxxxxxxx`
+
+#### 功能限制
+
+| 功能 | 支持情况 |
+|------|----------|
+| 模型列表获取 | ✅ 支持(`/v1beta/models`) |
+| 代理调用 | ✅ 支持(自动转换 OpenAI ⇄ Gemini 格式) |
+| 余额查询 | ❌ 不支持 |
+| 自动签到 | ❌ 不适用 |
+| 账号令牌管理 | ❌ 不适用 |
+
+**协议转换:** Metapi 自动处理 OpenAI 格式与 Gemini `generateContent` API 格式的双向转换。
+
+---
+
+## 🔧 高级配置
+
+### 站点级代理
+
+每个站点可单独配置代理,优先级高于全局 `SYSTEM_PROXY_URL`:
+
+```
+站点专用代理 > 全局系统代理 > 直连
+```
+
+**配置方式:**
+1. 在站点编辑页面填写「代理 URL」字段
+2. 格式:`http://proxy-host:port` 或 `socks5://proxy-host:port`
+3. 支持 HTTP / HTTPS / SOCKS5 代理
+
+### 站点权重
+
+**全局权重(`global_weight`):** 影响该站点下所有通道的路由概率。
+
+- 默认值:`1.0`
+- 范围:`0.1` ~ `10.0`
+- 示例:
+ - 设置为 `2.0` — 该站点通道被选中的概率翻倍
+ - 设置为 `0.5` — 该站点通道被选中的概率减半
+
+**配置位置:** 站点编辑页面 → 高级设置 → 全局权重
+
+### 外部签到 URL
+
+**适用场景:** 某些站点的签到接口非标准路径,或需要通过外部服务触发签到。
+
+**配置方式:**
+1. 在站点编辑页面填写「外部签到 URL」
+2. Metapi 会向该 URL 发送 POST 请求执行签到
+3. 请求头自动携带账号凭证
+
+---
+
+## 🔍 站点自动检测
+
+Metapi 支持自动识别站点类型,检测优先级如下:
+
+### 1. URL 特征检测
+
+根据 URL 中的关键字自动识别:
+
+| URL 特征 | 识别为 |
+|----------|--------|
+| `api.openai.com` | OpenAI |
+| `api.anthropic.com` | Claude |
+| `generativelanguage.googleapis.com` | Gemini |
+| `anyrouter` | AnyRouter |
+| `donehub` / `done-hub` | DoneHub |
+| `onehub` / `one-hub` | OneHub |
+| `veloera` | Veloera |
+| `sub2api` | Sub2API |
+
+### 2. 页面标题检测
+
+访问站点首页,解析 `` 标签识别平台类型。
+
+### 3. API 探测
+
+依次尝试各平台的特征接口:
+- New API:`/api/status` 返回 `system_name`
+- One API:`/api/status` 返回特定结构
+- OpenAI:`/v1/models` 返回模型列表
+
+**手动指定:** 如果自动检测失败,可在添加站点时手动选择平台类型。
+
+---
+
+## 📊 账号健康状态
+
+Metapi 自动追踪每个账号的健康状态:
+
+| 状态 | 说明 | 触发条件 |
+|------|------|----------|
+| `healthy` | 健康 | 最近请求成功,余额充足 |
+| `degraded` | 降级 | 部分模型不可用,或余额不足 |
+| `unhealthy` | 不健康 | 连续失败,或凭证过期 |
+| `disabled` | 已禁用 | 手动禁用或站点禁用 |
+
+**自动恢复:** `unhealthy` 状态的账号会定期重试,成功后自动恢复为 `healthy`。
+
+---
+
+## 📰 站点公告
+
+Metapi 会定期抓取已接入站点的公告,并在首次发现时写入站内通知与「站点公告」页面。
+
+当前支持的公告来源:
+
+- `new-api`:读取 `/api/notice` 的全站公告
+- `done-hub`:读取 `/api/notice` 的全站公告
+- `sub2api`:读取 `/api/v1/announcements` 的公告列表
+
+行为约定:
+
+1. 首次发现的公告会触发一次 Metapi 通知
+2. 已经保存过的公告再次同步时只更新本地记录
+3. `sub2api` 这类需要登录态的公告接口,会优先使用站点下已启用账号的会话令牌
+4. 「站点公告」页面的清空操作只影响 Metapi 本地数据库
+
+---
+
+## 🛠️ 故障排查
+
+### 问题:添加站点后无法获取模型列表
+
+**可能原因:**
+1. 站点 URL 填写错误(检查是否包含 `/v1` 后缀,应去除)
+2. 网络不通(检查代理配置或防火墙)
+3. 凭证无效(重新验证账号密码或 Token)
+
+**解决方法:**
+- 在站点详情页点击「测试连接」
+- 查看「事件日志」中的错误信息
+- 检查「代理日志」中的请求详情
+
+### 问题:New API 账号提示「需要 New-API-User 头」
+
+**原因:** 该站点是 New API 衍生版本,需要 User ID。
+
+**解决方法:**
+1. Metapi 会自动探测 User ID,通常无需手动配置
+2. 如果自动探测失败,可在账号编辑页面的「额外配置」中手动填写:
+ ```json
+ {
+ "platformUserId": 12345
+ }
+ ```
+
+### 问题:签到失败
+
+**可能原因:**
+1. 凭证过期(Access Token 有效期到期)
+2. 站点签到接口变更
+3. 已经签到过(部分站点限制每日一次)
+
+**解决方法:**
+- 查看「签到记录」中的失败原因
+- 尝试重新登录获取新凭证
+- 配置「外部签到 URL」使用备用签到方式
+
+### 问题:余额显示不准确
+
+**原因:** 不同平台的余额单位不同。
+
+**说明:**
+- New API 系列:余额单位为「美元」,内部存储为 `quota / 500000`
+- OpenAI / Claude / Gemini:官方 API 无余额查询接口,显示为 `N/A`
+
+---
+
+## 📝 最佳实践
+
+### 1. 凭证选择建议
+
+| 场景 | 推荐凭证类型 | 原因 |
+|------|-------------|------|
+| 个人站点,需要完整功能 | 用户名密码 | 支持自动签到、账号令牌管理 |
+| 共享账号,只读权限 | Access Token | 避免密码泄露 |
+| 仅用于模型调用 | API Token | 最小权限原则 |
+
+### 2. 站点命名规范
+
+建议使用清晰的命名规则,便于管理:
+
+```
+[平台类型] - [站点特征] - [用途]
+```
+
+示例:
+- `New API - 主站 - 生产环境`
+- `OneHub - 备用 - 测试`
+- `OpenAI - 官方 - 高优先级`
+
+### 3. 代理配置策略
+
+- **国内访问 OpenAI / Claude / Gemini:** 必须配置代理
+- **国内中转站:** 通常不需要代理
+- **海外中转站:** 根据网络情况选择
+
+### 4. 定期维护
+
+- **每周检查:** 账号健康状态、余额预警
+- **每月清理:** 禁用长期不可用的站点和账号
+- **凭证轮换:** 定期更新 Access Token 和 API Key
+
+---
+
+## 🔗 相关文档
+
+- [配置说明](./configuration.md) — 环境变量与路由参数
+- [客户端接入](./client-integration.md) — 下游应用配置
+- [常见问题](./faq.md) — 故障排查与优化建议
+
+---
+
+## 💡 提示
+
+- 添加站点后,系统会自动发现可用模型并生成路由表,无需手动配置
+- 支持同时接入多个相同类型的站点(如多个 New API 实例)
+- 站点禁用后,关联的所有账号和路由通道会自动禁用
+- 删除站点会级联删除所有关联账号、Token 和路由配置,请谨慎操作
diff --git a/drizzle/0006_site_disabled_models.sql b/drizzle/0006_site_disabled_models.sql
new file mode 100644
index 00000000..020fc917
--- /dev/null
+++ b/drizzle/0006_site_disabled_models.sql
@@ -0,0 +1,13 @@
+CREATE TABLE IF NOT EXISTS `site_disabled_models` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `site_id` integer NOT NULL,
+ `model_name` text NOT NULL,
+ `created_at` text DEFAULT (datetime('now')),
+ FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS `site_disabled_models_site_model_unique`
+ON `site_disabled_models` (`site_id`, `model_name`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `site_disabled_models_site_id_idx`
+ON `site_disabled_models` (`site_id`);
diff --git a/drizzle/0007_account_token_group.sql b/drizzle/0007_account_token_group.sql
new file mode 100644
index 00000000..9adddae0
--- /dev/null
+++ b/drizzle/0007_account_token_group.sql
@@ -0,0 +1 @@
+ALTER TABLE `account_tokens` ADD `token_group` text;
diff --git a/drizzle/0008_sqlite_schema_backfill.sql b/drizzle/0008_sqlite_schema_backfill.sql
new file mode 100644
index 00000000..0251ba36
--- /dev/null
+++ b/drizzle/0008_sqlite_schema_backfill.sql
@@ -0,0 +1,214 @@
+ALTER TABLE `sites` ADD `proxy_url` text;
+--> statement-breakpoint
+ALTER TABLE `sites` ADD `use_system_proxy` integer DEFAULT false;
+--> statement-breakpoint
+UPDATE `sites`
+SET `use_system_proxy` = false
+WHERE `use_system_proxy` IS NULL;
+--> statement-breakpoint
+ALTER TABLE `sites` ADD `custom_headers` text;
+--> statement-breakpoint
+ALTER TABLE `sites` ADD `external_checkin_url` text;
+--> statement-breakpoint
+ALTER TABLE `sites` ADD `global_weight` real DEFAULT 1;
+--> statement-breakpoint
+UPDATE `sites`
+SET `global_weight` = 1
+WHERE `global_weight` IS NULL
+ OR `global_weight` <= 0;
+--> statement-breakpoint
+ALTER TABLE `token_routes` ADD `display_name` text;
+--> statement-breakpoint
+ALTER TABLE `token_routes` ADD `display_icon` text;
+--> statement-breakpoint
+ALTER TABLE `token_routes` ADD `decision_snapshot` text;
+--> statement-breakpoint
+ALTER TABLE `token_routes` ADD `decision_refreshed_at` text;
+--> statement-breakpoint
+ALTER TABLE `token_routes` ADD `routing_strategy` text DEFAULT 'weighted';
+--> statement-breakpoint
+ALTER TABLE `route_channels` ADD `source_model` text;
+--> statement-breakpoint
+ALTER TABLE `route_channels` ADD `last_selected_at` text;
+--> statement-breakpoint
+ALTER TABLE `route_channels` ADD `consecutive_fail_count` integer NOT NULL DEFAULT 0;
+--> statement-breakpoint
+ALTER TABLE `route_channels` ADD `cooldown_level` integer NOT NULL DEFAULT 0;
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS `downstream_api_keys` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `name` text NOT NULL,
+ `key` text NOT NULL,
+ `description` text,
+ `enabled` integer DEFAULT true,
+ `expires_at` text,
+ `max_cost` real,
+ `used_cost` real DEFAULT 0,
+ `max_requests` integer,
+ `used_requests` integer DEFAULT 0,
+ `supported_models` text,
+ `allowed_route_ids` text,
+ `site_weight_multipliers` text,
+ `last_used_at` text,
+ `created_at` text DEFAULT (datetime('now')),
+ `updated_at` text DEFAULT (datetime('now'))
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS `downstream_api_keys_key_unique`
+ON `downstream_api_keys` (`key`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `downstream_api_keys_name_idx`
+ON `downstream_api_keys` (`name`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `downstream_api_keys_enabled_idx`
+ON `downstream_api_keys` (`enabled`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `downstream_api_keys_expires_at_idx`
+ON `downstream_api_keys` (`expires_at`);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS `proxy_files` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `public_id` text NOT NULL,
+ `owner_type` text NOT NULL,
+ `owner_id` text NOT NULL,
+ `filename` text NOT NULL,
+ `mime_type` text NOT NULL,
+ `purpose` text,
+ `byte_size` integer NOT NULL,
+ `sha256` text NOT NULL,
+ `content_base64` text NOT NULL,
+ `created_at` text DEFAULT (datetime('now')),
+ `updated_at` text DEFAULT (datetime('now')),
+ `deleted_at` text
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS `proxy_files_public_id_unique`
+ON `proxy_files` (`public_id`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `proxy_files_owner_lookup_idx`
+ON `proxy_files` (`owner_type`, `owner_id`, `deleted_at`);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS `proxy_video_tasks` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `public_id` text NOT NULL,
+ `upstream_video_id` text NOT NULL,
+ `site_url` text NOT NULL,
+ `token_value` text NOT NULL,
+ `requested_model` text,
+ `actual_model` text,
+ `channel_id` integer,
+ `account_id` integer,
+ `status_snapshot` text,
+ `upstream_response_meta` text,
+ `last_upstream_status` integer,
+ `last_polled_at` text,
+ `created_at` text DEFAULT (datetime('now')),
+ `updated_at` text DEFAULT (datetime('now'))
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS `proxy_video_tasks_public_id_unique`
+ON `proxy_video_tasks` (`public_id`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `proxy_video_tasks_upstream_video_id_idx`
+ON `proxy_video_tasks` (`upstream_video_id`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `proxy_video_tasks_created_at_idx`
+ON `proxy_video_tasks` (`created_at`);
+--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS `model_availability_account_model_unique`
+ON `model_availability` (`account_id`, `model_name`);
+--> statement-breakpoint
+PRAGMA foreign_keys=OFF;
+--> statement-breakpoint
+CREATE TABLE `__new_route_channels` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `route_id` integer NOT NULL,
+ `account_id` integer NOT NULL,
+ `token_id` integer,
+ `source_model` text,
+ `priority` integer DEFAULT 0,
+ `weight` integer DEFAULT 10,
+ `enabled` integer DEFAULT true,
+ `manual_override` integer DEFAULT false,
+ `success_count` integer DEFAULT 0,
+ `fail_count` integer DEFAULT 0,
+ `total_latency_ms` integer DEFAULT 0,
+ `total_cost` real DEFAULT 0,
+ `last_used_at` text,
+ `last_selected_at` text,
+ `last_fail_at` text,
+ `consecutive_fail_count` integer NOT NULL DEFAULT 0,
+ `cooldown_level` integer NOT NULL DEFAULT 0,
+ `cooldown_until` text,
+ FOREIGN KEY (`route_id`) REFERENCES `token_routes`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`token_id`) REFERENCES `account_tokens`(`id`) ON UPDATE no action ON DELETE set null
+);
+--> statement-breakpoint
+INSERT INTO `__new_route_channels` (
+ `id`,
+ `route_id`,
+ `account_id`,
+ `token_id`,
+ `source_model`,
+ `priority`,
+ `weight`,
+ `enabled`,
+ `manual_override`,
+ `success_count`,
+ `fail_count`,
+ `total_latency_ms`,
+ `total_cost`,
+ `last_used_at`,
+ `last_selected_at`,
+ `last_fail_at`,
+ `consecutive_fail_count`,
+ `cooldown_level`,
+ `cooldown_until`
+)
+SELECT
+ `id`,
+ `route_id`,
+ `account_id`,
+ CASE
+ WHEN `token_id` IS NULL THEN NULL
+ WHEN EXISTS (SELECT 1 FROM `account_tokens` WHERE `account_tokens`.`id` = `route_channels`.`token_id`) THEN `token_id`
+ ELSE NULL
+ END,
+ `source_model`,
+ `priority`,
+ `weight`,
+ `enabled`,
+ `manual_override`,
+ `success_count`,
+ `fail_count`,
+ `total_latency_ms`,
+ `total_cost`,
+ `last_used_at`,
+ `last_selected_at`,
+ `last_fail_at`,
+ `consecutive_fail_count`,
+ `cooldown_level`,
+ `cooldown_until`
+FROM `route_channels`;
+--> statement-breakpoint
+DROP TABLE `route_channels`;
+--> statement-breakpoint
+ALTER TABLE `__new_route_channels` RENAME TO `route_channels`;
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `route_channels_route_id_idx`
+ON `route_channels` (`route_id`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `route_channels_account_id_idx`
+ON `route_channels` (`account_id`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `route_channels_token_id_idx`
+ON `route_channels` (`token_id`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `route_channels_route_enabled_idx`
+ON `route_channels` (`route_id`, `enabled`);
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `route_channels_route_token_idx`
+ON `route_channels` (`route_id`, `token_id`);
+--> statement-breakpoint
+PRAGMA foreign_keys=ON;
diff --git a/drizzle/0009_model_availability_is_manual.sql b/drizzle/0009_model_availability_is_manual.sql
new file mode 100644
index 00000000..3d381703
--- /dev/null
+++ b/drizzle/0009_model_availability_is_manual.sql
@@ -0,0 +1 @@
+ALTER TABLE `model_availability` ADD `is_manual` integer DEFAULT false;
\ No newline at end of file
diff --git a/drizzle/0010_proxy_logs_downstream_api_key.sql b/drizzle/0010_proxy_logs_downstream_api_key.sql
new file mode 100644
index 00000000..67ccbf8f
--- /dev/null
+++ b/drizzle/0010_proxy_logs_downstream_api_key.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `proxy_logs` ADD `downstream_api_key_id` integer;
+--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS `proxy_logs_downstream_api_key_created_at_idx` ON `proxy_logs` (`downstream_api_key_id`, `created_at`);
diff --git a/drizzle/0011_downstream_api_key_metadata.sql b/drizzle/0011_downstream_api_key_metadata.sql
new file mode 100644
index 00000000..5a99895f
--- /dev/null
+++ b/drizzle/0011_downstream_api_key_metadata.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `downstream_api_keys` ADD `group_name` text;
+--> statement-breakpoint
+ALTER TABLE `downstream_api_keys` ADD `tags` text;
diff --git a/drizzle/0012_account_token_value_status.sql b/drizzle/0012_account_token_value_status.sql
new file mode 100644
index 00000000..0e999eb3
--- /dev/null
+++ b/drizzle/0012_account_token_value_status.sql
@@ -0,0 +1 @@
+ALTER TABLE `account_tokens` ADD `value_status` text DEFAULT 'ready' NOT NULL;
diff --git a/drizzle/0013_oauth_multi_provider.sql b/drizzle/0013_oauth_multi_provider.sql
new file mode 100644
index 00000000..92d53454
--- /dev/null
+++ b/drizzle/0013_oauth_multi_provider.sql
@@ -0,0 +1,24 @@
+ALTER TABLE `accounts` ADD `oauth_provider` text;
+--> statement-breakpoint
+ALTER TABLE `accounts` ADD `oauth_account_key` text;
+--> statement-breakpoint
+ALTER TABLE `accounts` ADD `oauth_project_id` text;
+--> statement-breakpoint
+UPDATE `accounts`
+SET
+ `oauth_provider` = NULLIF(TRIM(json_extract(`extra_config`, '$.oauth.provider')), ''),
+ `oauth_account_key` = NULLIF(
+ TRIM(COALESCE(
+ json_extract(`extra_config`, '$.oauth.accountKey'),
+ json_extract(`extra_config`, '$.oauth.accountId')
+ )),
+ ''
+ ),
+ `oauth_project_id` = NULLIF(TRIM(json_extract(`extra_config`, '$.oauth.projectId')), '')
+WHERE `extra_config` IS NOT NULL;
+--> statement-breakpoint
+CREATE INDEX `accounts_oauth_provider_idx` ON `accounts` (`oauth_provider`);
+--> statement-breakpoint
+CREATE INDEX `accounts_oauth_identity_idx` ON `accounts` (`oauth_provider`,`oauth_account_key`,`oauth_project_id`);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `sites_platform_url_unique` ON `sites` (`platform`,`url`);
diff --git a/drizzle/0014_explicit_group_routes.sql b/drizzle/0014_explicit_group_routes.sql
new file mode 100644
index 00000000..24d57228
--- /dev/null
+++ b/drizzle/0014_explicit_group_routes.sql
@@ -0,0 +1,13 @@
+ALTER TABLE `token_routes` ADD `route_mode` text DEFAULT 'pattern';
+--> statement-breakpoint
+CREATE TABLE `route_group_sources` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `group_route_id` integer NOT NULL,
+ `source_route_id` integer NOT NULL,
+ FOREIGN KEY (`group_route_id`) REFERENCES `token_routes`(`id`) ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY (`source_route_id`) REFERENCES `token_routes`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `route_group_sources_group_source_unique` ON `route_group_sources` (`group_route_id`,`source_route_id`);
+--> statement-breakpoint
+CREATE INDEX `route_group_sources_source_route_id_idx` ON `route_group_sources` (`source_route_id`);
diff --git a/drizzle/0015_site_announcements.sql b/drizzle/0015_site_announcements.sql
new file mode 100644
index 00000000..ea42d23b
--- /dev/null
+++ b/drizzle/0015_site_announcements.sql
@@ -0,0 +1,26 @@
+CREATE TABLE `site_announcements` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `site_id` integer NOT NULL,
+ `platform` text NOT NULL,
+ `source_key` text NOT NULL,
+ `title` text NOT NULL,
+ `content` text NOT NULL,
+ `level` text DEFAULT 'info' NOT NULL,
+ `source_url` text,
+ `starts_at` text,
+ `ends_at` text,
+ `upstream_created_at` text,
+ `upstream_updated_at` text,
+ `first_seen_at` text DEFAULT (datetime('now')),
+ `last_seen_at` text DEFAULT (datetime('now')),
+ `read_at` text,
+ `dismissed_at` text,
+ `raw_payload` text,
+ FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `site_announcements_site_source_key_unique` ON `site_announcements` (`site_id`,`source_key`);
+--> statement-breakpoint
+CREATE INDEX `site_announcements_site_id_first_seen_at_idx` ON `site_announcements` (`site_id`,`first_seen_at`);
+--> statement-breakpoint
+CREATE INDEX `site_announcements_read_at_idx` ON `site_announcements` (`read_at`);
diff --git a/drizzle/0016_proxy_logs_client_fields.sql b/drizzle/0016_proxy_logs_client_fields.sql
new file mode 100644
index 00000000..3e06c023
--- /dev/null
+++ b/drizzle/0016_proxy_logs_client_fields.sql
@@ -0,0 +1,6 @@
+ALTER TABLE `proxy_logs` ADD `client_family` text;--> statement-breakpoint
+ALTER TABLE `proxy_logs` ADD `client_app_id` text;--> statement-breakpoint
+ALTER TABLE `proxy_logs` ADD `client_app_name` text;--> statement-breakpoint
+ALTER TABLE `proxy_logs` ADD `client_confidence` text;--> statement-breakpoint
+CREATE INDEX `proxy_logs_client_app_id_created_at_idx` ON `proxy_logs` (`client_app_id`,`created_at`);--> statement-breakpoint
+CREATE INDEX `proxy_logs_client_family_created_at_idx` ON `proxy_logs` (`client_family`,`created_at`);
diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json
new file mode 100644
index 00000000..cdc0af3f
--- /dev/null
+++ b/drizzle/meta/0008_snapshot.json
@@ -0,0 +1,1855 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "4bdb796c-cedc-4b81-8bfb-78c5e0bde8c0",
+ "prevId": "886fa1ac-d97d-4a9d-bd5e-354568cba09d",
+ "tables": {
+ "account_tokens": {
+ "name": "account_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_group": {
+ "name": "token_group",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'manual'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "account_tokens_account_id_idx": {
+ "name": "account_tokens_account_id_idx",
+ "columns": [
+ "account_id"
+ ],
+ "isUnique": false
+ },
+ "account_tokens_account_enabled_idx": {
+ "name": "account_tokens_account_enabled_idx",
+ "columns": [
+ "account_id",
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "account_tokens_enabled_idx": {
+ "name": "account_tokens_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_tokens_account_id_accounts_id_fk": {
+ "name": "account_tokens_account_id_accounts_id_fk",
+ "tableFrom": "account_tokens",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "accounts": {
+ "name": "accounts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "site_id": {
+ "name": "site_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "api_token": {
+ "name": "api_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "balance": {
+ "name": "balance",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "balance_used": {
+ "name": "balance_used",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "quota": {
+ "name": "quota",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "unit_cost": {
+ "name": "unit_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "value_score": {
+ "name": "value_score",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "checkin_enabled": {
+ "name": "checkin_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "last_checkin_at": {
+ "name": "last_checkin_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_balance_refresh": {
+ "name": "last_balance_refresh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "extra_config": {
+ "name": "extra_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "accounts_site_id_idx": {
+ "name": "accounts_site_id_idx",
+ "columns": [
+ "site_id"
+ ],
+ "isUnique": false
+ },
+ "accounts_status_idx": {
+ "name": "accounts_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ },
+ "accounts_site_status_idx": {
+ "name": "accounts_site_status_idx",
+ "columns": [
+ "site_id",
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "accounts_site_id_sites_id_fk": {
+ "name": "accounts_site_id_sites_id_fk",
+ "tableFrom": "accounts",
+ "tableTo": "sites",
+ "columnsFrom": [
+ "site_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "checkin_logs": {
+ "name": "checkin_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reward": {
+ "name": "reward",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "checkin_logs_account_created_at_idx": {
+ "name": "checkin_logs_account_created_at_idx",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "checkin_logs_created_at_idx": {
+ "name": "checkin_logs_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "checkin_logs_status_idx": {
+ "name": "checkin_logs_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "checkin_logs_account_id_accounts_id_fk": {
+ "name": "checkin_logs_account_id_accounts_id_fk",
+ "tableFrom": "checkin_logs",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "downstream_api_keys": {
+ "name": "downstream_api_keys",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "max_cost": {
+ "name": "max_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "used_cost": {
+ "name": "used_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "max_requests": {
+ "name": "max_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "used_requests": {
+ "name": "used_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "supported_models": {
+ "name": "supported_models",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "allowed_route_ids": {
+ "name": "allowed_route_ids",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "site_weight_multipliers": {
+ "name": "site_weight_multipliers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "downstream_api_keys_key_unique": {
+ "name": "downstream_api_keys_key_unique",
+ "columns": [
+ "key"
+ ],
+ "isUnique": true
+ },
+ "downstream_api_keys_name_idx": {
+ "name": "downstream_api_keys_name_idx",
+ "columns": [
+ "name"
+ ],
+ "isUnique": false
+ },
+ "downstream_api_keys_enabled_idx": {
+ "name": "downstream_api_keys_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "downstream_api_keys_expires_at_idx": {
+ "name": "downstream_api_keys_expires_at_idx",
+ "columns": [
+ "expires_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "events": {
+ "name": "events",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'info'"
+ },
+ "read": {
+ "name": "read",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "related_id": {
+ "name": "related_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "related_type": {
+ "name": "related_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "events_read_created_at_idx": {
+ "name": "events_read_created_at_idx",
+ "columns": [
+ "read",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "events_type_created_at_idx": {
+ "name": "events_type_created_at_idx",
+ "columns": [
+ "type",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "events_created_at_idx": {
+ "name": "events_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "model_availability": {
+ "name": "model_availability",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "available": {
+ "name": "available",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_manual": {
+ "name": "is_manual",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checked_at": {
+ "name": "checked_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "model_availability_account_model_unique": {
+ "name": "model_availability_account_model_unique",
+ "columns": [
+ "account_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "model_availability_account_available_idx": {
+ "name": "model_availability_account_available_idx",
+ "columns": [
+ "account_id",
+ "available"
+ ],
+ "isUnique": false
+ },
+ "model_availability_model_name_idx": {
+ "name": "model_availability_model_name_idx",
+ "columns": [
+ "model_name"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "model_availability_account_id_accounts_id_fk": {
+ "name": "model_availability_account_id_accounts_id_fk",
+ "tableFrom": "model_availability",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_files": {
+ "name": "proxy_files",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "public_id": {
+ "name": "public_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_type": {
+ "name": "owner_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "byte_size": {
+ "name": "byte_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sha256": {
+ "name": "sha256",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content_base64": {
+ "name": "content_base64",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "proxy_files_public_id_unique": {
+ "name": "proxy_files_public_id_unique",
+ "columns": [
+ "public_id"
+ ],
+ "isUnique": true
+ },
+ "proxy_files_owner_lookup_idx": {
+ "name": "proxy_files_owner_lookup_idx",
+ "columns": [
+ "owner_type",
+ "owner_id",
+ "deleted_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_logs": {
+ "name": "proxy_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "route_id": {
+ "name": "route_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model_requested": {
+ "name": "model_requested",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model_actual": {
+ "name": "model_actual",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "http_status": {
+ "name": "http_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "prompt_tokens": {
+ "name": "prompt_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "completion_tokens": {
+ "name": "completion_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "total_tokens": {
+ "name": "total_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "estimated_cost": {
+ "name": "estimated_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "billing_details": {
+ "name": "billing_details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "retry_count": {
+ "name": "retry_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "proxy_logs_created_at_idx": {
+ "name": "proxy_logs_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_account_created_at_idx": {
+ "name": "proxy_logs_account_created_at_idx",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_status_created_at_idx": {
+ "name": "proxy_logs_status_created_at_idx",
+ "columns": [
+ "status",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_model_actual_created_at_idx": {
+ "name": "proxy_logs_model_actual_created_at_idx",
+ "columns": [
+ "model_actual",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_video_tasks": {
+ "name": "proxy_video_tasks",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "public_id": {
+ "name": "public_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "upstream_video_id": {
+ "name": "upstream_video_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_value": {
+ "name": "token_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "requested_model": {
+ "name": "requested_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "actual_model": {
+ "name": "actual_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status_snapshot": {
+ "name": "status_snapshot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "upstream_response_meta": {
+ "name": "upstream_response_meta",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_upstream_status": {
+ "name": "last_upstream_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_polled_at": {
+ "name": "last_polled_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "proxy_video_tasks_public_id_unique": {
+ "name": "proxy_video_tasks_public_id_unique",
+ "columns": [
+ "public_id"
+ ],
+ "isUnique": true
+ },
+ "proxy_video_tasks_upstream_video_id_idx": {
+ "name": "proxy_video_tasks_upstream_video_id_idx",
+ "columns": [
+ "upstream_video_id"
+ ],
+ "isUnique": false
+ },
+ "proxy_video_tasks_created_at_idx": {
+ "name": "proxy_video_tasks_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "route_channels": {
+ "name": "route_channels",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "route_id": {
+ "name": "route_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_id": {
+ "name": "token_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_model": {
+ "name": "source_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "weight": {
+ "name": "weight",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 10
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "manual_override": {
+ "name": "manual_override",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "success_count": {
+ "name": "success_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "fail_count": {
+ "name": "fail_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "total_latency_ms": {
+ "name": "total_latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_selected_at": {
+ "name": "last_selected_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_fail_at": {
+ "name": "last_fail_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "consecutive_fail_count": {
+ "name": "consecutive_fail_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "cooldown_level": {
+ "name": "cooldown_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "cooldown_until": {
+ "name": "cooldown_until",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "route_channels_route_id_idx": {
+ "name": "route_channels_route_id_idx",
+ "columns": [
+ "route_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_account_id_idx": {
+ "name": "route_channels_account_id_idx",
+ "columns": [
+ "account_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_token_id_idx": {
+ "name": "route_channels_token_id_idx",
+ "columns": [
+ "token_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_route_enabled_idx": {
+ "name": "route_channels_route_enabled_idx",
+ "columns": [
+ "route_id",
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "route_channels_route_token_idx": {
+ "name": "route_channels_route_token_idx",
+ "columns": [
+ "route_id",
+ "token_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "route_channels_route_id_token_routes_id_fk": {
+ "name": "route_channels_route_id_token_routes_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "token_routes",
+ "columnsFrom": [
+ "route_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "route_channels_account_id_accounts_id_fk": {
+ "name": "route_channels_account_id_accounts_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "route_channels_token_id_account_tokens_id_fk": {
+ "name": "route_channels_token_id_account_tokens_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "account_tokens",
+ "columnsFrom": [
+ "token_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "settings": {
+ "name": "settings",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "site_disabled_models": {
+ "name": "site_disabled_models",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "site_id": {
+ "name": "site_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "site_disabled_models_site_model_unique": {
+ "name": "site_disabled_models_site_model_unique",
+ "columns": [
+ "site_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "site_disabled_models_site_id_idx": {
+ "name": "site_disabled_models_site_id_idx",
+ "columns": [
+ "site_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "site_disabled_models_site_id_sites_id_fk": {
+ "name": "site_disabled_models_site_id_sites_id_fk",
+ "tableFrom": "site_disabled_models",
+ "tableTo": "sites",
+ "columnsFrom": [
+ "site_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "sites": {
+ "name": "sites",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "external_checkin_url": {
+ "name": "external_checkin_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "platform": {
+ "name": "platform",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "proxy_url": {
+ "name": "proxy_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "use_system_proxy": {
+ "name": "use_system_proxy",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "custom_headers": {
+ "name": "custom_headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "global_weight": {
+ "name": "global_weight",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 1
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "sites_status_idx": {
+ "name": "sites_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "token_model_availability": {
+ "name": "token_model_availability",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "token_id": {
+ "name": "token_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "available": {
+ "name": "available",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checked_at": {
+ "name": "checked_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "token_model_availability_token_model_unique": {
+ "name": "token_model_availability_token_model_unique",
+ "columns": [
+ "token_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "token_model_availability_token_available_idx": {
+ "name": "token_model_availability_token_available_idx",
+ "columns": [
+ "token_id",
+ "available"
+ ],
+ "isUnique": false
+ },
+ "token_model_availability_model_name_idx": {
+ "name": "token_model_availability_model_name_idx",
+ "columns": [
+ "model_name"
+ ],
+ "isUnique": false
+ },
+ "token_model_availability_available_idx": {
+ "name": "token_model_availability_available_idx",
+ "columns": [
+ "available"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "token_model_availability_token_id_account_tokens_id_fk": {
+ "name": "token_model_availability_token_id_account_tokens_id_fk",
+ "tableFrom": "token_model_availability",
+ "tableTo": "account_tokens",
+ "columnsFrom": [
+ "token_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "token_routes": {
+ "name": "token_routes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "model_pattern": {
+ "name": "model_pattern",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "display_icon": {
+ "name": "display_icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model_mapping": {
+ "name": "model_mapping",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "decision_snapshot": {
+ "name": "decision_snapshot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "decision_refreshed_at": {
+ "name": "decision_refreshed_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "routing_strategy": {
+ "name": "routing_strategy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'weighted'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "token_routes_model_pattern_idx": {
+ "name": "token_routes_model_pattern_idx",
+ "columns": [
+ "model_pattern"
+ ],
+ "isUnique": false
+ },
+ "token_routes_enabled_idx": {
+ "name": "token_routes_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/0012_snapshot.json b/drizzle/meta/0012_snapshot.json
new file mode 100644
index 00000000..bfcee45b
--- /dev/null
+++ b/drizzle/meta/0012_snapshot.json
@@ -0,0 +1,1892 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "b1aee21b-b789-4e47-ab03-7ba93cc26951",
+ "prevId": "4bdb796c-cedc-4b81-8bfb-78c5e0bde8c0",
+ "tables": {
+ "account_tokens": {
+ "name": "account_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_group": {
+ "name": "token_group",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "value_status": {
+ "name": "value_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'ready'"
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'manual'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "account_tokens_account_id_idx": {
+ "name": "account_tokens_account_id_idx",
+ "columns": [
+ "account_id"
+ ],
+ "isUnique": false
+ },
+ "account_tokens_account_enabled_idx": {
+ "name": "account_tokens_account_enabled_idx",
+ "columns": [
+ "account_id",
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "account_tokens_enabled_idx": {
+ "name": "account_tokens_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_tokens_account_id_accounts_id_fk": {
+ "name": "account_tokens_account_id_accounts_id_fk",
+ "tableFrom": "account_tokens",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "accounts": {
+ "name": "accounts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "site_id": {
+ "name": "site_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "api_token": {
+ "name": "api_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "balance": {
+ "name": "balance",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "balance_used": {
+ "name": "balance_used",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "quota": {
+ "name": "quota",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "unit_cost": {
+ "name": "unit_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "value_score": {
+ "name": "value_score",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "checkin_enabled": {
+ "name": "checkin_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "last_checkin_at": {
+ "name": "last_checkin_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_balance_refresh": {
+ "name": "last_balance_refresh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "extra_config": {
+ "name": "extra_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "accounts_site_id_idx": {
+ "name": "accounts_site_id_idx",
+ "columns": [
+ "site_id"
+ ],
+ "isUnique": false
+ },
+ "accounts_status_idx": {
+ "name": "accounts_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ },
+ "accounts_site_status_idx": {
+ "name": "accounts_site_status_idx",
+ "columns": [
+ "site_id",
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "accounts_site_id_sites_id_fk": {
+ "name": "accounts_site_id_sites_id_fk",
+ "tableFrom": "accounts",
+ "tableTo": "sites",
+ "columnsFrom": [
+ "site_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "checkin_logs": {
+ "name": "checkin_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reward": {
+ "name": "reward",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "checkin_logs_account_created_at_idx": {
+ "name": "checkin_logs_account_created_at_idx",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "checkin_logs_created_at_idx": {
+ "name": "checkin_logs_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "checkin_logs_status_idx": {
+ "name": "checkin_logs_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "checkin_logs_account_id_accounts_id_fk": {
+ "name": "checkin_logs_account_id_accounts_id_fk",
+ "tableFrom": "checkin_logs",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "downstream_api_keys": {
+ "name": "downstream_api_keys",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "group_name": {
+ "name": "group_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "max_cost": {
+ "name": "max_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "used_cost": {
+ "name": "used_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "max_requests": {
+ "name": "max_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "used_requests": {
+ "name": "used_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "supported_models": {
+ "name": "supported_models",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "allowed_route_ids": {
+ "name": "allowed_route_ids",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "site_weight_multipliers": {
+ "name": "site_weight_multipliers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "downstream_api_keys_key_unique": {
+ "name": "downstream_api_keys_key_unique",
+ "columns": [
+ "key"
+ ],
+ "isUnique": true
+ },
+ "downstream_api_keys_name_idx": {
+ "name": "downstream_api_keys_name_idx",
+ "columns": [
+ "name"
+ ],
+ "isUnique": false
+ },
+ "downstream_api_keys_enabled_idx": {
+ "name": "downstream_api_keys_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "downstream_api_keys_expires_at_idx": {
+ "name": "downstream_api_keys_expires_at_idx",
+ "columns": [
+ "expires_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "events": {
+ "name": "events",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'info'"
+ },
+ "read": {
+ "name": "read",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "related_id": {
+ "name": "related_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "related_type": {
+ "name": "related_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "events_read_created_at_idx": {
+ "name": "events_read_created_at_idx",
+ "columns": [
+ "read",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "events_type_created_at_idx": {
+ "name": "events_type_created_at_idx",
+ "columns": [
+ "type",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "events_created_at_idx": {
+ "name": "events_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "model_availability": {
+ "name": "model_availability",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "available": {
+ "name": "available",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_manual": {
+ "name": "is_manual",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checked_at": {
+ "name": "checked_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "model_availability_account_model_unique": {
+ "name": "model_availability_account_model_unique",
+ "columns": [
+ "account_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "model_availability_account_available_idx": {
+ "name": "model_availability_account_available_idx",
+ "columns": [
+ "account_id",
+ "available"
+ ],
+ "isUnique": false
+ },
+ "model_availability_model_name_idx": {
+ "name": "model_availability_model_name_idx",
+ "columns": [
+ "model_name"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "model_availability_account_id_accounts_id_fk": {
+ "name": "model_availability_account_id_accounts_id_fk",
+ "tableFrom": "model_availability",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_files": {
+ "name": "proxy_files",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "public_id": {
+ "name": "public_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_type": {
+ "name": "owner_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "byte_size": {
+ "name": "byte_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sha256": {
+ "name": "sha256",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content_base64": {
+ "name": "content_base64",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "proxy_files_public_id_unique": {
+ "name": "proxy_files_public_id_unique",
+ "columns": [
+ "public_id"
+ ],
+ "isUnique": true
+ },
+ "proxy_files_owner_lookup_idx": {
+ "name": "proxy_files_owner_lookup_idx",
+ "columns": [
+ "owner_type",
+ "owner_id",
+ "deleted_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_logs": {
+ "name": "proxy_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "route_id": {
+ "name": "route_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "downstream_api_key_id": {
+ "name": "downstream_api_key_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model_requested": {
+ "name": "model_requested",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model_actual": {
+ "name": "model_actual",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "http_status": {
+ "name": "http_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "prompt_tokens": {
+ "name": "prompt_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "completion_tokens": {
+ "name": "completion_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "total_tokens": {
+ "name": "total_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "estimated_cost": {
+ "name": "estimated_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "billing_details": {
+ "name": "billing_details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "retry_count": {
+ "name": "retry_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "proxy_logs_created_at_idx": {
+ "name": "proxy_logs_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_account_created_at_idx": {
+ "name": "proxy_logs_account_created_at_idx",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_status_created_at_idx": {
+ "name": "proxy_logs_status_created_at_idx",
+ "columns": [
+ "status",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_model_actual_created_at_idx": {
+ "name": "proxy_logs_model_actual_created_at_idx",
+ "columns": [
+ "model_actual",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_downstream_api_key_created_at_idx": {
+ "name": "proxy_logs_downstream_api_key_created_at_idx",
+ "columns": [
+ "downstream_api_key_id",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_video_tasks": {
+ "name": "proxy_video_tasks",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "public_id": {
+ "name": "public_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "upstream_video_id": {
+ "name": "upstream_video_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_value": {
+ "name": "token_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "requested_model": {
+ "name": "requested_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "actual_model": {
+ "name": "actual_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status_snapshot": {
+ "name": "status_snapshot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "upstream_response_meta": {
+ "name": "upstream_response_meta",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_upstream_status": {
+ "name": "last_upstream_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_polled_at": {
+ "name": "last_polled_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "proxy_video_tasks_public_id_unique": {
+ "name": "proxy_video_tasks_public_id_unique",
+ "columns": [
+ "public_id"
+ ],
+ "isUnique": true
+ },
+ "proxy_video_tasks_upstream_video_id_idx": {
+ "name": "proxy_video_tasks_upstream_video_id_idx",
+ "columns": [
+ "upstream_video_id"
+ ],
+ "isUnique": false
+ },
+ "proxy_video_tasks_created_at_idx": {
+ "name": "proxy_video_tasks_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "route_channels": {
+ "name": "route_channels",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "route_id": {
+ "name": "route_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_id": {
+ "name": "token_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_model": {
+ "name": "source_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "weight": {
+ "name": "weight",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 10
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "manual_override": {
+ "name": "manual_override",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "success_count": {
+ "name": "success_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "fail_count": {
+ "name": "fail_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "total_latency_ms": {
+ "name": "total_latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_selected_at": {
+ "name": "last_selected_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_fail_at": {
+ "name": "last_fail_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "consecutive_fail_count": {
+ "name": "consecutive_fail_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "cooldown_level": {
+ "name": "cooldown_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "cooldown_until": {
+ "name": "cooldown_until",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "route_channels_route_id_idx": {
+ "name": "route_channels_route_id_idx",
+ "columns": [
+ "route_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_account_id_idx": {
+ "name": "route_channels_account_id_idx",
+ "columns": [
+ "account_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_token_id_idx": {
+ "name": "route_channels_token_id_idx",
+ "columns": [
+ "token_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_route_enabled_idx": {
+ "name": "route_channels_route_enabled_idx",
+ "columns": [
+ "route_id",
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "route_channels_route_token_idx": {
+ "name": "route_channels_route_token_idx",
+ "columns": [
+ "route_id",
+ "token_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "route_channels_route_id_token_routes_id_fk": {
+ "name": "route_channels_route_id_token_routes_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "token_routes",
+ "columnsFrom": [
+ "route_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "route_channels_account_id_accounts_id_fk": {
+ "name": "route_channels_account_id_accounts_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "route_channels_token_id_account_tokens_id_fk": {
+ "name": "route_channels_token_id_account_tokens_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "account_tokens",
+ "columnsFrom": [
+ "token_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "settings": {
+ "name": "settings",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "site_disabled_models": {
+ "name": "site_disabled_models",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "site_id": {
+ "name": "site_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "site_disabled_models_site_model_unique": {
+ "name": "site_disabled_models_site_model_unique",
+ "columns": [
+ "site_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "site_disabled_models_site_id_idx": {
+ "name": "site_disabled_models_site_id_idx",
+ "columns": [
+ "site_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "site_disabled_models_site_id_sites_id_fk": {
+ "name": "site_disabled_models_site_id_sites_id_fk",
+ "tableFrom": "site_disabled_models",
+ "tableTo": "sites",
+ "columnsFrom": [
+ "site_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "sites": {
+ "name": "sites",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "external_checkin_url": {
+ "name": "external_checkin_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "platform": {
+ "name": "platform",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "proxy_url": {
+ "name": "proxy_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "use_system_proxy": {
+ "name": "use_system_proxy",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "custom_headers": {
+ "name": "custom_headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "global_weight": {
+ "name": "global_weight",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 1
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "sites_status_idx": {
+ "name": "sites_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "token_model_availability": {
+ "name": "token_model_availability",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "token_id": {
+ "name": "token_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "available": {
+ "name": "available",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checked_at": {
+ "name": "checked_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "token_model_availability_token_model_unique": {
+ "name": "token_model_availability_token_model_unique",
+ "columns": [
+ "token_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "token_model_availability_token_available_idx": {
+ "name": "token_model_availability_token_available_idx",
+ "columns": [
+ "token_id",
+ "available"
+ ],
+ "isUnique": false
+ },
+ "token_model_availability_model_name_idx": {
+ "name": "token_model_availability_model_name_idx",
+ "columns": [
+ "model_name"
+ ],
+ "isUnique": false
+ },
+ "token_model_availability_available_idx": {
+ "name": "token_model_availability_available_idx",
+ "columns": [
+ "available"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "token_model_availability_token_id_account_tokens_id_fk": {
+ "name": "token_model_availability_token_id_account_tokens_id_fk",
+ "tableFrom": "token_model_availability",
+ "tableTo": "account_tokens",
+ "columnsFrom": [
+ "token_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "token_routes": {
+ "name": "token_routes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "model_pattern": {
+ "name": "model_pattern",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "display_icon": {
+ "name": "display_icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model_mapping": {
+ "name": "model_mapping",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "decision_snapshot": {
+ "name": "decision_snapshot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "decision_refreshed_at": {
+ "name": "decision_refreshed_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "routing_strategy": {
+ "name": "routing_strategy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'weighted'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "token_routes_model_pattern_idx": {
+ "name": "token_routes_model_pattern_idx",
+ "columns": [
+ "model_pattern"
+ ],
+ "isUnique": false
+ },
+ "token_routes_enabled_idx": {
+ "name": "token_routes_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/0016_snapshot.json b/drizzle/meta/0016_snapshot.json
new file mode 100644
index 00000000..e6cab774
--- /dev/null
+++ b/drizzle/meta/0016_snapshot.json
@@ -0,0 +1,2063 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "6c654ca3-d194-4c7b-a991-9b91891c3913",
+ "prevId": "b1aee21b-b789-4e47-ab03-7ba93cc26951",
+ "tables": {
+ "account_tokens": {
+ "name": "account_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_group": {
+ "name": "token_group",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "value_status": {
+ "name": "value_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'ready'"
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'manual'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "account_tokens_account_id_idx": {
+ "name": "account_tokens_account_id_idx",
+ "columns": [
+ "account_id"
+ ],
+ "isUnique": false
+ },
+ "account_tokens_account_enabled_idx": {
+ "name": "account_tokens_account_enabled_idx",
+ "columns": [
+ "account_id",
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "account_tokens_enabled_idx": {
+ "name": "account_tokens_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_tokens_account_id_accounts_id_fk": {
+ "name": "account_tokens_account_id_accounts_id_fk",
+ "tableFrom": "account_tokens",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "accounts": {
+ "name": "accounts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "site_id": {
+ "name": "site_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "api_token": {
+ "name": "api_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "balance": {
+ "name": "balance",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "balance_used": {
+ "name": "balance_used",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "quota": {
+ "name": "quota",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "unit_cost": {
+ "name": "unit_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "value_score": {
+ "name": "value_score",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "checkin_enabled": {
+ "name": "checkin_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "last_checkin_at": {
+ "name": "last_checkin_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_balance_refresh": {
+ "name": "last_balance_refresh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "oauth_provider": {
+ "name": "oauth_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "oauth_account_key": {
+ "name": "oauth_account_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "oauth_project_id": {
+ "name": "oauth_project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "extra_config": {
+ "name": "extra_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "accounts_site_id_idx": {
+ "name": "accounts_site_id_idx",
+ "columns": [
+ "site_id"
+ ],
+ "isUnique": false
+ },
+ "accounts_status_idx": {
+ "name": "accounts_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ },
+ "accounts_site_status_idx": {
+ "name": "accounts_site_status_idx",
+ "columns": [
+ "site_id",
+ "status"
+ ],
+ "isUnique": false
+ },
+ "accounts_oauth_provider_idx": {
+ "name": "accounts_oauth_provider_idx",
+ "columns": [
+ "oauth_provider"
+ ],
+ "isUnique": false
+ },
+ "accounts_oauth_identity_idx": {
+ "name": "accounts_oauth_identity_idx",
+ "columns": [
+ "oauth_provider",
+ "oauth_account_key",
+ "oauth_project_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "accounts_site_id_sites_id_fk": {
+ "name": "accounts_site_id_sites_id_fk",
+ "tableFrom": "accounts",
+ "tableTo": "sites",
+ "columnsFrom": [
+ "site_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "checkin_logs": {
+ "name": "checkin_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reward": {
+ "name": "reward",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "checkin_logs_account_created_at_idx": {
+ "name": "checkin_logs_account_created_at_idx",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "checkin_logs_created_at_idx": {
+ "name": "checkin_logs_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "checkin_logs_status_idx": {
+ "name": "checkin_logs_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "checkin_logs_account_id_accounts_id_fk": {
+ "name": "checkin_logs_account_id_accounts_id_fk",
+ "tableFrom": "checkin_logs",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "downstream_api_keys": {
+ "name": "downstream_api_keys",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "group_name": {
+ "name": "group_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "max_cost": {
+ "name": "max_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "used_cost": {
+ "name": "used_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "max_requests": {
+ "name": "max_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "used_requests": {
+ "name": "used_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "supported_models": {
+ "name": "supported_models",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "allowed_route_ids": {
+ "name": "allowed_route_ids",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "site_weight_multipliers": {
+ "name": "site_weight_multipliers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "downstream_api_keys_key_unique": {
+ "name": "downstream_api_keys_key_unique",
+ "columns": [
+ "key"
+ ],
+ "isUnique": true
+ },
+ "downstream_api_keys_name_idx": {
+ "name": "downstream_api_keys_name_idx",
+ "columns": [
+ "name"
+ ],
+ "isUnique": false
+ },
+ "downstream_api_keys_enabled_idx": {
+ "name": "downstream_api_keys_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "downstream_api_keys_expires_at_idx": {
+ "name": "downstream_api_keys_expires_at_idx",
+ "columns": [
+ "expires_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "events": {
+ "name": "events",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'info'"
+ },
+ "read": {
+ "name": "read",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "related_id": {
+ "name": "related_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "related_type": {
+ "name": "related_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "events_read_created_at_idx": {
+ "name": "events_read_created_at_idx",
+ "columns": [
+ "read",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "events_type_created_at_idx": {
+ "name": "events_type_created_at_idx",
+ "columns": [
+ "type",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "events_created_at_idx": {
+ "name": "events_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "model_availability": {
+ "name": "model_availability",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "available": {
+ "name": "available",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_manual": {
+ "name": "is_manual",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checked_at": {
+ "name": "checked_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "model_availability_account_model_unique": {
+ "name": "model_availability_account_model_unique",
+ "columns": [
+ "account_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "model_availability_account_available_idx": {
+ "name": "model_availability_account_available_idx",
+ "columns": [
+ "account_id",
+ "available"
+ ],
+ "isUnique": false
+ },
+ "model_availability_model_name_idx": {
+ "name": "model_availability_model_name_idx",
+ "columns": [
+ "model_name"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "model_availability_account_id_accounts_id_fk": {
+ "name": "model_availability_account_id_accounts_id_fk",
+ "tableFrom": "model_availability",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_files": {
+ "name": "proxy_files",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "public_id": {
+ "name": "public_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_type": {
+ "name": "owner_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "byte_size": {
+ "name": "byte_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "sha256": {
+ "name": "sha256",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content_base64": {
+ "name": "content_base64",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "proxy_files_public_id_unique": {
+ "name": "proxy_files_public_id_unique",
+ "columns": [
+ "public_id"
+ ],
+ "isUnique": true
+ },
+ "proxy_files_owner_lookup_idx": {
+ "name": "proxy_files_owner_lookup_idx",
+ "columns": [
+ "owner_type",
+ "owner_id",
+ "deleted_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_logs": {
+ "name": "proxy_logs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "route_id": {
+ "name": "route_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "downstream_api_key_id": {
+ "name": "downstream_api_key_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model_requested": {
+ "name": "model_requested",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model_actual": {
+ "name": "model_actual",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "http_status": {
+ "name": "http_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "prompt_tokens": {
+ "name": "prompt_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "completion_tokens": {
+ "name": "completion_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "total_tokens": {
+ "name": "total_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "estimated_cost": {
+ "name": "estimated_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "billing_details": {
+ "name": "billing_details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "client_family": {
+ "name": "client_family",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "client_app_id": {
+ "name": "client_app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "client_app_name": {
+ "name": "client_app_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "client_confidence": {
+ "name": "client_confidence",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "retry_count": {
+ "name": "retry_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "proxy_logs_created_at_idx": {
+ "name": "proxy_logs_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_account_created_at_idx": {
+ "name": "proxy_logs_account_created_at_idx",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_status_created_at_idx": {
+ "name": "proxy_logs_status_created_at_idx",
+ "columns": [
+ "status",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_model_actual_created_at_idx": {
+ "name": "proxy_logs_model_actual_created_at_idx",
+ "columns": [
+ "model_actual",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_downstream_api_key_created_at_idx": {
+ "name": "proxy_logs_downstream_api_key_created_at_idx",
+ "columns": [
+ "downstream_api_key_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_client_app_id_created_at_idx": {
+ "name": "proxy_logs_client_app_id_created_at_idx",
+ "columns": [
+ "client_app_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "proxy_logs_client_family_created_at_idx": {
+ "name": "proxy_logs_client_family_created_at_idx",
+ "columns": [
+ "client_family",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "proxy_video_tasks": {
+ "name": "proxy_video_tasks",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "public_id": {
+ "name": "public_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "upstream_video_id": {
+ "name": "upstream_video_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_value": {
+ "name": "token_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "requested_model": {
+ "name": "requested_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "actual_model": {
+ "name": "actual_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status_snapshot": {
+ "name": "status_snapshot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "upstream_response_meta": {
+ "name": "upstream_response_meta",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_upstream_status": {
+ "name": "last_upstream_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_polled_at": {
+ "name": "last_polled_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "proxy_video_tasks_public_id_unique": {
+ "name": "proxy_video_tasks_public_id_unique",
+ "columns": [
+ "public_id"
+ ],
+ "isUnique": true
+ },
+ "proxy_video_tasks_upstream_video_id_idx": {
+ "name": "proxy_video_tasks_upstream_video_id_idx",
+ "columns": [
+ "upstream_video_id"
+ ],
+ "isUnique": false
+ },
+ "proxy_video_tasks_created_at_idx": {
+ "name": "proxy_video_tasks_created_at_idx",
+ "columns": [
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "route_channels": {
+ "name": "route_channels",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "route_id": {
+ "name": "route_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token_id": {
+ "name": "token_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_model": {
+ "name": "source_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "weight": {
+ "name": "weight",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 10
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "manual_override": {
+ "name": "manual_override",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "success_count": {
+ "name": "success_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "fail_count": {
+ "name": "fail_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "total_latency_ms": {
+ "name": "total_latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_selected_at": {
+ "name": "last_selected_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "last_fail_at": {
+ "name": "last_fail_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "consecutive_fail_count": {
+ "name": "consecutive_fail_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "cooldown_level": {
+ "name": "cooldown_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "cooldown_until": {
+ "name": "cooldown_until",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "route_channels_route_id_idx": {
+ "name": "route_channels_route_id_idx",
+ "columns": [
+ "route_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_account_id_idx": {
+ "name": "route_channels_account_id_idx",
+ "columns": [
+ "account_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_token_id_idx": {
+ "name": "route_channels_token_id_idx",
+ "columns": [
+ "token_id"
+ ],
+ "isUnique": false
+ },
+ "route_channels_route_enabled_idx": {
+ "name": "route_channels_route_enabled_idx",
+ "columns": [
+ "route_id",
+ "enabled"
+ ],
+ "isUnique": false
+ },
+ "route_channels_route_token_idx": {
+ "name": "route_channels_route_token_idx",
+ "columns": [
+ "route_id",
+ "token_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "route_channels_route_id_token_routes_id_fk": {
+ "name": "route_channels_route_id_token_routes_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "token_routes",
+ "columnsFrom": [
+ "route_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "route_channels_account_id_accounts_id_fk": {
+ "name": "route_channels_account_id_accounts_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "route_channels_token_id_account_tokens_id_fk": {
+ "name": "route_channels_token_id_account_tokens_id_fk",
+ "tableFrom": "route_channels",
+ "tableTo": "account_tokens",
+ "columnsFrom": [
+ "token_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "route_group_sources": {
+ "name": "route_group_sources",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "group_route_id": {
+ "name": "group_route_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_route_id": {
+ "name": "source_route_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "route_group_sources_group_source_unique": {
+ "name": "route_group_sources_group_source_unique",
+ "columns": [
+ "group_route_id",
+ "source_route_id"
+ ],
+ "isUnique": true
+ },
+ "route_group_sources_source_route_id_idx": {
+ "name": "route_group_sources_source_route_id_idx",
+ "columns": [
+ "source_route_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "route_group_sources_group_route_id_token_routes_id_fk": {
+ "name": "route_group_sources_group_route_id_token_routes_id_fk",
+ "tableFrom": "route_group_sources",
+ "tableTo": "token_routes",
+ "columnsFrom": [
+ "group_route_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "route_group_sources_source_route_id_token_routes_id_fk": {
+ "name": "route_group_sources_source_route_id_token_routes_id_fk",
+ "tableFrom": "route_group_sources",
+ "tableTo": "token_routes",
+ "columnsFrom": [
+ "source_route_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "settings": {
+ "name": "settings",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "site_disabled_models": {
+ "name": "site_disabled_models",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "site_id": {
+ "name": "site_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "site_disabled_models_site_model_unique": {
+ "name": "site_disabled_models_site_model_unique",
+ "columns": [
+ "site_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "site_disabled_models_site_id_idx": {
+ "name": "site_disabled_models_site_id_idx",
+ "columns": [
+ "site_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "site_disabled_models_site_id_sites_id_fk": {
+ "name": "site_disabled_models_site_id_sites_id_fk",
+ "tableFrom": "site_disabled_models",
+ "tableTo": "sites",
+ "columnsFrom": [
+ "site_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "sites": {
+ "name": "sites",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "external_checkin_url": {
+ "name": "external_checkin_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "platform": {
+ "name": "platform",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "proxy_url": {
+ "name": "proxy_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "use_system_proxy": {
+ "name": "use_system_proxy",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "custom_headers": {
+ "name": "custom_headers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "is_pinned": {
+ "name": "is_pinned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "global_weight": {
+ "name": "global_weight",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 1
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "sites_status_idx": {
+ "name": "sites_status_idx",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ },
+ "sites_platform_url_unique": {
+ "name": "sites_platform_url_unique",
+ "columns": [
+ "platform",
+ "url"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "token_model_availability": {
+ "name": "token_model_availability",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "token_id": {
+ "name": "token_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model_name": {
+ "name": "model_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "available": {
+ "name": "available",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "checked_at": {
+ "name": "checked_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "token_model_availability_token_model_unique": {
+ "name": "token_model_availability_token_model_unique",
+ "columns": [
+ "token_id",
+ "model_name"
+ ],
+ "isUnique": true
+ },
+ "token_model_availability_token_available_idx": {
+ "name": "token_model_availability_token_available_idx",
+ "columns": [
+ "token_id",
+ "available"
+ ],
+ "isUnique": false
+ },
+ "token_model_availability_model_name_idx": {
+ "name": "token_model_availability_model_name_idx",
+ "columns": [
+ "model_name"
+ ],
+ "isUnique": false
+ },
+ "token_model_availability_available_idx": {
+ "name": "token_model_availability_available_idx",
+ "columns": [
+ "available"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "token_model_availability_token_id_account_tokens_id_fk": {
+ "name": "token_model_availability_token_id_account_tokens_id_fk",
+ "tableFrom": "token_model_availability",
+ "tableTo": "account_tokens",
+ "columnsFrom": [
+ "token_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "token_routes": {
+ "name": "token_routes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "model_pattern": {
+ "name": "model_pattern",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "display_icon": {
+ "name": "display_icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "route_mode": {
+ "name": "route_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'pattern'"
+ },
+ "model_mapping": {
+ "name": "model_mapping",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "decision_snapshot": {
+ "name": "decision_snapshot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "decision_refreshed_at": {
+ "name": "decision_refreshed_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "routing_strategy": {
+ "name": "routing_strategy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'weighted'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(datetime('now'))"
+ }
+ },
+ "indexes": {
+ "token_routes_model_pattern_idx": {
+ "name": "token_routes_model_pattern_idx",
+ "columns": [
+ "model_pattern"
+ ],
+ "isUnique": false
+ },
+ "token_routes_enabled_idx": {
+ "name": "token_routes_enabled_idx",
+ "columns": [
+ "enabled"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 921bc283..e3c667ba 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -36,6 +36,83 @@
"when": 1772320800000,
"tag": "0004_sorting_preferences",
"breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "6",
+ "when": 1773500331000,
+ "tag": "0006_site_disabled_models",
+ "breakpoints": true
+ },
+ {
+ "idx": 6,
+ "version": "6",
+ "when": 1773500400000,
+ "tag": "0007_account_token_group",
+ "breakpoints": true
+ },
+ {
+ "idx": 7,
+ "version": "6",
+ "when": 1773500500000,
+ "tag": "0008_sqlite_schema_backfill",
+ "breakpoints": true
+ },
+ {
+ "idx": 8,
+ "version": "6",
+ "when": 1773528832339,
+ "tag": "0009_model_availability_is_manual",
+ "breakpoints": true
+ },
+ {
+ "idx": 9,
+ "version": "6",
+ "when": 1773553945763,
+ "tag": "0010_proxy_logs_downstream_api_key",
+ "breakpoints": true
+ },
+ {
+ "idx": 10,
+ "version": "6",
+ "when": 1773605400000,
+ "tag": "0011_downstream_api_key_metadata",
+ "breakpoints": true
+ },
+ {
+ "idx": 11,
+ "version": "6",
+ "when": 1773665311013,
+ "tag": "0012_account_token_value_status",
+ "breakpoints": true
+ },
+ {
+ "idx": 12,
+ "version": "6",
+ "when": 1773682200000,
+ "tag": "0013_oauth_multi_provider",
+ "breakpoints": true
+ },
+ {
+ "idx": 13,
+ "version": "6",
+ "when": 1773855000000,
+ "tag": "0014_explicit_group_routes",
+ "breakpoints": true
+ },
+ {
+ "idx": 14,
+ "version": "6",
+ "when": 1773900000000,
+ "tag": "0015_site_announcements",
+ "breakpoints": true
+ },
+ {
+ "idx": 15,
+ "version": "6",
+ "when": 1773979652606,
+ "tag": "0016_proxy_logs_client_fields",
+ "breakpoints": true
}
]
}
diff --git a/electron-builder.yml b/electron-builder.yml
index e99c6466..ae093b1e 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -4,7 +4,7 @@ artifactName: ${name}-${version}-${os}-${arch}.${ext}
directories:
output: release
electronDist: node_modules/electron/dist
-icon: build/icon.png
+icon: build/desktop-icon.png
asar: false
files:
- dist/**/*
diff --git a/package-lock.json b/package-lock.json
index 49e5c715..3c1c3d17 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "metapi",
- "version": "1.2.1",
+ "version": "1.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metapi",
- "version": "1.2.1",
+ "version": "1.2.3",
"license": "MIT",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -14,25 +14,28 @@
"@dnd-kit/utilities": "^3.2.2",
"@fastify/cors": "^11.2.0",
"@fastify/static": "^9.0.0",
- "@visactor/react-vchart": "^2.0.16",
- "better-sqlite3": "^11.3.0",
- "dotenv": "^16.4.5",
- "drizzle-orm": "^0.36.4",
+ "@visactor/react-vchart": "^2.0.19",
+ "better-sqlite3": "^12.8.0",
+ "dotenv": "^17.3.1",
+ "drizzle-orm": "^0.45.1",
"electron-log": "^5.2.4",
"electron-updater": "^6.6.2",
- "fastify": "^5.7.4",
+ "fastify": "^5.8.2",
"get-port": "^7.1.0",
+ "marked": "^17.0.4",
"minimatch": "^10.2.4",
"minimist": "^1.2.8",
- "mysql2": "^3.15.3",
- "node-cron": "^3.0.3",
- "nodemailer": "^8.0.1",
+ "mysql2": "^3.20.0",
+ "node-cron": "^4.2.1",
+ "nodemailer": "^8.0.3",
"pg": "^8.16.3",
- "undici": "^6.20.1"
+ "socks": "^2.8.7",
+ "undici": "^6.24.1",
+ "ws": "^8.19.0"
},
"devDependencies": {
"@electron/notarize": "^3.1.0",
- "@tailwindcss/vite": "^4.0.0",
+ "@tailwindcss/vite": "^4.2.2",
"@types/better-sqlite3": "^7.6.11",
"@types/node": "^22.10.1",
"@types/node-cron": "^3.0.11",
@@ -40,16 +43,18 @@
"@types/pg": "^8.15.6",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
+ "@types/react-test-renderer": "^19.1.0",
"@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.1.0",
"cross-env": "^7.0.3",
- "drizzle-kit": "^0.28.1",
- "electron": "^35.0.1",
+ "drizzle-kit": "^0.31.10",
+ "electron": "^41.0.3",
"electron-builder": "^26.0.12",
- "mermaid": "^11.12.3",
+ "jsdom": "^29.0.1",
+ "mermaid": "^11.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-router-dom": "^6.28.0",
+ "react-router-dom": "^7.13.1",
"react-test-renderer": "^18.3.1",
"sharp": "^0.34.5",
"tailwindcss": "^4.0.0",
@@ -59,7 +64,10 @@
"vitepress": "^1.6.4",
"vitepress-plugin-mermaid": "^2.0.17",
"vitest": "^2.1.8",
- "wait-on": "^8.0.3"
+ "wait-on": "^9.0.4"
+ },
+ "engines": {
+ "node": ">=22.15.0"
}
},
"node_modules/@algolia/abtesting": {
@@ -223,6 +231,7 @@
"integrity": "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@algolia/client-common": "5.49.1",
"@algolia/requester-browser-xhr": "5.49.1",
@@ -344,6 +353,67 @@
"node": ">=18"
}
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
+ "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^3.1.1",
+ "@csstools/css-color-parser": "^4.0.2",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0",
+ "lru-cache": "^11.2.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "11.2.7",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
+ "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz",
+ "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.7"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
+ "version": "11.2.7",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
+ "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -375,6 +445,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -633,6 +704,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
"node_modules/@chevrotain/cst-dts-gen": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.2.tgz",
@@ -677,6 +761,148 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+ "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
+ "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz",
+ "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.0.2",
+ "@csstools/css-calc": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz",
+ "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -701,6 +927,7 @@
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -746,6 +973,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -1235,7 +1463,6 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
- "peer": true,
"dependencies": {
"cross-dirname": "^0.1.0",
"debug": "^4.3.4",
@@ -1257,7 +1484,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -1274,7 +1500,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"universalify": "^2.0.0"
},
@@ -1289,7 +1514,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"engines": {
"node": ">= 10.0.0"
}
@@ -1742,9 +1966,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
- "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
@@ -1755,13 +1979,13 @@
"aix"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
- "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
@@ -1772,13 +1996,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
- "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
@@ -1789,13 +2013,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
- "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
@@ -1806,13 +2030,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
- "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
@@ -1823,13 +2047,13 @@
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
- "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
@@ -1840,13 +2064,13 @@
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
- "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
@@ -1857,13 +2081,13 @@
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
- "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
@@ -1874,13 +2098,13 @@
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
- "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
@@ -1891,13 +2115,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
- "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
@@ -1908,13 +2132,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
- "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
@@ -1925,13 +2149,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
- "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
@@ -1942,13 +2166,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
- "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
@@ -1959,13 +2183,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
- "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
@@ -1976,13 +2200,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
- "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
@@ -1993,13 +2217,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
- "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
@@ -2010,13 +2234,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
- "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
@@ -2027,7 +2251,7 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
@@ -2048,9 +2272,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
- "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
@@ -2061,7 +2285,7 @@
"netbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
@@ -2082,9 +2306,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
- "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
@@ -2095,7 +2319,7 @@
"openbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
@@ -2116,9 +2340,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
- "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
@@ -2129,13 +2353,13 @@
"sunos"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
- "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
@@ -2146,15 +2370,15 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
- "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
- "cpu": [
- "ia32"
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
],
"dev": true,
"license": "MIT",
@@ -2163,13 +2387,13 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
- "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
@@ -2180,7 +2404,25 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
+ "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
}
},
"node_modules/@fastify/accept-negotiator": {
@@ -3198,9 +3440,9 @@
"optional": true
},
"node_modules/@mermaid-js/parser": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz",
- "integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.1.tgz",
+ "integrity": "sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3274,16 +3516,6 @@
"node": ">=14"
}
},
- "node_modules/@remix-run/router": {
- "version": "1.23.2",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
- "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/@resvg/resvg-js": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.4.1.tgz",
@@ -3977,49 +4209,49 @@
}
},
"node_modules/@tailwindcss/node": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
- "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
+ "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.19.0",
"jiti": "^2.6.1",
- "lightningcss": "1.31.1",
+ "lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.2.1"
+ "tailwindcss": "4.2.2"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
- "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
+ "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.2.1",
- "@tailwindcss/oxide-darwin-arm64": "4.2.1",
- "@tailwindcss/oxide-darwin-x64": "4.2.1",
- "@tailwindcss/oxide-freebsd-x64": "4.2.1",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
- "@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
- "@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
- "@tailwindcss/oxide-linux-x64-musl": "4.2.1",
- "@tailwindcss/oxide-wasm32-wasi": "4.2.1",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
- "@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
+ "@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-android-arm64": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
- "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
+ "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
"cpu": [
"arm64"
],
@@ -4034,9 +4266,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
- "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
+ "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
"cpu": [
"arm64"
],
@@ -4051,9 +4283,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
- "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
+ "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
"cpu": [
"x64"
],
@@ -4068,9 +4300,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
- "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
+ "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
"cpu": [
"x64"
],
@@ -4085,9 +4317,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
- "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
+ "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
"cpu": [
"arm"
],
@@ -4102,9 +4334,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
- "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
+ "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
"cpu": [
"arm64"
],
@@ -4119,9 +4351,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
- "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
+ "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
"cpu": [
"arm64"
],
@@ -4136,9 +4368,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
- "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
+ "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
"cpu": [
"x64"
],
@@ -4153,9 +4385,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
- "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
+ "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
"cpu": [
"x64"
],
@@ -4170,9 +4402,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
- "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
+ "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -4199,10 +4431,74 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
- "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
+ "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
"cpu": [
"arm64"
],
@@ -4217,9 +4513,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
- "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
+ "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
"cpu": [
"x64"
],
@@ -4234,18 +4530,18 @@
}
},
"node_modules/@tailwindcss/vite": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
- "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
+ "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@tailwindcss/node": "4.2.1",
- "@tailwindcss/oxide": "4.2.1",
- "tailwindcss": "4.2.1"
+ "@tailwindcss/node": "4.2.2",
+ "@tailwindcss/oxide": "4.2.2",
+ "tailwindcss": "4.2.2"
},
"peerDependencies": {
- "vite": "^5.2.0 || ^6 || ^7"
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@turf/boolean-clockwise": {
@@ -4386,6 +4682,7 @@
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/node": "*"
}
@@ -4795,6 +5092,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -4822,6 +5120,7 @@
"integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
@@ -4844,15 +5143,16 @@
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.28",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -4868,6 +5168,16 @@
"@types/react": "^18.0.0"
}
},
+ "node_modules/@types/react-test-renderer": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
+ "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
@@ -4926,16 +5236,27 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/@upsetjs/venn.js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
+ "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "d3-selection": "^3.0.0",
+ "d3-transition": "^3.0.1"
+ }
+ },
"node_modules/@visactor/react-vchart": {
- "version": "2.0.16",
- "resolved": "https://registry.npmjs.org/@visactor/react-vchart/-/react-vchart-2.0.16.tgz",
- "integrity": "sha512-HOsrWBYdP/17DR+GH7Y9gVqIDV4Tb+uLldQeR5HgUjyfabxtz5GtVE/3vv1PlZUEKgCOEXCa+QC9bS/PyS459g==",
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@visactor/react-vchart/-/react-vchart-2.0.19.tgz",
+ "integrity": "sha512-gLEJ8Q6DhI21xX6PDUEmhSUu4Ft8+Foho7TktiMMX6Nll5RYgXagjtjRAI1kSSKm6t/7ZXbBsmP4RdL+KaGtNA==",
"license": "MIT",
"dependencies": {
- "@visactor/vchart": "2.0.16",
- "@visactor/vchart-extension": "2.0.16",
- "@visactor/vrender-core": "~1.0.40",
- "@visactor/vrender-kits": "~1.0.40",
+ "@visactor/vchart": "2.0.19",
+ "@visactor/vchart-extension": "2.0.19",
+ "@visactor/vrender-core": "~1.0.41",
+ "@visactor/vrender-kits": "~1.0.41",
"@visactor/vutils": "~1.0.22",
"react-is": "^18.2.0"
},
@@ -4945,48 +5266,48 @@
}
},
"node_modules/@visactor/vchart": {
- "version": "2.0.16",
- "resolved": "https://registry.npmjs.org/@visactor/vchart/-/vchart-2.0.16.tgz",
- "integrity": "sha512-jRcMyxuq7C1lJfOH3s4Wkaz0lcspyIvaOsFXkxkHQN5h3qsSKxLKdHZCIXgHjsv/MFytZCaRKtb+gNPMZQ93Iw==",
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@visactor/vchart/-/vchart-2.0.19.tgz",
+ "integrity": "sha512-pPNAHUuNlw1FNgbSSwT9comeJUXpaTAe8yed1MVI5rT+TspIg9Wpio29ZlUjrevUxosQqka5VAD63jQS6PMaOg==",
"license": "MIT",
"dependencies": {
"@visactor/vdataset": "~1.0.22",
"@visactor/vlayouts": "~1.0.22",
- "@visactor/vrender-animate": "~1.0.40",
- "@visactor/vrender-components": "~1.0.40",
- "@visactor/vrender-core": "~1.0.40",
- "@visactor/vrender-kits": "~1.0.40",
+ "@visactor/vrender-animate": "~1.0.41",
+ "@visactor/vrender-components": "~1.0.41",
+ "@visactor/vrender-core": "~1.0.41",
+ "@visactor/vrender-kits": "~1.0.41",
"@visactor/vscale": "~1.0.22",
"@visactor/vutils": "~1.0.22",
- "@visactor/vutils-extension": "2.0.16"
+ "@visactor/vutils-extension": "2.0.19"
}
},
"node_modules/@visactor/vchart-extension": {
- "version": "2.0.16",
- "resolved": "https://registry.npmjs.org/@visactor/vchart-extension/-/vchart-extension-2.0.16.tgz",
- "integrity": "sha512-n88Tu6V8FFY4E4zqVeECwlFJjeIlyiF3vrlnYlR3dlnRbt/OCBC0ThGK3eCqkV9LuwD632cRFKqjQeYinYFrTA==",
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@visactor/vchart-extension/-/vchart-extension-2.0.19.tgz",
+ "integrity": "sha512-SCFysGMRGdNK6wYFKDrvZzGdYbVjGhdbqlWA5t2fdbuVcCN7wNBTDKI+pL4yUL3n9M1ZtVxQp5uOw9I0AjFHZQ==",
"license": "MIT",
"dependencies": {
- "@visactor/vchart": "2.0.16",
+ "@visactor/vchart": "2.0.19",
"@visactor/vdataset": "~1.0.22",
"@visactor/vlayouts": "~1.0.22",
- "@visactor/vrender-animate": "~1.0.40",
- "@visactor/vrender-components": "~1.0.40",
- "@visactor/vrender-core": "~1.0.40",
- "@visactor/vrender-kits": "~1.0.40",
+ "@visactor/vrender-animate": "~1.0.41",
+ "@visactor/vrender-components": "~1.0.41",
+ "@visactor/vrender-core": "~1.0.41",
+ "@visactor/vrender-kits": "~1.0.41",
"@visactor/vutils": "~1.0.22"
}
},
"node_modules/@visactor/vdataset": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@visactor/vdataset/-/vdataset-1.0.22.tgz",
- "integrity": "sha512-B+Xd7UuXNNLWRsH0ySd9LsvFFBz/S+lFN7ha7GU7SdTYr9P0ZExYyumQPntaa5iOMLxKysFxJw7t/Or70ZA6hQ==",
+ "version": "1.0.23",
+ "resolved": "https://registry.npmjs.org/@visactor/vdataset/-/vdataset-1.0.23.tgz",
+ "integrity": "sha512-zrLk9FBUWJoW6b30XnPKzXwAXl8USdLDfed6QZLsmdkylRU8V7yZeXE2aKwU8Lg1U4HmQngqmqOx7/QlbX44Tg==",
"license": "MIT",
"dependencies": {
"@turf/flatten": "^6.5.0",
"@turf/helpers": "^6.5.0",
"@turf/rewind": "^6.5.0",
- "@visactor/vutils": "1.0.22",
+ "@visactor/vutils": "1.0.23",
"d3-dsv": "^2.0.0",
"d3-geo": "^1.12.1",
"d3-hexbin": "^0.2.2",
@@ -5003,45 +5324,45 @@
}
},
"node_modules/@visactor/vlayouts": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@visactor/vlayouts/-/vlayouts-1.0.22.tgz",
- "integrity": "sha512-iTPJeROqmwBOBDoewDHYdEAe6MlDPgJ/1V1j+FcWr3zWGPDOSduoIDyjhB/FpkLaUQyhYCTCfCH3p2BDs3Wy4g==",
+ "version": "1.0.23",
+ "resolved": "https://registry.npmjs.org/@visactor/vlayouts/-/vlayouts-1.0.23.tgz",
+ "integrity": "sha512-fK1f5LmuumhYanLArk5yrT4BZxu4IAmdc8WMwfB/KAvV+2dTPFuBUMWbWnDl0siQoU9SX9l/bLozUnI9n7BwBQ==",
"license": "MIT",
"dependencies": {
"@turf/helpers": "^6.5.0",
"@turf/invariant": "^6.5.0",
- "@visactor/vscale": "1.0.22",
- "@visactor/vutils": "1.0.22",
+ "@visactor/vscale": "1.0.23",
+ "@visactor/vutils": "1.0.23",
"eventemitter3": "^4.0.7"
}
},
"node_modules/@visactor/vrender-animate": {
- "version": "1.0.40",
- "resolved": "https://registry.npmjs.org/@visactor/vrender-animate/-/vrender-animate-1.0.40.tgz",
- "integrity": "sha512-/OT+xKwTdRa+bhTF28mQxl+9HsaIncNFGv00G3m2EHQT7Ls8SCDAyD46J36+bU3eYKz/+V+CzL2ApwI/zXP8/A==",
+ "version": "1.0.41",
+ "resolved": "https://registry.npmjs.org/@visactor/vrender-animate/-/vrender-animate-1.0.41.tgz",
+ "integrity": "sha512-kdMoIh7OEo6z4rZfnJHX7d+izBhGVWq6MR22uppk0LPilfdBd/1hSNAEKO6C9JWAy5uROGFpEkh+kk+ar/zSZg==",
"license": "MIT",
"dependencies": {
- "@visactor/vrender-core": "1.0.40",
+ "@visactor/vrender-core": "1.0.41",
"@visactor/vutils": "~1.0.12"
}
},
"node_modules/@visactor/vrender-components": {
- "version": "1.0.40",
- "resolved": "https://registry.npmjs.org/@visactor/vrender-components/-/vrender-components-1.0.40.tgz",
- "integrity": "sha512-+mooyFfpAaAjhBDN5XHKz1SH0vHe3IjabLJQWYwDZGvpPHieVIlZstkTIRMH63pJek0ViIZLHcxpf/i/qli+sw==",
+ "version": "1.0.41",
+ "resolved": "https://registry.npmjs.org/@visactor/vrender-components/-/vrender-components-1.0.41.tgz",
+ "integrity": "sha512-B7iXJE1TdkYapPZN6DNxoaErY4FzGf5AbcbG/z6Q0hnzO4Iw1hKdtlTGOIYA1+JXhehDWcyy+D0bnoNQNf+rgw==",
"license": "MIT",
"dependencies": {
- "@visactor/vrender-animate": "1.0.40",
- "@visactor/vrender-core": "1.0.40",
- "@visactor/vrender-kits": "1.0.40",
+ "@visactor/vrender-animate": "1.0.41",
+ "@visactor/vrender-core": "1.0.41",
+ "@visactor/vrender-kits": "1.0.41",
"@visactor/vscale": "~1.0.12",
"@visactor/vutils": "~1.0.12"
}
},
"node_modules/@visactor/vrender-core": {
- "version": "1.0.40",
- "resolved": "https://registry.npmjs.org/@visactor/vrender-core/-/vrender-core-1.0.40.tgz",
- "integrity": "sha512-VNfxYGvNS2k1v2/+H0y9jIxjsFjgy3ieFGTDYz+o1rES2RY4wQIr6xJtdEkzXg6Foavp1gJj4fSf/wl2idEuTA==",
+ "version": "1.0.41",
+ "resolved": "https://registry.npmjs.org/@visactor/vrender-core/-/vrender-core-1.0.41.tgz",
+ "integrity": "sha512-P7YVUJ45vwqPA460W6JJjg201ThvxBrjEgTBJ4tHKaHZPP/nVyF8rPyUOCL8QesEFDB+R0e/sUJUVs+ckHhd2w==",
"license": "MIT",
"dependencies": {
"@visactor/vutils": "~1.0.12",
@@ -5049,13 +5370,13 @@
}
},
"node_modules/@visactor/vrender-kits": {
- "version": "1.0.40",
- "resolved": "https://registry.npmjs.org/@visactor/vrender-kits/-/vrender-kits-1.0.40.tgz",
- "integrity": "sha512-tMD2C4vQd5kN2WyDIkel4tId1NYGclKM1exIsJY51rgOjfJsjpRzC5WPe6KYqoIcDO5xDarX8TY4dkTR9S0l6w==",
+ "version": "1.0.41",
+ "resolved": "https://registry.npmjs.org/@visactor/vrender-kits/-/vrender-kits-1.0.41.tgz",
+ "integrity": "sha512-ffJlKkNseOsnRjvBmxKxQ9dsQXu+K288NFCtB0ZZ2N/mtcTsZUlH5SDCcsAvDhPGEvJ22Ye4MFKcXSEDctpGzA==",
"license": "MIT",
"dependencies": {
"@resvg/resvg-js": "2.4.1",
- "@visactor/vrender-core": "1.0.40",
+ "@visactor/vrender-core": "1.0.41",
"@visactor/vutils": "~1.0.12",
"gifuct-js": "2.1.2",
"lottie-web": "^5.12.2",
@@ -5063,18 +5384,18 @@
}
},
"node_modules/@visactor/vscale": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@visactor/vscale/-/vscale-1.0.22.tgz",
- "integrity": "sha512-DCCqBA9gBZexCsfYmUjRZM1OBp2Qr8dRIGOqWcFerINIDtpQatSS4c8Ah2r8pqkgiizSNeKgDoXOtsiPe9zqBQ==",
+ "version": "1.0.23",
+ "resolved": "https://registry.npmjs.org/@visactor/vscale/-/vscale-1.0.23.tgz",
+ "integrity": "sha512-XePhYuRoNAp+8MeSMuEOOvhVAlOwvM1sDT2yFxE6zdwVB2GjZk8mH+5N2xQGQWk75YmGJjlJASFtgwjlb1yWxw==",
"license": "MIT",
"dependencies": {
- "@visactor/vutils": "1.0.22"
+ "@visactor/vutils": "1.0.23"
}
},
"node_modules/@visactor/vutils": {
- "version": "1.0.22",
- "resolved": "https://registry.npmjs.org/@visactor/vutils/-/vutils-1.0.22.tgz",
- "integrity": "sha512-n+lKEeSQl1wGqRG829oQ4SwU6UysUiP9rpp0K87TwmERVMupSy6C0KcM2SGXGBLvnp1Cyjjciw5ZtYQXv3ZfKQ==",
+ "version": "1.0.23",
+ "resolved": "https://registry.npmjs.org/@visactor/vutils/-/vutils-1.0.23.tgz",
+ "integrity": "sha512-M8SLqgdHhKN8QmQKTWD1gzEaHptpIV9pvMYvC6+VeOsqYvZZ6UdhSCAAczTYVo+m/uwcEC2JHSUspbrs8rzlRQ==",
"license": "MIT",
"dependencies": {
"@turf/helpers": "^6.5.0",
@@ -5083,9 +5404,9 @@
}
},
"node_modules/@visactor/vutils-extension": {
- "version": "2.0.16",
- "resolved": "https://registry.npmjs.org/@visactor/vutils-extension/-/vutils-extension-2.0.16.tgz",
- "integrity": "sha512-2ioPsCZ95a8QbCNNtjZQ0ZtsDqrrwLEx2WYR9lwrz8AqlXqHKahaOFRpse1MeUf/x99KnKuPTvFV9uaToDexJQ==",
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@visactor/vutils-extension/-/vutils-extension-2.0.19.tgz",
+ "integrity": "sha512-uRYCZFw7bYFdrSPQNIrYYsEGKuVn7lTMd4zDKBA2qnAfAZ/Y4WeXzZhQKdXgTFu5UGa292CqmVqMlVKFNWjC+A==",
"license": "MIT",
"dependencies": {
"@visactor/vdataset": "~1.0.22",
@@ -5579,6 +5900,7 @@
"integrity": "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@algolia/abtesting": "1.15.1",
"@algolia/client-abtesting": "5.49.1",
@@ -5801,6 +6123,19 @@
"node": ">=8"
}
},
+ "node_modules/app-builder-lib/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/app-builder-lib/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -6000,6 +6335,7 @@
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
@@ -6049,14 +6385,28 @@
}
},
"node_modules/better-sqlite3": {
- "version": "11.10.0",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
+ "version": "12.8.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz",
+ "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==",
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
+ },
+ "engines": {
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x"
+ }
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
}
},
"node_modules/bindings": {
@@ -6130,6 +6480,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6533,6 +6884,7 @@
"integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.1.2",
"@chevrotain/gast": "11.1.2",
@@ -6888,8 +7240,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/cross-env": {
"version": "7.0.3",
@@ -6925,11 +7276,25 @@
"node": ">= 8"
}
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/cytoscape": {
@@ -6938,6 +7303,7 @@
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10"
}
@@ -7371,6 +7737,7 @@
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"dev": true,
"license": "ISC",
+ "peer": true,
"engines": {
"node": ">=12"
}
@@ -7550,9 +7917,9 @@
}
},
"node_modules/dagre-d3-es": {
- "version": "7.0.13",
- "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz",
- "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
+ "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7560,6 +7927,20 @@
"lodash-es": "^4.17.21"
}
},
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
@@ -7584,6 +7965,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -7805,6 +8193,7 @@
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"app-builder-lib": "26.8.1",
"builder-util": "26.8.1",
@@ -7931,9 +8320,9 @@
}
},
"node_modules/dotenv": {
- "version": "16.6.1",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
- "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "version": "17.3.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
+ "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -7958,54 +8347,67 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/dotenv-expand/node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/drizzle-kit": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.28.1.tgz",
- "integrity": "sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==",
+ "version": "0.31.10",
+ "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
+ "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@drizzle-team/brocli": "^0.10.2",
"@esbuild-kit/esm-loader": "^2.5.5",
- "esbuild": "^0.19.7",
- "esbuild-register": "^3.5.0"
+ "esbuild": "^0.25.4",
+ "tsx": "^4.21.0"
},
"bin": {
"drizzle-kit": "bin.cjs"
}
},
"node_modules/drizzle-orm": {
- "version": "0.36.4",
- "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.36.4.tgz",
- "integrity": "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==",
+ "version": "0.45.1",
+ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz",
+ "integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
- "@cloudflare/workers-types": ">=3",
+ "@cloudflare/workers-types": ">=4",
"@electric-sql/pglite": ">=0.2.0",
"@libsql/client": ">=0.10.0",
"@libsql/client-wasm": ">=0.10.0",
"@neondatabase/serverless": ">=0.10.0",
"@op-engineering/op-sqlite": ">=2",
"@opentelemetry/api": "^1.4.1",
- "@planetscale/database": ">=1",
+ "@planetscale/database": ">=1.13",
"@prisma/client": "*",
"@tidbcloud/serverless": "*",
"@types/better-sqlite3": "*",
"@types/pg": "*",
- "@types/react": ">=18",
"@types/sql.js": "*",
+ "@upstash/redis": ">=1.34.7",
"@vercel/postgres": ">=0.8.0",
"@xata.io/client": "*",
"better-sqlite3": ">=7",
"bun-types": "*",
"expo-sqlite": ">=14.0.0",
+ "gel": ">=2",
"knex": "*",
"kysely": "*",
"mysql2": ">=2",
"pg": ">=8",
"postgres": ">=3",
- "react": ">=18",
"sql.js": ">=1",
"sqlite3": ">=5"
},
@@ -8049,10 +8451,10 @@
"@types/pg": {
"optional": true
},
- "@types/react": {
+ "@types/sql.js": {
"optional": true
},
- "@types/sql.js": {
+ "@upstash/redis": {
"optional": true
},
"@vercel/postgres": {
@@ -8070,6 +8472,9 @@
"expo-sqlite": {
"optional": true
},
+ "gel": {
+ "optional": true
+ },
"knex": {
"optional": true
},
@@ -8088,9 +8493,6 @@
"prisma": {
"optional": true
},
- "react": {
- "optional": true
- },
"sql.js": {
"optional": true
},
@@ -8138,15 +8540,15 @@
}
},
"node_modules/electron": {
- "version": "35.7.5",
- "resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz",
- "integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==",
+ "version": "41.0.3",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-41.0.3.tgz",
+ "integrity": "sha512-IDjx8liW1q+r7+MOip5W1Eo1eMwJzVObmYrd9yz2dPCkS7XlgLq3qPVMR80TpiROFp73iY30kTzMdpA6fEVs3A==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
- "@types/node": "^22.7.7",
+ "@types/node": "^24.9.0",
"extract-zip": "^2.0.1"
},
"bin": {
@@ -8387,7 +8789,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@electron/asar": "^3.2.1",
"debug": "^4.1.1",
@@ -8408,7 +8809,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
@@ -8418,6 +8818,23 @@
"node": ">=6 <7 || >=8"
}
},
+ "node_modules/electron/node_modules/@types/node": {
+ "version": "24.12.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
+ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/electron/node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -8467,9 +8884,9 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.19.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
- "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
+ "version": "5.20.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
+ "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8575,9 +8992,9 @@
"optional": true
},
"node_modules/esbuild": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
- "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -8585,61 +9002,102 @@
"esbuild": "bin/esbuild"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.19.12",
- "@esbuild/android-arm": "0.19.12",
- "@esbuild/android-arm64": "0.19.12",
- "@esbuild/android-x64": "0.19.12",
- "@esbuild/darwin-arm64": "0.19.12",
- "@esbuild/darwin-x64": "0.19.12",
- "@esbuild/freebsd-arm64": "0.19.12",
- "@esbuild/freebsd-x64": "0.19.12",
- "@esbuild/linux-arm": "0.19.12",
- "@esbuild/linux-arm64": "0.19.12",
- "@esbuild/linux-ia32": "0.19.12",
- "@esbuild/linux-loong64": "0.19.12",
- "@esbuild/linux-mips64el": "0.19.12",
- "@esbuild/linux-ppc64": "0.19.12",
- "@esbuild/linux-riscv64": "0.19.12",
- "@esbuild/linux-s390x": "0.19.12",
- "@esbuild/linux-x64": "0.19.12",
- "@esbuild/netbsd-x64": "0.19.12",
- "@esbuild/openbsd-x64": "0.19.12",
- "@esbuild/sunos-x64": "0.19.12",
- "@esbuild/win32-arm64": "0.19.12",
- "@esbuild/win32-ia32": "0.19.12",
- "@esbuild/win32-x64": "0.19.12"
- }
- },
- "node_modules/esbuild-register": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
- "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "debug": "^4.3.4"
- },
- "peerDependencies": {
- "esbuild": ">=0.12 <1"
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
}
},
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "node_modules/esbuild/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
"engines": {
- "node": ">=6"
+ "node": ">=18"
}
},
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/escape-string-regexp": {
@@ -8799,9 +9257,9 @@
"license": "BSD-3-Clause"
},
"node_modules/fastify": {
- "version": "5.7.4",
- "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.7.4.tgz",
- "integrity": "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==",
+ "version": "5.8.2",
+ "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.2.tgz",
+ "integrity": "sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==",
"funding": [
{
"type": "github",
@@ -8823,7 +9281,7 @@
"fast-json-stringify": "^6.0.0",
"find-my-way": "^9.0.0",
"light-my-request": "^6.0.0",
- "pino": "^10.1.0",
+ "pino": "^9.14.0 || ^10.1.0",
"process-warning": "^5.0.0",
"rfdc": "^1.3.1",
"secure-json-parse": "^4.0.0",
@@ -8971,6 +9429,7 @@
"integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"tabbable": "^6.4.0"
}
@@ -9559,6 +10018,19 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
@@ -9737,7 +10209,6 @@
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -9772,6 +10243,13 @@
"node": ">=8"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
@@ -9917,6 +10395,67 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsdom": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz",
+ "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.0.1",
+ "@asamuzakjp/dom-selector": "^7.0.3",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.1",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.7",
+ "parse5": "^8.0.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.24.5",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/lru-cache": {
+ "version": "11.2.7",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
+ "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/jsdom/node_modules/undici": {
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz",
+ "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -10105,9 +10644,9 @@
"license": "MIT"
},
"node_modules/lightningcss": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
- "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
@@ -10121,23 +10660,23 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
- "lightningcss-android-arm64": "1.31.1",
- "lightningcss-darwin-arm64": "1.31.1",
- "lightningcss-darwin-x64": "1.31.1",
- "lightningcss-freebsd-x64": "1.31.1",
- "lightningcss-linux-arm-gnueabihf": "1.31.1",
- "lightningcss-linux-arm64-gnu": "1.31.1",
- "lightningcss-linux-arm64-musl": "1.31.1",
- "lightningcss-linux-x64-gnu": "1.31.1",
- "lightningcss-linux-x64-musl": "1.31.1",
- "lightningcss-win32-arm64-msvc": "1.31.1",
- "lightningcss-win32-x64-msvc": "1.31.1"
+ "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-android-arm64": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
- "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
@@ -10156,9 +10695,9 @@
}
},
"node_modules/lightningcss-darwin-arm64": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
- "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
@@ -10177,9 +10716,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
- "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
@@ -10198,9 +10737,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
- "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
@@ -10219,9 +10758,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
- "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
@@ -10240,9 +10779,9 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
- "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
@@ -10261,9 +10800,9 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
- "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
@@ -10282,9 +10821,9 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
- "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
@@ -10303,9 +10842,9 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
- "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
@@ -10324,9 +10863,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
- "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
@@ -10345,9 +10884,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
- "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
@@ -10516,10 +11055,9 @@
"license": "MIT"
},
"node_modules/marked": {
- "version": "16.4.2",
- "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
- "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
- "dev": true,
+ "version": "17.0.4",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
+ "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
@@ -10574,35 +11112,56 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/mermaid": {
- "version": "11.12.3",
- "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz",
- "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==",
+ "version": "11.13.0",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.13.0.tgz",
+ "integrity": "sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.1",
- "@iconify/utils": "^3.0.1",
- "@mermaid-js/parser": "^1.0.0",
+ "@iconify/utils": "^3.0.2",
+ "@mermaid-js/parser": "^1.0.1",
"@types/d3": "^7.4.3",
- "cytoscape": "^3.29.3",
+ "@upsetjs/venn.js": "^2.0.0",
+ "cytoscape": "^3.33.1",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
- "dagre-d3-es": "7.0.13",
- "dayjs": "^1.11.18",
- "dompurify": "^3.2.5",
- "katex": "^0.16.22",
+ "dagre-d3-es": "7.0.14",
+ "dayjs": "^1.11.19",
+ "dompurify": "^3.3.1",
+ "katex": "^0.16.25",
"khroma": "^2.1.0",
"lodash-es": "^4.17.23",
- "marked": "^16.2.1",
+ "marked": "^16.3.0",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
"uuid": "^11.1.0"
}
},
+ "node_modules/mermaid/node_modules/marked": {
+ "version": "16.4.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
+ "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
"node_modules/mermaid/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
@@ -10964,7 +11523,6 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -11005,10 +11563,11 @@
"license": "MIT"
},
"node_modules/mysql2": {
- "version": "3.18.2",
- "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.2.tgz",
- "integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==",
+ "version": "3.20.0",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz",
+ "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"aws-ssl-profiles": "^1.1.2",
"denque": "^2.1.0",
@@ -11145,13 +11704,10 @@
}
},
"node_modules/node-cron": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
- "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
+ "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
"license": "ISC",
- "dependencies": {
- "uuid": "8.3.2"
- },
"engines": {
"node": ">=6.0.0"
}
@@ -11228,9 +11784,9 @@
"license": "MIT"
},
"node_modules/nodemailer": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
- "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz",
+ "integrity": "sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
@@ -11423,6 +11979,32 @@
"integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==",
"license": "MIT"
},
+ "node_modules/parse5": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+ "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -11554,6 +12136,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"pg-connection-string": "^2.12.0",
"pg-pool": "^3.13.0",
@@ -11651,6 +12234,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -11831,7 +12415,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"commander": "^9.4.0"
},
@@ -11849,7 +12432,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"engines": {
"node": "^12.20.0 || >=14"
}
@@ -12037,6 +12619,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -12049,6 +12632,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -12074,37 +12658,43 @@
}
},
"node_modules/react-router": {
- "version": "6.30.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
- "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
+ "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.23.2"
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
},
"engines": {
- "node": ">=14.0.0"
+ "node": ">=20.0.0"
},
"peerDependencies": {
- "react": ">=16.8"
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
}
},
"node_modules/react-router-dom": {
- "version": "6.30.3",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
- "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
+ "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@remix-run/router": "1.23.2",
- "react-router": "6.30.3"
+ "react-router": "7.13.1"
},
"engines": {
- "node": ">=14.0.0"
+ "node": ">=20.0.0"
},
"peerDependencies": {
- "react": ">=16.8",
- "react-dom": ">=16.8"
+ "react": ">=18",
+ "react-dom": ">=18"
}
},
"node_modules/react-shallow-renderer": {
@@ -12331,7 +12921,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
- "peer": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -12344,8 +12933,7 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.12",
@@ -12353,7 +12941,6 @@
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -12366,7 +12953,6 @@
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
- "peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -12388,7 +12974,6 @@
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
- "peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -12568,6 +13153,19 @@
"node": ">=11.0.0"
}
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -12837,9 +13435,9 @@
}
},
"node_modules/simple-statistics": {
- "version": "7.8.8",
- "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.8.tgz",
- "integrity": "sha512-CUtP0+uZbcbsFpqEyvNDYjJCl+612fNgjT8GaVuvMG7tBuJg8gXGpsP5M7X658zy0IcepWOZ6nPBu1Qb9ezA1w==",
+ "version": "7.8.9",
+ "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.9.tgz",
+ "integrity": "sha512-YT6MLqYsz7y1rQZOLFlOCCgSRpCi6bqY417yhoOLI7aVoBi29dD39EPrOE03W9DY25H0J0jizVsHZnkLzyGJFg==",
"license": "ISC",
"engines": {
"node": "*"
@@ -12949,7 +13547,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
@@ -12960,7 +13557,6 @@
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
@@ -13271,6 +13867,13 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tabbable": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
@@ -13279,9 +13882,9 @@
"license": "MIT"
},
"node_modules/tailwindcss": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
- "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
+ "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
"dev": true,
"license": "MIT"
},
@@ -13370,7 +13973,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"
@@ -13534,6 +14136,26 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tldts": {
+ "version": "7.0.26",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz",
+ "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.26"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.26",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz",
+ "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
@@ -13598,6 +14220,32 @@
"geo2topo": "bin/geo2topo"
}
},
+ "node_modules/tough-cookie": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -14136,6 +14784,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14152,9 +14801,9 @@
"license": "MIT"
},
"node_modules/undici": {
- "version": "6.23.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
- "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
+ "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -14329,15 +14978,6 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
- "node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/verror": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
@@ -14398,6 +15038,7 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -14980,530 +15621,46 @@
}
}
},
- "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
- "cpu": [
- "ppc64"
- ],
+ "node_modules/vitepress": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz",
+ "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
- }
- },
- "node_modules/vitepress": {
- "version": "1.6.4",
- "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz",
- "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@docsearch/css": "3.8.2",
- "@docsearch/js": "3.8.2",
- "@iconify-json/simple-icons": "^1.2.21",
- "@shikijs/core": "^2.1.0",
- "@shikijs/transformers": "^2.1.0",
- "@shikijs/types": "^2.1.0",
- "@types/markdown-it": "^14.1.2",
- "@vitejs/plugin-vue": "^5.2.1",
- "@vue/devtools-api": "^7.7.0",
- "@vue/shared": "^3.5.13",
- "@vueuse/core": "^12.4.0",
- "@vueuse/integrations": "^12.4.0",
- "focus-trap": "^7.6.4",
- "mark.js": "8.11.1",
- "minisearch": "^7.1.1",
- "shiki": "^2.1.0",
- "vite": "^5.4.14",
- "vue": "^3.5.13"
- },
- "bin": {
- "vitepress": "bin/vitepress.js"
- },
- "peerDependencies": {
- "markdown-it-mathjax3": "^4",
- "postcss": "^8"
- },
- "peerDependenciesMeta": {
- "markdown-it-mathjax3": {
- "optional": true
- },
- "postcss": {
- "optional": true
- }
+ "dependencies": {
+ "@docsearch/css": "3.8.2",
+ "@docsearch/js": "3.8.2",
+ "@iconify-json/simple-icons": "^1.2.21",
+ "@shikijs/core": "^2.1.0",
+ "@shikijs/transformers": "^2.1.0",
+ "@shikijs/types": "^2.1.0",
+ "@types/markdown-it": "^14.1.2",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "@vue/devtools-api": "^7.7.0",
+ "@vue/shared": "^3.5.13",
+ "@vueuse/core": "^12.4.0",
+ "@vueuse/integrations": "^12.4.0",
+ "focus-trap": "^7.6.4",
+ "mark.js": "8.11.1",
+ "minisearch": "^7.1.1",
+ "shiki": "^2.1.0",
+ "vite": "^5.4.14",
+ "vue": "^3.5.13"
+ },
+ "bin": {
+ "vitepress": "bin/vitepress.js"
+ },
+ "peerDependencies": {
+ "markdown-it-mathjax3": "^4",
+ "postcss": "^8"
+ },
+ "peerDependenciesMeta": {
+ "markdown-it-mathjax3": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ }
}
},
"node_modules/vitepress-plugin-mermaid": {
@@ -16539,6 +16696,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -16654,6 +16812,7 @@
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.29",
"@vue/compiler-sfc": "3.5.29",
@@ -16670,16 +16829,29 @@
}
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/wait-on": {
- "version": "8.0.5",
- "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz",
- "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==",
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz",
+ "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "axios": "^1.12.1",
- "joi": "^18.0.1",
- "lodash": "^4.17.21",
+ "axios": "^1.13.5",
+ "joi": "^18.0.2",
+ "lodash": "^4.17.23",
"minimist": "^1.2.8",
"rxjs": "^7.8.2"
},
@@ -16687,7 +16859,7 @@
"wait-on": "bin/wait-on"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">=20.0.0"
}
},
"node_modules/wcwidth": {
@@ -16700,6 +16872,41 @@
"defaults": "^1.0.3"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -16843,6 +17050,37 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@@ -16853,6 +17091,13 @@
"node": ">=8.0"
}
},
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/package.json b/package.json
index fd62d4a1..f9d944a6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "metapi",
- "version": "1.2.1",
+ "version": "1.2.3",
"description": "Meta-layer management and unified proxy for AI API aggregation platforms",
"main": "dist/desktop/main.js",
"keywords": [
@@ -21,17 +21,28 @@
"url": "git+https://github.com/cita-777/metapi.git"
},
"license": "MIT",
+ "engines": {
+ "node": ">=22.15.0"
+ },
"type": "module",
"scripts": {
"dev:server": "tsx scripts/dev/run-server.ts --watch",
"dev": "concurrently \"npm run dev:server\" \"vite\"",
"dev:desktop": "concurrently \"npm run dev:server\" \"vite\" \"tsc -p tsconfig.desktop.json --watch --preserveWatchOutput\" \"wait-on tcp:127.0.0.1:4000 http://127.0.0.1:5173 file:dist/desktop/main.js && cross-env METAPI_DESKTOP_DEV_SERVER_URL=http://127.0.0.1:5173 METAPI_DESKTOP_EXTERNAL_SERVER_URL=http://127.0.0.1:4000 electron dist/desktop/main.js\"",
- "build:web": "vite build",
- "build:server": "tsc -p tsconfig.server.json",
+ "desktop:icons": "node scripts/desktop/generate-icons.mjs",
+ "build:web": "npm run desktop:icons && vite build",
+ "build:server": "tsc -p tsconfig.server.json && tsx scripts/dev/copy-runtime-db-generated.ts",
"build:desktop": "tsc -p tsconfig.desktop.json",
"build": "npm run build:web && npm run build:server && npm run build:desktop",
+ "typecheck:web": "tsc -p tsconfig.web.json",
+ "typecheck:web:test": "tsc -p tsconfig.web.test.json",
+ "typecheck:server": "tsc --noEmit -p tsconfig.server.json",
+ "typecheck:desktop": "tsc --noEmit -p tsconfig.desktop.json",
+ "typecheck": "npm run typecheck:web && npm run typecheck:web:test && npm run typecheck:server && npm run typecheck:desktop",
"dist:desktop": "npm run build && electron-builder --config electron-builder.yml --publish never",
- "package:desktop": "electron-builder --config electron-builder.yml --publish never",
+ "dist:desktop:mac:intel": "npm run build && electron-builder --config electron-builder.yml --publish never --mac --x64",
+ "package:desktop": "npm run desktop:icons && electron-builder --config electron-builder.yml --publish never",
+ "package:desktop:mac:intel": "npm run desktop:icons && electron-builder --config electron-builder.yml --publish never --mac --x64",
"docs:dev": "vitepress dev docs --host 0.0.0.0 --port 4173",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173",
@@ -39,6 +50,13 @@
"start:desktop": "electron dist/desktop/main.js",
"db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/server/db/migrate.ts",
+ "schema:contract": "tsx scripts/dev/generate-schema-contract.ts",
+ "schema:generate": "npm run db:generate && npm run schema:contract",
+ "repo:drift-check": "tsx scripts/dev/repo-drift-check.ts",
+ "test:schema:unit": "vitest run --root . src/server/db/schemaContract.test.ts src/server/db/schemaArtifactGenerator.test.ts src/server/db/schemaIntrospection.test.ts src/server/db/schemaParity.test.ts",
+ "test:schema:parity": "vitest run --root . src/server/db/schemaParity.live.test.ts",
+ "test:schema:upgrade": "vitest run --root . src/server/db/schemaUpgrade.live.test.ts",
+ "test:schema:runtime": "vitest run --root . src/server/db/runtimeSchemaBootstrap.live.test.ts",
"smoke:db": "tsx scripts/dev/db-smoke.ts",
"smoke:db:sqlite": "tsx scripts/dev/db-smoke.ts --db-type sqlite",
"smoke:db:mysql": "tsx scripts/dev/db-smoke.ts --db-type mysql",
@@ -52,24 +70,28 @@
"@dnd-kit/utilities": "^3.2.2",
"@fastify/cors": "^11.2.0",
"@fastify/static": "^9.0.0",
- "@visactor/react-vchart": "^2.0.16",
- "better-sqlite3": "^11.3.0",
- "dotenv": "^16.4.5",
- "drizzle-orm": "^0.36.4",
+ "@visactor/react-vchart": "^2.0.19",
+ "better-sqlite3": "^12.8.0",
+ "dotenv": "^17.3.1",
+ "drizzle-orm": "^0.45.1",
"electron-log": "^5.2.4",
"electron-updater": "^6.6.2",
- "fastify": "^5.7.4",
+ "fastify": "^5.8.2",
"get-port": "^7.1.0",
+ "marked": "^17.0.4",
"minimatch": "^10.2.4",
"minimist": "^1.2.8",
- "mysql2": "^3.15.3",
- "node-cron": "^3.0.3",
- "nodemailer": "^8.0.1",
+ "mysql2": "^3.20.0",
+ "node-cron": "^4.2.1",
+ "nodemailer": "^8.0.3",
"pg": "^8.16.3",
- "undici": "^6.20.1"
+ "socks": "^2.8.7",
+ "undici": "^6.24.1",
+ "ws": "^8.19.0"
},
"devDependencies": {
- "@tailwindcss/vite": "^4.0.0",
+ "@electron/notarize": "^3.1.0",
+ "@tailwindcss/vite": "^4.2.2",
"@types/better-sqlite3": "^7.6.11",
"@types/node": "^22.10.1",
"@types/node-cron": "^3.0.11",
@@ -77,17 +99,18 @@
"@types/pg": "^8.15.6",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
+ "@types/react-test-renderer": "^19.1.0",
"@vitejs/plugin-react": "^4.3.4",
- "@electron/notarize": "^3.1.0",
"concurrently": "^9.1.0",
"cross-env": "^7.0.3",
- "drizzle-kit": "^0.28.1",
- "electron": "^35.0.1",
+ "drizzle-kit": "^0.31.10",
+ "electron": "^41.0.3",
"electron-builder": "^26.0.12",
- "mermaid": "^11.12.3",
+ "jsdom": "^29.0.1",
+ "mermaid": "^11.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-router-dom": "^6.28.0",
+ "react-router-dom": "^7.13.1",
"react-test-renderer": "^18.3.1",
"sharp": "^0.34.5",
"tailwindcss": "^4.0.0",
@@ -97,7 +120,7 @@
"vitepress": "^1.6.4",
"vitepress-plugin-mermaid": "^2.0.17",
"vitest": "^2.1.8",
- "wait-on": "^8.0.3"
+ "wait-on": "^9.0.4"
},
"overrides": {
"minimist": "^1.2.8"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 00000000..1738722c
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,9706 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@dnd-kit/core':
+ specifier: ^6.3.1
+ version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@dnd-kit/sortable':
+ specifier: ^10.0.0
+ version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
+ '@dnd-kit/utilities':
+ specifier: ^3.2.2
+ version: 3.2.2(react@18.3.1)
+ '@fastify/cors':
+ specifier: ^11.2.0
+ version: 11.2.0
+ '@fastify/static':
+ specifier: ^9.0.0
+ version: 9.0.0
+ '@visactor/react-vchart':
+ specifier: ^2.0.19
+ version: 2.0.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ better-sqlite3:
+ specifier: ^12.8.0
+ version: 12.8.0
+ dotenv:
+ specifier: ^17.3.1
+ version: 17.3.1
+ drizzle-orm:
+ specifier: ^0.45.1
+ version: 0.45.1(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@12.8.0)(mysql2@3.20.0(@types/node@22.19.15))(pg@8.20.0)
+ electron-log:
+ specifier: ^5.2.4
+ version: 5.4.3
+ electron-updater:
+ specifier: ^6.6.2
+ version: 6.8.3
+ fastify:
+ specifier: ^5.8.2
+ version: 5.8.2
+ get-port:
+ specifier: ^7.1.0
+ version: 7.1.0
+ marked:
+ specifier: ^17.0.4
+ version: 17.0.4
+ minimatch:
+ specifier: ^10.2.4
+ version: 10.2.4
+ minimist:
+ specifier: ^1.2.8
+ version: 1.2.8
+ mysql2:
+ specifier: ^3.20.0
+ version: 3.20.0(@types/node@22.19.15)
+ node-cron:
+ specifier: ^4.2.1
+ version: 4.2.1
+ nodemailer:
+ specifier: ^8.0.3
+ version: 8.0.3
+ pg:
+ specifier: ^8.16.3
+ version: 8.20.0
+ socks:
+ specifier: ^2.8.7
+ version: 2.8.7
+ undici:
+ specifier: ^6.24.1
+ version: 6.24.1
+ ws:
+ specifier: ^8.19.0
+ version: 8.19.0
+ devDependencies:
+ '@electron/notarize':
+ specifier: ^3.1.0
+ version: 3.1.1
+ '@tailwindcss/vite':
+ specifier: ^4.2.2
+ version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))
+ '@types/better-sqlite3':
+ specifier: ^7.6.11
+ version: 7.6.13
+ '@types/node':
+ specifier: ^22.10.1
+ version: 22.19.15
+ '@types/node-cron':
+ specifier: ^3.0.11
+ version: 3.0.11
+ '@types/nodemailer':
+ specifier: ^7.0.11
+ version: 7.0.11
+ '@types/pg':
+ specifier: ^8.15.6
+ version: 8.18.0
+ '@types/react':
+ specifier: ^18.3.12
+ version: 18.3.28
+ '@types/react-dom':
+ specifier: ^18.3.1
+ version: 18.3.7(@types/react@18.3.28)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))
+ concurrently:
+ specifier: ^9.1.0
+ version: 9.2.1
+ cross-env:
+ specifier: ^7.0.3
+ version: 7.0.3
+ drizzle-kit:
+ specifier: ^0.31.10
+ version: 0.31.10
+ electron:
+ specifier: ^41.0.3
+ version: 41.0.3
+ electron-builder:
+ specifier: ^26.0.12
+ version: 26.8.1(electron-builder-squirrel-windows@26.8.1)
+ jsdom:
+ specifier: ^29.0.1
+ version: 29.0.1
+ mermaid:
+ specifier: ^11.13.0
+ version: 11.13.0
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ react-router-dom:
+ specifier: ^7.13.1
+ version: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react-test-renderer:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ sharp:
+ specifier: ^0.34.5
+ version: 0.34.5
+ tailwindcss:
+ specifier: ^4.0.0
+ version: 4.2.1
+ tsx:
+ specifier: ^4.19.2
+ version: 4.21.0
+ typescript:
+ specifier: ^5.7.2
+ version: 5.9.3
+ vite:
+ specifier: ^6.0.3
+ version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)
+ vitepress:
+ specifier: ^1.6.4
+ version: 1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)
+ vitepress-plugin-mermaid:
+ specifier: ^2.0.17
+ version: 2.0.17(mermaid@11.13.0)(vitepress@1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3))
+ vitest:
+ specifier: ^2.1.8
+ version: 2.1.9(@types/node@22.19.15)(jsdom@29.0.1)(lightningcss@1.32.0)
+ wait-on:
+ specifier: ^9.0.4
+ version: 9.0.4
+
+packages:
+
+ 7zip-bin@5.2.0:
+ resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==}
+
+ '@algolia/abtesting@1.15.1':
+ resolution: {integrity: sha512-2yuIC48rUuHGhU1U5qJ9kJHaxYpJ0jpDHJVI5ekOxSMYXlH4+HP+pA31G820lsAznfmu2nzDV7n5RO44zIY1zw==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/autocomplete-core@1.17.7':
+ resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==}
+
+ '@algolia/autocomplete-plugin-algolia-insights@1.17.7':
+ resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==}
+ peerDependencies:
+ search-insights: '>= 1 < 3'
+
+ '@algolia/autocomplete-preset-algolia@1.17.7':
+ resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==}
+ peerDependencies:
+ '@algolia/client-search': '>= 4.9.1 < 6'
+ algoliasearch: '>= 4.9.1 < 6'
+
+ '@algolia/autocomplete-shared@1.17.7':
+ resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==}
+ peerDependencies:
+ '@algolia/client-search': '>= 4.9.1 < 6'
+ algoliasearch: '>= 4.9.1 < 6'
+
+ '@algolia/client-abtesting@5.49.1':
+ resolution: {integrity: sha512-h6M7HzPin+45/l09q0r2dYmocSSt2MMGOOk5c4O5K/bBBlEwf1BKfN6z+iX4b8WXcQQhf7rgQwC52kBZJt/ZZw==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-analytics@5.49.1':
+ resolution: {integrity: sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-common@5.49.1':
+ resolution: {integrity: sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-insights@5.49.1':
+ resolution: {integrity: sha512-B6N7PgkvYrul3bntTz/l6uXnhQ2bvP+M7NqTcayh681tSqPaA5cJCUBp/vrP7vpPRpej4Eeyx2qz5p0tE/2N2g==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-personalization@5.49.1':
+ resolution: {integrity: sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-query-suggestions@5.49.1':
+ resolution: {integrity: sha512-Un11cab6ZCv0W+Jiak8UktGIqoa4+gSNgEZNfG8m8eTsXGqwIEr370H3Rqwj87zeNSlFpH2BslMXJ/cLNS1qtg==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/client-search@5.49.1':
+ resolution: {integrity: sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/ingestion@1.49.1':
+ resolution: {integrity: sha512-b5hUXwDqje0Y4CpU6VL481DXgPgxpTD5sYMnfQTHKgUispGnaCLCm2/T9WbJo1YNUbX3iHtYDArp804eD6CmRQ==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/monitoring@1.49.1':
+ resolution: {integrity: sha512-bvrXwZ0WsL3rN6Q4m4QqxsXFCo6WAew7sAdrpMQMK4Efn4/W920r9ptOuckejOSSvyLr9pAWgC5rsHhR2FYuYw==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/recommend@5.49.1':
+ resolution: {integrity: sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/requester-browser-xhr@5.49.1':
+ resolution: {integrity: sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/requester-fetch@5.49.1':
+ resolution: {integrity: sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw==}
+ engines: {node: '>= 14.0.0'}
+
+ '@algolia/requester-node-http@5.49.1':
+ resolution: {integrity: sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA==}
+ engines: {node: '>= 14.0.0'}
+
+ '@antfu/install-pkg@1.1.0':
+ resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
+
+ '@asamuzakjp/css-color@5.0.1':
+ resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/dom-selector@7.0.3':
+ resolution: {integrity: sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/nwsapi@2.3.9':
+ resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
+
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.0':
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.6':
+ resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.0':
+ resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
+ '@braintree/sanitize-url@6.0.4':
+ resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
+
+ '@braintree/sanitize-url@7.1.2':
+ resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
+
+ '@bramus/specificity@2.4.2':
+ resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
+ hasBin: true
+
+ '@chevrotain/cst-dts-gen@11.1.2':
+ resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==}
+
+ '@chevrotain/gast@11.1.2':
+ resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==}
+
+ '@chevrotain/regexp-to-ast@11.1.2':
+ resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==}
+
+ '@chevrotain/types@11.1.2':
+ resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
+
+ '@chevrotain/utils@11.1.2':
+ resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==}
+
+ '@csstools/color-helpers@6.0.2':
+ resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
+ engines: {node: '>=20.19.0'}
+
+ '@csstools/css-calc@3.1.1':
+ resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-color-parser@4.0.2':
+ resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0':
+ resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.1.1':
+ resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==}
+ peerDependencies:
+ css-tree: ^3.2.1
+ peerDependenciesMeta:
+ css-tree:
+ optional: true
+
+ '@csstools/css-tokenizer@4.0.0':
+ resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
+ engines: {node: '>=20.19.0'}
+
+ '@develar/schema-utils@2.6.5':
+ resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
+ engines: {node: '>= 8.9.0'}
+
+ '@dnd-kit/accessibility@3.1.1':
+ resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@dnd-kit/core@6.3.1':
+ resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@dnd-kit/sortable@10.0.0':
+ resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
+ peerDependencies:
+ '@dnd-kit/core': ^6.3.0
+ react: '>=16.8.0'
+
+ '@dnd-kit/utilities@3.2.2':
+ resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@docsearch/css@3.8.2':
+ resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==}
+
+ '@docsearch/js@3.8.2':
+ resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==}
+
+ '@docsearch/react@3.8.2':
+ resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==}
+ peerDependencies:
+ '@types/react': '>= 16.8.0 < 19.0.0'
+ react: '>= 16.8.0 < 19.0.0'
+ react-dom: '>= 16.8.0 < 19.0.0'
+ search-insights: '>= 1 < 3'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ search-insights:
+ optional: true
+
+ '@drizzle-team/brocli@0.10.2':
+ resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
+
+ '@electron/asar@3.4.1':
+ resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==}
+ engines: {node: '>=10.12.0'}
+ hasBin: true
+
+ '@electron/fuses@1.8.0':
+ resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==}
+ hasBin: true
+
+ '@electron/get@2.0.3':
+ resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==}
+ engines: {node: '>=12'}
+
+ '@electron/get@3.1.0':
+ resolution: {integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==}
+ engines: {node: '>=14'}
+
+ '@electron/notarize@2.5.0':
+ resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==}
+ engines: {node: '>= 10.0.0'}
+
+ '@electron/notarize@3.1.1':
+ resolution: {integrity: sha512-uQQSlOiJnqRkTL1wlEBAxe90nVN/Fc/hEmk0bqpKk8nKjV1if/tXLHKUPePtv9Xsx90PtZU8aidx5lAiOpjkQQ==}
+ engines: {node: '>= 22.12.0'}
+
+ '@electron/osx-sign@1.3.3':
+ resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==}
+ engines: {node: '>=12.0.0'}
+ hasBin: true
+
+ '@electron/rebuild@4.0.3':
+ resolution: {integrity: sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==}
+ engines: {node: '>=22.12.0'}
+ hasBin: true
+
+ '@electron/universal@2.0.3':
+ resolution: {integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==}
+ engines: {node: '>=16.4'}
+
+ '@electron/windows-sign@1.2.2':
+ resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==}
+ engines: {node: '>=14.14'}
+ hasBin: true
+
+ '@emnapi/runtime@1.8.1':
+ resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
+
+ '@esbuild-kit/core-utils@3.3.2':
+ resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
+ deprecated: 'Merged into tsx: https://tsx.is'
+
+ '@esbuild-kit/esm-loader@2.6.5':
+ resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
+ deprecated: 'Merged into tsx: https://tsx.is'
+
+ '@esbuild/aix-ppc64@0.21.5':
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/aix-ppc64@0.27.3':
+ resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.18.20':
+ resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm64@0.21.5':
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm64@0.27.3':
+ resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.18.20':
+ resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-arm@0.21.5':
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-arm@0.27.3':
+ resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.18.20':
+ resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/android-x64@0.21.5':
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/android-x64@0.27.3':
+ resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.18.20':
+ resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-arm64@0.21.5':
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-arm64@0.27.3':
+ resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.18.20':
+ resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.21.5':
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.27.3':
+ resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.18.20':
+ resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-arm64@0.21.5':
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-arm64@0.27.3':
+ resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.18.20':
+ resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.21.5':
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.27.3':
+ resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.18.20':
+ resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm64@0.21.5':
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm64@0.27.3':
+ resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.18.20':
+ resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.21.5':
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.27.3':
+ resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.18.20':
+ resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.21.5':
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.27.3':
+ resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.18.20':
+ resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.21.5':
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.27.3':
+ resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.18.20':
+ resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.21.5':
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.27.3':
+ resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.18.20':
+ resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.21.5':
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.27.3':
+ resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.18.20':
+ resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.21.5':
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.27.3':
+ resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.18.20':
+ resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.21.5':
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.27.3':
+ resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.18.20':
+ resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.21.5':
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.27.3':
+ resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-arm64@0.27.3':
+ resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.18.20':
+ resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.21.5':
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.27.3':
+ resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-arm64@0.27.3':
+ resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.18.20':
+ resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.21.5':
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.27.3':
+ resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/openharmony-arm64@0.27.3':
+ resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.18.20':
+ resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/sunos-x64@0.21.5':
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/sunos-x64@0.27.3':
+ resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.18.20':
+ resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-arm64@0.21.5':
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-arm64@0.27.3':
+ resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.18.20':
+ resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.21.5':
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.27.3':
+ resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.18.20':
+ resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.21.5':
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.27.3':
+ resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@exodus/bytes@1.15.0':
+ resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ '@noble/hashes': ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ '@noble/hashes':
+ optional: true
+
+ '@fastify/accept-negotiator@2.0.1':
+ resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==}
+
+ '@fastify/ajv-compiler@4.0.5':
+ resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==}
+
+ '@fastify/cors@11.2.0':
+ resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==}
+
+ '@fastify/error@4.2.0':
+ resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
+
+ '@fastify/fast-json-stringify-compiler@5.0.3':
+ resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==}
+
+ '@fastify/forwarded@3.0.1':
+ resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==}
+
+ '@fastify/merge-json-schemas@0.2.1':
+ resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
+
+ '@fastify/proxy-addr@5.1.0':
+ resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
+
+ '@fastify/send@4.1.0':
+ resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==}
+
+ '@fastify/static@9.0.0':
+ resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==}
+
+ '@hapi/address@5.1.1':
+ resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==}
+ engines: {node: '>=14.0.0'}
+
+ '@hapi/formula@3.0.2':
+ resolution: {integrity: sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==}
+
+ '@hapi/hoek@11.0.7':
+ resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==}
+
+ '@hapi/pinpoint@2.0.1':
+ resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==}
+
+ '@hapi/tlds@1.1.6':
+ resolution: {integrity: sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==}
+ engines: {node: '>=14.0.0'}
+
+ '@hapi/topo@6.0.2':
+ resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==}
+
+ '@iconify-json/simple-icons@1.2.72':
+ resolution: {integrity: sha512-wkcixntHvaCoqPqerGrNFcHQ3Yx1ux4ZkhscCDK0DEHpP62XCH+cxq1HTsRjbUiQl/M9K8bj03HF6Wgn5iE2rQ==}
+
+ '@iconify/types@2.0.0':
+ resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
+
+ '@iconify/utils@3.1.0':
+ resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
+
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.34.5':
+ resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-linux-arm64@0.34.5':
+ resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-linux-arm@0.34.5':
+ resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-linux-ppc64@0.34.5':
+ resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-linux-s390x@0.34.5':
+ resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@img/sharp-linux-x64@0.34.5':
+ resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-wasm32@0.34.5':
+ resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-arm64@0.34.5':
+ resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.34.5':
+ resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.34.5':
+ resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+
+ '@isaacs/fs-minipass@4.0.1':
+ resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
+ engines: {node: '>=18.0.0'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@lukeed/ms@2.0.2':
+ resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
+ engines: {node: '>=8'}
+
+ '@malept/cross-spawn-promise@2.0.0':
+ resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==}
+ engines: {node: '>= 12.13.0'}
+
+ '@malept/flatpak-bundler@0.4.0':
+ resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
+ engines: {node: '>= 10.0.0'}
+
+ '@mermaid-js/mermaid-mindmap@9.3.0':
+ resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==}
+
+ '@mermaid-js/parser@1.0.1':
+ resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==}
+
+ '@npmcli/agent@3.0.0':
+ resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ '@npmcli/fs@4.0.0':
+ resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ '@pinojs/redact@0.4.0':
+ resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
+
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
+ '@resvg/resvg-js-android-arm-eabi@2.4.1':
+ resolution: {integrity: sha512-AA6f7hS0FAPpvQMhBCf6f1oD1LdlqNXKCxAAPpKh6tR11kqV0YIB9zOlIYgITM14mq2YooLFl6XIbbvmY+jwUw==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [android]
+
+ '@resvg/resvg-js-android-arm64@2.4.1':
+ resolution: {integrity: sha512-/QleoRdPfsEuH9jUjilYcDtKK/BkmWcK+1LXM8L2nsnf/CI8EnFyv7ZzCj4xAIvZGAy9dTYr/5NZBcTwxG2HQg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@resvg/resvg-js-darwin-arm64@2.4.1':
+ resolution: {integrity: sha512-U1oMNhea+kAXgiEXgzo7EbFGCD1Edq5aSlQoe6LMly6UjHzgx2W3N5kEXCwU/CgN5FiQhZr7PlSJSlcr7mdhfg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@resvg/resvg-js-darwin-x64@2.4.1':
+ resolution: {integrity: sha512-avyVh6DpebBfHHtTQTZYSr6NG1Ur6TEilk1+H0n7V+g4F7x7WPOo8zL00ZhQCeRQ5H4f8WXNWIEKL8fwqcOkYw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@resvg/resvg-js-linux-arm-gnueabihf@2.4.1':
+ resolution: {integrity: sha512-isY/mdKoBWH4VB5v621co+8l101jxxYjuTkwOLsbW+5RK9EbLciPlCB02M99ThAHzI2MYxIUjXNmNgOW8btXvw==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@resvg/resvg-js-linux-arm64-gnu@2.4.1':
+ resolution: {integrity: sha512-uY5voSCrFI8TH95vIYBm5blpkOtltLxLRODyhKJhGfskOI7XkRw5/t1u0sWAGYD8rRSNX+CA+np86otKjubrNg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@resvg/resvg-js-linux-arm64-musl@2.4.1':
+ resolution: {integrity: sha512-6mT0+JBCsermKMdi/O2mMk3m7SqOjwi9TKAwSngRZ/nQoL3Z0Z5zV+572ztgbWr0GODB422uD8e9R9zzz38dRQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@resvg/resvg-js-linux-x64-gnu@2.4.1':
+ resolution: {integrity: sha512-60KnrscLj6VGhkYOJEmmzPlqqfcw1keDh6U+vMcNDjPhV3B5vRSkpP/D/a8sfokyeh4VEacPSYkWGezvzS2/mg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@resvg/resvg-js-linux-x64-musl@2.4.1':
+ resolution: {integrity: sha512-0AMyZSICC1D7ge115cOZQW8Pcad6PjWuZkBFF3FJuSxC6Dgok0MQnLTs2MfMdKBlAcwO9dXsf3bv9tJZj8pATA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@resvg/resvg-js-win32-arm64-msvc@2.4.1':
+ resolution: {integrity: sha512-76XDFOFSa3d0QotmcNyChh2xHwk+JTFiEQBVxMlHpHMeq7hNrQJ1IpE1zcHSQvrckvkdfLboKRrlGB86B10Qjw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@resvg/resvg-js-win32-ia32-msvc@2.4.1':
+ resolution: {integrity: sha512-odyVFGrEWZIzzJ89KdaFtiYWaIJh9hJRW/frcEcG3agJ464VXkN/2oEVF5ulD+5mpGlug9qJg7htzHcKxDN8sg==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@resvg/resvg-js-win32-x64-msvc@2.4.1':
+ resolution: {integrity: sha512-vY4kTLH2S3bP+puU5x7hlAxHv+ulFgcK6Zn3efKSr0M0KnZ9A3qeAjZteIpkowEFfUeMPNg2dvvoFRJA9zqxSw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@resvg/resvg-js@2.4.1':
+ resolution: {integrity: sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A==}
+ engines: {node: '>= 10'}
+
+ '@rolldown/pluginutils@1.0.0-beta.27':
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
+
+ '@rollup/rollup-android-arm-eabi@4.59.0':
+ resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.59.0':
+ resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.59.0':
+ resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.59.0':
+ resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.59.0':
+ resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.59.0':
+ resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.59.0':
+ resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.59.0':
+ resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.59.0':
+ resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.59.0':
+ resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.59.0':
+ resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-musl@4.59.0':
+ resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.59.0':
+ resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-musl@4.59.0':
+ resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.59.0':
+ resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.59.0':
+ resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.59.0':
+ resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.59.0':
+ resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.59.0':
+ resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openbsd-x64@4.59.0':
+ resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.59.0':
+ resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.59.0':
+ resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.59.0':
+ resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.59.0':
+ resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.59.0':
+ resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@shikijs/core@2.5.0':
+ resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==}
+
+ '@shikijs/engine-javascript@2.5.0':
+ resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==}
+
+ '@shikijs/engine-oniguruma@2.5.0':
+ resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==}
+
+ '@shikijs/langs@2.5.0':
+ resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==}
+
+ '@shikijs/themes@2.5.0':
+ resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==}
+
+ '@shikijs/transformers@2.5.0':
+ resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==}
+
+ '@shikijs/types@2.5.0':
+ resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
+ '@sindresorhus/is@4.6.0':
+ resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
+ engines: {node: '>=10'}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@szmarczak/http-timer@4.0.6':
+ resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
+ engines: {node: '>=10'}
+
+ '@tailwindcss/node@4.2.2':
+ resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
+
+ '@tailwindcss/oxide-android-arm64@4.2.2':
+ resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.2':
+ resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.2.2':
+ resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.2':
+ resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
+ resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
+ resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
+ resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
+ resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.2':
+ resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.2':
+ resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
+ resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.2':
+ resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.2.2':
+ resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.2.2':
+ resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@turf/boolean-clockwise@6.5.0':
+ resolution: {integrity: sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw==}
+
+ '@turf/clone@6.5.0':
+ resolution: {integrity: sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw==}
+
+ '@turf/flatten@6.5.0':
+ resolution: {integrity: sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ==}
+
+ '@turf/helpers@6.5.0':
+ resolution: {integrity: sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==}
+
+ '@turf/invariant@6.5.0':
+ resolution: {integrity: sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==}
+
+ '@turf/meta@3.14.0':
+ resolution: {integrity: sha512-OtXqLQuR9hlQ/HkAF/OdzRea7E0eZK1ay8y8CBXkoO2R6v34CsDrWYLMSo0ZzMsaQDpKo76NPP2GGo+PyG1cSg==}
+
+ '@turf/meta@6.5.0':
+ resolution: {integrity: sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==}
+
+ '@turf/rewind@6.5.0':
+ resolution: {integrity: sha512-IoUAMcHWotBWYwSYuYypw/LlqZmO+wcBpn8ysrBNbazkFNkLf3btSDZMkKJO/bvOzl55imr/Xj4fi3DdsLsbzQ==}
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/better-sqlite3@7.6.13':
+ resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
+
+ '@types/cacheable-request@6.0.3':
+ resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
+
+ '@types/d3-array@3.2.2':
+ resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+ '@types/d3-axis@3.0.6':
+ resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
+
+ '@types/d3-brush@3.0.6':
+ resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
+
+ '@types/d3-chord@3.0.6':
+ resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-contour@3.0.6':
+ resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
+
+ '@types/d3-delaunay@6.0.4':
+ resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
+
+ '@types/d3-dispatch@3.0.7':
+ resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
+
+ '@types/d3-drag@3.0.7':
+ resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
+
+ '@types/d3-dsv@3.0.7':
+ resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-fetch@3.0.7':
+ resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
+
+ '@types/d3-force@3.0.10':
+ resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
+
+ '@types/d3-format@3.0.4':
+ resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
+
+ '@types/d3-geo@3.1.0':
+ resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
+
+ '@types/d3-hierarchy@3.1.7':
+ resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.1':
+ resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
+
+ '@types/d3-polygon@3.0.2':
+ resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
+
+ '@types/d3-quadtree@3.0.6':
+ resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
+
+ '@types/d3-random@3.0.3':
+ resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
+
+ '@types/d3-scale-chromatic@3.1.0':
+ resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
+
+ '@types/d3-scale@4.0.9':
+ resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
+ '@types/d3-selection@3.0.11':
+ resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
+
+ '@types/d3-shape@3.1.8':
+ resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
+
+ '@types/d3-time-format@4.0.3':
+ resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
+ '@types/d3-transition@3.0.9':
+ resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
+
+ '@types/d3-zoom@3.0.8':
+ resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
+
+ '@types/d3@7.4.3':
+ resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
+
+ '@types/debug@4.1.12':
+ resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/fs-extra@9.0.13':
+ resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
+
+ '@types/geojson@7946.0.16':
+ resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
+
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
+ '@types/http-cache-semantics@4.2.0':
+ resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==}
+
+ '@types/keyv@3.1.4':
+ resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
+
+ '@types/linkify-it@5.0.0':
+ resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
+
+ '@types/markdown-it@14.1.2':
+ resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
+
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
+ '@types/mdurl@2.0.0':
+ resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
+
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
+ '@types/node-cron@3.0.11':
+ resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==}
+
+ '@types/node@22.19.15':
+ resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
+
+ '@types/node@24.12.0':
+ resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
+
+ '@types/nodemailer@7.0.11':
+ resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==}
+
+ '@types/pg@8.18.0':
+ resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==}
+
+ '@types/plist@3.0.5':
+ resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
+
+ '@types/prop-types@15.7.15':
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+
+ '@types/react-dom@18.3.7':
+ resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
+ peerDependencies:
+ '@types/react': ^18.0.0
+
+ '@types/react@18.3.28':
+ resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==}
+
+ '@types/responselike@1.0.3':
+ resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
+
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
+ '@types/verror@1.10.11':
+ resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==}
+
+ '@types/web-bluetooth@0.0.21':
+ resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
+ '@types/yauzl@2.10.3':
+ resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
+
+ '@ungap/structured-clone@1.3.0':
+ resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+
+ '@upsetjs/venn.js@2.0.0':
+ resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
+
+ '@visactor/react-vchart@2.0.19':
+ resolution: {integrity: sha512-gLEJ8Q6DhI21xX6PDUEmhSUu4Ft8+Foho7TktiMMX6Nll5RYgXagjtjRAI1kSSKm6t/7ZXbBsmP4RdL+KaGtNA==}
+ peerDependencies:
+ react: '>=16.0.0'
+ react-dom: '>=16.0.0'
+
+ '@visactor/vchart-extension@2.0.19':
+ resolution: {integrity: sha512-SCFysGMRGdNK6wYFKDrvZzGdYbVjGhdbqlWA5t2fdbuVcCN7wNBTDKI+pL4yUL3n9M1ZtVxQp5uOw9I0AjFHZQ==}
+
+ '@visactor/vchart@2.0.19':
+ resolution: {integrity: sha512-pPNAHUuNlw1FNgbSSwT9comeJUXpaTAe8yed1MVI5rT+TspIg9Wpio29ZlUjrevUxosQqka5VAD63jQS6PMaOg==}
+
+ '@visactor/vdataset@1.0.23':
+ resolution: {integrity: sha512-zrLk9FBUWJoW6b30XnPKzXwAXl8USdLDfed6QZLsmdkylRU8V7yZeXE2aKwU8Lg1U4HmQngqmqOx7/QlbX44Tg==}
+
+ '@visactor/vlayouts@1.0.23':
+ resolution: {integrity: sha512-fK1f5LmuumhYanLArk5yrT4BZxu4IAmdc8WMwfB/KAvV+2dTPFuBUMWbWnDl0siQoU9SX9l/bLozUnI9n7BwBQ==}
+
+ '@visactor/vrender-animate@1.0.41':
+ resolution: {integrity: sha512-kdMoIh7OEo6z4rZfnJHX7d+izBhGVWq6MR22uppk0LPilfdBd/1hSNAEKO6C9JWAy5uROGFpEkh+kk+ar/zSZg==}
+
+ '@visactor/vrender-components@1.0.41':
+ resolution: {integrity: sha512-B7iXJE1TdkYapPZN6DNxoaErY4FzGf5AbcbG/z6Q0hnzO4Iw1hKdtlTGOIYA1+JXhehDWcyy+D0bnoNQNf+rgw==}
+
+ '@visactor/vrender-core@1.0.41':
+ resolution: {integrity: sha512-P7YVUJ45vwqPA460W6JJjg201ThvxBrjEgTBJ4tHKaHZPP/nVyF8rPyUOCL8QesEFDB+R0e/sUJUVs+ckHhd2w==}
+
+ '@visactor/vrender-kits@1.0.41':
+ resolution: {integrity: sha512-ffJlKkNseOsnRjvBmxKxQ9dsQXu+K288NFCtB0ZZ2N/mtcTsZUlH5SDCcsAvDhPGEvJ22Ye4MFKcXSEDctpGzA==}
+
+ '@visactor/vscale@1.0.23':
+ resolution: {integrity: sha512-XePhYuRoNAp+8MeSMuEOOvhVAlOwvM1sDT2yFxE6zdwVB2GjZk8mH+5N2xQGQWk75YmGJjlJASFtgwjlb1yWxw==}
+
+ '@visactor/vutils-extension@2.0.19':
+ resolution: {integrity: sha512-uRYCZFw7bYFdrSPQNIrYYsEGKuVn7lTMd4zDKBA2qnAfAZ/Y4WeXzZhQKdXgTFu5UGa292CqmVqMlVKFNWjC+A==}
+
+ '@visactor/vutils@1.0.23':
+ resolution: {integrity: sha512-M8SLqgdHhKN8QmQKTWD1gzEaHptpIV9pvMYvC6+VeOsqYvZZ6UdhSCAAczTYVo+m/uwcEC2JHSUspbrs8rzlRQ==}
+
+ '@vitejs/plugin-react@4.7.0':
+ resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+
+ '@vitejs/plugin-vue@5.2.4':
+ resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ peerDependencies:
+ vite: ^5.0.0 || ^6.0.0
+ vue: ^3.2.25
+
+ '@vitest/expect@2.1.9':
+ resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
+
+ '@vitest/mocker@2.1.9':
+ resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@2.1.9':
+ resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
+
+ '@vitest/runner@2.1.9':
+ resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
+
+ '@vitest/snapshot@2.1.9':
+ resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
+
+ '@vitest/spy@2.1.9':
+ resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
+
+ '@vitest/utils@2.1.9':
+ resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
+
+ '@vue/compiler-core@3.5.30':
+ resolution: {integrity: sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==}
+
+ '@vue/compiler-dom@3.5.30':
+ resolution: {integrity: sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==}
+
+ '@vue/compiler-sfc@3.5.30':
+ resolution: {integrity: sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==}
+
+ '@vue/compiler-ssr@3.5.30':
+ resolution: {integrity: sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==}
+
+ '@vue/devtools-api@7.7.9':
+ resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
+
+ '@vue/devtools-kit@7.7.9':
+ resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==}
+
+ '@vue/devtools-shared@7.7.9':
+ resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==}
+
+ '@vue/reactivity@3.5.30':
+ resolution: {integrity: sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==}
+
+ '@vue/runtime-core@3.5.30':
+ resolution: {integrity: sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==}
+
+ '@vue/runtime-dom@3.5.30':
+ resolution: {integrity: sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==}
+
+ '@vue/server-renderer@3.5.30':
+ resolution: {integrity: sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==}
+ peerDependencies:
+ vue: 3.5.30
+
+ '@vue/shared@3.5.30':
+ resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==}
+
+ '@vueuse/core@12.8.2':
+ resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
+
+ '@vueuse/integrations@12.8.2':
+ resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==}
+ peerDependencies:
+ async-validator: ^4
+ axios: ^1
+ change-case: ^5
+ drauu: ^0.4
+ focus-trap: ^7
+ fuse.js: ^7
+ idb-keyval: ^6
+ jwt-decode: ^4
+ nprogress: ^0.2
+ qrcode: ^1.5
+ sortablejs: ^1
+ universal-cookie: ^7
+ peerDependenciesMeta:
+ async-validator:
+ optional: true
+ axios:
+ optional: true
+ change-case:
+ optional: true
+ drauu:
+ optional: true
+ focus-trap:
+ optional: true
+ fuse.js:
+ optional: true
+ idb-keyval:
+ optional: true
+ jwt-decode:
+ optional: true
+ nprogress:
+ optional: true
+ qrcode:
+ optional: true
+ sortablejs:
+ optional: true
+ universal-cookie:
+ optional: true
+
+ '@vueuse/metadata@12.8.2':
+ resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
+
+ '@vueuse/shared@12.8.2':
+ resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
+
+ '@xmldom/xmldom@0.8.11':
+ resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
+ engines: {node: '>=10.0.0'}
+
+ abbrev@3.0.1:
+ resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ abs-svg-path@0.1.1:
+ resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
+
+ abstract-logging@2.0.1:
+ resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
+
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ ajv-formats@3.0.1:
+ resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
+ peerDependencies:
+ ajv: ^8.0.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+
+ ajv-keywords@3.5.2:
+ resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
+ peerDependencies:
+ ajv: ^6.9.1
+
+ ajv@6.14.0:
+ resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
+
+ ajv@8.18.0:
+ resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
+
+ algoliasearch@5.49.1:
+ resolution: {integrity: sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==}
+ engines: {node: '>= 14.0.0'}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
+ app-builder-bin@5.0.0-alpha.12:
+ resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==}
+
+ app-builder-lib@26.8.1:
+ resolution: {integrity: sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ dmg-builder: 26.8.1
+ electron-builder-squirrel-windows: 26.8.1
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ array-source@0.0.4:
+ resolution: {integrity: sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==}
+
+ assert-plus@1.0.0:
+ resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
+ engines: {node: '>=0.8'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ astral-regex@2.0.0:
+ resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
+ engines: {node: '>=8'}
+
+ async-exit-hook@2.0.1:
+ resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==}
+ engines: {node: '>=0.12.0'}
+
+ async@3.2.6:
+ resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ at-least-node@1.0.0:
+ resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
+ engines: {node: '>= 4.0.0'}
+
+ atomic-sleep@1.0.0:
+ resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
+ engines: {node: '>=8.0.0'}
+
+ avvio@9.2.0:
+ resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==}
+
+ aws-ssl-profiles@1.1.2:
+ resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
+ engines: {node: '>= 6.0.0'}
+
+ axios@1.13.6:
+ resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ baseline-browser-mapping@2.10.0:
+ resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ better-sqlite3@12.8.0:
+ resolution: {integrity: sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==}
+ engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
+
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+
+ bindings@1.5.0:
+ resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
+
+ birpc@2.9.0:
+ resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
+
+ bl@4.1.0:
+ resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+
+ boolean@3.2.0:
+ resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
+ deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ brace-expansion@2.0.2:
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+ brace-expansion@5.0.4:
+ resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
+ engines: {node: 18 || 20 || >=22}
+
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ buffer-crc32@0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+
+ buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
+ buffer@5.7.1:
+ resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+
+ builder-util-runtime@9.5.1:
+ resolution: {integrity: sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==}
+ engines: {node: '>=12.0.0'}
+
+ builder-util@26.8.1:
+ resolution: {integrity: sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==}
+
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
+ cacache@19.0.1:
+ resolution: {integrity: sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ cacheable-lookup@5.0.4:
+ resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==}
+ engines: {node: '>=10.6.0'}
+
+ cacheable-request@7.0.4:
+ resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==}
+ engines: {node: '>=8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ caniuse-lite@1.0.30001777:
+ resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==}
+
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
+ chai@5.3.3:
+ resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
+ engines: {node: '>=18'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ check-error@2.1.3:
+ resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
+ engines: {node: '>= 16'}
+
+ chevrotain-allstar@0.3.1:
+ resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==}
+ peerDependencies:
+ chevrotain: ^11.0.0
+
+ chevrotain@11.1.2:
+ resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==}
+
+ chownr@1.1.4:
+ resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
+
+ chownr@3.0.0:
+ resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
+ engines: {node: '>=18'}
+
+ chromium-pickle-js@0.2.0:
+ resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==}
+
+ ci-info@4.3.1:
+ resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==}
+ engines: {node: '>=8'}
+
+ ci-info@4.4.0:
+ resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
+ engines: {node: '>=8'}
+
+ cli-cursor@3.1.0:
+ resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
+ engines: {node: '>=8'}
+
+ cli-spinners@2.9.2:
+ resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
+ engines: {node: '>=6'}
+
+ cli-truncate@2.1.0:
+ resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
+ engines: {node: '>=8'}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ clone-response@1.0.3:
+ resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
+
+ clone@1.0.4:
+ resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
+ engines: {node: '>=0.8'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
+ commander@2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+ commander@5.1.0:
+ resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
+ engines: {node: '>= 6'}
+
+ commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+
+ commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+
+ commander@9.5.0:
+ resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
+ engines: {node: ^12.20.0 || >=14}
+
+ compare-version@0.1.2:
+ resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==}
+ engines: {node: '>=0.10.0'}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ concat-stream@1.4.11:
+ resolution: {integrity: sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==}
+ engines: {'0': node >= 0.8}
+
+ concat-stream@2.0.0:
+ resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
+ engines: {'0': node >= 6.0}
+
+ concurrently@9.2.1:
+ resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ confbox@0.1.8:
+ resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
+
+ content-disposition@1.0.1:
+ resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
+ engines: {node: '>=18'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ copy-anything@4.0.5:
+ resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
+ engines: {node: '>=18'}
+
+ core-util-is@1.0.2:
+ resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
+
+ core-util-is@1.0.3:
+ resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
+ cose-base@1.0.3:
+ resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
+
+ cose-base@2.2.0:
+ resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
+
+ crc@3.8.0:
+ resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==}
+
+ cross-dirname@0.1.0:
+ resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==}
+
+ cross-env@7.0.3:
+ resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
+ engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
+ hasBin: true
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ cytoscape-cose-bilkent@4.1.0:
+ resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape-fcose@2.2.0:
+ resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape@3.33.1:
+ resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==}
+ engines: {node: '>=0.10'}
+
+ d3-array@1.2.4:
+ resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==}
+
+ d3-array@2.12.1:
+ resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
+
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-axis@3.0.0:
+ resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
+ engines: {node: '>=12'}
+
+ d3-brush@3.0.0:
+ resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
+ engines: {node: '>=12'}
+
+ d3-chord@3.0.1:
+ resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-contour@4.0.2:
+ resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
+ engines: {node: '>=12'}
+
+ d3-delaunay@6.0.4:
+ resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
+ engines: {node: '>=12'}
+
+ d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+
+ d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+
+ d3-dsv@2.0.0:
+ resolution: {integrity: sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==}
+ hasBin: true
+
+ d3-dsv@3.0.1:
+ resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-fetch@3.0.1:
+ resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
+ engines: {node: '>=12'}
+
+ d3-force@3.0.0:
+ resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.2:
+ resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
+ engines: {node: '>=12'}
+
+ d3-geo@1.12.1:
+ resolution: {integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==}
+
+ d3-geo@3.1.1:
+ resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
+ engines: {node: '>=12'}
+
+ d3-hexbin@0.2.2:
+ resolution: {integrity: sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==}
+
+ d3-hierarchy@3.1.2:
+ resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@1.0.9:
+ resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-polygon@3.0.1:
+ resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
+ engines: {node: '>=12'}
+
+ d3-quadtree@3.0.1:
+ resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
+ engines: {node: '>=12'}
+
+ d3-random@3.0.1:
+ resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
+ engines: {node: '>=12'}
+
+ d3-sankey@0.12.3:
+ resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==}
+
+ d3-scale-chromatic@3.1.0:
+ resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@1.3.7:
+ resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
+ d3-transition@3.0.1:
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+
+ d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+
+ d3@7.9.0:
+ resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
+ engines: {node: '>=12'}
+
+ dagre-d3-es@7.0.14:
+ resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==}
+
+ data-urls@7.0.0:
+ resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ dayjs@1.11.20:
+ resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
+ decompress-response@6.0.0:
+ resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
+ engines: {node: '>=10'}
+
+ deep-eql@5.0.2:
+ resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+ engines: {node: '>=6'}
+
+ deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+
+ defaults@1.0.4:
+ resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
+
+ defer-to-connect@2.0.1:
+ resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
+ engines: {node: '>=10'}
+
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
+ delaunator@5.0.1:
+ resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ denque@2.1.0:
+ resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
+ engines: {node: '>=0.10'}
+
+ depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ detect-node@2.1.0:
+ resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
+
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
+ dir-compare@4.2.0:
+ resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
+
+ dmg-builder@26.8.1:
+ resolution: {integrity: sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==}
+
+ dmg-license@1.0.11:
+ resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==}
+ engines: {node: '>=8'}
+ os: [darwin]
+ hasBin: true
+
+ dompurify@3.3.3:
+ resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
+
+ dotenv-expand@11.0.7:
+ resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==}
+ engines: {node: '>=12'}
+
+ dotenv@16.6.1:
+ resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
+ engines: {node: '>=12'}
+
+ dotenv@17.3.1:
+ resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
+ engines: {node: '>=12'}
+
+ drizzle-kit@0.31.10:
+ resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
+ hasBin: true
+
+ drizzle-orm@0.45.1:
+ resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==}
+ peerDependencies:
+ '@aws-sdk/client-rds-data': '>=3'
+ '@cloudflare/workers-types': '>=4'
+ '@electric-sql/pglite': '>=0.2.0'
+ '@libsql/client': '>=0.10.0'
+ '@libsql/client-wasm': '>=0.10.0'
+ '@neondatabase/serverless': '>=0.10.0'
+ '@op-engineering/op-sqlite': '>=2'
+ '@opentelemetry/api': ^1.4.1
+ '@planetscale/database': '>=1.13'
+ '@prisma/client': '*'
+ '@tidbcloud/serverless': '*'
+ '@types/better-sqlite3': '*'
+ '@types/pg': '*'
+ '@types/sql.js': '*'
+ '@upstash/redis': '>=1.34.7'
+ '@vercel/postgres': '>=0.8.0'
+ '@xata.io/client': '*'
+ better-sqlite3: '>=7'
+ bun-types: '*'
+ expo-sqlite: '>=14.0.0'
+ gel: '>=2'
+ knex: '*'
+ kysely: '*'
+ mysql2: '>=2'
+ pg: '>=8'
+ postgres: '>=3'
+ prisma: '*'
+ sql.js: '>=1'
+ sqlite3: '>=5'
+ peerDependenciesMeta:
+ '@aws-sdk/client-rds-data':
+ optional: true
+ '@cloudflare/workers-types':
+ optional: true
+ '@electric-sql/pglite':
+ optional: true
+ '@libsql/client':
+ optional: true
+ '@libsql/client-wasm':
+ optional: true
+ '@neondatabase/serverless':
+ optional: true
+ '@op-engineering/op-sqlite':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@planetscale/database':
+ optional: true
+ '@prisma/client':
+ optional: true
+ '@tidbcloud/serverless':
+ optional: true
+ '@types/better-sqlite3':
+ optional: true
+ '@types/pg':
+ optional: true
+ '@types/sql.js':
+ optional: true
+ '@upstash/redis':
+ optional: true
+ '@vercel/postgres':
+ optional: true
+ '@xata.io/client':
+ optional: true
+ better-sqlite3:
+ optional: true
+ bun-types:
+ optional: true
+ expo-sqlite:
+ optional: true
+ gel:
+ optional: true
+ knex:
+ optional: true
+ kysely:
+ optional: true
+ mysql2:
+ optional: true
+ pg:
+ optional: true
+ postgres:
+ optional: true
+ prisma:
+ optional: true
+ sql.js:
+ optional: true
+ sqlite3:
+ optional: true
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
+ ejs@3.1.10:
+ resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
+ engines: {node: '>=0.10.0'}
+ hasBin: true
+
+ electron-builder-squirrel-windows@26.8.1:
+ resolution: {integrity: sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==}
+
+ electron-builder@26.8.1:
+ resolution: {integrity: sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ electron-log@5.4.3:
+ resolution: {integrity: sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==}
+ engines: {node: '>= 14'}
+
+ electron-publish@26.8.1:
+ resolution: {integrity: sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==}
+
+ electron-to-chromium@1.5.307:
+ resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==}
+
+ electron-updater@6.8.3:
+ resolution: {integrity: sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==}
+
+ electron-winstaller@5.4.0:
+ resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
+ engines: {node: '>=8.0.0'}
+
+ electron@41.0.3:
+ resolution: {integrity: sha512-IDjx8liW1q+r7+MOip5W1Eo1eMwJzVObmYrd9yz2dPCkS7XlgLq3qPVMR80TpiROFp73iY30kTzMdpA6fEVs3A==}
+ engines: {node: '>= 12.20.55'}
+ hasBin: true
+
+ emoji-regex-xs@1.0.0:
+ resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
+ encoding@0.1.13:
+ resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
+
+ end-of-stream@1.4.5:
+ resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
+ enhanced-resolve@5.20.1:
+ resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
+ engines: {node: '>=10.13.0'}
+
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
+ entities@7.0.1:
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+ engines: {node: '>=0.12'}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ err-code@2.0.3:
+ resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ es6-error@4.1.1:
+ resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
+
+ esbuild@0.18.20:
+ resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ esbuild@0.21.5:
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ esbuild@0.27.3:
+ resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
+ expand-template@2.0.3:
+ resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
+ engines: {node: '>=6'}
+
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ engines: {node: '>=12.0.0'}
+
+ exponential-backoff@3.1.3:
+ resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
+
+ extract-zip@2.0.1:
+ resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
+ engines: {node: '>= 10.17.0'}
+ hasBin: true
+
+ extsprintf@1.4.1:
+ resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==}
+ engines: {'0': node >=0.6.0}
+
+ fast-decode-uri-component@1.0.1:
+ resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-json-stringify@6.3.0:
+ resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==}
+
+ fast-querystring@1.1.2:
+ resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
+
+ fast-uri@3.1.0:
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+
+ fastify-plugin@5.1.0:
+ resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==}
+
+ fastify@5.8.2:
+ resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==}
+
+ fastq@1.20.1:
+ resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
+
+ fd-slicer@1.1.0:
+ resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-source@0.6.1:
+ resolution: {integrity: sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==}
+
+ file-uri-to-path@1.0.0:
+ resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
+
+ filelist@1.0.6:
+ resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==}
+
+ find-my-way@9.5.0:
+ resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==}
+ engines: {node: '>=20'}
+
+ focus-trap@7.8.0:
+ resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==}
+
+ follow-redirects@1.15.11:
+ resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ foreground-child@3.3.1:
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
+ engines: {node: '>=14'}
+
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
+ fs-constants@1.0.0:
+ resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+
+ fs-extra@10.1.0:
+ resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
+ engines: {node: '>=12'}
+
+ fs-extra@11.3.4:
+ resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
+ engines: {node: '>=14.14'}
+
+ fs-extra@7.0.1:
+ resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
+ engines: {node: '>=6 <7 || >=8'}
+
+ fs-extra@8.1.0:
+ resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
+ engines: {node: '>=6 <7 || >=8'}
+
+ fs-extra@9.1.0:
+ resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
+ engines: {node: '>=10'}
+
+ fs-minipass@3.0.3:
+ resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ generate-function@2.3.1:
+ resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ geobuf@3.0.2:
+ resolution: {integrity: sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==}
+ hasBin: true
+
+ geojson-dissolve@3.1.0:
+ resolution: {integrity: sha512-JXHfn+A3tU392HA703gJbjmuHaQOAE/C1KzbELCczFRFux+GdY6zt1nKb1VMBHp4LWeE7gUY2ql+g06vJqhiwQ==}
+
+ geojson-flatten@0.2.4:
+ resolution: {integrity: sha512-LiX6Jmot8adiIdZ/fthbcKKPOfWjTQchX/ggHnwMZ2e4b0I243N1ANUos0LvnzepTEsj0+D4fIJ5bKhBrWnAHA==}
+ hasBin: true
+
+ geojson-linestring-dissolve@0.0.1:
+ resolution: {integrity: sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw==}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-port@7.1.0:
+ resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==}
+ engines: {node: '>=16'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-stdin@6.0.0:
+ resolution: {integrity: sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==}
+ engines: {node: '>=4'}
+
+ get-stream@5.2.0:
+ resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
+ engines: {node: '>=8'}
+
+ get-tsconfig@4.13.6:
+ resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
+
+ gifuct-js@2.1.2:
+ resolution: {integrity: sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==}
+
+ github-from-package@0.0.0:
+ resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
+
+ glob@10.5.0:
+ resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+ hasBin: true
+
+ glob@13.0.6:
+ resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
+ engines: {node: 18 || 20 || >=22}
+
+ glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+
+ global-agent@3.0.0:
+ resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
+ engines: {node: '>=10.0'}
+
+ globalthis@1.0.4:
+ resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ got@11.8.6:
+ resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
+ engines: {node: '>=10.19.0'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ hachure-fill@0.5.2:
+ resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ hast-util-to-html@9.0.5:
+ resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
+ hookable@5.5.3:
+ resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+
+ hosted-git-info@4.1.0:
+ resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
+ engines: {node: '>=10'}
+
+ html-encoding-sniffer@6.0.0:
+ resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
+ http-cache-semantics@4.2.0:
+ resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
+
+ http-errors@2.0.1:
+ resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
+ engines: {node: '>= 0.8'}
+
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
+ http2-wrapper@1.0.3:
+ resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
+ engines: {node: '>=10.19.0'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ iconv-corefoundation@1.1.7:
+ resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
+ engines: {node: ^8.11.2 || >=10}
+ os: [darwin]
+
+ iconv-lite@0.4.24:
+ resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
+ engines: {node: '>=0.10.0'}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ iconv-lite@0.7.2:
+ resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
+ engines: {node: '>=0.10.0'}
+
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+ internmap@1.0.1:
+ resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
+
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
+ ip-address@10.1.0:
+ resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
+ engines: {node: '>= 12'}
+
+ ipaddr.js@2.3.0:
+ resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
+ engines: {node: '>= 10'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-interactive@1.0.0:
+ resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
+ engines: {node: '>=8'}
+
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
+ is-property@1.0.2:
+ resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
+
+ is-unicode-supported@0.1.0:
+ resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
+ engines: {node: '>=10'}
+
+ is-what@5.5.0:
+ resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
+ engines: {node: '>=18'}
+
+ isarray@0.0.1:
+ resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
+
+ isbinaryfile@4.0.10:
+ resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
+ engines: {node: '>= 8.0.0'}
+
+ isbinaryfile@5.0.7:
+ resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==}
+ engines: {node: '>= 18.0.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ isexe@3.1.5:
+ resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==}
+ engines: {node: '>=18'}
+
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+
+ jake@10.9.4:
+ resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
+ joi@18.0.2:
+ resolution: {integrity: sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==}
+ engines: {node: '>= 20'}
+
+ js-binary-schema-parser@2.0.3:
+ resolution: {integrity: sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsdom@29.0.1:
+ resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-ref-resolver@3.0.0:
+ resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json-stringify-safe@5.0.1:
+ resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsonfile@4.0.0:
+ resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
+
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+
+ katex@0.16.39:
+ resolution: {integrity: sha512-FR2f6y85+81ZLO0GPhyQ+EJl/E5ILNWltJhpAeOTzRny952Z13x2867lTFDmvMZix//Ux3CuMQ2VkLXRbUwOFg==}
+ hasBin: true
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ khroma@2.1.0:
+ resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
+
+ langium@4.2.1:
+ resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==}
+ engines: {node: '>=20.10.0', npm: '>=10.2.3'}
+
+ layout-base@1.0.2:
+ resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
+
+ layout-base@2.0.1:
+ resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
+
+ lazy-val@1.0.5:
+ resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
+
+ light-my-request@6.6.0:
+ resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lodash-es@4.17.23:
+ resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
+
+ lodash.escaperegexp@4.1.2:
+ resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
+
+ lodash.isequal@4.5.0:
+ resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
+ deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
+
+ lodash@4.17.23:
+ resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
+
+ log-symbols@4.1.0:
+ resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
+ engines: {node: '>=10'}
+
+ long@5.3.2:
+ resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
+
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ lottie-web@5.13.0:
+ resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==}
+
+ loupe@3.2.1:
+ resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+
+ lowercase-keys@2.0.0:
+ resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
+ engines: {node: '>=8'}
+
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
+ lru-cache@11.2.7:
+ resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
+ engines: {node: 20 || >=22}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ lru-cache@6.0.0:
+ resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+ engines: {node: '>=10'}
+
+ lru.min@1.1.4:
+ resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==}
+ engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ make-fetch-happen@14.0.3:
+ resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ mark.js@8.11.1:
+ resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
+
+ marked@16.4.2:
+ resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
+ marked@17.0.4:
+ resolution: {integrity: sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
+ matcher@3.0.0:
+ resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
+ engines: {node: '>=10'}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
+ mermaid@11.13.0:
+ resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.2:
+ resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ mime@2.6.0:
+ resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
+ engines: {node: '>=4.0.0'}
+ hasBin: true
+
+ mime@3.0.0:
+ resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+
+ mimic-fn@2.1.0:
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ engines: {node: '>=6'}
+
+ mimic-response@1.0.1:
+ resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
+ engines: {node: '>=4'}
+
+ mimic-response@3.1.0:
+ resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
+ engines: {node: '>=10'}
+
+ minimatch@10.2.4:
+ resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
+ engines: {node: 18 || 20 || >=22}
+
+ minimatch@3.1.5:
+ resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
+ minimatch@5.1.9:
+ resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
+ engines: {node: '>=10'}
+
+ minimatch@9.0.9:
+ resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minimist@1.2.0:
+ resolution: {integrity: sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==}
+
+ minimist@1.2.6:
+ resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ minipass-collect@2.0.1:
+ resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minipass-fetch@4.0.1:
+ resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ minipass-flush@1.0.5:
+ resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
+ engines: {node: '>= 8'}
+
+ minipass-pipeline@1.2.4:
+ resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
+ engines: {node: '>=8'}
+
+ minipass-sized@1.0.3:
+ resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
+ engines: {node: '>=8'}
+
+ minipass@3.3.6:
+ resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
+ engines: {node: '>=8'}
+
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minisearch@7.2.0:
+ resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==}
+
+ minizlib@3.1.0:
+ resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
+ engines: {node: '>= 18'}
+
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
+ mkdirp-classic@0.5.3:
+ resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+
+ mkdirp@0.5.6:
+ resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+ hasBin: true
+
+ mlly@1.8.2:
+ resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ mysql2@3.20.0:
+ resolution: {integrity: sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==}
+ engines: {node: '>= 8.0'}
+ peerDependencies:
+ '@types/node': '>= 8'
+
+ named-placeholders@1.1.6:
+ resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
+ engines: {node: '>=8.0.0'}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ napi-build-utils@2.0.0:
+ resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
+
+ negotiator@1.0.0:
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+ engines: {node: '>= 0.6'}
+
+ node-abi@3.89.0:
+ resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==}
+ engines: {node: '>=10'}
+
+ node-abi@4.26.0:
+ resolution: {integrity: sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==}
+ engines: {node: '>=22.12.0'}
+
+ node-addon-api@1.7.2:
+ resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
+
+ node-api-version@0.2.1:
+ resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==}
+
+ node-cron@4.2.1:
+ resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
+ engines: {node: '>=6.0.0'}
+
+ node-gyp@11.5.0:
+ resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+ hasBin: true
+
+ node-releases@2.0.36:
+ resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
+
+ nodemailer@8.0.3:
+ resolution: {integrity: sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==}
+ engines: {node: '>=6.0.0'}
+
+ non-layered-tidy-tree-layout@2.0.2:
+ resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
+
+ nopt@8.1.0:
+ resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+ hasBin: true
+
+ normalize-url@6.1.0:
+ resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
+ engines: {node: '>=10'}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+
+ on-exit-leak-free@2.1.2:
+ resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
+ engines: {node: '>=14.0.0'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ onetime@5.1.2:
+ resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+ engines: {node: '>=6'}
+
+ oniguruma-to-es@3.1.1:
+ resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==}
+
+ ora@5.4.1:
+ resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
+ engines: {node: '>=10'}
+
+ p-cancelable@2.1.1:
+ resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
+ engines: {node: '>=8'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-map@7.0.4:
+ resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==}
+ engines: {node: '>=18'}
+
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
+ package-manager-detector@1.6.0:
+ resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+
+ parse-svg-path@0.1.2:
+ resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==}
+
+ parse5@8.0.0:
+ resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ path-data-parser@0.1.0:
+ resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==}
+
+ path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
+ path-scurry@2.0.2:
+ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
+ engines: {node: 18 || 20 || >=22}
+
+ path-source@0.1.3:
+ resolution: {integrity: sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==}
+
+ pathe@1.1.2:
+ resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ pathval@2.0.1:
+ resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
+ engines: {node: '>= 14.16'}
+
+ pbf@3.3.0:
+ resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==}
+ hasBin: true
+
+ pe-library@0.4.1:
+ resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==}
+ engines: {node: '>=12', npm: '>=6'}
+
+ pend@1.2.0:
+ resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+
+ perfect-debounce@1.0.0:
+ resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+
+ pg-cloudflare@1.3.0:
+ resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
+
+ pg-connection-string@2.12.0:
+ resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==}
+
+ pg-int8@1.0.1:
+ resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
+ engines: {node: '>=4.0.0'}
+
+ pg-pool@3.13.0:
+ resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==}
+ peerDependencies:
+ pg: '>=8.0'
+
+ pg-protocol@1.13.0:
+ resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==}
+
+ pg-types@2.2.0:
+ resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
+ engines: {node: '>=4'}
+
+ pg@8.20.0:
+ resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==}
+ engines: {node: '>= 16.0.0'}
+ peerDependencies:
+ pg-native: '>=3.0.1'
+ peerDependenciesMeta:
+ pg-native:
+ optional: true
+
+ pgpass@1.0.5:
+ resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ pino-abstract-transport@3.0.0:
+ resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
+
+ pino-std-serializers@7.1.0:
+ resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
+
+ pino@10.3.1:
+ resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
+ hasBin: true
+
+ pkg-types@1.3.1:
+ resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
+
+ plist@3.1.0:
+ resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
+ engines: {node: '>=10.4.0'}
+
+ point-at-length@1.1.0:
+ resolution: {integrity: sha512-nNHDk9rNEh/91o2Y8kHLzBLNpLf80RYd2gCun9ss+V0ytRSf6XhryBTx071fesktjbachRmGuUbId+JQmzhRXw==}
+
+ points-on-curve@0.2.0:
+ resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
+
+ points-on-path@0.2.1:
+ resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
+
+ postcss@8.5.8:
+ resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ postgres-array@2.0.0:
+ resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
+ engines: {node: '>=4'}
+
+ postgres-bytea@1.0.1:
+ resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
+ engines: {node: '>=0.10.0'}
+
+ postgres-date@1.0.7:
+ resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
+ engines: {node: '>=0.10.0'}
+
+ postgres-interval@1.2.0:
+ resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
+ engines: {node: '>=0.10.0'}
+
+ postject@1.0.0-alpha.6:
+ resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ preact@10.28.4:
+ resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==}
+
+ prebuild-install@7.1.3:
+ resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
+ engines: {node: '>=10'}
+ deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
+ hasBin: true
+
+ proc-log@5.0.0:
+ resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ process-warning@4.0.1:
+ resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
+
+ process-warning@5.0.0:
+ resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
+
+ progress@2.0.3:
+ resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
+ engines: {node: '>=0.4.0'}
+
+ promise-retry@2.0.1:
+ resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
+ engines: {node: '>=10'}
+
+ proper-lockfile@4.1.2:
+ resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
+
+ property-information@7.1.0:
+ resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
+
+ protocol-buffers-schema@3.6.0:
+ resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ pump@3.0.4:
+ resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ quick-format-unescaped@4.0.4:
+ resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+
+ quick-lru@5.1.1:
+ resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
+ engines: {node: '>=10'}
+
+ rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+
+ react-dom@18.3.1:
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
+ peerDependencies:
+ react: ^18.3.1
+
+ react-is@18.3.1:
+ resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
+ react-refresh@0.17.0:
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
+ engines: {node: '>=0.10.0'}
+
+ react-router-dom@7.13.1:
+ resolution: {integrity: sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.13.1:
+ resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react-shallow-renderer@16.15.0:
+ resolution: {integrity: sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==}
+ peerDependencies:
+ react: ^16.0.0 || ^17.0.0 || ^18.0.0
+
+ react-test-renderer@18.3.1:
+ resolution: {integrity: sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==}
+ peerDependencies:
+ react: ^18.3.1
+
+ react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+
+ read-binary-file-arch@1.0.6:
+ resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==}
+ hasBin: true
+
+ readable-stream@1.1.14:
+ resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
+
+ readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+
+ real-require@0.2.0:
+ resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
+ engines: {node: '>= 12.13.0'}
+
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ resedit@1.7.2:
+ resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
+ engines: {node: '>=12', npm: '>=6'}
+
+ resolve-alpn@1.2.1:
+ resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ resolve-protobuf-schema@2.1.0:
+ resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
+
+ responselike@2.0.1:
+ resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
+
+ restore-cursor@3.1.0:
+ resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
+ engines: {node: '>=8'}
+
+ ret@0.5.0:
+ resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==}
+ engines: {node: '>=10'}
+
+ retry@0.12.0:
+ resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
+ engines: {node: '>= 4'}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rfdc@1.4.1:
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
+ rimraf@2.6.3:
+ resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
+ hasBin: true
+
+ roarr@2.15.4:
+ resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
+ engines: {node: '>=8.0'}
+
+ robust-predicates@3.0.2:
+ resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
+
+ rollup@4.59.0:
+ resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ roughjs@4.6.6:
+ resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
+
+ rw@1.3.3:
+ resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
+
+ rxjs@7.8.2:
+ resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ safe-regex2@5.0.0:
+ resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==}
+
+ safe-stable-stringify@2.5.0:
+ resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
+ engines: {node: '>=10'}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ sanitize-filename@1.6.3:
+ resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
+
+ sax@1.5.0:
+ resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==}
+ engines: {node: '>=11.0.0'}
+
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
+ scheduler@0.23.2:
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+
+ search-insights@2.17.3:
+ resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==}
+
+ secure-json-parse@4.1.0:
+ resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
+
+ semver-compare@1.0.0:
+ resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
+
+ semver@5.7.2:
+ resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
+ hasBin: true
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ serialize-error@7.0.1:
+ resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
+ engines: {node: '>=10'}
+
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+
+ setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+
+ shapefile@0.6.6:
+ resolution: {integrity: sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==}
+ hasBin: true
+
+ sharp@0.34.5:
+ resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ shell-quote@1.8.3:
+ resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
+ engines: {node: '>= 0.4'}
+
+ shiki@2.5.0:
+ resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ simple-concat@1.0.1:
+ resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
+
+ simple-get@4.0.1:
+ resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
+
+ simple-statistics@7.8.9:
+ resolution: {integrity: sha512-YT6MLqYsz7y1rQZOLFlOCCgSRpCi6bqY417yhoOLI7aVoBi29dD39EPrOE03W9DY25H0J0jizVsHZnkLzyGJFg==}
+
+ simple-update-notifier@2.0.0:
+ resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
+ engines: {node: '>=10'}
+
+ simplify-geojson@1.0.5:
+ resolution: {integrity: sha512-02l1W4UipP5ivNVq6kX15mAzCRIV1oI3tz0FUEyOsNiv1ltuFDjbNhO+nbv/xhbDEtKqWLYuzpWhUsJrjR/ypA==}
+ hasBin: true
+
+ simplify-geometry@0.0.2:
+ resolution: {integrity: sha512-ZEyrplkqgCqDlL7V8GbbYgTLlcnNF+MWWUdy8s8ZeJru50bnI71rDew/I+HG36QS2mPOYAq1ZjwNXxHJ8XOVBw==}
+
+ slice-ansi@3.0.0:
+ resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
+ engines: {node: '>=8'}
+
+ slice-source@0.4.1:
+ resolution: {integrity: sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==}
+
+ smart-buffer@4.2.0:
+ resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
+ engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
+
+ socks-proxy-agent@8.0.5:
+ resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
+ engines: {node: '>= 14'}
+
+ socks@2.8.7:
+ resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
+ engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+
+ sonic-boom@4.2.1:
+ resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map-support@0.5.21:
+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
+ speakingurl@14.0.1:
+ resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
+ engines: {node: '>=0.10.0'}
+
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+
+ sprintf-js@1.1.3:
+ resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
+
+ sql-escaper@1.3.3:
+ resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==}
+ engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'}
+
+ ssri@12.0.0:
+ resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ stat-mode@1.0.0:
+ resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==}
+ engines: {node: '>= 6'}
+
+ statuses@2.0.2:
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
+ engines: {node: '>= 0.8'}
+
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
+ stream-source@0.3.5:
+ resolution: {integrity: sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
+ string_decoder@0.10.31:
+ resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
+
+ string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
+ strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+
+ stylis@4.3.6:
+ resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
+
+ sumchecker@3.0.1:
+ resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
+ engines: {node: '>= 8.0'}
+
+ superjson@2.2.6:
+ resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
+ engines: {node: '>=16'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-color@8.1.1:
+ resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
+ engines: {node: '>=10'}
+
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
+ tabbable@6.4.0:
+ resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
+
+ tailwindcss@4.2.1:
+ resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
+
+ tailwindcss@4.2.2:
+ resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
+
+ tapable@2.3.0:
+ resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
+ engines: {node: '>=6'}
+
+ tar-fs@2.1.4:
+ resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
+
+ tar-stream@2.2.0:
+ resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
+ engines: {node: '>=6'}
+
+ tar@7.5.11:
+ resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
+ engines: {node: '>=18'}
+
+ temp-file@3.4.0:
+ resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==}
+
+ temp@0.9.4:
+ resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==}
+ engines: {node: '>=6.0.0'}
+
+ text-encoding@0.6.4:
+ resolution: {integrity: sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==}
+ deprecated: no longer maintained
+
+ thread-stream@4.0.0:
+ resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
+ engines: {node: '>=20'}
+
+ tiny-async-pool@1.3.0:
+ resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==}
+
+ tiny-typed-emitter@2.1.0:
+ resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
+ tinyexec@1.0.4:
+ resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tinypool@1.1.1:
+ resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+
+ tinyrainbow@1.2.0:
+ resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
+ engines: {node: '>=14.0.0'}
+
+ tinyspy@3.0.2:
+ resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+ engines: {node: '>=14.0.0'}
+
+ tldts-core@7.0.26:
+ resolution: {integrity: sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==}
+
+ tldts@7.0.26:
+ resolution: {integrity: sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==}
+ hasBin: true
+
+ tmp-promise@3.0.3:
+ resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
+
+ tmp@0.2.5:
+ resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
+ engines: {node: '>=14.14'}
+
+ toad-cache@3.7.0:
+ resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
+ engines: {node: '>=12'}
+
+ toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+
+ topojson-client@3.1.0:
+ resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==}
+ hasBin: true
+
+ topojson-server@3.0.1:
+ resolution: {integrity: sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==}
+ hasBin: true
+
+ tough-cookie@6.0.1:
+ resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
+ engines: {node: '>=16'}
+
+ tr46@6.0.0:
+ resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
+ engines: {node: '>=20'}
+
+ tree-kill@1.2.2:
+ resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
+ hasBin: true
+
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
+ truncate-utf8-bytes@1.0.2:
+ resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
+
+ ts-dedent@2.2.0:
+ resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
+ engines: {node: '>=6.10'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ tsx@4.21.0:
+ resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
+ tunnel-agent@0.6.0:
+ resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
+
+ type-fest@0.13.1:
+ resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
+ engines: {node: '>=10'}
+
+ typedarray@0.0.6:
+ resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+
+ typedarray@0.0.7:
+ resolution: {integrity: sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ==}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ ufo@1.6.3:
+ resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
+
+ undici-types@6.21.0:
+ resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ undici@6.24.1:
+ resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
+ engines: {node: '>=18.17'}
+
+ undici@7.24.5:
+ resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==}
+ engines: {node: '>=20.18.1'}
+
+ unique-filename@4.0.0:
+ resolution: {integrity: sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ unique-slug@5.0.0:
+ resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
+ universalify@0.1.2:
+ resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
+ engines: {node: '>= 4.0.0'}
+
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ utf8-byte-length@1.0.5:
+ resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ uuid@11.1.0:
+ resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
+ hasBin: true
+
+ verror@1.10.1:
+ resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==}
+ engines: {node: '>=0.6.0'}
+
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
+ vite-node@2.1.9:
+ resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+
+ vite@5.4.21:
+ resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+
+ vite@6.4.1:
+ resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitepress-plugin-mermaid@2.0.17:
+ resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==}
+ peerDependencies:
+ mermaid: 10 || 11
+ vitepress: ^1.0.0 || ^1.0.0-alpha
+
+ vitepress@1.6.4:
+ resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==}
+ hasBin: true
+ peerDependencies:
+ markdown-it-mathjax3: ^4
+ postcss: ^8
+ peerDependenciesMeta:
+ markdown-it-mathjax3:
+ optional: true
+ postcss:
+ optional: true
+
+ vitest@2.1.9:
+ resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/node': ^18.0.0 || >=20.0.0
+ '@vitest/browser': 2.1.9
+ '@vitest/ui': 2.1.9
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ vscode-jsonrpc@8.2.0:
+ resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
+ engines: {node: '>=14.0.0'}
+
+ vscode-languageserver-protocol@3.17.5:
+ resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
+
+ vscode-languageserver-textdocument@1.0.12:
+ resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
+
+ vscode-languageserver-types@3.17.5:
+ resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
+
+ vscode-languageserver@9.0.1:
+ resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
+ hasBin: true
+
+ vscode-uri@3.1.0:
+ resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
+
+ vue@3.5.30:
+ resolution: {integrity: sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
+ wait-on@9.0.4:
+ resolution: {integrity: sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==}
+ engines: {node: '>=20.0.0'}
+ hasBin: true
+
+ wcwidth@1.0.1:
+ resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
+
+ webidl-conversions@8.0.1:
+ resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
+ engines: {node: '>=20'}
+
+ whatwg-mimetype@5.0.0:
+ resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
+ engines: {node: '>=20'}
+
+ whatwg-url@16.0.1:
+ resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ which@5.0.0:
+ resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+ hasBin: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ ws@8.19.0:
+ resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlbuilder@15.1.1:
+ resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
+ engines: {node: '>=8.0'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
+ xtend@4.0.2:
+ resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+ engines: {node: '>=0.4'}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yallist@4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+
+ yallist@5.0.0:
+ resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
+ engines: {node: '>=18'}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
+ yauzl@2.10.0:
+ resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
+snapshots:
+
+ 7zip-bin@5.2.0: {}
+
+ '@algolia/abtesting@1.15.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)':
+ dependencies:
+ '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)
+ '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
+ transitivePeerDependencies:
+ - '@algolia/client-search'
+ - algoliasearch
+ - search-insights
+
+ '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)':
+ dependencies:
+ '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
+ search-insights: 2.17.3
+ transitivePeerDependencies:
+ - '@algolia/client-search'
+ - algoliasearch
+
+ '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)':
+ dependencies:
+ '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
+ '@algolia/client-search': 5.49.1
+ algoliasearch: 5.49.1
+
+ '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)':
+ dependencies:
+ '@algolia/client-search': 5.49.1
+ algoliasearch: 5.49.1
+
+ '@algolia/client-abtesting@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/client-analytics@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/client-common@5.49.1': {}
+
+ '@algolia/client-insights@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/client-personalization@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/client-query-suggestions@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/client-search@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/ingestion@1.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/monitoring@1.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/recommend@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ '@algolia/requester-browser-xhr@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+
+ '@algolia/requester-fetch@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+
+ '@algolia/requester-node-http@5.49.1':
+ dependencies:
+ '@algolia/client-common': 5.49.1
+
+ '@antfu/install-pkg@1.1.0':
+ dependencies:
+ package-manager-detector: 1.6.0
+ tinyexec: 1.0.4
+
+ '@asamuzakjp/css-color@5.0.1':
+ dependencies:
+ '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+ lru-cache: 11.2.7
+
+ '@asamuzakjp/dom-selector@7.0.3':
+ dependencies:
+ '@asamuzakjp/nwsapi': 2.3.9
+ bidi-js: 1.0.3
+ css-tree: 3.2.1
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.2.7
+
+ '@asamuzakjp/nwsapi@2.3.9': {}
+
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.0': {}
+
+ '@babel/core@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@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.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.1':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.0
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.1
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.28.6': {}
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.6':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+
+ '@babel/parser@7.29.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@braintree/sanitize-url@6.0.4':
+ optional: true
+
+ '@braintree/sanitize-url@7.1.2': {}
+
+ '@bramus/specificity@2.4.2':
+ dependencies:
+ css-tree: 3.2.1
+
+ '@chevrotain/cst-dts-gen@11.1.2':
+ dependencies:
+ '@chevrotain/gast': 11.1.2
+ '@chevrotain/types': 11.1.2
+ lodash-es: 4.17.23
+
+ '@chevrotain/gast@11.1.2':
+ dependencies:
+ '@chevrotain/types': 11.1.2
+ lodash-es: 4.17.23
+
+ '@chevrotain/regexp-to-ast@11.1.2': {}
+
+ '@chevrotain/types@11.1.2': {}
+
+ '@chevrotain/utils@11.1.2': {}
+
+ '@csstools/color-helpers@6.0.2': {}
+
+ '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/color-helpers': 6.0.2
+ '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)':
+ optionalDependencies:
+ css-tree: 3.2.1
+
+ '@csstools/css-tokenizer@4.0.0': {}
+
+ '@develar/schema-utils@2.6.5':
+ dependencies:
+ ajv: 6.14.0
+ ajv-keywords: 3.5.2(ajv@6.14.0)
+
+ '@dnd-kit/accessibility@3.1.1(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ tslib: 2.8.1
+
+ '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@dnd-kit/accessibility': 3.1.1(react@18.3.1)
+ '@dnd-kit/utilities': 3.2.2(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ tslib: 2.8.1
+
+ '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@dnd-kit/utilities': 3.2.2(react@18.3.1)
+ react: 18.3.1
+ tslib: 2.8.1
+
+ '@dnd-kit/utilities@3.2.2(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+ tslib: 2.8.1
+
+ '@docsearch/css@3.8.2': {}
+
+ '@docsearch/js@3.8.2(@algolia/client-search@5.49.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)':
+ dependencies:
+ '@docsearch/react': 3.8.2(@algolia/client-search@5.49.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)
+ preact: 10.28.4
+ transitivePeerDependencies:
+ - '@algolia/client-search'
+ - '@types/react'
+ - react
+ - react-dom
+ - search-insights
+
+ '@docsearch/react@3.8.2(@algolia/client-search@5.49.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)':
+ dependencies:
+ '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)
+ '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)
+ '@docsearch/css': 3.8.2
+ algoliasearch: 5.49.1
+ optionalDependencies:
+ '@types/react': 18.3.28
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ search-insights: 2.17.3
+ transitivePeerDependencies:
+ - '@algolia/client-search'
+
+ '@drizzle-team/brocli@0.10.2': {}
+
+ '@electron/asar@3.4.1':
+ dependencies:
+ commander: 5.1.0
+ glob: 7.2.3
+ minimatch: 3.1.5
+
+ '@electron/fuses@1.8.0':
+ dependencies:
+ chalk: 4.1.2
+ fs-extra: 9.1.0
+ minimist: 1.2.8
+
+ '@electron/get@2.0.3':
+ dependencies:
+ debug: 4.4.3
+ env-paths: 2.2.1
+ fs-extra: 8.1.0
+ got: 11.8.6
+ progress: 2.0.3
+ semver: 6.3.1
+ sumchecker: 3.0.1
+ optionalDependencies:
+ global-agent: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@electron/get@3.1.0':
+ dependencies:
+ debug: 4.4.3
+ env-paths: 2.2.1
+ fs-extra: 8.1.0
+ got: 11.8.6
+ progress: 2.0.3
+ semver: 6.3.1
+ sumchecker: 3.0.1
+ optionalDependencies:
+ global-agent: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@electron/notarize@2.5.0':
+ dependencies:
+ debug: 4.4.3
+ fs-extra: 9.1.0
+ promise-retry: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@electron/notarize@3.1.1':
+ dependencies:
+ debug: 4.4.3
+ promise-retry: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@electron/osx-sign@1.3.3':
+ dependencies:
+ compare-version: 0.1.2
+ debug: 4.4.3
+ fs-extra: 10.1.0
+ isbinaryfile: 4.0.10
+ minimist: 1.2.8
+ plist: 3.1.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@electron/rebuild@4.0.3':
+ dependencies:
+ '@malept/cross-spawn-promise': 2.0.0
+ debug: 4.4.3
+ detect-libc: 2.1.2
+ got: 11.8.6
+ graceful-fs: 4.2.11
+ node-abi: 4.26.0
+ node-api-version: 0.2.1
+ node-gyp: 11.5.0
+ ora: 5.4.1
+ read-binary-file-arch: 1.0.6
+ semver: 7.7.4
+ tar: 7.5.11
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@electron/universal@2.0.3':
+ dependencies:
+ '@electron/asar': 3.4.1
+ '@malept/cross-spawn-promise': 2.0.0
+ debug: 4.4.3
+ dir-compare: 4.2.0
+ fs-extra: 11.3.4
+ minimatch: 9.0.9
+ plist: 3.1.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@electron/windows-sign@1.2.2':
+ dependencies:
+ cross-dirname: 0.1.0
+ debug: 4.4.3
+ fs-extra: 11.3.4
+ minimist: 1.2.8
+ postject: 1.0.0-alpha.6
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ '@emnapi/runtime@1.8.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@esbuild-kit/core-utils@3.3.2':
+ dependencies:
+ esbuild: 0.18.20
+ source-map-support: 0.5.21
+
+ '@esbuild-kit/esm-loader@2.6.5':
+ dependencies:
+ '@esbuild-kit/core-utils': 3.3.2
+ get-tsconfig: 4.13.6
+
+ '@esbuild/aix-ppc64@0.21.5':
+ optional: true
+
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/aix-ppc64@0.27.3':
+ optional: true
+
+ '@esbuild/android-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/android-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/android-arm@0.18.20':
+ optional: true
+
+ '@esbuild/android-arm@0.21.5':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.27.3':
+ optional: true
+
+ '@esbuild/android-x64@0.18.20':
+ optional: true
+
+ '@esbuild/android-x64@0.21.5':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.27.3':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/darwin-x64@0.18.20':
+ optional: true
+
+ '@esbuild/darwin-x64@0.21.5':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.27.3':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.18.20':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-arm@0.18.20':
+ optional: true
+
+ '@esbuild/linux-arm@0.21.5':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.27.3':
+ optional: true
+
+ '@esbuild/linux-ia32@0.18.20':
+ optional: true
+
+ '@esbuild/linux-ia32@0.21.5':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.27.3':
+ optional: true
+
+ '@esbuild/linux-loong64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-loong64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.18.20':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.21.5':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.27.3':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-s390x@0.18.20':
+ optional: true
+
+ '@esbuild/linux-s390x@0.21.5':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.27.3':
+ optional: true
+
+ '@esbuild/linux-x64@0.18.20':
+ optional: true
+
+ '@esbuild/linux-x64@0.21.5':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.27.3':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.18.20':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.18.20':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.21.5':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/sunos-x64@0.18.20':
+ optional: true
+
+ '@esbuild/sunos-x64@0.21.5':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.27.3':
+ optional: true
+
+ '@esbuild/win32-arm64@0.18.20':
+ optional: true
+
+ '@esbuild/win32-arm64@0.21.5':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/win32-ia32@0.18.20':
+ optional: true
+
+ '@esbuild/win32-ia32@0.21.5':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.27.3':
+ optional: true
+
+ '@esbuild/win32-x64@0.18.20':
+ optional: true
+
+ '@esbuild/win32-x64@0.21.5':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.27.3':
+ optional: true
+
+ '@exodus/bytes@1.15.0': {}
+
+ '@fastify/accept-negotiator@2.0.1': {}
+
+ '@fastify/ajv-compiler@4.0.5':
+ dependencies:
+ ajv: 8.18.0
+ ajv-formats: 3.0.1(ajv@8.18.0)
+ fast-uri: 3.1.0
+
+ '@fastify/cors@11.2.0':
+ dependencies:
+ fastify-plugin: 5.1.0
+ toad-cache: 3.7.0
+
+ '@fastify/error@4.2.0': {}
+
+ '@fastify/fast-json-stringify-compiler@5.0.3':
+ dependencies:
+ fast-json-stringify: 6.3.0
+
+ '@fastify/forwarded@3.0.1': {}
+
+ '@fastify/merge-json-schemas@0.2.1':
+ dependencies:
+ dequal: 2.0.3
+
+ '@fastify/proxy-addr@5.1.0':
+ dependencies:
+ '@fastify/forwarded': 3.0.1
+ ipaddr.js: 2.3.0
+
+ '@fastify/send@4.1.0':
+ dependencies:
+ '@lukeed/ms': 2.0.2
+ escape-html: 1.0.3
+ fast-decode-uri-component: 1.0.1
+ http-errors: 2.0.1
+ mime: 3.0.0
+
+ '@fastify/static@9.0.0':
+ dependencies:
+ '@fastify/accept-negotiator': 2.0.1
+ '@fastify/send': 4.1.0
+ content-disposition: 1.0.1
+ fastify-plugin: 5.1.0
+ fastq: 1.20.1
+ glob: 13.0.6
+
+ '@hapi/address@5.1.1':
+ dependencies:
+ '@hapi/hoek': 11.0.7
+
+ '@hapi/formula@3.0.2': {}
+
+ '@hapi/hoek@11.0.7': {}
+
+ '@hapi/pinpoint@2.0.1': {}
+
+ '@hapi/tlds@1.1.6': {}
+
+ '@hapi/topo@6.0.2':
+ dependencies:
+ '@hapi/hoek': 11.0.7
+
+ '@iconify-json/simple-icons@1.2.72':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify/types@2.0.0': {}
+
+ '@iconify/utils@3.1.0':
+ dependencies:
+ '@antfu/install-pkg': 1.1.0
+ '@iconify/types': 2.0.0
+ mlly: 1.8.2
+
+ '@img/colour@1.1.0': {}
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-darwin-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-arm@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-ppc64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-s390x@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-wasm32@0.34.5':
+ dependencies:
+ '@emnapi/runtime': 1.8.1
+ optional: true
+
+ '@img/sharp-win32-arm64@0.34.5':
+ optional: true
+
+ '@img/sharp-win32-ia32@0.34.5':
+ optional: true
+
+ '@img/sharp-win32-x64@0.34.5':
+ optional: true
+
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.3
+ strip-ansi: 7.2.0
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
+ '@isaacs/fs-minipass@4.0.1':
+ dependencies:
+ minipass: 7.1.3
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@lukeed/ms@2.0.2': {}
+
+ '@malept/cross-spawn-promise@2.0.0':
+ dependencies:
+ cross-spawn: 7.0.6
+
+ '@malept/flatpak-bundler@0.4.0':
+ dependencies:
+ debug: 4.4.3
+ fs-extra: 9.1.0
+ lodash: 4.17.23
+ tmp-promise: 3.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mermaid-js/mermaid-mindmap@9.3.0':
+ dependencies:
+ '@braintree/sanitize-url': 6.0.4
+ cytoscape: 3.33.1
+ cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1)
+ cytoscape-fcose: 2.2.0(cytoscape@3.33.1)
+ d3: 7.9.0
+ khroma: 2.1.0
+ non-layered-tidy-tree-layout: 2.0.2
+ optional: true
+
+ '@mermaid-js/parser@1.0.1':
+ dependencies:
+ langium: 4.2.1
+
+ '@npmcli/agent@3.0.0':
+ dependencies:
+ agent-base: 7.1.4
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ lru-cache: 10.4.3
+ socks-proxy-agent: 8.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@npmcli/fs@4.0.0':
+ dependencies:
+ semver: 7.7.4
+
+ '@pinojs/redact@0.4.0': {}
+
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
+ '@resvg/resvg-js-android-arm-eabi@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-android-arm64@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-darwin-arm64@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-darwin-x64@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-linux-arm-gnueabihf@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-linux-arm64-gnu@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-linux-arm64-musl@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-linux-x64-gnu@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-linux-x64-musl@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-win32-arm64-msvc@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-win32-ia32-msvc@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js-win32-x64-msvc@2.4.1':
+ optional: true
+
+ '@resvg/resvg-js@2.4.1':
+ optionalDependencies:
+ '@resvg/resvg-js-android-arm-eabi': 2.4.1
+ '@resvg/resvg-js-android-arm64': 2.4.1
+ '@resvg/resvg-js-darwin-arm64': 2.4.1
+ '@resvg/resvg-js-darwin-x64': 2.4.1
+ '@resvg/resvg-js-linux-arm-gnueabihf': 2.4.1
+ '@resvg/resvg-js-linux-arm64-gnu': 2.4.1
+ '@resvg/resvg-js-linux-arm64-musl': 2.4.1
+ '@resvg/resvg-js-linux-x64-gnu': 2.4.1
+ '@resvg/resvg-js-linux-x64-musl': 2.4.1
+ '@resvg/resvg-js-win32-arm64-msvc': 2.4.1
+ '@resvg/resvg-js-win32-ia32-msvc': 2.4.1
+ '@resvg/resvg-js-win32-x64-msvc': 2.4.1
+
+ '@rolldown/pluginutils@1.0.0-beta.27': {}
+
+ '@rollup/rollup-android-arm-eabi@4.59.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.59.0':
+ optional: true
+
+ '@shikijs/core@2.5.0':
+ dependencies:
+ '@shikijs/engine-javascript': 2.5.0
+ '@shikijs/engine-oniguruma': 2.5.0
+ '@shikijs/types': 2.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@2.5.0':
+ dependencies:
+ '@shikijs/types': 2.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 3.1.1
+
+ '@shikijs/engine-oniguruma@2.5.0':
+ dependencies:
+ '@shikijs/types': 2.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@2.5.0':
+ dependencies:
+ '@shikijs/types': 2.5.0
+
+ '@shikijs/themes@2.5.0':
+ dependencies:
+ '@shikijs/types': 2.5.0
+
+ '@shikijs/transformers@2.5.0':
+ dependencies:
+ '@shikijs/core': 2.5.0
+ '@shikijs/types': 2.5.0
+
+ '@shikijs/types@2.5.0':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
+ '@sindresorhus/is@4.6.0': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@szmarczak/http-timer@4.0.6':
+ dependencies:
+ defer-to-connect: 2.0.1
+
+ '@tailwindcss/node@4.2.2':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.20.1
+ jiti: 2.6.1
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.2.2
+
+ '@tailwindcss/oxide-android-arm64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide@4.2.2':
+ 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
+
+ '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))':
+ dependencies:
+ '@tailwindcss/node': 4.2.2
+ '@tailwindcss/oxide': 4.2.2
+ tailwindcss: 4.2.2
+ vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)
+
+ '@turf/boolean-clockwise@6.5.0':
+ dependencies:
+ '@turf/helpers': 6.5.0
+ '@turf/invariant': 6.5.0
+
+ '@turf/clone@6.5.0':
+ dependencies:
+ '@turf/helpers': 6.5.0
+
+ '@turf/flatten@6.5.0':
+ dependencies:
+ '@turf/helpers': 6.5.0
+ '@turf/meta': 6.5.0
+
+ '@turf/helpers@6.5.0': {}
+
+ '@turf/invariant@6.5.0':
+ dependencies:
+ '@turf/helpers': 6.5.0
+
+ '@turf/meta@3.14.0': {}
+
+ '@turf/meta@6.5.0':
+ dependencies:
+ '@turf/helpers': 6.5.0
+
+ '@turf/rewind@6.5.0':
+ dependencies:
+ '@turf/boolean-clockwise': 6.5.0
+ '@turf/clone': 6.5.0
+ '@turf/helpers': 6.5.0
+ '@turf/invariant': 6.5.0
+ '@turf/meta': 6.5.0
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@types/better-sqlite3@7.6.13':
+ dependencies:
+ '@types/node': 22.19.15
+
+ '@types/cacheable-request@6.0.3':
+ dependencies:
+ '@types/http-cache-semantics': 4.2.0
+ '@types/keyv': 3.1.4
+ '@types/node': 22.19.15
+ '@types/responselike': 1.0.3
+
+ '@types/d3-array@3.2.2': {}
+
+ '@types/d3-axis@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-brush@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-chord@3.0.6': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-contour@3.0.6':
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/geojson': 7946.0.16
+
+ '@types/d3-delaunay@6.0.4': {}
+
+ '@types/d3-dispatch@3.0.7': {}
+
+ '@types/d3-drag@3.0.7':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-dsv@3.0.7': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-fetch@3.0.7':
+ dependencies:
+ '@types/d3-dsv': 3.0.7
+
+ '@types/d3-force@3.0.10': {}
+
+ '@types/d3-format@3.0.4': {}
+
+ '@types/d3-geo@3.1.0':
+ dependencies:
+ '@types/geojson': 7946.0.16
+
+ '@types/d3-hierarchy@3.1.7': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.1': {}
+
+ '@types/d3-polygon@3.0.2': {}
+
+ '@types/d3-quadtree@3.0.6': {}
+
+ '@types/d3-random@3.0.3': {}
+
+ '@types/d3-scale-chromatic@3.1.0': {}
+
+ '@types/d3-scale@4.0.9':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-selection@3.0.11': {}
+
+ '@types/d3-shape@3.1.8':
+ dependencies:
+ '@types/d3-path': 3.1.1
+
+ '@types/d3-time-format@4.0.3': {}
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
+ '@types/d3-transition@3.0.9':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-zoom@3.0.8':
+ dependencies:
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3@7.4.3':
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-axis': 3.0.6
+ '@types/d3-brush': 3.0.6
+ '@types/d3-chord': 3.0.6
+ '@types/d3-color': 3.1.3
+ '@types/d3-contour': 3.0.6
+ '@types/d3-delaunay': 6.0.4
+ '@types/d3-dispatch': 3.0.7
+ '@types/d3-drag': 3.0.7
+ '@types/d3-dsv': 3.0.7
+ '@types/d3-ease': 3.0.2
+ '@types/d3-fetch': 3.0.7
+ '@types/d3-force': 3.0.10
+ '@types/d3-format': 3.0.4
+ '@types/d3-geo': 3.1.0
+ '@types/d3-hierarchy': 3.1.7
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-path': 3.1.1
+ '@types/d3-polygon': 3.0.2
+ '@types/d3-quadtree': 3.0.6
+ '@types/d3-random': 3.0.3
+ '@types/d3-scale': 4.0.9
+ '@types/d3-scale-chromatic': 3.1.0
+ '@types/d3-selection': 3.0.11
+ '@types/d3-shape': 3.1.8
+ '@types/d3-time': 3.0.4
+ '@types/d3-time-format': 4.0.3
+ '@types/d3-timer': 3.0.2
+ '@types/d3-transition': 3.0.9
+ '@types/d3-zoom': 3.0.8
+
+ '@types/debug@4.1.12':
+ dependencies:
+ '@types/ms': 2.1.0
+
+ '@types/estree@1.0.8': {}
+
+ '@types/fs-extra@9.0.13':
+ dependencies:
+ '@types/node': 22.19.15
+
+ '@types/geojson@7946.0.16': {}
+
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/http-cache-semantics@4.2.0': {}
+
+ '@types/keyv@3.1.4':
+ dependencies:
+ '@types/node': 22.19.15
+
+ '@types/linkify-it@5.0.0': {}
+
+ '@types/markdown-it@14.1.2':
+ dependencies:
+ '@types/linkify-it': 5.0.0
+ '@types/mdurl': 2.0.0
+
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/mdurl@2.0.0': {}
+
+ '@types/ms@2.1.0': {}
+
+ '@types/node-cron@3.0.11': {}
+
+ '@types/node@22.19.15':
+ dependencies:
+ undici-types: 6.21.0
+
+ '@types/node@24.12.0':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/nodemailer@7.0.11':
+ dependencies:
+ '@types/node': 22.19.15
+
+ '@types/pg@8.18.0':
+ dependencies:
+ '@types/node': 22.19.15
+ pg-protocol: 1.13.0
+ pg-types: 2.2.0
+
+ '@types/plist@3.0.5':
+ dependencies:
+ '@types/node': 22.19.15
+ xmlbuilder: 15.1.1
+ optional: true
+
+ '@types/prop-types@15.7.15': {}
+
+ '@types/react-dom@18.3.7(@types/react@18.3.28)':
+ dependencies:
+ '@types/react': 18.3.28
+
+ '@types/react@18.3.28':
+ dependencies:
+ '@types/prop-types': 15.7.15
+ csstype: 3.2.3
+
+ '@types/responselike@1.0.3':
+ dependencies:
+ '@types/node': 22.19.15
+
+ '@types/trusted-types@2.0.7':
+ optional: true
+
+ '@types/unist@3.0.3': {}
+
+ '@types/verror@1.10.11':
+ optional: true
+
+ '@types/web-bluetooth@0.0.21': {}
+
+ '@types/yauzl@2.10.3':
+ dependencies:
+ '@types/node': 22.19.15
+ optional: true
+
+ '@ungap/structured-clone@1.3.0': {}
+
+ '@upsetjs/venn.js@2.0.0':
+ optionalDependencies:
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ '@visactor/react-vchart@2.0.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@visactor/vchart': 2.0.19
+ '@visactor/vchart-extension': 2.0.19
+ '@visactor/vrender-core': 1.0.41
+ '@visactor/vrender-kits': 1.0.41
+ '@visactor/vutils': 1.0.23
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-is: 18.3.1
+
+ '@visactor/vchart-extension@2.0.19':
+ dependencies:
+ '@visactor/vchart': 2.0.19
+ '@visactor/vdataset': 1.0.23
+ '@visactor/vlayouts': 1.0.23
+ '@visactor/vrender-animate': 1.0.41
+ '@visactor/vrender-components': 1.0.41
+ '@visactor/vrender-core': 1.0.41
+ '@visactor/vrender-kits': 1.0.41
+ '@visactor/vutils': 1.0.23
+
+ '@visactor/vchart@2.0.19':
+ dependencies:
+ '@visactor/vdataset': 1.0.23
+ '@visactor/vlayouts': 1.0.23
+ '@visactor/vrender-animate': 1.0.41
+ '@visactor/vrender-components': 1.0.41
+ '@visactor/vrender-core': 1.0.41
+ '@visactor/vrender-kits': 1.0.41
+ '@visactor/vscale': 1.0.23
+ '@visactor/vutils': 1.0.23
+ '@visactor/vutils-extension': 2.0.19
+
+ '@visactor/vdataset@1.0.23':
+ dependencies:
+ '@turf/flatten': 6.5.0
+ '@turf/helpers': 6.5.0
+ '@turf/rewind': 6.5.0
+ '@visactor/vutils': 1.0.23
+ d3-dsv: 2.0.0
+ d3-geo: 1.12.1
+ d3-hexbin: 0.2.2
+ d3-hierarchy: 3.1.2
+ eventemitter3: 4.0.7
+ geobuf: 3.0.2
+ geojson-dissolve: 3.1.0
+ path-browserify: 1.0.1
+ pbf: 3.3.0
+ point-at-length: 1.1.0
+ simple-statistics: 7.8.9
+ simplify-geojson: 1.0.5
+ topojson-client: 3.1.0
+
+ '@visactor/vlayouts@1.0.23':
+ dependencies:
+ '@turf/helpers': 6.5.0
+ '@turf/invariant': 6.5.0
+ '@visactor/vscale': 1.0.23
+ '@visactor/vutils': 1.0.23
+ eventemitter3: 4.0.7
+
+ '@visactor/vrender-animate@1.0.41':
+ dependencies:
+ '@visactor/vrender-core': 1.0.41
+ '@visactor/vutils': 1.0.23
+
+ '@visactor/vrender-components@1.0.41':
+ dependencies:
+ '@visactor/vrender-animate': 1.0.41
+ '@visactor/vrender-core': 1.0.41
+ '@visactor/vrender-kits': 1.0.41
+ '@visactor/vscale': 1.0.23
+ '@visactor/vutils': 1.0.23
+
+ '@visactor/vrender-core@1.0.41':
+ dependencies:
+ '@visactor/vutils': 1.0.23
+ color-convert: 2.0.1
+
+ '@visactor/vrender-kits@1.0.41':
+ dependencies:
+ '@resvg/resvg-js': 2.4.1
+ '@visactor/vrender-core': 1.0.41
+ '@visactor/vutils': 1.0.23
+ gifuct-js: 2.1.2
+ lottie-web: 5.13.0
+ roughjs: 4.6.6
+
+ '@visactor/vscale@1.0.23':
+ dependencies:
+ '@visactor/vutils': 1.0.23
+
+ '@visactor/vutils-extension@2.0.19':
+ dependencies:
+ '@visactor/vdataset': 1.0.23
+ '@visactor/vutils': 1.0.23
+
+ '@visactor/vutils@1.0.23':
+ dependencies:
+ '@turf/helpers': 6.5.0
+ '@turf/invariant': 6.5.0
+ eventemitter3: 4.0.7
+
+ '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0))':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
+ '@rolldown/pluginutils': 1.0.0-beta.27
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.17.0
+ vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.32.0))(vue@3.5.30(typescript@5.9.3))':
+ dependencies:
+ vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.32.0)
+ vue: 3.5.30(typescript@5.9.3)
+
+ '@vitest/expect@2.1.9':
+ dependencies:
+ '@vitest/spy': 2.1.9
+ '@vitest/utils': 2.1.9
+ chai: 5.3.3
+ tinyrainbow: 1.2.0
+
+ '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.32.0))':
+ dependencies:
+ '@vitest/spy': 2.1.9
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.32.0)
+
+ '@vitest/pretty-format@2.1.9':
+ dependencies:
+ tinyrainbow: 1.2.0
+
+ '@vitest/runner@2.1.9':
+ dependencies:
+ '@vitest/utils': 2.1.9
+ pathe: 1.1.2
+
+ '@vitest/snapshot@2.1.9':
+ dependencies:
+ '@vitest/pretty-format': 2.1.9
+ magic-string: 0.30.21
+ pathe: 1.1.2
+
+ '@vitest/spy@2.1.9':
+ dependencies:
+ tinyspy: 3.0.2
+
+ '@vitest/utils@2.1.9':
+ dependencies:
+ '@vitest/pretty-format': 2.1.9
+ loupe: 3.2.1
+ tinyrainbow: 1.2.0
+
+ '@vue/compiler-core@3.5.30':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@vue/shared': 3.5.30
+ entities: 7.0.1
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.5.30':
+ dependencies:
+ '@vue/compiler-core': 3.5.30
+ '@vue/shared': 3.5.30
+
+ '@vue/compiler-sfc@3.5.30':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@vue/compiler-core': 3.5.30
+ '@vue/compiler-dom': 3.5.30
+ '@vue/compiler-ssr': 3.5.30
+ '@vue/shared': 3.5.30
+ estree-walker: 2.0.2
+ magic-string: 0.30.21
+ postcss: 8.5.8
+ source-map-js: 1.2.1
+
+ '@vue/compiler-ssr@3.5.30':
+ dependencies:
+ '@vue/compiler-dom': 3.5.30
+ '@vue/shared': 3.5.30
+
+ '@vue/devtools-api@7.7.9':
+ dependencies:
+ '@vue/devtools-kit': 7.7.9
+
+ '@vue/devtools-kit@7.7.9':
+ dependencies:
+ '@vue/devtools-shared': 7.7.9
+ birpc: 2.9.0
+ hookable: 5.5.3
+ mitt: 3.0.1
+ perfect-debounce: 1.0.0
+ speakingurl: 14.0.1
+ superjson: 2.2.6
+
+ '@vue/devtools-shared@7.7.9':
+ dependencies:
+ rfdc: 1.4.1
+
+ '@vue/reactivity@3.5.30':
+ dependencies:
+ '@vue/shared': 3.5.30
+
+ '@vue/runtime-core@3.5.30':
+ dependencies:
+ '@vue/reactivity': 3.5.30
+ '@vue/shared': 3.5.30
+
+ '@vue/runtime-dom@3.5.30':
+ dependencies:
+ '@vue/reactivity': 3.5.30
+ '@vue/runtime-core': 3.5.30
+ '@vue/shared': 3.5.30
+ csstype: 3.2.3
+
+ '@vue/server-renderer@3.5.30(vue@3.5.30(typescript@5.9.3))':
+ dependencies:
+ '@vue/compiler-ssr': 3.5.30
+ '@vue/shared': 3.5.30
+ vue: 3.5.30(typescript@5.9.3)
+
+ '@vue/shared@3.5.30': {}
+
+ '@vueuse/core@12.8.2(typescript@5.9.3)':
+ dependencies:
+ '@types/web-bluetooth': 0.0.21
+ '@vueuse/metadata': 12.8.2
+ '@vueuse/shared': 12.8.2(typescript@5.9.3)
+ vue: 3.5.30(typescript@5.9.3)
+ transitivePeerDependencies:
+ - typescript
+
+ '@vueuse/integrations@12.8.2(axios@1.13.6)(focus-trap@7.8.0)(typescript@5.9.3)':
+ dependencies:
+ '@vueuse/core': 12.8.2(typescript@5.9.3)
+ '@vueuse/shared': 12.8.2(typescript@5.9.3)
+ vue: 3.5.30(typescript@5.9.3)
+ optionalDependencies:
+ axios: 1.13.6
+ focus-trap: 7.8.0
+ transitivePeerDependencies:
+ - typescript
+
+ '@vueuse/metadata@12.8.2': {}
+
+ '@vueuse/shared@12.8.2(typescript@5.9.3)':
+ dependencies:
+ vue: 3.5.30(typescript@5.9.3)
+ transitivePeerDependencies:
+ - typescript
+
+ '@xmldom/xmldom@0.8.11': {}
+
+ abbrev@3.0.1: {}
+
+ abs-svg-path@0.1.1: {}
+
+ abstract-logging@2.0.1: {}
+
+ acorn@8.16.0: {}
+
+ agent-base@7.1.4: {}
+
+ ajv-formats@3.0.1(ajv@8.18.0):
+ optionalDependencies:
+ ajv: 8.18.0
+
+ ajv-keywords@3.5.2(ajv@6.14.0):
+ dependencies:
+ ajv: 6.14.0
+
+ ajv@6.14.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ajv@8.18.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ algoliasearch@5.49.1:
+ dependencies:
+ '@algolia/abtesting': 1.15.1
+ '@algolia/client-abtesting': 5.49.1
+ '@algolia/client-analytics': 5.49.1
+ '@algolia/client-common': 5.49.1
+ '@algolia/client-insights': 5.49.1
+ '@algolia/client-personalization': 5.49.1
+ '@algolia/client-query-suggestions': 5.49.1
+ '@algolia/client-search': 5.49.1
+ '@algolia/ingestion': 1.49.1
+ '@algolia/monitoring': 1.49.1
+ '@algolia/recommend': 5.49.1
+ '@algolia/requester-browser-xhr': 5.49.1
+ '@algolia/requester-fetch': 5.49.1
+ '@algolia/requester-node-http': 5.49.1
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansi-styles@6.2.3: {}
+
+ app-builder-bin@5.0.0-alpha.12: {}
+
+ app-builder-lib@26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1):
+ dependencies:
+ '@develar/schema-utils': 2.6.5
+ '@electron/asar': 3.4.1
+ '@electron/fuses': 1.8.0
+ '@electron/get': 3.1.0
+ '@electron/notarize': 2.5.0
+ '@electron/osx-sign': 1.3.3
+ '@electron/rebuild': 4.0.3
+ '@electron/universal': 2.0.3
+ '@malept/flatpak-bundler': 0.4.0
+ '@types/fs-extra': 9.0.13
+ async-exit-hook: 2.0.1
+ builder-util: 26.8.1
+ builder-util-runtime: 9.5.1
+ chromium-pickle-js: 0.2.0
+ ci-info: 4.3.1
+ debug: 4.4.3
+ dmg-builder: 26.8.1(electron-builder-squirrel-windows@26.8.1)
+ dotenv: 16.6.1
+ dotenv-expand: 11.0.7
+ ejs: 3.1.10
+ electron-builder-squirrel-windows: 26.8.1(dmg-builder@26.8.1)
+ electron-publish: 26.8.1
+ fs-extra: 10.1.0
+ hosted-git-info: 4.1.0
+ isbinaryfile: 5.0.7
+ jiti: 2.6.1
+ js-yaml: 4.1.1
+ json5: 2.2.3
+ lazy-val: 1.0.5
+ minimatch: 10.2.4
+ plist: 3.1.0
+ proper-lockfile: 4.1.2
+ resedit: 1.7.2
+ semver: 7.7.4
+ tar: 7.5.11
+ temp-file: 3.4.0
+ tiny-async-pool: 1.3.0
+ which: 5.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ argparse@2.0.1: {}
+
+ array-source@0.0.4: {}
+
+ assert-plus@1.0.0:
+ optional: true
+
+ assertion-error@2.0.1: {}
+
+ astral-regex@2.0.0:
+ optional: true
+
+ async-exit-hook@2.0.1: {}
+
+ async@3.2.6: {}
+
+ asynckit@0.4.0: {}
+
+ at-least-node@1.0.0: {}
+
+ atomic-sleep@1.0.0: {}
+
+ avvio@9.2.0:
+ dependencies:
+ '@fastify/error': 4.2.0
+ fastq: 1.20.1
+
+ aws-ssl-profiles@1.1.2: {}
+
+ axios@1.13.6:
+ dependencies:
+ follow-redirects: 1.15.11
+ form-data: 4.0.5
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ balanced-match@1.0.2: {}
+
+ balanced-match@4.0.4: {}
+
+ base64-js@1.5.1: {}
+
+ baseline-browser-mapping@2.10.0: {}
+
+ better-sqlite3@12.8.0:
+ dependencies:
+ bindings: 1.5.0
+ prebuild-install: 7.1.3
+
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
+
+ bindings@1.5.0:
+ dependencies:
+ file-uri-to-path: 1.0.0
+
+ birpc@2.9.0: {}
+
+ bl@4.1.0:
+ dependencies:
+ buffer: 5.7.1
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
+ boolean@3.2.0:
+ optional: true
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ brace-expansion@5.0.4:
+ dependencies:
+ balanced-match: 4.0.4
+
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.10.0
+ caniuse-lite: 1.0.30001777
+ electron-to-chromium: 1.5.307
+ node-releases: 2.0.36
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
+
+ buffer-crc32@0.2.13: {}
+
+ buffer-from@1.1.2: {}
+
+ buffer@5.7.1:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ builder-util-runtime@9.5.1:
+ dependencies:
+ debug: 4.4.3
+ sax: 1.5.0
+ transitivePeerDependencies:
+ - supports-color
+
+ builder-util@26.8.1:
+ dependencies:
+ 7zip-bin: 5.2.0
+ '@types/debug': 4.1.12
+ app-builder-bin: 5.0.0-alpha.12
+ builder-util-runtime: 9.5.1
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ fs-extra: 10.1.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ js-yaml: 4.1.1
+ sanitize-filename: 1.6.3
+ source-map-support: 0.5.21
+ stat-mode: 1.0.0
+ temp-file: 3.4.0
+ tiny-async-pool: 1.3.0
+ transitivePeerDependencies:
+ - supports-color
+
+ cac@6.7.14: {}
+
+ cacache@19.0.1:
+ dependencies:
+ '@npmcli/fs': 4.0.0
+ fs-minipass: 3.0.3
+ glob: 10.5.0
+ lru-cache: 10.4.3
+ minipass: 7.1.3
+ minipass-collect: 2.0.1
+ minipass-flush: 1.0.5
+ minipass-pipeline: 1.2.4
+ p-map: 7.0.4
+ ssri: 12.0.0
+ tar: 7.5.11
+ unique-filename: 4.0.0
+
+ cacheable-lookup@5.0.4: {}
+
+ cacheable-request@7.0.4:
+ dependencies:
+ clone-response: 1.0.3
+ get-stream: 5.2.0
+ http-cache-semantics: 4.2.0
+ keyv: 4.5.4
+ lowercase-keys: 2.0.0
+ normalize-url: 6.1.0
+ responselike: 2.0.1
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ caniuse-lite@1.0.30001777: {}
+
+ ccount@2.0.1: {}
+
+ chai@5.3.3:
+ dependencies:
+ assertion-error: 2.0.1
+ check-error: 2.1.3
+ deep-eql: 5.0.2
+ loupe: 3.2.1
+ pathval: 2.0.1
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ check-error@2.1.3: {}
+
+ chevrotain-allstar@0.3.1(chevrotain@11.1.2):
+ dependencies:
+ chevrotain: 11.1.2
+ lodash-es: 4.17.23
+
+ chevrotain@11.1.2:
+ dependencies:
+ '@chevrotain/cst-dts-gen': 11.1.2
+ '@chevrotain/gast': 11.1.2
+ '@chevrotain/regexp-to-ast': 11.1.2
+ '@chevrotain/types': 11.1.2
+ '@chevrotain/utils': 11.1.2
+ lodash-es: 4.17.23
+
+ chownr@1.1.4: {}
+
+ chownr@3.0.0: {}
+
+ chromium-pickle-js@0.2.0: {}
+
+ ci-info@4.3.1: {}
+
+ ci-info@4.4.0: {}
+
+ cli-cursor@3.1.0:
+ dependencies:
+ restore-cursor: 3.1.0
+
+ cli-spinners@2.9.2: {}
+
+ cli-truncate@2.1.0:
+ dependencies:
+ slice-ansi: 3.0.0
+ string-width: 4.2.3
+ optional: true
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ clone-response@1.0.3:
+ dependencies:
+ mimic-response: 1.0.1
+
+ clone@1.0.4: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ comma-separated-tokens@2.0.3: {}
+
+ commander@2.20.3: {}
+
+ commander@5.1.0: {}
+
+ commander@7.2.0: {}
+
+ commander@8.3.0: {}
+
+ commander@9.5.0:
+ optional: true
+
+ compare-version@0.1.2: {}
+
+ concat-map@0.0.1: {}
+
+ concat-stream@1.4.11:
+ dependencies:
+ inherits: 2.0.4
+ readable-stream: 1.1.14
+ typedarray: 0.0.7
+
+ concat-stream@2.0.0:
+ dependencies:
+ buffer-from: 1.1.2
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ typedarray: 0.0.6
+
+ concurrently@9.2.1:
+ dependencies:
+ chalk: 4.1.2
+ rxjs: 7.8.2
+ shell-quote: 1.8.3
+ supports-color: 8.1.1
+ tree-kill: 1.2.2
+ yargs: 17.7.2
+
+ confbox@0.1.8: {}
+
+ content-disposition@1.0.1: {}
+
+ convert-source-map@2.0.0: {}
+
+ cookie@1.1.1: {}
+
+ copy-anything@4.0.5:
+ dependencies:
+ is-what: 5.5.0
+
+ core-util-is@1.0.2:
+ optional: true
+
+ core-util-is@1.0.3: {}
+
+ cose-base@1.0.3:
+ dependencies:
+ layout-base: 1.0.2
+
+ cose-base@2.2.0:
+ dependencies:
+ layout-base: 2.0.1
+
+ crc@3.8.0:
+ dependencies:
+ buffer: 5.7.1
+ optional: true
+
+ cross-dirname@0.1.0:
+ optional: true
+
+ cross-env@7.0.3:
+ dependencies:
+ cross-spawn: 7.0.6
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.1
+
+ csstype@3.2.3: {}
+
+ cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1):
+ dependencies:
+ cose-base: 1.0.3
+ cytoscape: 3.33.1
+
+ cytoscape-fcose@2.2.0(cytoscape@3.33.1):
+ dependencies:
+ cose-base: 2.2.0
+ cytoscape: 3.33.1
+
+ cytoscape@3.33.1: {}
+
+ d3-array@1.2.4: {}
+
+ d3-array@2.12.1:
+ dependencies:
+ internmap: 1.0.1
+
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-axis@3.0.0: {}
+
+ d3-brush@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ d3-chord@3.0.1:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-color@3.1.0: {}
+
+ d3-contour@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-delaunay@6.0.4:
+ dependencies:
+ delaunator: 5.0.1
+
+ d3-dispatch@3.0.1: {}
+
+ d3-drag@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
+
+ d3-dsv@2.0.0:
+ dependencies:
+ commander: 2.20.3
+ iconv-lite: 0.4.24
+ rw: 1.3.3
+
+ d3-dsv@3.0.1:
+ dependencies:
+ commander: 7.2.0
+ iconv-lite: 0.6.3
+ rw: 1.3.3
+
+ d3-ease@3.0.1: {}
+
+ d3-fetch@3.0.1:
+ dependencies:
+ d3-dsv: 3.0.1
+
+ d3-force@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-timer: 3.0.1
+
+ d3-format@3.1.2: {}
+
+ d3-geo@1.12.1:
+ dependencies:
+ d3-array: 1.2.4
+
+ d3-geo@3.1.1:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-hexbin@0.2.2: {}
+
+ d3-hierarchy@3.1.2: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-path@1.0.9: {}
+
+ d3-path@3.1.0: {}
+
+ d3-polygon@3.0.1: {}
+
+ d3-quadtree@3.0.1: {}
+
+ d3-random@3.0.1: {}
+
+ d3-sankey@0.12.3:
+ dependencies:
+ d3-array: 2.12.1
+ d3-shape: 1.3.7
+
+ d3-scale-chromatic@3.1.0:
+ dependencies:
+ d3-color: 3.1.0
+ d3-interpolate: 3.0.1
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
+ d3-selection@3.0.0: {}
+
+ d3-shape@1.3.7:
+ dependencies:
+ d3-path: 1.0.9
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-timer@3.0.1: {}
+
+ d3-transition@3.0.1(d3-selection@3.0.0):
+ dependencies:
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
+
+ d3-zoom@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ d3@7.9.0:
+ dependencies:
+ d3-array: 3.2.4
+ d3-axis: 3.0.0
+ d3-brush: 3.0.0
+ d3-chord: 3.0.1
+ d3-color: 3.1.0
+ d3-contour: 4.0.2
+ d3-delaunay: 6.0.4
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-dsv: 3.0.1
+ d3-ease: 3.0.1
+ d3-fetch: 3.0.1
+ d3-force: 3.0.0
+ d3-format: 3.1.2
+ d3-geo: 3.1.1
+ d3-hierarchy: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-path: 3.1.0
+ d3-polygon: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-random: 3.0.1
+ d3-scale: 4.0.2
+ d3-scale-chromatic: 3.1.0
+ d3-selection: 3.0.0
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+ d3-timer: 3.0.1
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+ d3-zoom: 3.0.0
+
+ dagre-d3-es@7.0.14:
+ dependencies:
+ d3: 7.9.0
+ lodash-es: 4.17.23
+
+ data-urls@7.0.0:
+ dependencies:
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ dayjs@1.11.20: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ decimal.js@10.6.0: {}
+
+ decompress-response@6.0.0:
+ dependencies:
+ mimic-response: 3.1.0
+
+ deep-eql@5.0.2: {}
+
+ deep-extend@0.6.0: {}
+
+ defaults@1.0.4:
+ dependencies:
+ clone: 1.0.4
+
+ defer-to-connect@2.0.1: {}
+
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+ optional: true
+
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+ optional: true
+
+ delaunator@5.0.1:
+ dependencies:
+ robust-predicates: 3.0.2
+
+ delayed-stream@1.0.0: {}
+
+ denque@2.1.0: {}
+
+ depd@2.0.0: {}
+
+ dequal@2.0.3: {}
+
+ detect-libc@2.1.2: {}
+
+ detect-node@2.1.0:
+ optional: true
+
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
+ dir-compare@4.2.0:
+ dependencies:
+ minimatch: 3.1.5
+ p-limit: 3.1.0
+
+ dmg-builder@26.8.1(electron-builder-squirrel-windows@26.8.1):
+ dependencies:
+ app-builder-lib: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1)
+ builder-util: 26.8.1
+ fs-extra: 10.1.0
+ iconv-lite: 0.6.3
+ js-yaml: 4.1.1
+ optionalDependencies:
+ dmg-license: 1.0.11
+ transitivePeerDependencies:
+ - electron-builder-squirrel-windows
+ - supports-color
+
+ dmg-license@1.0.11:
+ dependencies:
+ '@types/plist': 3.0.5
+ '@types/verror': 1.10.11
+ ajv: 6.14.0
+ crc: 3.8.0
+ iconv-corefoundation: 1.1.7
+ plist: 3.1.0
+ smart-buffer: 4.2.0
+ verror: 1.10.1
+ optional: true
+
+ dompurify@3.3.3:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
+ dotenv-expand@11.0.7:
+ dependencies:
+ dotenv: 16.6.1
+
+ dotenv@16.6.1: {}
+
+ dotenv@17.3.1: {}
+
+ drizzle-kit@0.31.10:
+ dependencies:
+ '@drizzle-team/brocli': 0.10.2
+ '@esbuild-kit/esm-loader': 2.6.5
+ esbuild: 0.25.12
+ tsx: 4.21.0
+
+ drizzle-orm@0.45.1(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@12.8.0)(mysql2@3.20.0(@types/node@22.19.15))(pg@8.20.0):
+ optionalDependencies:
+ '@types/better-sqlite3': 7.6.13
+ '@types/pg': 8.18.0
+ better-sqlite3: 12.8.0
+ mysql2: 3.20.0(@types/node@22.19.15)
+ pg: 8.20.0
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ eastasianwidth@0.2.0: {}
+
+ ejs@3.1.10:
+ dependencies:
+ jake: 10.9.4
+
+ electron-builder-squirrel-windows@26.8.1(dmg-builder@26.8.1):
+ dependencies:
+ app-builder-lib: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1)
+ builder-util: 26.8.1
+ electron-winstaller: 5.4.0
+ transitivePeerDependencies:
+ - dmg-builder
+ - supports-color
+
+ electron-builder@26.8.1(electron-builder-squirrel-windows@26.8.1):
+ dependencies:
+ app-builder-lib: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1)
+ builder-util: 26.8.1
+ builder-util-runtime: 9.5.1
+ chalk: 4.1.2
+ ci-info: 4.4.0
+ dmg-builder: 26.8.1(electron-builder-squirrel-windows@26.8.1)
+ fs-extra: 10.1.0
+ lazy-val: 1.0.5
+ simple-update-notifier: 2.0.0
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - electron-builder-squirrel-windows
+ - supports-color
+
+ electron-log@5.4.3: {}
+
+ electron-publish@26.8.1:
+ dependencies:
+ '@types/fs-extra': 9.0.13
+ builder-util: 26.8.1
+ builder-util-runtime: 9.5.1
+ chalk: 4.1.2
+ form-data: 4.0.5
+ fs-extra: 10.1.0
+ lazy-val: 1.0.5
+ mime: 2.6.0
+ transitivePeerDependencies:
+ - supports-color
+
+ electron-to-chromium@1.5.307: {}
+
+ electron-updater@6.8.3:
+ dependencies:
+ builder-util-runtime: 9.5.1
+ fs-extra: 10.1.0
+ js-yaml: 4.1.1
+ lazy-val: 1.0.5
+ lodash.escaperegexp: 4.1.2
+ lodash.isequal: 4.5.0
+ semver: 7.7.4
+ tiny-typed-emitter: 2.1.0
+ transitivePeerDependencies:
+ - supports-color
+
+ electron-winstaller@5.4.0:
+ dependencies:
+ '@electron/asar': 3.4.1
+ debug: 4.4.3
+ fs-extra: 7.0.1
+ lodash: 4.17.23
+ temp: 0.9.4
+ optionalDependencies:
+ '@electron/windows-sign': 1.2.2
+ transitivePeerDependencies:
+ - supports-color
+
+ electron@41.0.3:
+ dependencies:
+ '@electron/get': 2.0.3
+ '@types/node': 24.12.0
+ extract-zip: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ emoji-regex-xs@1.0.0: {}
+
+ emoji-regex@8.0.0: {}
+
+ emoji-regex@9.2.2: {}
+
+ encoding@0.1.13:
+ dependencies:
+ iconv-lite: 0.6.3
+ optional: true
+
+ end-of-stream@1.4.5:
+ dependencies:
+ once: 1.4.0
+
+ enhanced-resolve@5.20.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
+
+ entities@6.0.1: {}
+
+ entities@7.0.1: {}
+
+ env-paths@2.2.1: {}
+
+ err-code@2.0.3: {}
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-module-lexer@1.7.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ es6-error@4.1.1:
+ optional: true
+
+ esbuild@0.18.20:
+ optionalDependencies:
+ '@esbuild/android-arm': 0.18.20
+ '@esbuild/android-arm64': 0.18.20
+ '@esbuild/android-x64': 0.18.20
+ '@esbuild/darwin-arm64': 0.18.20
+ '@esbuild/darwin-x64': 0.18.20
+ '@esbuild/freebsd-arm64': 0.18.20
+ '@esbuild/freebsd-x64': 0.18.20
+ '@esbuild/linux-arm': 0.18.20
+ '@esbuild/linux-arm64': 0.18.20
+ '@esbuild/linux-ia32': 0.18.20
+ '@esbuild/linux-loong64': 0.18.20
+ '@esbuild/linux-mips64el': 0.18.20
+ '@esbuild/linux-ppc64': 0.18.20
+ '@esbuild/linux-riscv64': 0.18.20
+ '@esbuild/linux-s390x': 0.18.20
+ '@esbuild/linux-x64': 0.18.20
+ '@esbuild/netbsd-x64': 0.18.20
+ '@esbuild/openbsd-x64': 0.18.20
+ '@esbuild/sunos-x64': 0.18.20
+ '@esbuild/win32-arm64': 0.18.20
+ '@esbuild/win32-ia32': 0.18.20
+ '@esbuild/win32-x64': 0.18.20
+
+ esbuild@0.21.5:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.21.5
+ '@esbuild/android-arm': 0.21.5
+ '@esbuild/android-arm64': 0.21.5
+ '@esbuild/android-x64': 0.21.5
+ '@esbuild/darwin-arm64': 0.21.5
+ '@esbuild/darwin-x64': 0.21.5
+ '@esbuild/freebsd-arm64': 0.21.5
+ '@esbuild/freebsd-x64': 0.21.5
+ '@esbuild/linux-arm': 0.21.5
+ '@esbuild/linux-arm64': 0.21.5
+ '@esbuild/linux-ia32': 0.21.5
+ '@esbuild/linux-loong64': 0.21.5
+ '@esbuild/linux-mips64el': 0.21.5
+ '@esbuild/linux-ppc64': 0.21.5
+ '@esbuild/linux-riscv64': 0.21.5
+ '@esbuild/linux-s390x': 0.21.5
+ '@esbuild/linux-x64': 0.21.5
+ '@esbuild/netbsd-x64': 0.21.5
+ '@esbuild/openbsd-x64': 0.21.5
+ '@esbuild/sunos-x64': 0.21.5
+ '@esbuild/win32-arm64': 0.21.5
+ '@esbuild/win32-ia32': 0.21.5
+ '@esbuild/win32-x64': 0.21.5
+
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
+ esbuild@0.27.3:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.27.3
+ '@esbuild/android-arm': 0.27.3
+ '@esbuild/android-arm64': 0.27.3
+ '@esbuild/android-x64': 0.27.3
+ '@esbuild/darwin-arm64': 0.27.3
+ '@esbuild/darwin-x64': 0.27.3
+ '@esbuild/freebsd-arm64': 0.27.3
+ '@esbuild/freebsd-x64': 0.27.3
+ '@esbuild/linux-arm': 0.27.3
+ '@esbuild/linux-arm64': 0.27.3
+ '@esbuild/linux-ia32': 0.27.3
+ '@esbuild/linux-loong64': 0.27.3
+ '@esbuild/linux-mips64el': 0.27.3
+ '@esbuild/linux-ppc64': 0.27.3
+ '@esbuild/linux-riscv64': 0.27.3
+ '@esbuild/linux-s390x': 0.27.3
+ '@esbuild/linux-x64': 0.27.3
+ '@esbuild/netbsd-arm64': 0.27.3
+ '@esbuild/netbsd-x64': 0.27.3
+ '@esbuild/openbsd-arm64': 0.27.3
+ '@esbuild/openbsd-x64': 0.27.3
+ '@esbuild/openharmony-arm64': 0.27.3
+ '@esbuild/sunos-x64': 0.27.3
+ '@esbuild/win32-arm64': 0.27.3
+ '@esbuild/win32-ia32': 0.27.3
+ '@esbuild/win32-x64': 0.27.3
+
+ escalade@3.2.0: {}
+
+ escape-html@1.0.3: {}
+
+ escape-string-regexp@4.0.0:
+ optional: true
+
+ estree-walker@2.0.2: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ eventemitter3@4.0.7: {}
+
+ expand-template@2.0.3: {}
+
+ expect-type@1.3.0: {}
+
+ exponential-backoff@3.1.3: {}
+
+ extract-zip@2.0.1:
+ dependencies:
+ debug: 4.4.3
+ get-stream: 5.2.0
+ yauzl: 2.10.0
+ optionalDependencies:
+ '@types/yauzl': 2.10.3
+ transitivePeerDependencies:
+ - supports-color
+
+ extsprintf@1.4.1:
+ optional: true
+
+ fast-decode-uri-component@1.0.1: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-json-stringify@6.3.0:
+ dependencies:
+ '@fastify/merge-json-schemas': 0.2.1
+ ajv: 8.18.0
+ ajv-formats: 3.0.1(ajv@8.18.0)
+ fast-uri: 3.1.0
+ json-schema-ref-resolver: 3.0.0
+ rfdc: 1.4.1
+
+ fast-querystring@1.1.2:
+ dependencies:
+ fast-decode-uri-component: 1.0.1
+
+ fast-uri@3.1.0: {}
+
+ fastify-plugin@5.1.0: {}
+
+ fastify@5.8.2:
+ dependencies:
+ '@fastify/ajv-compiler': 4.0.5
+ '@fastify/error': 4.2.0
+ '@fastify/fast-json-stringify-compiler': 5.0.3
+ '@fastify/proxy-addr': 5.1.0
+ abstract-logging: 2.0.1
+ avvio: 9.2.0
+ fast-json-stringify: 6.3.0
+ find-my-way: 9.5.0
+ light-my-request: 6.6.0
+ pino: 10.3.1
+ process-warning: 5.0.0
+ rfdc: 1.4.1
+ secure-json-parse: 4.1.0
+ semver: 7.7.4
+ toad-cache: 3.7.0
+
+ fastq@1.20.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fd-slicer@1.1.0:
+ dependencies:
+ pend: 1.2.0
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ file-source@0.6.1:
+ dependencies:
+ stream-source: 0.3.5
+
+ file-uri-to-path@1.0.0: {}
+
+ filelist@1.0.6:
+ dependencies:
+ minimatch: 5.1.9
+
+ find-my-way@9.5.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-querystring: 1.1.2
+ safe-regex2: 5.0.0
+
+ focus-trap@7.8.0:
+ dependencies:
+ tabbable: 6.4.0
+
+ follow-redirects@1.15.11: {}
+
+ foreground-child@3.3.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
+ fs-constants@1.0.0: {}
+
+ fs-extra@10.1.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
+ fs-extra@11.3.4:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
+ fs-extra@7.0.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 4.0.0
+ universalify: 0.1.2
+
+ fs-extra@8.1.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 4.0.0
+ universalify: 0.1.2
+
+ fs-extra@9.1.0:
+ dependencies:
+ at-least-node: 1.0.0
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
+ fs-minipass@3.0.3:
+ dependencies:
+ minipass: 7.1.3
+
+ fs.realpath@1.0.0: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ generate-function@2.3.1:
+ dependencies:
+ is-property: 1.0.2
+
+ gensync@1.0.0-beta.2: {}
+
+ geobuf@3.0.2:
+ dependencies:
+ concat-stream: 2.0.0
+ pbf: 3.3.0
+ shapefile: 0.6.6
+
+ geojson-dissolve@3.1.0:
+ dependencies:
+ '@turf/meta': 3.14.0
+ geojson-flatten: 0.2.4
+ geojson-linestring-dissolve: 0.0.1
+ topojson-client: 3.1.0
+ topojson-server: 3.0.1
+
+ geojson-flatten@0.2.4:
+ dependencies:
+ get-stdin: 6.0.0
+ minimist: 1.2.0
+
+ geojson-linestring-dissolve@0.0.1: {}
+
+ get-caller-file@2.0.5: {}
+
+ get-intrinsic@1.3.0:
+ 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
+
+ get-port@7.1.0: {}
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-stdin@6.0.0: {}
+
+ get-stream@5.2.0:
+ dependencies:
+ pump: 3.0.4
+
+ get-tsconfig@4.13.6:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ gifuct-js@2.1.2:
+ dependencies:
+ js-binary-schema-parser: 2.0.3
+
+ github-from-package@0.0.0: {}
+
+ glob@10.5.0:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.9
+ minipass: 7.1.3
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
+ glob@13.0.6:
+ dependencies:
+ minimatch: 10.2.4
+ minipass: 7.1.3
+ path-scurry: 2.0.2
+
+ glob@7.2.3:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.5
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
+ global-agent@3.0.0:
+ dependencies:
+ boolean: 3.2.0
+ es6-error: 4.1.1
+ matcher: 3.0.0
+ roarr: 2.15.4
+ semver: 7.7.4
+ serialize-error: 7.0.1
+ optional: true
+
+ globalthis@1.0.4:
+ dependencies:
+ define-properties: 1.2.1
+ gopd: 1.2.0
+ optional: true
+
+ gopd@1.2.0: {}
+
+ got@11.8.6:
+ dependencies:
+ '@sindresorhus/is': 4.6.0
+ '@szmarczak/http-timer': 4.0.6
+ '@types/cacheable-request': 6.0.3
+ '@types/responselike': 1.0.3
+ cacheable-lookup: 5.0.4
+ cacheable-request: 7.0.4
+ decompress-response: 6.0.0
+ http2-wrapper: 1.0.3
+ lowercase-keys: 2.0.0
+ p-cancelable: 2.1.1
+ responselike: 2.0.1
+
+ graceful-fs@4.2.11: {}
+
+ hachure-fill@0.5.2: {}
+
+ has-flag@4.0.0: {}
+
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+ optional: true
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ hast-util-to-html@9.0.5:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ comma-separated-tokens: 2.0.3
+ hast-util-whitespace: 3.0.0
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ stringify-entities: 4.0.4
+ zwitch: 2.0.4
+
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hookable@5.5.3: {}
+
+ hosted-git-info@4.1.0:
+ dependencies:
+ lru-cache: 6.0.0
+
+ html-encoding-sniffer@6.0.0:
+ dependencies:
+ '@exodus/bytes': 1.15.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ html-void-elements@3.0.0: {}
+
+ http-cache-semantics@4.2.0: {}
+
+ http-errors@2.0.1:
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.2
+ toidentifier: 1.0.1
+
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ http2-wrapper@1.0.3:
+ dependencies:
+ quick-lru: 5.1.1
+ resolve-alpn: 1.2.1
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ iconv-corefoundation@1.1.7:
+ dependencies:
+ cli-truncate: 2.1.0
+ node-addon-api: 1.7.2
+ optional: true
+
+ iconv-lite@0.4.24:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ iconv-lite@0.7.2:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ ieee754@1.2.1: {}
+
+ imurmurhash@0.1.4: {}
+
+ inflight@1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ inherits@2.0.4: {}
+
+ ini@1.3.8: {}
+
+ internmap@1.0.1: {}
+
+ internmap@2.0.3: {}
+
+ ip-address@10.1.0: {}
+
+ ipaddr.js@2.3.0: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-interactive@1.0.0: {}
+
+ is-potential-custom-element-name@1.0.1: {}
+
+ is-property@1.0.2: {}
+
+ is-unicode-supported@0.1.0: {}
+
+ is-what@5.5.0: {}
+
+ isarray@0.0.1: {}
+
+ isbinaryfile@4.0.10: {}
+
+ isbinaryfile@5.0.7: {}
+
+ isexe@2.0.0: {}
+
+ isexe@3.1.5: {}
+
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
+ jake@10.9.4:
+ dependencies:
+ async: 3.2.6
+ filelist: 1.0.6
+ picocolors: 1.1.1
+
+ jiti@2.6.1: {}
+
+ joi@18.0.2:
+ dependencies:
+ '@hapi/address': 5.1.1
+ '@hapi/formula': 3.0.2
+ '@hapi/hoek': 11.0.7
+ '@hapi/pinpoint': 2.0.1
+ '@hapi/tlds': 1.1.6
+ '@hapi/topo': 6.0.2
+ '@standard-schema/spec': 1.1.0
+
+ js-binary-schema-parser@2.0.3: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ jsdom@29.0.1:
+ dependencies:
+ '@asamuzakjp/css-color': 5.0.1
+ '@asamuzakjp/dom-selector': 7.0.3
+ '@bramus/specificity': 2.4.2
+ '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1)
+ '@exodus/bytes': 1.15.0
+ css-tree: 3.2.1
+ data-urls: 7.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 6.0.0
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.2.7
+ parse5: 8.0.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.1
+ undici: 7.24.5
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.1
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-ref-resolver@3.0.0:
+ dependencies:
+ dequal: 2.0.3
+
+ json-schema-traverse@0.4.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json-stringify-safe@5.0.1:
+ optional: true
+
+ json5@2.2.3: {}
+
+ jsonfile@4.0.0:
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
+ jsonfile@6.2.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
+ katex@0.16.39:
+ dependencies:
+ commander: 8.3.0
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ khroma@2.1.0: {}
+
+ langium@4.2.1:
+ dependencies:
+ chevrotain: 11.1.2
+ chevrotain-allstar: 0.3.1(chevrotain@11.1.2)
+ vscode-languageserver: 9.0.1
+ vscode-languageserver-textdocument: 1.0.12
+ vscode-uri: 3.1.0
+
+ layout-base@1.0.2: {}
+
+ layout-base@2.0.1: {}
+
+ lazy-val@1.0.5: {}
+
+ light-my-request@6.6.0:
+ dependencies:
+ cookie: 1.1.1
+ process-warning: 4.0.1
+ set-cookie-parser: 2.7.2
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ 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
+
+ lodash-es@4.17.23: {}
+
+ lodash.escaperegexp@4.1.2: {}
+
+ lodash.isequal@4.5.0: {}
+
+ lodash@4.17.23: {}
+
+ log-symbols@4.1.0:
+ dependencies:
+ chalk: 4.1.2
+ is-unicode-supported: 0.1.0
+
+ long@5.3.2: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ lottie-web@5.13.0: {}
+
+ loupe@3.2.1: {}
+
+ lowercase-keys@2.0.0: {}
+
+ lru-cache@10.4.3: {}
+
+ lru-cache@11.2.7: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ lru-cache@6.0.0:
+ dependencies:
+ yallist: 4.0.0
+
+ lru.min@1.1.4: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ make-fetch-happen@14.0.3:
+ dependencies:
+ '@npmcli/agent': 3.0.0
+ cacache: 19.0.1
+ http-cache-semantics: 4.2.0
+ minipass: 7.1.3
+ minipass-fetch: 4.0.1
+ minipass-flush: 1.0.5
+ minipass-pipeline: 1.2.4
+ negotiator: 1.0.0
+ proc-log: 5.0.0
+ promise-retry: 2.0.1
+ ssri: 12.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ mark.js@8.11.1: {}
+
+ marked@16.4.2: {}
+
+ marked@17.0.4: {}
+
+ matcher@3.0.0:
+ dependencies:
+ escape-string-regexp: 4.0.0
+ optional: true
+
+ math-intrinsics@1.1.0: {}
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.0
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+
+ mdn-data@2.27.1: {}
+
+ mermaid@11.13.0:
+ dependencies:
+ '@braintree/sanitize-url': 7.1.2
+ '@iconify/utils': 3.1.0
+ '@mermaid-js/parser': 1.0.1
+ '@types/d3': 7.4.3
+ '@upsetjs/venn.js': 2.0.0
+ cytoscape: 3.33.1
+ cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1)
+ cytoscape-fcose: 2.2.0(cytoscape@3.33.1)
+ d3: 7.9.0
+ d3-sankey: 0.12.3
+ dagre-d3-es: 7.0.14
+ dayjs: 1.11.20
+ dompurify: 3.3.3
+ katex: 0.16.39
+ khroma: 2.1.0
+ lodash-es: 4.17.23
+ marked: 16.4.2
+ roughjs: 4.6.6
+ stylis: 4.3.6
+ ts-dedent: 2.2.0
+ uuid: 11.1.0
+
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.2: {}
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ mime@2.6.0: {}
+
+ mime@3.0.0: {}
+
+ mimic-fn@2.1.0: {}
+
+ mimic-response@1.0.1: {}
+
+ mimic-response@3.1.0: {}
+
+ minimatch@10.2.4:
+ dependencies:
+ brace-expansion: 5.0.4
+
+ minimatch@3.1.5:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimatch@5.1.9:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ minimatch@9.0.9:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ minimist@1.2.0: {}
+
+ minimist@1.2.6: {}
+
+ minimist@1.2.8: {}
+
+ minipass-collect@2.0.1:
+ dependencies:
+ minipass: 7.1.3
+
+ minipass-fetch@4.0.1:
+ dependencies:
+ minipass: 7.1.3
+ minipass-sized: 1.0.3
+ minizlib: 3.1.0
+ optionalDependencies:
+ encoding: 0.1.13
+
+ minipass-flush@1.0.5:
+ dependencies:
+ minipass: 3.3.6
+
+ minipass-pipeline@1.2.4:
+ dependencies:
+ minipass: 3.3.6
+
+ minipass-sized@1.0.3:
+ dependencies:
+ minipass: 3.3.6
+
+ minipass@3.3.6:
+ dependencies:
+ yallist: 4.0.0
+
+ minipass@7.1.3: {}
+
+ minisearch@7.2.0: {}
+
+ minizlib@3.1.0:
+ dependencies:
+ minipass: 7.1.3
+
+ mitt@3.0.1: {}
+
+ mkdirp-classic@0.5.3: {}
+
+ mkdirp@0.5.6:
+ dependencies:
+ minimist: 1.2.8
+
+ mlly@1.8.2:
+ dependencies:
+ acorn: 8.16.0
+ pathe: 2.0.3
+ pkg-types: 1.3.1
+ ufo: 1.6.3
+
+ ms@2.1.3: {}
+
+ mysql2@3.20.0(@types/node@22.19.15):
+ dependencies:
+ '@types/node': 22.19.15
+ aws-ssl-profiles: 1.1.2
+ denque: 2.1.0
+ generate-function: 2.3.1
+ iconv-lite: 0.7.2
+ long: 5.3.2
+ lru.min: 1.1.4
+ named-placeholders: 1.1.6
+ sql-escaper: 1.3.3
+
+ named-placeholders@1.1.6:
+ dependencies:
+ lru.min: 1.1.4
+
+ nanoid@3.3.11: {}
+
+ napi-build-utils@2.0.0: {}
+
+ negotiator@1.0.0: {}
+
+ node-abi@3.89.0:
+ dependencies:
+ semver: 7.7.4
+
+ node-abi@4.26.0:
+ dependencies:
+ semver: 7.7.4
+
+ node-addon-api@1.7.2:
+ optional: true
+
+ node-api-version@0.2.1:
+ dependencies:
+ semver: 7.7.4
+
+ node-cron@4.2.1: {}
+
+ node-gyp@11.5.0:
+ dependencies:
+ env-paths: 2.2.1
+ exponential-backoff: 3.1.3
+ graceful-fs: 4.2.11
+ make-fetch-happen: 14.0.3
+ nopt: 8.1.0
+ proc-log: 5.0.0
+ semver: 7.7.4
+ tar: 7.5.11
+ tinyglobby: 0.2.15
+ which: 5.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ node-releases@2.0.36: {}
+
+ nodemailer@8.0.3: {}
+
+ non-layered-tidy-tree-layout@2.0.2:
+ optional: true
+
+ nopt@8.1.0:
+ dependencies:
+ abbrev: 3.0.1
+
+ normalize-url@6.1.0: {}
+
+ object-assign@4.1.1: {}
+
+ object-keys@1.1.1:
+ optional: true
+
+ on-exit-leak-free@2.1.2: {}
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ onetime@5.1.2:
+ dependencies:
+ mimic-fn: 2.1.0
+
+ oniguruma-to-es@3.1.1:
+ dependencies:
+ emoji-regex-xs: 1.0.0
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
+ ora@5.4.1:
+ dependencies:
+ bl: 4.1.0
+ chalk: 4.1.2
+ cli-cursor: 3.1.0
+ cli-spinners: 2.9.2
+ is-interactive: 1.0.0
+ is-unicode-supported: 0.1.0
+ log-symbols: 4.1.0
+ strip-ansi: 6.0.1
+ wcwidth: 1.0.1
+
+ p-cancelable@2.1.1: {}
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-map@7.0.4: {}
+
+ package-json-from-dist@1.0.1: {}
+
+ package-manager-detector@1.6.0: {}
+
+ parse-svg-path@0.1.2: {}
+
+ parse5@8.0.0:
+ dependencies:
+ entities: 6.0.1
+
+ path-browserify@1.0.1: {}
+
+ path-data-parser@0.1.0: {}
+
+ path-is-absolute@1.0.1: {}
+
+ path-key@3.1.1: {}
+
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.3
+
+ path-scurry@2.0.2:
+ dependencies:
+ lru-cache: 11.2.7
+ minipass: 7.1.3
+
+ path-source@0.1.3:
+ dependencies:
+ array-source: 0.0.4
+ file-source: 0.6.1
+
+ pathe@1.1.2: {}
+
+ pathe@2.0.3: {}
+
+ pathval@2.0.1: {}
+
+ pbf@3.3.0:
+ dependencies:
+ ieee754: 1.2.1
+ resolve-protobuf-schema: 2.1.0
+
+ pe-library@0.4.1: {}
+
+ pend@1.2.0: {}
+
+ perfect-debounce@1.0.0: {}
+
+ pg-cloudflare@1.3.0:
+ optional: true
+
+ pg-connection-string@2.12.0: {}
+
+ pg-int8@1.0.1: {}
+
+ pg-pool@3.13.0(pg@8.20.0):
+ dependencies:
+ pg: 8.20.0
+
+ pg-protocol@1.13.0: {}
+
+ pg-types@2.2.0:
+ dependencies:
+ pg-int8: 1.0.1
+ postgres-array: 2.0.0
+ postgres-bytea: 1.0.1
+ postgres-date: 1.0.7
+ postgres-interval: 1.2.0
+
+ pg@8.20.0:
+ dependencies:
+ pg-connection-string: 2.12.0
+ pg-pool: 3.13.0(pg@8.20.0)
+ pg-protocol: 1.13.0
+ pg-types: 2.2.0
+ pgpass: 1.0.5
+ optionalDependencies:
+ pg-cloudflare: 1.3.0
+
+ pgpass@1.0.5:
+ dependencies:
+ split2: 4.2.0
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.3: {}
+
+ pino-abstract-transport@3.0.0:
+ dependencies:
+ split2: 4.2.0
+
+ pino-std-serializers@7.1.0: {}
+
+ pino@10.3.1:
+ dependencies:
+ '@pinojs/redact': 0.4.0
+ atomic-sleep: 1.0.0
+ on-exit-leak-free: 2.1.2
+ pino-abstract-transport: 3.0.0
+ pino-std-serializers: 7.1.0
+ process-warning: 5.0.0
+ quick-format-unescaped: 4.0.4
+ real-require: 0.2.0
+ safe-stable-stringify: 2.5.0
+ sonic-boom: 4.2.1
+ thread-stream: 4.0.0
+
+ pkg-types@1.3.1:
+ dependencies:
+ confbox: 0.1.8
+ mlly: 1.8.2
+ pathe: 2.0.3
+
+ plist@3.1.0:
+ dependencies:
+ '@xmldom/xmldom': 0.8.11
+ base64-js: 1.5.1
+ xmlbuilder: 15.1.1
+
+ point-at-length@1.1.0:
+ dependencies:
+ abs-svg-path: 0.1.1
+ isarray: 0.0.1
+ parse-svg-path: 0.1.2
+
+ points-on-curve@0.2.0: {}
+
+ points-on-path@0.2.1:
+ dependencies:
+ path-data-parser: 0.1.0
+ points-on-curve: 0.2.0
+
+ postcss@8.5.8:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ postgres-array@2.0.0: {}
+
+ postgres-bytea@1.0.1: {}
+
+ postgres-date@1.0.7: {}
+
+ postgres-interval@1.2.0:
+ dependencies:
+ xtend: 4.0.2
+
+ postject@1.0.0-alpha.6:
+ dependencies:
+ commander: 9.5.0
+ optional: true
+
+ preact@10.28.4: {}
+
+ prebuild-install@7.1.3:
+ dependencies:
+ detect-libc: 2.1.2
+ expand-template: 2.0.3
+ github-from-package: 0.0.0
+ minimist: 1.2.8
+ mkdirp-classic: 0.5.3
+ napi-build-utils: 2.0.0
+ node-abi: 3.89.0
+ pump: 3.0.4
+ rc: 1.2.8
+ simple-get: 4.0.1
+ tar-fs: 2.1.4
+ tunnel-agent: 0.6.0
+
+ proc-log@5.0.0: {}
+
+ process-warning@4.0.1: {}
+
+ process-warning@5.0.0: {}
+
+ progress@2.0.3: {}
+
+ promise-retry@2.0.1:
+ dependencies:
+ err-code: 2.0.3
+ retry: 0.12.0
+
+ proper-lockfile@4.1.2:
+ dependencies:
+ graceful-fs: 4.2.11
+ retry: 0.12.0
+ signal-exit: 3.0.7
+
+ property-information@7.1.0: {}
+
+ protocol-buffers-schema@3.6.0: {}
+
+ proxy-from-env@1.1.0: {}
+
+ pump@3.0.4:
+ dependencies:
+ end-of-stream: 1.4.5
+ once: 1.4.0
+
+ punycode@2.3.1: {}
+
+ quick-format-unescaped@4.0.4: {}
+
+ quick-lru@5.1.1: {}
+
+ rc@1.2.8:
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.8
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+
+ react-dom@18.3.1(react@18.3.1):
+ dependencies:
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
+
+ react-is@18.3.1: {}
+
+ react-refresh@0.17.0: {}
+
+ react-router-dom@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-router: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+
+ react-router@7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ cookie: 1.1.1
+ react: 18.3.1
+ set-cookie-parser: 2.7.2
+ optionalDependencies:
+ react-dom: 18.3.1(react@18.3.1)
+
+ react-shallow-renderer@16.15.0(react@18.3.1):
+ dependencies:
+ object-assign: 4.1.1
+ react: 18.3.1
+ react-is: 18.3.1
+
+ react-test-renderer@18.3.1(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-is: 18.3.1
+ react-shallow-renderer: 16.15.0(react@18.3.1)
+ scheduler: 0.23.2
+
+ react@18.3.1:
+ dependencies:
+ loose-envify: 1.4.0
+
+ read-binary-file-arch@1.0.6:
+ dependencies:
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ readable-stream@1.1.14:
+ dependencies:
+ core-util-is: 1.0.3
+ inherits: 2.0.4
+ isarray: 0.0.1
+ string_decoder: 0.10.31
+
+ readable-stream@3.6.2:
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+
+ real-require@0.2.0: {}
+
+ regex-recursion@6.0.2:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
+
+ regex@6.1.0:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ require-directory@2.1.1: {}
+
+ require-from-string@2.0.2: {}
+
+ resedit@1.7.2:
+ dependencies:
+ pe-library: 0.4.1
+
+ resolve-alpn@1.2.1: {}
+
+ resolve-pkg-maps@1.0.0: {}
+
+ resolve-protobuf-schema@2.1.0:
+ dependencies:
+ protocol-buffers-schema: 3.6.0
+
+ responselike@2.0.1:
+ dependencies:
+ lowercase-keys: 2.0.0
+
+ restore-cursor@3.1.0:
+ dependencies:
+ onetime: 5.1.2
+ signal-exit: 3.0.7
+
+ ret@0.5.0: {}
+
+ retry@0.12.0: {}
+
+ reusify@1.1.0: {}
+
+ rfdc@1.4.1: {}
+
+ rimraf@2.6.3:
+ dependencies:
+ glob: 7.2.3
+
+ roarr@2.15.4:
+ dependencies:
+ boolean: 3.2.0
+ detect-node: 2.1.0
+ globalthis: 1.0.4
+ json-stringify-safe: 5.0.1
+ semver-compare: 1.0.0
+ sprintf-js: 1.1.3
+ optional: true
+
+ robust-predicates@3.0.2: {}
+
+ rollup@4.59.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.59.0
+ '@rollup/rollup-android-arm64': 4.59.0
+ '@rollup/rollup-darwin-arm64': 4.59.0
+ '@rollup/rollup-darwin-x64': 4.59.0
+ '@rollup/rollup-freebsd-arm64': 4.59.0
+ '@rollup/rollup-freebsd-x64': 4.59.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.59.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.59.0
+ '@rollup/rollup-linux-arm64-gnu': 4.59.0
+ '@rollup/rollup-linux-arm64-musl': 4.59.0
+ '@rollup/rollup-linux-loong64-gnu': 4.59.0
+ '@rollup/rollup-linux-loong64-musl': 4.59.0
+ '@rollup/rollup-linux-ppc64-gnu': 4.59.0
+ '@rollup/rollup-linux-ppc64-musl': 4.59.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.59.0
+ '@rollup/rollup-linux-riscv64-musl': 4.59.0
+ '@rollup/rollup-linux-s390x-gnu': 4.59.0
+ '@rollup/rollup-linux-x64-gnu': 4.59.0
+ '@rollup/rollup-linux-x64-musl': 4.59.0
+ '@rollup/rollup-openbsd-x64': 4.59.0
+ '@rollup/rollup-openharmony-arm64': 4.59.0
+ '@rollup/rollup-win32-arm64-msvc': 4.59.0
+ '@rollup/rollup-win32-ia32-msvc': 4.59.0
+ '@rollup/rollup-win32-x64-gnu': 4.59.0
+ '@rollup/rollup-win32-x64-msvc': 4.59.0
+ fsevents: 2.3.3
+
+ roughjs@4.6.6:
+ dependencies:
+ hachure-fill: 0.5.2
+ path-data-parser: 0.1.0
+ points-on-curve: 0.2.0
+ points-on-path: 0.2.1
+
+ rw@1.3.3: {}
+
+ rxjs@7.8.2:
+ dependencies:
+ tslib: 2.8.1
+
+ safe-buffer@5.2.1: {}
+
+ safe-regex2@5.0.0:
+ dependencies:
+ ret: 0.5.0
+
+ safe-stable-stringify@2.5.0: {}
+
+ safer-buffer@2.1.2: {}
+
+ sanitize-filename@1.6.3:
+ dependencies:
+ truncate-utf8-bytes: 1.0.2
+
+ sax@1.5.0: {}
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
+ scheduler@0.23.2:
+ dependencies:
+ loose-envify: 1.4.0
+
+ search-insights@2.17.3: {}
+
+ secure-json-parse@4.1.0: {}
+
+ semver-compare@1.0.0:
+ optional: true
+
+ semver@5.7.2: {}
+
+ semver@6.3.1: {}
+
+ semver@7.7.4: {}
+
+ serialize-error@7.0.1:
+ dependencies:
+ type-fest: 0.13.1
+ optional: true
+
+ set-cookie-parser@2.7.2: {}
+
+ setprototypeof@1.2.0: {}
+
+ shapefile@0.6.6:
+ dependencies:
+ array-source: 0.0.4
+ commander: 2.20.3
+ path-source: 0.1.3
+ slice-source: 0.4.1
+ stream-source: 0.3.5
+ text-encoding: 0.6.4
+
+ sharp@0.34.5:
+ dependencies:
+ '@img/colour': 1.1.0
+ detect-libc: 2.1.2
+ semver: 7.7.4
+ 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
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ shell-quote@1.8.3: {}
+
+ shiki@2.5.0:
+ dependencies:
+ '@shikijs/core': 2.5.0
+ '@shikijs/engine-javascript': 2.5.0
+ '@shikijs/engine-oniguruma': 2.5.0
+ '@shikijs/langs': 2.5.0
+ '@shikijs/themes': 2.5.0
+ '@shikijs/types': 2.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ siginfo@2.0.0: {}
+
+ signal-exit@3.0.7: {}
+
+ signal-exit@4.1.0: {}
+
+ simple-concat@1.0.1: {}
+
+ simple-get@4.0.1:
+ dependencies:
+ decompress-response: 6.0.0
+ once: 1.4.0
+ simple-concat: 1.0.1
+
+ simple-statistics@7.8.9: {}
+
+ simple-update-notifier@2.0.0:
+ dependencies:
+ semver: 7.7.4
+
+ simplify-geojson@1.0.5:
+ dependencies:
+ concat-stream: 1.4.11
+ minimist: 1.2.6
+ simplify-geometry: 0.0.2
+
+ simplify-geometry@0.0.2: {}
+
+ slice-ansi@3.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ astral-regex: 2.0.0
+ is-fullwidth-code-point: 3.0.0
+ optional: true
+
+ slice-source@0.4.1: {}
+
+ smart-buffer@4.2.0: {}
+
+ socks-proxy-agent@8.0.5:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ socks: 2.8.7
+ transitivePeerDependencies:
+ - supports-color
+
+ socks@2.8.7:
+ dependencies:
+ ip-address: 10.1.0
+ smart-buffer: 4.2.0
+
+ sonic-boom@4.2.1:
+ dependencies:
+ atomic-sleep: 1.0.0
+
+ source-map-js@1.2.1: {}
+
+ source-map-support@0.5.21:
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+
+ source-map@0.6.1: {}
+
+ space-separated-tokens@2.0.2: {}
+
+ speakingurl@14.0.1: {}
+
+ split2@4.2.0: {}
+
+ sprintf-js@1.1.3:
+ optional: true
+
+ sql-escaper@1.3.3: {}
+
+ ssri@12.0.0:
+ dependencies:
+ minipass: 7.1.3
+
+ stackback@0.0.2: {}
+
+ stat-mode@1.0.0: {}
+
+ statuses@2.0.2: {}
+
+ std-env@3.10.0: {}
+
+ stream-source@0.3.5: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.2.0
+
+ string_decoder@0.10.31: {}
+
+ string_decoder@1.3.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.2.2
+
+ strip-json-comments@2.0.1: {}
+
+ stylis@4.3.6: {}
+
+ sumchecker@3.0.1:
+ dependencies:
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ superjson@2.2.6:
+ dependencies:
+ copy-anything: 4.0.5
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-color@8.1.1:
+ dependencies:
+ has-flag: 4.0.0
+
+ symbol-tree@3.2.4: {}
+
+ tabbable@6.4.0: {}
+
+ tailwindcss@4.2.1: {}
+
+ tailwindcss@4.2.2: {}
+
+ tapable@2.3.0: {}
+
+ tar-fs@2.1.4:
+ dependencies:
+ chownr: 1.1.4
+ mkdirp-classic: 0.5.3
+ pump: 3.0.4
+ tar-stream: 2.2.0
+
+ tar-stream@2.2.0:
+ dependencies:
+ bl: 4.1.0
+ end-of-stream: 1.4.5
+ fs-constants: 1.0.0
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
+ tar@7.5.11:
+ dependencies:
+ '@isaacs/fs-minipass': 4.0.1
+ chownr: 3.0.0
+ minipass: 7.1.3
+ minizlib: 3.1.0
+ yallist: 5.0.0
+
+ temp-file@3.4.0:
+ dependencies:
+ async-exit-hook: 2.0.1
+ fs-extra: 10.1.0
+
+ temp@0.9.4:
+ dependencies:
+ mkdirp: 0.5.6
+ rimraf: 2.6.3
+
+ text-encoding@0.6.4: {}
+
+ thread-stream@4.0.0:
+ dependencies:
+ real-require: 0.2.0
+
+ tiny-async-pool@1.3.0:
+ dependencies:
+ semver: 5.7.2
+
+ tiny-typed-emitter@2.1.0: {}
+
+ tinybench@2.9.0: {}
+
+ tinyexec@0.3.2: {}
+
+ tinyexec@1.0.4: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tinypool@1.1.1: {}
+
+ tinyrainbow@1.2.0: {}
+
+ tinyspy@3.0.2: {}
+
+ tldts-core@7.0.26: {}
+
+ tldts@7.0.26:
+ dependencies:
+ tldts-core: 7.0.26
+
+ tmp-promise@3.0.3:
+ dependencies:
+ tmp: 0.2.5
+
+ tmp@0.2.5: {}
+
+ toad-cache@3.7.0: {}
+
+ toidentifier@1.0.1: {}
+
+ topojson-client@3.1.0:
+ dependencies:
+ commander: 2.20.3
+
+ topojson-server@3.0.1:
+ dependencies:
+ commander: 2.20.3
+
+ tough-cookie@6.0.1:
+ dependencies:
+ tldts: 7.0.26
+
+ tr46@6.0.0:
+ dependencies:
+ punycode: 2.3.1
+
+ tree-kill@1.2.2: {}
+
+ trim-lines@3.0.1: {}
+
+ truncate-utf8-bytes@1.0.2:
+ dependencies:
+ utf8-byte-length: 1.0.5
+
+ ts-dedent@2.2.0: {}
+
+ tslib@2.8.1: {}
+
+ tsx@4.21.0:
+ dependencies:
+ esbuild: 0.27.3
+ get-tsconfig: 4.13.6
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ tunnel-agent@0.6.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ type-fest@0.13.1:
+ optional: true
+
+ typedarray@0.0.6: {}
+
+ typedarray@0.0.7: {}
+
+ typescript@5.9.3: {}
+
+ ufo@1.6.3: {}
+
+ undici-types@6.21.0: {}
+
+ undici-types@7.16.0: {}
+
+ undici@6.24.1: {}
+
+ undici@7.24.5: {}
+
+ unique-filename@4.0.0:
+ dependencies:
+ unique-slug: 5.0.0
+
+ unique-slug@5.0.0:
+ dependencies:
+ imurmurhash: 0.1.4
+
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ universalify@0.1.2: {}
+
+ universalify@2.0.1: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
+ dependencies:
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ utf8-byte-length@1.0.5: {}
+
+ util-deprecate@1.0.2: {}
+
+ uuid@11.1.0: {}
+
+ verror@1.10.1:
+ dependencies:
+ assert-plus: 1.0.0
+ core-util-is: 1.0.2
+ extsprintf: 1.4.1
+ optional: true
+
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
+ vite-node@2.1.9(@types/node@22.19.15)(lightningcss@1.32.0):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ pathe: 1.1.2
+ vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.32.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
+ vite@5.4.21(@types/node@22.19.15)(lightningcss@1.32.0):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.5.8
+ rollup: 4.59.0
+ optionalDependencies:
+ '@types/node': 22.19.15
+ fsevents: 2.3.3
+ lightningcss: 1.32.0
+
+ vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0):
+ dependencies:
+ esbuild: 0.25.12
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.8
+ rollup: 4.59.0
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 22.19.15
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ lightningcss: 1.32.0
+ tsx: 4.21.0
+
+ vitepress-plugin-mermaid@2.0.17(mermaid@11.13.0)(vitepress@1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)):
+ dependencies:
+ mermaid: 11.13.0
+ vitepress: 1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)
+ optionalDependencies:
+ '@mermaid-js/mermaid-mindmap': 9.3.0
+
+ vitepress@1.6.4(@algolia/client-search@5.49.1)(@types/node@22.19.15)(@types/react@18.3.28)(axios@1.13.6)(lightningcss@1.32.0)(postcss@8.5.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3):
+ dependencies:
+ '@docsearch/css': 3.8.2
+ '@docsearch/js': 3.8.2(@algolia/client-search@5.49.1)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)
+ '@iconify-json/simple-icons': 1.2.72
+ '@shikijs/core': 2.5.0
+ '@shikijs/transformers': 2.5.0
+ '@shikijs/types': 2.5.0
+ '@types/markdown-it': 14.1.2
+ '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.32.0))(vue@3.5.30(typescript@5.9.3))
+ '@vue/devtools-api': 7.7.9
+ '@vue/shared': 3.5.30
+ '@vueuse/core': 12.8.2(typescript@5.9.3)
+ '@vueuse/integrations': 12.8.2(axios@1.13.6)(focus-trap@7.8.0)(typescript@5.9.3)
+ focus-trap: 7.8.0
+ mark.js: 8.11.1
+ minisearch: 7.2.0
+ shiki: 2.5.0
+ vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.32.0)
+ vue: 3.5.30(typescript@5.9.3)
+ optionalDependencies:
+ postcss: 8.5.8
+ transitivePeerDependencies:
+ - '@algolia/client-search'
+ - '@types/node'
+ - '@types/react'
+ - async-validator
+ - axios
+ - change-case
+ - drauu
+ - fuse.js
+ - idb-keyval
+ - jwt-decode
+ - less
+ - lightningcss
+ - nprogress
+ - qrcode
+ - react
+ - react-dom
+ - sass
+ - sass-embedded
+ - search-insights
+ - sortablejs
+ - stylus
+ - sugarss
+ - terser
+ - typescript
+ - universal-cookie
+
+ vitest@2.1.9(@types/node@22.19.15)(jsdom@29.0.1)(lightningcss@1.32.0):
+ dependencies:
+ '@vitest/expect': 2.1.9
+ '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15)(lightningcss@1.32.0))
+ '@vitest/pretty-format': 2.1.9
+ '@vitest/runner': 2.1.9
+ '@vitest/snapshot': 2.1.9
+ '@vitest/spy': 2.1.9
+ '@vitest/utils': 2.1.9
+ chai: 5.3.3
+ debug: 4.4.3
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ pathe: 1.1.2
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinypool: 1.1.1
+ tinyrainbow: 1.2.0
+ vite: 5.4.21(@types/node@22.19.15)(lightningcss@1.32.0)
+ vite-node: 2.1.9(@types/node@22.19.15)(lightningcss@1.32.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 22.19.15
+ jsdom: 29.0.1
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
+ vscode-jsonrpc@8.2.0: {}
+
+ vscode-languageserver-protocol@3.17.5:
+ dependencies:
+ vscode-jsonrpc: 8.2.0
+ vscode-languageserver-types: 3.17.5
+
+ vscode-languageserver-textdocument@1.0.12: {}
+
+ vscode-languageserver-types@3.17.5: {}
+
+ vscode-languageserver@9.0.1:
+ dependencies:
+ vscode-languageserver-protocol: 3.17.5
+
+ vscode-uri@3.1.0: {}
+
+ vue@3.5.30(typescript@5.9.3):
+ dependencies:
+ '@vue/compiler-dom': 3.5.30
+ '@vue/compiler-sfc': 3.5.30
+ '@vue/runtime-dom': 3.5.30
+ '@vue/server-renderer': 3.5.30(vue@3.5.30(typescript@5.9.3))
+ '@vue/shared': 3.5.30
+ optionalDependencies:
+ typescript: 5.9.3
+
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
+ wait-on@9.0.4:
+ dependencies:
+ axios: 1.13.6
+ joi: 18.0.2
+ lodash: 4.17.23
+ minimist: 1.2.8
+ rxjs: 7.8.2
+ transitivePeerDependencies:
+ - debug
+
+ wcwidth@1.0.1:
+ dependencies:
+ defaults: 1.0.4
+
+ webidl-conversions@8.0.1: {}
+
+ whatwg-mimetype@5.0.0: {}
+
+ whatwg-url@16.0.1:
+ dependencies:
+ '@exodus/bytes': 1.15.0
+ tr46: 6.0.0
+ webidl-conversions: 8.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ which@5.0.0:
+ dependencies:
+ isexe: 3.1.5
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 5.1.2
+ strip-ansi: 7.2.0
+
+ wrappy@1.0.2: {}
+
+ ws@8.19.0: {}
+
+ xml-name-validator@5.0.0: {}
+
+ xmlbuilder@15.1.1: {}
+
+ xmlchars@2.2.0: {}
+
+ xtend@4.0.2: {}
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
+ yallist@4.0.0: {}
+
+ yallist@5.0.0: {}
+
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
+ yauzl@2.10.0:
+ dependencies:
+ buffer-crc32: 0.2.13
+ fd-slicer: 1.1.0
+
+ yocto-queue@0.1.0: {}
+
+ zwitch@2.0.4: {}
diff --git a/render.yaml b/render.yaml
new file mode 100644
index 00000000..fde4697c
--- /dev/null
+++ b/render.yaml
@@ -0,0 +1,31 @@
+services:
+ - type: web
+ name: metapi
+ runtime: docker
+ repo: https://github.com/cita-777/metapi
+ dockerfilePath: ./docker/Dockerfile
+ dockerContext: .
+ plan: free
+ envVars:
+ - key: AUTH_TOKEN
+ sync: false
+ - key: PROXY_TOKEN
+ sync: false
+ - key: TZ
+ value: Asia/Shanghai
+ - key: PORT
+ value: "4000"
+ - key: DATA_DIR
+ value: /app/data
+ - key: NODE_ENV
+ value: production
+ - key: DB_TYPE
+ value: mysql
+ - key: DB_URL
+ sync: false
+ - key: DB_SSL
+ value: "true"
+ - key: CHECKIN_CRON
+ value: "0 8 * * *"
+ - key: BALANCE_REFRESH_CRON
+ value: "0 * * * *"
diff --git a/scripts/desktop/generate-icons.mjs b/scripts/desktop/generate-icons.mjs
new file mode 100644
index 00000000..81b67220
--- /dev/null
+++ b/scripts/desktop/generate-icons.mjs
@@ -0,0 +1,126 @@
+import { mkdir } from 'node:fs/promises';
+import { dirname, join } from 'node:path';
+import { pathToFileURL } from 'node:url';
+import sharp from 'sharp';
+
+export const DESKTOP_ICON_SIZE = 512;
+export const DESKTOP_ICON_PADDING = 40;
+export const DESKTOP_ICON_RADIUS = 96;
+export const DESKTOP_TRAY_TEMPLATE_PADDING = 152;
+
+function createRoundedMask(size, cornerRadius) {
+ return Buffer.from(
+ ` `,
+ );
+}
+
+async function renderDesktopIconBuffer({
+ sourcePath,
+ size = DESKTOP_ICON_SIZE,
+ padding = DESKTOP_ICON_PADDING,
+ cornerRadius = DESKTOP_ICON_RADIUS,
+}) {
+ const innerSize = size - padding * 2;
+ const roundedMask = createRoundedMask(innerSize, Math.min(cornerRadius, Math.floor(innerSize / 2)));
+
+ const roundedInner = await sharp(sourcePath)
+ .resize(innerSize, innerSize, {
+ fit: 'contain',
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
+ })
+ .composite([{ input: roundedMask, blend: 'dest-in' }])
+ .png()
+ .toBuffer();
+
+ return sharp({
+ create: {
+ width: size,
+ height: size,
+ channels: 4,
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
+ },
+ })
+ .composite([{ input: roundedInner, left: padding, top: padding }])
+ .png()
+ .toBuffer();
+}
+
+async function renderTrayTemplateIconBuffer({
+ sourcePath,
+ size = DESKTOP_ICON_SIZE,
+ padding = DESKTOP_TRAY_TEMPLATE_PADDING,
+}) {
+ const innerSize = Math.max(1, size - padding * 2);
+ const alphaMask = await sharp(sourcePath)
+ .resize(innerSize, innerSize, {
+ fit: 'contain',
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
+ })
+ .ensureAlpha()
+ .extractChannel('alpha')
+ .png()
+ .toBuffer();
+
+ return sharp({
+ create: {
+ width: size,
+ height: size,
+ channels: 4,
+ background: { r: 0, g: 0, b: 0, alpha: 1 },
+ },
+ })
+ .composite([{ input: alphaMask, left: padding, top: padding, blend: 'dest-in' }])
+ .png()
+ .toBuffer();
+}
+
+export async function generateDesktopIconAssets({
+ sourcePath = join(process.cwd(), 'src', 'web', 'public', 'logo.png'),
+ buildOutputPath = join(process.cwd(), 'build', 'desktop-icon.png'),
+ webOutputPath = join(process.cwd(), 'src', 'web', 'public', 'desktop-icon.png'),
+ trayTemplateOutputPath = join(process.cwd(), 'src', 'web', 'public', 'desktop-tray-template.png'),
+ size = DESKTOP_ICON_SIZE,
+ padding = DESKTOP_ICON_PADDING,
+ cornerRadius = DESKTOP_ICON_RADIUS,
+} = {}) {
+ const [outputBuffer, trayTemplateBuffer] = await Promise.all([
+ renderDesktopIconBuffer({
+ sourcePath,
+ size,
+ padding,
+ cornerRadius,
+ }),
+ renderTrayTemplateIconBuffer({
+ sourcePath,
+ size,
+ }),
+ ]);
+
+ await Promise.all([
+ mkdir(dirname(buildOutputPath), { recursive: true }),
+ mkdir(dirname(webOutputPath), { recursive: true }),
+ mkdir(dirname(trayTemplateOutputPath), { recursive: true }),
+ ]);
+
+ await Promise.all([
+ sharp(outputBuffer).toFile(buildOutputPath),
+ sharp(outputBuffer).toFile(webOutputPath),
+ sharp(trayTemplateBuffer).toFile(trayTemplateOutputPath),
+ ]);
+
+ return {
+ buildOutputPath,
+ webOutputPath,
+ trayTemplateOutputPath,
+ };
+}
+
+const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
+
+if (isDirectRun) {
+ const outputs = await generateDesktopIconAssets();
+ console.log(`[metapi-desktop] Generated desktop icons:
+- ${outputs.buildOutputPath}
+- ${outputs.webOutputPath}
+- ${outputs.trayTemplateOutputPath}`);
+}
diff --git a/scripts/desktop/generate-icons.test.ts b/scripts/desktop/generate-icons.test.ts
new file mode 100644
index 00000000..e2e6151d
--- /dev/null
+++ b/scripts/desktop/generate-icons.test.ts
@@ -0,0 +1,86 @@
+import { mkdtemp, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import sharp from 'sharp';
+import { afterEach, describe, expect, it } from 'vitest';
+
+const tempDirs: string[] = [];
+
+async function makeTempDir() {
+ const dir = await mkdtemp(join(tmpdir(), 'metapi-desktop-icons-'));
+ tempDirs.push(dir);
+ return dir;
+}
+
+function alphaAt(buffer: Buffer, width: number, x: number, y: number) {
+ return buffer[(y * width + x) * 4 + 3];
+}
+
+afterEach(async () => {
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
+});
+
+describe('generateDesktopIconAssets', () => {
+ it('writes rounded desktop icon outputs and a tray template asset', async () => {
+ const { generateDesktopIconAssets } = await import('./generate-icons.mjs');
+
+ const dir = await makeTempDir();
+ const sourcePath = join(dir, 'logo.png');
+ const buildOutputPath = join(dir, 'build.png');
+ const webOutputPath = join(dir, 'desktop-icon.png');
+ const trayTemplateOutputPath = join(dir, 'desktop-tray-template.png');
+
+ await sharp({
+ create: {
+ width: 512,
+ height: 512,
+ channels: 4,
+ background: { r: 255, g: 112, b: 48, alpha: 1 },
+ },
+ }).png().toFile(sourcePath);
+
+ await generateDesktopIconAssets({
+ sourcePath,
+ buildOutputPath,
+ webOutputPath,
+ trayTemplateOutputPath,
+ });
+
+ const buildMeta = await sharp(buildOutputPath).metadata();
+ const webMeta = await sharp(webOutputPath).metadata();
+ const trayMeta = await sharp(trayTemplateOutputPath).metadata();
+
+ expect(buildMeta.width).toBe(512);
+ expect(buildMeta.height).toBe(512);
+ expect(webMeta.width).toBe(512);
+ expect(webMeta.height).toBe(512);
+ expect(trayMeta.width).toBe(512);
+ expect(trayMeta.height).toBe(512);
+
+ const { data, info } = await sharp(buildOutputPath)
+ .ensureAlpha()
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+
+ expect(alphaAt(data, info.width, 0, 0)).toBe(0);
+ expect(alphaAt(data, info.width, 12, 12)).toBe(0);
+ expect(alphaAt(data, info.width, Math.floor(info.width / 2), 6)).toBe(0);
+ expect(alphaAt(data, info.width, Math.floor(info.width / 2), Math.floor(info.height / 2))).toBe(255);
+
+ expect(await sharp(buildOutputPath).png().toBuffer())
+ .toEqual(await sharp(webOutputPath).png().toBuffer());
+
+ const trayBuffer = await sharp(trayTemplateOutputPath)
+ .ensureAlpha()
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+
+ expect(alphaAt(trayBuffer.data, trayBuffer.info.width, 0, 0)).toBe(0);
+ expect(alphaAt(trayBuffer.data, trayBuffer.info.width, Math.floor(trayBuffer.info.width / 2), Math.floor(trayBuffer.info.height / 2))).toBe(255);
+
+ const trayCenterOffset = (Math.floor(trayBuffer.info.height / 2) * trayBuffer.info.width + Math.floor(trayBuffer.info.width / 2)) * 4;
+ expect(trayBuffer.data[trayCenterOffset]).toBe(0);
+ expect(trayBuffer.data[trayCenterOffset + 1]).toBe(0);
+ expect(trayBuffer.data[trayCenterOffset + 2]).toBe(0);
+ });
+});
diff --git a/scripts/desktop/release.workflow.test.ts b/scripts/desktop/release.workflow.test.ts
new file mode 100644
index 00000000..e8a4b6fe
--- /dev/null
+++ b/scripts/desktop/release.workflow.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+describe('release workflow', () => {
+ it('builds macOS arm64 and x64 on dedicated runners and verifies packaged app architecture', () => {
+ const workflow = readFileSync(resolve(process.cwd(), '.github/workflows/release.yml'), 'utf8');
+
+ expect(workflow).toContain('runner: macos-15-intel');
+ expect(workflow).toContain('runner: macos-15');
+ expect(workflow).toContain('expectedMacArch: x64');
+ expect(workflow).toContain('expectedMacArch: arm64');
+ expect(workflow).toContain('Verify packaged macOS architecture');
+ expect(workflow).toContain('node scripts/desktop/verifyMacArchitecture.mjs');
+ });
+});
diff --git a/scripts/desktop/verifyMacArchitecture.mjs b/scripts/desktop/verifyMacArchitecture.mjs
new file mode 100644
index 00000000..650609a2
--- /dev/null
+++ b/scripts/desktop/verifyMacArchitecture.mjs
@@ -0,0 +1,122 @@
+import { execFileSync } from 'node:child_process';
+import { readdirSync } from 'node:fs';
+import { join, resolve } from 'node:path';
+import { pathToFileURL } from 'node:url';
+
+const MAC_BINARY_SEGMENTS = ['Metapi.app', 'Contents', 'MacOS', 'Metapi'];
+
+function walkDirectories(rootDir) {
+ const queue = [rootDir];
+ const files = [];
+
+ while (queue.length > 0) {
+ const currentDir = queue.shift();
+ if (!currentDir) {
+ continue;
+ }
+
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
+ const entryPath = join(currentDir, entry.name);
+ if (entry.isDirectory()) {
+ queue.push(entryPath);
+ continue;
+ }
+
+ if (entry.isFile()) {
+ files.push(entryPath);
+ }
+ }
+ }
+
+ return files;
+}
+
+export function findPackagedMacBinaries(releaseDir) {
+ const normalizedSuffix = MAC_BINARY_SEGMENTS.join('/');
+
+ return walkDirectories(releaseDir).filter((filePath) =>
+ filePath.replaceAll('\\', '/').endsWith(normalizedSuffix),
+ );
+}
+
+export function normalizeExpectedMacArch(expectedArch) {
+ if (expectedArch === 'x64') {
+ return 'x86_64';
+ }
+
+ if (expectedArch === 'arm64') {
+ return 'arm64';
+ }
+
+ throw new Error(`Unsupported expected mac architecture: ${expectedArch}`);
+}
+
+export function inspectBinaryArchsWithLipo(binaryPath) {
+ try {
+ return execFileSync('lipo', ['-archs', binaryPath], { encoding: 'utf8' }).trim();
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ throw new Error(`Unable to detect architecture via lipo for ${binaryPath}: ${message}`);
+ }
+}
+
+export function verifyMacArchitecture({
+ releaseDir,
+ expectedArch,
+ inspectBinaryArchs = inspectBinaryArchsWithLipo,
+}) {
+ const binaries = findPackagedMacBinaries(releaseDir);
+ if (binaries.length === 0) {
+ throw new Error(`No packaged macOS app binary found under ${releaseDir}`);
+ }
+
+ const expectedBinaryArch = normalizeExpectedMacArch(expectedArch);
+
+ return binaries.map((binaryPath) => {
+ const archs = inspectBinaryArchs(binaryPath).trim().replace(/\s+/g, ' ');
+ if (!archs) {
+ throw new Error(`Unable to detect architecture via lipo for ${binaryPath}`);
+ }
+
+ if (archs !== expectedBinaryArch) {
+ throw new Error(`Expected ${expectedBinaryArch}-only binary but got: ${archs}`);
+ }
+
+ return { binaryPath, archs };
+ });
+}
+
+function parseArgValue(args, name) {
+ const index = args.indexOf(name);
+ if (index === -1) {
+ return undefined;
+ }
+
+ return args[index + 1];
+}
+
+function main() {
+ const args = process.argv.slice(2);
+ const releaseDirArg = parseArgValue(args, '--release-dir');
+ const expectedArch = parseArgValue(args, '--expected-arch');
+
+ if (!releaseDirArg || !expectedArch) {
+ throw new Error('Usage: node scripts/desktop/verifyMacArchitecture.mjs --release-dir --expected-arch ');
+ }
+
+ const releaseDir = resolve(releaseDirArg);
+ const verifiedBinaries = verifyMacArchitecture({ releaseDir, expectedArch });
+
+ for (const { binaryPath, archs } of verifiedBinaries) {
+ console.log(`${binaryPath} => ${archs}`);
+ }
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ try {
+ main();
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ process.exitCode = 1;
+ }
+}
diff --git a/scripts/desktop/verifyMacArchitecture.test.ts b/scripts/desktop/verifyMacArchitecture.test.ts
new file mode 100644
index 00000000..f02cb282
--- /dev/null
+++ b/scripts/desktop/verifyMacArchitecture.test.ts
@@ -0,0 +1,54 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { tmpdir } from 'node:os';
+
+import { verifyMacArchitecture } from './verifyMacArchitecture.mjs';
+
+const tempDirs: string[] = [];
+
+function createReleaseBinary() {
+ const releaseDir = mkdtempSync(join(tmpdir(), 'metapi-mac-arch-'));
+ tempDirs.push(releaseDir);
+
+ const binaryPath = join(releaseDir, 'mac-x64', 'Metapi.app', 'Contents', 'MacOS', 'Metapi');
+ mkdirSync(dirname(binaryPath), { recursive: true });
+ writeFileSync(binaryPath, 'binary');
+
+ return { releaseDir, binaryPath };
+}
+
+afterEach(() => {
+ while (tempDirs.length > 0) {
+ const dir = tempDirs.pop();
+ if (dir) {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ }
+});
+
+describe('verifyMacArchitecture', () => {
+ it('accepts x64 binaries when lipo reports x86_64', () => {
+ const { releaseDir, binaryPath } = createReleaseBinary();
+
+ expect(
+ verifyMacArchitecture({
+ releaseDir,
+ expectedArch: 'x64',
+ inspectBinaryArchs: () => 'x86_64',
+ }),
+ ).toEqual([{ archs: 'x86_64', binaryPath }]);
+ });
+
+ it('rejects x64 binaries when lipo reports arm64', () => {
+ const { releaseDir } = createReleaseBinary();
+
+ expect(() =>
+ verifyMacArchitecture({
+ releaseDir,
+ expectedArch: 'x64',
+ inspectBinaryArchs: () => 'arm64',
+ }),
+ ).toThrow(/Expected x86_64-only binary but got: arm64/);
+ });
+});
diff --git a/scripts/dev/copy-runtime-db-generated.test.ts b/scripts/dev/copy-runtime-db-generated.test.ts
new file mode 100644
index 00000000..61a7d987
--- /dev/null
+++ b/scripts/dev/copy-runtime-db-generated.test.ts
@@ -0,0 +1,55 @@
+import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import { tmpdir } from 'node:os';
+import { afterEach, describe, expect, it } from 'vitest';
+import { copyRuntimeDbGeneratedAssets } from './copy-runtime-db-generated.js';
+
+const tempDirs: string[] = [];
+
+afterEach(() => {
+ while (tempDirs.length > 0) {
+ const dir = tempDirs.pop();
+ if (dir) {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ }
+});
+
+describe('copyRuntimeDbGeneratedAssets', () => {
+ it('copies generated runtime db artifacts and shared runtime modules into dist', () => {
+ const repoRoot = mkdtempSync(join(tmpdir(), 'metapi-runtime-db-assets-'));
+ tempDirs.push(repoRoot);
+
+ const sourceDir = join(repoRoot, 'src', 'server', 'db', 'generated');
+ const nestedDir = join(sourceDir, 'fixtures');
+ const sharedDir = join(repoRoot, 'src', 'shared');
+ const staleSharedDistDir = join(repoRoot, 'dist', 'shared');
+ mkdirSync(nestedDir, { recursive: true });
+ mkdirSync(sharedDir, { recursive: true });
+ mkdirSync(staleSharedDistDir, { recursive: true });
+ writeFileSync(join(sourceDir, 'mysql.bootstrap.sql'), 'create table demo ();');
+ writeFileSync(join(nestedDir, 'baseline.json'), '{"ok":true}');
+ writeFileSync(join(sharedDir, 'tokenRouteContract.d.ts'), 'export declare const demo: number;\n');
+ writeFileSync(join(sharedDir, 'tokenRouteContract.js'), 'export const demo = 1;\n');
+ writeFileSync(join(sharedDir, 'tokenRouteContract.test.ts'), 'throw new Error("should not ship");\n');
+ writeFileSync(join(staleSharedDistDir, 'tokenRouteContract.test.ts'), 'stale test artifact\n');
+
+ copyRuntimeDbGeneratedAssets(repoRoot);
+
+ expect(
+ readFileSync(join(repoRoot, 'dist', 'server', 'db', 'generated', 'mysql.bootstrap.sql'), 'utf8'),
+ ).toBe('create table demo ();');
+ expect(
+ readFileSync(join(repoRoot, 'dist', 'server', 'db', 'generated', 'fixtures', 'baseline.json'), 'utf8'),
+ ).toBe('{"ok":true}');
+ expect(
+ readFileSync(join(repoRoot, 'dist', 'shared', 'tokenRouteContract.js'), 'utf8'),
+ ).toBe('export const demo = 1;\n');
+ expect(
+ readFileSync(join(repoRoot, 'dist', 'shared', 'tokenRouteContract.d.ts'), 'utf8'),
+ ).toBe('export declare const demo: number;\n');
+ expect(
+ existsSync(join(repoRoot, 'dist', 'shared', 'tokenRouteContract.test.ts')),
+ ).toBe(false);
+ });
+});
diff --git a/scripts/dev/copy-runtime-db-generated.ts b/scripts/dev/copy-runtime-db-generated.ts
new file mode 100644
index 00000000..ca75128a
--- /dev/null
+++ b/scripts/dev/copy-runtime-db-generated.ts
@@ -0,0 +1,41 @@
+import { cpSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
+import { basename, dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+function resolveRepoRoot(): string {
+ return resolve(dirname(fileURLToPath(import.meta.url)), '../..');
+}
+
+function shouldCopySharedArtifact(sourcePath: string): boolean {
+ if (statSync(sourcePath).isDirectory()) return true;
+ const filename = basename(sourcePath);
+ return filename.endsWith('.js') || filename.endsWith('.d.ts');
+}
+
+export function copyRuntimeDbGeneratedAssets(repoRoot: string = resolveRepoRoot()): void {
+ const sourceDir = resolve(repoRoot, 'src/server/db/generated');
+ const targetDir = resolve(repoRoot, 'dist/server/db/generated');
+ const sharedSourceDir = resolve(repoRoot, 'src/shared');
+ const sharedTargetDir = resolve(repoRoot, 'dist/shared');
+
+ if (!existsSync(sourceDir)) {
+ throw new Error(`Runtime DB generated assets directory does not exist: ${sourceDir}`);
+ }
+
+ mkdirSync(dirname(targetDir), { recursive: true });
+ cpSync(sourceDir, targetDir, { recursive: true, force: true });
+
+ if (existsSync(sharedSourceDir)) {
+ rmSync(sharedTargetDir, { recursive: true, force: true });
+ mkdirSync(dirname(sharedTargetDir), { recursive: true });
+ cpSync(sharedSourceDir, sharedTargetDir, {
+ recursive: true,
+ force: true,
+ filter: (sourcePath) => shouldCopySharedArtifact(sourcePath),
+ });
+ }
+}
+
+if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+ copyRuntimeDbGeneratedAssets();
+}
diff --git a/scripts/dev/db-smoke.ts b/scripts/dev/db-smoke.ts
index 4518a558..289192a0 100644
--- a/scripts/dev/db-smoke.ts
+++ b/scripts/dev/db-smoke.ts
@@ -52,6 +52,22 @@ function normalizeFirstScalar(value: unknown): number | string | null {
return null;
}
+async function selectScalar(db: any, query: ReturnType): Promise {
+ if (typeof db?.get === 'function') {
+ return normalizeFirstScalar(await db.get(query));
+ }
+ if (typeof db?.all === 'function') {
+ return normalizeFirstScalar(await db.all(query));
+ }
+ if (typeof db?.values === 'function') {
+ return normalizeFirstScalar(await db.values(query));
+ }
+ if (typeof db?.execute === 'function') {
+ return normalizeFirstScalar(await db.execute(query));
+ }
+ throw new Error('database client does not expose a scalar query method');
+}
+
async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.dbType) process.env.DB_TYPE = options.dbType;
@@ -75,26 +91,23 @@ async function main() {
console.log('[db-smoke] dbUrl=(empty, using default sqlite path)');
}
- const pingRows = await db.execute(sql`select 1 as ok`);
- const pingScalar = normalizeFirstScalar(pingRows);
+ const pingScalar = await selectScalar(db, sql`select 1 as ok`);
if (Number(pingScalar) !== 1) {
- throw new Error(`unexpected ping result: ${JSON.stringify(pingRows)}`);
+ throw new Error(`unexpected ping result: ${JSON.stringify(pingScalar)}`);
}
console.log('[db-smoke] ping ok');
- const txRows = await db.transaction(async (tx: any) => tx.execute(sql`select 1 as ok`));
- const txScalar = normalizeFirstScalar(txRows);
+ const txScalar = await db.transaction(async (tx: any) => selectScalar(tx, sql`select 1 as ok`));
if (Number(txScalar) !== 1) {
- throw new Error(`unexpected transaction ping result: ${JSON.stringify(txRows)}`);
+ throw new Error(`unexpected transaction ping result: ${JSON.stringify(txScalar)}`);
}
console.log('[db-smoke] transaction ok');
- const versionRows = dbType === 'sqlite'
- ? await db.execute(sql`select sqlite_version() as v`)
- : await db.execute(sql`select version() as v`);
- const version = normalizeFirstScalar(versionRows);
+ const version = dbType === 'sqlite'
+ ? await selectScalar(db, sql`select sqlite_version() as v`)
+ : await selectScalar(db, sql`select version() as v`);
if (typeof version !== 'string' || version.trim().length === 0) {
- throw new Error(`failed to read server version: ${JSON.stringify(versionRows)}`);
+ throw new Error(`failed to read server version: ${JSON.stringify(version)}`);
}
console.log(`[db-smoke] version=${version.slice(0, 120)}`);
diff --git a/scripts/dev/docker.workflow.test.ts b/scripts/dev/docker.workflow.test.ts
new file mode 100644
index 00000000..2242fb51
--- /dev/null
+++ b/scripts/dev/docker.workflow.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+describe('docker workflows', () => {
+ it('publishes armv7 docker images in ci and release workflows', () => {
+ const ciWorkflow = readFileSync(resolve(process.cwd(), '.github/workflows/ci.yml'), 'utf8');
+ const releaseWorkflow = readFileSync(resolve(process.cwd(), '.github/workflows/release.yml'), 'utf8');
+
+ expect(ciWorkflow).toContain('arch: armv7');
+ expect(ciWorkflow).toContain('platform: linux/arm/v7');
+ expect(ciWorkflow).toContain('"${tag}-armv7"');
+
+ expect(releaseWorkflow).toContain('arch: armv7');
+ expect(releaseWorkflow).toContain('platform: linux/arm/v7');
+ expect(releaseWorkflow).toContain('"${tag}-armv7"');
+ });
+
+ it('uses an armv7-capable node base image in the Dockerfile', () => {
+ const dockerfile = readFileSync(resolve(process.cwd(), 'docker/Dockerfile'), 'utf8');
+
+ expect(dockerfile).toContain('FROM node:22-bookworm-slim AS builder');
+ expect(dockerfile).toContain('FROM node:22-bookworm-slim');
+ });
+
+ it('keeps server docker builds isolated from desktop packaging dependencies', () => {
+ const dockerfile = readFileSync(resolve(process.cwd(), 'docker/Dockerfile'), 'utf8');
+
+ expect(dockerfile).toContain('npm ci --ignore-scripts --no-audit --no-fund');
+ expect(dockerfile).toContain('npm rebuild esbuild sharp better-sqlite3 --no-audit --no-fund');
+ expect(dockerfile).not.toContain('npm ci --no-audit --no-fund');
+ expect(dockerfile).toContain('RUN npm run build:web && npm run build:server');
+ expect(dockerfile).toContain('npm prune --omit=dev --no-audit --no-fund');
+ });
+});
diff --git a/scripts/dev/generate-schema-contract.ts b/scripts/dev/generate-schema-contract.ts
new file mode 100644
index 00000000..5ecb5740
--- /dev/null
+++ b/scripts/dev/generate-schema-contract.ts
@@ -0,0 +1,23 @@
+import { existsSync, readFileSync } from 'node:fs';
+import { writeDialectArtifactFiles } from '../../src/server/db/schemaArtifactGenerator.js';
+import {
+ resolveGeneratedSchemaContractPath,
+ type SchemaContract,
+ writeSchemaContractFile,
+} from '../../src/server/db/schemaContract.js';
+
+function readPreviousSchemaContract(): SchemaContract | null {
+ const contractPath = resolveGeneratedSchemaContractPath();
+ if (!existsSync(contractPath)) {
+ return null;
+ }
+
+ return JSON.parse(readFileSync(contractPath, 'utf8')) as SchemaContract;
+}
+
+const previousContract = readPreviousSchemaContract();
+const contract = writeSchemaContractFile();
+writeDialectArtifactFiles(contract, previousContract);
+const tableCount = Object.keys(contract.tables).length;
+
+console.log(`[schema:contract] wrote ${tableCount} tables and dialect artifacts`);
diff --git a/scripts/dev/generate-upgrade-fixture.ts b/scripts/dev/generate-upgrade-fixture.ts
new file mode 100644
index 00000000..8dabcbfe
--- /dev/null
+++ b/scripts/dev/generate-upgrade-fixture.ts
@@ -0,0 +1,113 @@
+import { execFileSync } from 'node:child_process';
+import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import {
+ resolveGeneratedSchemaContractPath,
+ type SchemaContract,
+} from '../../src/server/db/schemaContract.js';
+
+interface FixtureScriptOptions {
+ fromRef?: string;
+ outputPath: string;
+ dropTables: string[];
+ dropColumns: string[];
+}
+
+function parseArgs(argv: string[]): FixtureScriptOptions {
+ const options: FixtureScriptOptions = {
+ outputPath: 'src/server/db/generated/fixtures/2026-03-14-baseline.schemaContract.json',
+ dropTables: [],
+ dropColumns: [],
+ };
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const argument = argv[index];
+ if (argument === '--from-ref') {
+ options.fromRef = argv[index + 1];
+ index += 1;
+ continue;
+ }
+ if (argument === '--output') {
+ options.outputPath = argv[index + 1];
+ index += 1;
+ continue;
+ }
+ if (argument === '--drop-table') {
+ options.dropTables.push(argv[index + 1]);
+ index += 1;
+ continue;
+ }
+ if (argument === '--drop-column') {
+ options.dropColumns.push(argv[index + 1]);
+ index += 1;
+ }
+ }
+
+ return options;
+}
+
+function pruneContract(contract: SchemaContract, options: FixtureScriptOptions): SchemaContract {
+ const droppedTables = new Set(options.dropTables);
+ const droppedColumns = new Map>();
+
+ for (const entry of options.dropColumns) {
+ const [tableName, columnName] = entry.split('.', 2);
+ if (!tableName || !columnName) {
+ throw new Error(`invalid --drop-column value: ${entry}`);
+ }
+ const tableColumns = droppedColumns.get(tableName) ?? new Set();
+ tableColumns.add(columnName);
+ droppedColumns.set(tableName, tableColumns);
+ }
+
+ const tables = Object.fromEntries(
+ Object.entries(contract.tables)
+ .filter(([tableName]) => !droppedTables.has(tableName))
+ .map(([tableName, table]) => {
+ const removedColumns = droppedColumns.get(tableName) ?? new Set();
+ const columns = Object.fromEntries(
+ Object.entries(table.columns).filter(([columnName]) => !removedColumns.has(columnName)),
+ );
+ return [tableName, { columns }];
+ }),
+ );
+
+ const hasColumn = (tableName: string, columnName: string): boolean =>
+ !!tables[tableName]?.columns[columnName];
+
+ return {
+ tables,
+ indexes: contract.indexes.filter((index) =>
+ !!tables[index.table] && index.columns.every((columnName) => hasColumn(index.table, columnName))),
+ uniques: contract.uniques.filter((unique) =>
+ !!tables[unique.table] && unique.columns.every((columnName) => hasColumn(unique.table, columnName))),
+ foreignKeys: contract.foreignKeys.filter((foreignKey) =>
+ !!tables[foreignKey.table]
+ && !!tables[foreignKey.referencedTable]
+ && foreignKey.columns.every((columnName) => hasColumn(foreignKey.table, columnName))
+ && foreignKey.referencedColumns.every((columnName) => hasColumn(foreignKey.referencedTable, columnName))),
+ };
+}
+
+function readContractFromRef(fromRef: string): SchemaContract {
+ const contractJson = execFileSync(
+ 'git',
+ ['-c', 'safe.directory=.', 'show', `${fromRef}:src/server/db/generated/schemaContract.json`],
+ { encoding: 'utf8' },
+ );
+ return JSON.parse(contractJson) as SchemaContract;
+}
+
+function readCurrentContract(): SchemaContract {
+ return JSON.parse(readFileSync(resolveGeneratedSchemaContractPath(), 'utf8')) as SchemaContract;
+}
+
+const options = parseArgs(process.argv.slice(2));
+const sourceContract = options.fromRef ? readContractFromRef(options.fromRef) : readCurrentContract();
+const contract = pruneContract(sourceContract, options);
+const outputPath = resolve(process.cwd(), options.outputPath);
+
+mkdirSync(dirname(outputPath), { recursive: true });
+writeFileSync(outputPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
+
+console.log(`[schema:upgrade-fixture] wrote ${outputPath}`);
diff --git a/scripts/dev/harness.workflow.test.ts b/scripts/dev/harness.workflow.test.ts
new file mode 100644
index 00000000..e0a9a451
--- /dev/null
+++ b/scripts/dev/harness.workflow.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+describe('harness workflows', () => {
+ it('keeps repo drift checks wired into ci and scheduled reporting', () => {
+ const ciWorkflow = readFileSync(resolve(process.cwd(), '.github/workflows/ci.yml'), 'utf8');
+ const driftWorkflow = readFileSync(resolve(process.cwd(), '.github/workflows/harness-drift-report.yml'), 'utf8');
+
+ expect(ciWorkflow).toContain('name: Repo Drift Check');
+ expect(ciWorkflow).toContain('npm run repo:drift-check');
+ expect(ciWorkflow).toContain('name: Test Core');
+ expect(ciWorkflow).toContain('name: Build Web');
+ expect(ciWorkflow).toContain('name: Typecheck');
+
+ expect(driftWorkflow).toContain('schedule:');
+ expect(driftWorkflow).toContain('workflow_dispatch:');
+ expect(driftWorkflow).toContain('npm run repo:drift-check -- --format markdown --output tmp/repo-drift-report.md --report-only');
+ expect(driftWorkflow).toContain('actions/upload-artifact@v4');
+ expect(driftWorkflow).toContain('repo-drift-report');
+ });
+});
diff --git a/scripts/dev/repo-drift-check.test.ts b/scripts/dev/repo-drift-check.test.ts
new file mode 100644
index 00000000..5e187c6f
--- /dev/null
+++ b/scripts/dev/repo-drift-check.test.ts
@@ -0,0 +1,64 @@
+import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join } from 'node:path';
+import { describe, expect, it } from 'vitest';
+import { formatRepoDriftReport, runRepoDriftCheck } from './repo-drift-check.js';
+
+function writeWorkspaceFiles(root: string, files: Record): void {
+ for (const [relativePath, contents] of Object.entries(files)) {
+ const fullPath = join(root, relativePath);
+ mkdirSync(dirname(fullPath), { recursive: true });
+ writeFileSync(fullPath, contents);
+ }
+}
+
+describe('repo drift check', () => {
+ it('separates new violations from tracked debt', () => {
+ const root = mkdtempSync(join(tmpdir(), 'metapi-repo-drift-'));
+ writeWorkspaceFiles(root, {
+ 'src/server/transformers/openai/responses/routeCompatibility.ts': "import type { EndpointAttemptContext } from '../../../routes/proxy/endpointFlow.js';\n",
+ 'src/server/proxy-core/surfaces/chatSurface.ts': 'const payload = await upstream.text();\n',
+ 'src/server/proxy-core/surfaces/sharedSurface.ts': "import { dispatchRuntimeRequest } from '../../routes/proxy/runtimeExecutor.js';\n",
+ 'src/web/pages/Accounts.tsx': "import { TokensPanel } from './Tokens.js';\n",
+ 'src/web/pages/Tokens.tsx': 'export const TokensPanel = () => null;\n',
+ });
+
+ const report = runRepoDriftCheck({ root });
+
+ expect(report.violations).toEqual(expect.arrayContaining([
+ expect.objectContaining({
+ ruleId: 'proxy-surface-body-read',
+ file: 'src/server/proxy-core/surfaces/chatSurface.ts',
+ }),
+ expect.objectContaining({
+ ruleId: 'transformers-route-blind',
+ file: 'src/server/transformers/openai/responses/routeCompatibility.ts',
+ }),
+ ]));
+ expect(report.trackedDebt).toEqual(expect.arrayContaining([
+ expect.objectContaining({
+ ruleId: 'proxy-core-routes-proxy-import',
+ file: 'src/server/proxy-core/surfaces/sharedSurface.ts',
+ }),
+ expect.objectContaining({
+ ruleId: 'web-page-to-page-import',
+ file: 'src/web/pages/Accounts.tsx',
+ }),
+ ]));
+ });
+
+ it('keeps the current repository within the first-wave ratchet', () => {
+ const report = runRepoDriftCheck({ root: process.cwd() });
+ expect(report.violations).toEqual([]);
+ expect(report.trackedDebt).toEqual(expect.any(Array));
+ });
+
+ it('can render markdown reports for scheduled cleanup jobs', () => {
+ const report = runRepoDriftCheck({ root: process.cwd() });
+ const markdown = formatRepoDriftReport(report, 'markdown');
+
+ expect(markdown).toContain('# Repo Drift Report');
+ expect(markdown).toContain('## Violations');
+ expect(markdown).toContain('## Tracked Debt');
+ });
+});
diff --git a/scripts/dev/repo-drift-check.ts b/scripts/dev/repo-drift-check.ts
new file mode 100644
index 00000000..f258fa6f
--- /dev/null
+++ b/scripts/dev/repo-drift-check.ts
@@ -0,0 +1,338 @@
+import { lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { dirname, extname, relative, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import process from 'node:process';
+
+type ReportFormat = 'text' | 'markdown';
+type Severity = 'violation' | 'tracked_debt';
+
+type DriftFinding = {
+ ruleId: string;
+ severity: Severity;
+ file: string;
+ line: number;
+ message: string;
+ excerpt: string;
+};
+
+type DriftReport = {
+ root: string;
+ generatedAt: string;
+ violations: DriftFinding[];
+ trackedDebt: DriftFinding[];
+};
+
+type RuleSpec = {
+ id: string;
+ description: string;
+ fileFilter: (file: string) => boolean;
+ lineMatch: (line: string, file: string) => boolean;
+ message: string | ((file: string, line: string) => string);
+ allowlistedFiles?: Set;
+};
+
+type RunOptions = {
+ root: string;
+};
+
+const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
+
+const ROUTES_PROXY_IMPORT_ALLOWLIST = new Set([
+ 'src/server/proxy-core/surfaces/chatSurface.ts',
+ 'src/server/proxy-core/surfaces/filesSurface.ts',
+ 'src/server/proxy-core/surfaces/geminiSurface.ts',
+ 'src/server/proxy-core/surfaces/openAiResponsesSurface.ts',
+ 'src/server/proxy-core/surfaces/sharedSurface.ts',
+]);
+
+const TOP_LEVEL_PAGE_IMPORT_ALLOWLIST = new Set([
+ 'src/web/pages/Accounts.tsx',
+]);
+
+function normalizeRelativePath(root: string, fullPath: string): string {
+ return relative(root, fullPath).replaceAll('\\', '/');
+}
+
+function walkFiles(root: string, currentDir = root): string[] {
+ const entries = readdirSync(currentDir).sort((left, right) => left.localeCompare(right, 'en'));
+ const files: string[] = [];
+
+ for (const entry of entries) {
+ if (entry === '.git' || entry === 'node_modules' || entry === 'dist' || entry === 'coverage') {
+ continue;
+ }
+
+ const fullPath = resolve(currentDir, entry);
+ const stat = lstatSync(fullPath);
+ if (stat.isSymbolicLink()) {
+ continue;
+ }
+ if (stat.isDirectory()) {
+ files.push(...walkFiles(root, fullPath));
+ continue;
+ }
+
+ const extension = extname(entry);
+ if (SOURCE_EXTENSIONS.has(extension)) {
+ files.push(fullPath);
+ }
+ }
+
+ return files;
+}
+
+function readWorkspaceLines(root: string, file: string): Array<{ lineNumber: number; text: string }> {
+ const source = readFileSync(resolve(root, file), 'utf8').replaceAll('\r\n', '\n');
+ return source.split('\n').map((text, index) => ({
+ lineNumber: index + 1,
+ text,
+ }));
+}
+
+function isNonTestSource(file: string): boolean {
+ return !file.endsWith('.test.ts')
+ && !file.endsWith('.test.tsx')
+ && !file.endsWith('.test.js')
+ && !file.endsWith('.test.jsx');
+}
+
+function isTopLevelPageFile(file: string): boolean {
+ return /^src\/web\/pages\/[^/]+\.(ts|tsx|js|jsx)$/.test(file)
+ && isNonTestSource(file);
+}
+
+function createRules(): RuleSpec[] {
+ return [
+ {
+ id: 'transformers-route-blind',
+ description: 'Transformers must not import route-layer proxy helpers',
+ fileFilter: (file) => file.startsWith('src/server/transformers/')
+ && isNonTestSource(file),
+ lineMatch: (line) => /from\s+['"][^'"]*routes\/proxy\//.test(line),
+ message: 'transformer imports route-layer proxy code',
+ },
+ {
+ id: 'proxy-surface-body-read',
+ description: 'Proxy-core surfaces should use readRuntimeResponseText() for whole-body reads',
+ fileFilter: (file) => file.startsWith('src/server/proxy-core/surfaces/')
+ && isNonTestSource(file),
+ lineMatch: (line) => /\.text\(/.test(line),
+ message: 'proxy-core surface reads a full upstream body via .text()',
+ },
+ {
+ id: 'proxy-core-routes-proxy-import',
+ description: 'Proxy-core imports from routes/proxy are tracked debt and should not grow',
+ fileFilter: (file) => file.startsWith('src/server/proxy-core/')
+ && isNonTestSource(file),
+ lineMatch: (line) => /from\s+['"][^'"]*routes\/proxy\//.test(line),
+ message: 'proxy-core imports a helper from routes/proxy',
+ allowlistedFiles: ROUTES_PROXY_IMPORT_ALLOWLIST,
+ },
+ {
+ id: 'web-page-to-page-import',
+ description: 'Top-level route pages should not import other top-level route pages',
+ fileFilter: (file) => isTopLevelPageFile(file),
+ lineMatch: (line, file) => {
+ const match = line.match(/from\s+['"]\.\/([^/'"]+?)(?:\.(?:js|ts|tsx|jsx))?['"]/);
+ if (!match) return false;
+ const importedPage = match[1];
+ const currentPage = file.replace(/^src\/web\/pages\//, '').replace(/\.(ts|tsx|js|jsx)$/, '');
+ return importedPage !== currentPage;
+ },
+ message: (file, line) => {
+ const match = line.match(/from\s+['"](\.\/[^'"]+)['"]/);
+ const imported = match?.[1] ?? 'another page file';
+ return `top-level page imports ${imported}`;
+ },
+ allowlistedFiles: TOP_LEVEL_PAGE_IMPORT_ALLOWLIST,
+ },
+ ];
+}
+
+export function runRepoDriftCheck(options: Partial = {}): DriftReport {
+ const root = resolve(options.root ?? process.cwd());
+ const report: DriftReport = {
+ root,
+ generatedAt: new Date().toISOString(),
+ violations: [],
+ trackedDebt: [],
+ };
+
+ const files = walkFiles(root).map((file) => normalizeRelativePath(root, file));
+ const rules = createRules();
+
+ for (const rule of rules) {
+ for (const file of files) {
+ if (!rule.fileFilter(file)) continue;
+
+ for (const { lineNumber, text } of readWorkspaceLines(root, file)) {
+ if (!rule.lineMatch(text, file)) continue;
+
+ const finding: DriftFinding = {
+ ruleId: rule.id,
+ severity: rule.allowlistedFiles?.has(file) ? 'tracked_debt' : 'violation',
+ file,
+ line: lineNumber,
+ message: typeof rule.message === 'function' ? rule.message(file, text) : rule.message,
+ excerpt: text.trim(),
+ };
+
+ if (finding.severity === 'tracked_debt') {
+ report.trackedDebt.push(finding);
+ } else {
+ report.violations.push(finding);
+ }
+ }
+ }
+ }
+
+ return report;
+}
+
+function formatFindingText(finding: DriftFinding): string {
+ return `- [${finding.ruleId}] ${finding.file}:${finding.line} ${finding.message}\n ${finding.excerpt}`;
+}
+
+function formatFindingMarkdownRow(finding: DriftFinding): string {
+ const escapedMessage = finding.message.replaceAll('|', '\\|');
+ const escapedExcerpt = finding.excerpt.replaceAll('|', '\\|');
+ return `| \`${finding.ruleId}\` | \`${finding.file}:${finding.line}\` | ${escapedMessage} | \`${escapedExcerpt}\` |`;
+}
+
+export function formatRepoDriftReport(report: DriftReport, format: ReportFormat = 'text'): string {
+ if (format === 'markdown') {
+ const lines: string[] = [
+ '# Repo Drift Report',
+ '',
+ `- Root: \`${report.root}\``,
+ `- Generated at: \`${report.generatedAt}\``,
+ `- Violations: **${report.violations.length}**`,
+ `- Tracked debt: **${report.trackedDebt.length}**`,
+ '',
+ ];
+
+ if (report.violations.length > 0) {
+ lines.push('## Violations', '', '| Rule | Location | Message | Excerpt |', '| --- | --- | --- | --- |');
+ for (const finding of report.violations) {
+ lines.push(formatFindingMarkdownRow(finding));
+ }
+ lines.push('');
+ } else {
+ lines.push('## Violations', '', 'No new violations found.', '');
+ }
+
+ if (report.trackedDebt.length > 0) {
+ lines.push('## Tracked Debt', '', '| Rule | Location | Message | Excerpt |', '| --- | --- | --- | --- |');
+ for (const finding of report.trackedDebt) {
+ lines.push(formatFindingMarkdownRow(finding));
+ }
+ lines.push('');
+ } else {
+ lines.push('## Tracked Debt', '', 'No tracked debt entries were observed.', '');
+ }
+
+ return lines.join('\n');
+ }
+
+ const lines: string[] = [
+ `Repo drift report for ${report.root}`,
+ `Generated at ${report.generatedAt}`,
+ '',
+ `Violations: ${report.violations.length}`,
+ ];
+
+ if (report.violations.length > 0) {
+ lines.push(...report.violations.map(formatFindingText));
+ } else {
+ lines.push('- none');
+ }
+
+ lines.push('', `Tracked debt: ${report.trackedDebt.length}`);
+ if (report.trackedDebt.length > 0) {
+ lines.push(...report.trackedDebt.map(formatFindingText));
+ } else {
+ lines.push('- none');
+ }
+
+ return lines.join('\n');
+}
+
+type CliOptions = {
+ format: ReportFormat;
+ output?: string;
+ reportOnly: boolean;
+ root?: string;
+};
+
+function parseCliOptions(argv: string[]): CliOptions {
+ const options: CliOptions = {
+ format: 'text',
+ reportOnly: false,
+ };
+
+ const readRequiredValue = (flag: string, index: number): string => {
+ const value = argv[index + 1];
+ if (!value || value.startsWith('--')) {
+ throw new Error(`${flag} requires a value`);
+ }
+ return value;
+ };
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const arg = argv[index];
+ if (arg === '--format') {
+ const value = readRequiredValue(arg, index);
+ if (value !== 'text' && value !== 'markdown') {
+ throw new Error(`--format must be one of: text, markdown`);
+ }
+ options.format = value;
+ index += 1;
+ continue;
+ }
+ if (arg === '--output') {
+ options.output = readRequiredValue(arg, index);
+ index += 1;
+ continue;
+ }
+ if (arg === '--report-only') {
+ options.reportOnly = true;
+ continue;
+ }
+ if (arg === '--root') {
+ options.root = readRequiredValue(arg, index);
+ index += 1;
+ }
+ }
+
+ return options;
+}
+
+function maybeWriteReport(outputPath: string | undefined, contents: string): void {
+ if (!outputPath) return;
+ const resolved = resolve(outputPath);
+ mkdirSync(dirname(resolved), { recursive: true });
+ writeFileSync(resolved, contents);
+}
+
+const isMainModule = (() => {
+ try {
+ return process.argv[1] != null && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
+ } catch {
+ return false;
+ }
+})();
+
+if (isMainModule) {
+ try {
+ const options = parseCliOptions(process.argv.slice(2));
+ const report = runRepoDriftCheck({ root: options.root });
+ const contents = formatRepoDriftReport(report, options.format);
+ maybeWriteReport(options.output, contents);
+ console.log(contents);
+ process.exit(report.violations.length > 0 && !options.reportOnly ? 1 : 0);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.error(message);
+ process.exit(1);
+ }
+}
diff --git a/src/desktop/iconAssets.test.ts b/src/desktop/iconAssets.test.ts
new file mode 100644
index 00000000..caea7524
--- /dev/null
+++ b/src/desktop/iconAssets.test.ts
@@ -0,0 +1,29 @@
+import { readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+import { describe, expect, it } from 'vitest';
+
+describe('desktop icon assets', () => {
+ it('keeps runtime icon paths aligned to the generated desktop icon', async () => {
+ const {
+ DESKTOP_BUILD_ICON_RELATIVE_PATH,
+ DESKTOP_RUNTIME_ICON_RELATIVE_PATH,
+ DESKTOP_TRAY_TEMPLATE_ICON_RELATIVE_PATH,
+ getDesktopRuntimeIconPath,
+ getDesktopTrayIconPath,
+ } = await import('./iconAssets.js');
+
+ expect(DESKTOP_RUNTIME_ICON_RELATIVE_PATH).toBe(join('dist', 'web', 'desktop-icon.png'));
+ expect(DESKTOP_BUILD_ICON_RELATIVE_PATH).toBe(join('build', 'desktop-icon.png'));
+ expect(DESKTOP_TRAY_TEMPLATE_ICON_RELATIVE_PATH).toBe(join('dist', 'web', 'desktop-tray-template.png'));
+ expect(getDesktopRuntimeIconPath('/app')).toBe(join('/app', 'dist', 'web', 'desktop-icon.png'));
+ expect(getDesktopTrayIconPath('/app', 'darwin')).toBe(join('/app', 'dist', 'web', 'desktop-tray-template.png'));
+ expect(getDesktopTrayIconPath('/app', 'win32')).toBe(join('/app', 'dist', 'web', 'desktop-icon.png'));
+ });
+
+ it('points electron-builder at the generated desktop package icon', async () => {
+ const configPath = join(process.cwd(), 'electron-builder.yml');
+ const config = await readFile(configPath, 'utf8');
+
+ expect(config).toContain('icon: build/desktop-icon.png');
+ });
+});
diff --git a/src/desktop/iconAssets.ts b/src/desktop/iconAssets.ts
new file mode 100644
index 00000000..92ec1481
--- /dev/null
+++ b/src/desktop/iconAssets.ts
@@ -0,0 +1,20 @@
+import { join } from 'node:path';
+
+export const DESKTOP_RUNTIME_ICON_RELATIVE_PATH = join('dist', 'web', 'desktop-icon.png');
+export const DESKTOP_BUILD_ICON_RELATIVE_PATH = join('build', 'desktop-icon.png');
+export const DESKTOP_TRAY_TEMPLATE_ICON_RELATIVE_PATH = join('dist', 'web', 'desktop-tray-template.png');
+export const DESKTOP_ICON_PUBLIC_PATH = '/desktop-icon.png';
+
+export function getDesktopRuntimeIconPath(appPath: string) {
+ return join(appPath, DESKTOP_RUNTIME_ICON_RELATIVE_PATH);
+}
+
+export function getDesktopTrayTemplateIconPath(appPath: string) {
+ return join(appPath, DESKTOP_TRAY_TEMPLATE_ICON_RELATIVE_PATH);
+}
+
+export function getDesktopTrayIconPath(appPath: string, platform = process.platform) {
+ return platform === 'darwin'
+ ? getDesktopTrayTemplateIconPath(appPath)
+ : getDesktopRuntimeIconPath(appPath);
+}
diff --git a/src/desktop/main.ts b/src/desktop/main.ts
index f6d7ffe0..b0296739 100644
--- a/src/desktop/main.ts
+++ b/src/desktop/main.ts
@@ -9,7 +9,6 @@ import {
} from 'electron';
import log from 'electron-log';
import electronUpdater from 'electron-updater';
-import getPort, { portNumbers } from 'get-port';
import { spawn, type ChildProcess } from 'node:child_process';
import { existsSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
@@ -18,9 +17,11 @@ import {
createDesktopHealthUrl,
createDesktopServerUrl,
isFatalServerExit,
+ resolveDesktopServerPort,
resolveDesktopServerWorkingDir,
waitForServerReady,
} from './runtime.js';
+import { getDesktopRuntimeIconPath, getDesktopTrayIconPath } from './iconAssets.js';
const { autoUpdater } = electronUpdater;
@@ -51,7 +52,7 @@ function getServerEntryPath() {
}
function getTrayIconPath() {
- return join(app.getAppPath(), 'dist', 'web', 'logo.png');
+ return getDesktopTrayIconPath(app.getAppPath(), process.platform);
}
function getWindowIconPath() {
@@ -68,12 +69,6 @@ function resolveExternalServerUrl() {
return (process.env.METAPI_DESKTOP_EXTERNAL_SERVER_URL || '').trim();
}
-async function resolveServerPort() {
- const forcedPort = Number.parseInt(process.env.METAPI_DESKTOP_SERVER_PORT || '', 10);
- if (Number.isFinite(forcedPort) && forcedPort > 0) return forcedPort;
- return getPort({ port: portNumbers(4310, 4399) });
-}
-
function showMainWindow() {
if (!mainWindow) return;
if (mainWindow.isMinimized()) mainWindow.restore();
@@ -126,7 +121,15 @@ function buildTrayMenu() {
function setupTray() {
if (tray) return;
- const trayImage = nativeImage.createFromPath(getTrayIconPath());
+ let trayImage = nativeImage.createFromPath(getTrayIconPath());
+ if (process.platform === 'darwin' && !trayImage.isEmpty()) {
+ trayImage = trayImage.resize({
+ width: 18,
+ height: 18,
+ quality: 'best',
+ });
+ trayImage.setTemplateImage(true);
+ }
tray = new Tray(trayImage);
tray.setToolTip('Metapi');
tray.setContextMenu(buildTrayMenu());
@@ -185,7 +188,7 @@ async function waitForManagedServerReady(url: string) {
async function startManagedBackend() {
ensureDesktopDirs();
const serverEntryPath = getServerEntryPath();
- const port = await resolveServerPort();
+ const port = resolveDesktopServerPort(process.env);
serverUrl = createDesktopServerUrl(port);
const env = buildDesktopServerEnv({
diff --git a/src/desktop/runtime.test.ts b/src/desktop/runtime.test.ts
index f2bf9575..1dbea804 100644
--- a/src/desktop/runtime.test.ts
+++ b/src/desktop/runtime.test.ts
@@ -3,12 +3,13 @@ import {
buildDesktopServerEnv,
createDesktopServerUrl,
isFatalServerExit,
+ resolveDesktopServerPort,
resolveDesktopServerWorkingDir,
waitForServerReady,
} from './runtime.js';
describe('desktop runtime helpers', () => {
- it('builds desktop server env with loopback host and app directories', () => {
+ it('builds desktop server env with external listen host and app directories', () => {
const env = buildDesktopServerEnv({
inheritedEnv: {
AUTH_TOKEN: 'admin-token',
@@ -19,7 +20,7 @@ describe('desktop runtime helpers', () => {
port: 4312,
});
- expect(env.HOST).toBe('127.0.0.1');
+ expect(env.HOST).toBe('0.0.0.0');
expect(env.PORT).toBe('4312');
expect(env.DATA_DIR).toBe('/tmp/metapi-data');
expect(env.METAPI_LOG_DIR).toBe('/tmp/metapi-logs');
@@ -31,6 +32,16 @@ describe('desktop runtime helpers', () => {
expect(createDesktopServerUrl(4312)).toBe('http://127.0.0.1:4312');
});
+ it('defaults desktop backend port to 4000', () => {
+ expect(resolveDesktopServerPort({})).toBe(4000);
+ });
+
+ it('honors explicit desktop backend port override', () => {
+ expect(resolveDesktopServerPort({
+ METAPI_DESKTOP_SERVER_PORT: '4312',
+ })).toBe(4312);
+ });
+
it('uses resources path as backend cwd for packaged desktop builds', () => {
expect(resolveDesktopServerWorkingDir({
appPath: 'C:/Users/test/AppData/Local/Programs/Metapi/resources/app.asar',
diff --git a/src/desktop/runtime.ts b/src/desktop/runtime.ts
index 71d0d2a7..1ae4022a 100644
--- a/src/desktop/runtime.ts
+++ b/src/desktop/runtime.ts
@@ -23,6 +23,7 @@ type DesktopServerWorkingDirInput = {
isPackaged: boolean;
};
+const DEFAULT_DESKTOP_SERVER_PORT = 4000;
const DEFAULT_READY_TIMEOUT_MS = 30_000;
const DEFAULT_READY_INTERVAL_MS = 250;
@@ -31,9 +32,11 @@ function delay(ms: number) {
}
export function buildDesktopServerEnv(input: DesktopServerEnvInput): NodeJS.ProcessEnv {
+ const host = (input.inheritedEnv?.HOST || '0.0.0.0').trim() || '0.0.0.0';
+
return {
...(input.inheritedEnv || {}),
- HOST: '127.0.0.1',
+ HOST: host,
PORT: String(input.port),
DATA_DIR: input.userDataDir,
METAPI_DESKTOP: '1',
@@ -49,6 +52,12 @@ export function createDesktopHealthUrl(port: number): string {
return `${createDesktopServerUrl(port)}/api/desktop/health`;
}
+export function resolveDesktopServerPort(env?: NodeJS.ProcessEnv): number {
+ const forcedPort = Number.parseInt(env?.METAPI_DESKTOP_SERVER_PORT || '', 10);
+ if (Number.isFinite(forcedPort) && forcedPort > 0) return forcedPort;
+ return DEFAULT_DESKTOP_SERVER_PORT;
+}
+
export function resolveDesktopServerWorkingDir(input: DesktopServerWorkingDirInput): string {
return input.isPackaged ? input.resourcesPath : input.appPath;
}
diff --git a/src/server/config.test.ts b/src/server/config.test.ts
index 515759ac..53ea86c5 100644
--- a/src/server/config.test.ts
+++ b/src/server/config.test.ts
@@ -11,7 +11,7 @@ describe('buildConfig', () => {
expect(config.dataDir).toBe('./data');
});
- it('keeps desktop deployments bound to loopback', () => {
+ it('aligns desktop deployments with server deployments for listen host', () => {
const config = buildConfig({
HOST: '0.0.0.0',
METAPI_DESKTOP: '1',
@@ -19,7 +19,7 @@ describe('buildConfig', () => {
DATA_DIR: '/tmp/metapi-data',
});
- expect(config.listenHost).toBe('127.0.0.1');
+ expect(config.listenHost).toBe('0.0.0.0');
expect(config.port).toBe(4312);
expect(config.dataDir).toBe('/tmp/metapi-data');
});
@@ -32,6 +32,40 @@ describe('buildConfig', () => {
expect(config.listenHost).toBe('127.0.0.1');
});
+ it('defaults telegram api base url to the official endpoint', () => {
+ const config = buildConfig({});
+
+ expect(config.telegramApiBaseUrl).toBe('https://api.telegram.org');
+ expect(config.telegramMessageThreadId).toBe('');
+ });
+
+ it('accepts telegram message thread id from environment', () => {
+ const config = buildConfig({
+ TELEGRAM_MESSAGE_THREAD_ID: '77',
+ });
+
+ expect(config.telegramMessageThreadId).toBe('77');
+ });
+
+ it('ships CLI-aligned OAuth defaults', () => {
+ const config = buildConfig({});
+
+ expect(config.codexClientId).toBe('app_EMoamEEZ73f0CkXaXp7hrann');
+ expect(config.codexResponsesWebsocketBeta).toBe('responses_websockets=2026-02-06');
+ expect(config.claudeClientId).toBe('9d1c250a-e61b-44d9-88ed-5944d1962f5e');
+ expect(config.claudeClientSecret).toBe('');
+ expect(config.geminiCliClientId).toBe('681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com');
+ expect(config.geminiCliClientSecret).toBe('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');
+ });
+
+ it('allows overriding the codex websocket beta gate from environment', () => {
+ const config = buildConfig({
+ CODEX_RESPONSES_WEBSOCKET_BETA: 'responses_websockets=2099-01-01',
+ });
+
+ expect(config.codexResponsesWebsocketBeta).toBe('responses_websockets=2099-01-01');
+ });
+
it('accepts JSON request bodies larger than Fastify default 1 MiB', async () => {
const app = Fastify(buildFastifyOptions(buildConfig({})));
const largeText = 'a'.repeat(2 * 1024 * 1024);
@@ -51,4 +85,25 @@ describe('buildConfig', () => {
expect(response.json()).toEqual({ textLength: largeText.length });
await app.close();
});
+
+ it('trusts forwarded client IP headers for reverse-proxy deployments', async () => {
+ const app = Fastify(buildFastifyOptions(buildConfig({})));
+
+ app.get('/ip', async (request) => ({
+ ip: request.ip,
+ }));
+
+ const response = await app.inject({
+ method: 'GET',
+ url: '/ip',
+ remoteAddress: '10.0.0.8',
+ headers: {
+ 'x-forwarded-for': '203.0.113.5, 10.0.0.8',
+ },
+ });
+
+ expect(response.statusCode).toBe(200);
+ expect(response.json()).toEqual({ ip: '203.0.113.5' });
+ await app.close();
+ });
});
diff --git a/src/server/config.ts b/src/server/config.ts
index 5bef4d12..5fa73016 100644
--- a/src/server/config.ts
+++ b/src/server/config.ts
@@ -1,7 +1,12 @@
import 'dotenv/config';
import type { FastifyServerOptions } from 'fastify';
+import { normalizePayloadRulesConfig } from './services/payloadRules.js';
const DEFAULT_REQUEST_BODY_LIMIT = 20 * 1024 * 1024;
+const DEFAULT_CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
+const DEFAULT_CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
+const DEFAULT_GEMINI_CLI_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
+const DEFAULT_GEMINI_CLI_CLIENT_SECRET = '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';
function parseBoolean(value: string | undefined, fallback = false): boolean {
if (value === undefined) return fallback;
@@ -24,6 +29,19 @@ function parseCsvList(value: string | undefined): string[] {
.filter((item) => item.length > 0);
}
+function parseOptionalSecret(value: string | undefined): string {
+ return (value || '').trim();
+}
+
+function parseJsonValue(value: string | undefined): unknown {
+ if (!value) return undefined;
+ try {
+ return JSON.parse(value);
+ } catch {
+ return undefined;
+ }
+}
+
function parseDbType(value: string | undefined): 'sqlite' | 'mysql' | 'postgres' {
const normalized = (value || 'sqlite').trim().toLowerCase();
if (normalized === 'mysql') return 'mysql';
@@ -32,11 +50,7 @@ function parseDbType(value: string | undefined): 'sqlite' | 'mysql' | 'postgres'
}
function parseListenHost(env: NodeJS.ProcessEnv): string {
- const requestedHost = (env.HOST || '0.0.0.0').trim() || '0.0.0.0';
- if (parseBoolean(env.METAPI_DESKTOP, false)) {
- return '127.0.0.1';
- }
- return requestedHost;
+ return (env.HOST || '0.0.0.0').trim() || '0.0.0.0';
}
export function buildConfig(env: NodeJS.ProcessEnv) {
@@ -45,10 +59,24 @@ export function buildConfig(env: NodeJS.ProcessEnv) {
return {
authToken: env.AUTH_TOKEN || 'change-me-admin-token',
proxyToken: env.PROXY_TOKEN || 'change-me-proxy-sk-token',
+ codexClientId: parseOptionalSecret(env.CODEX_CLIENT_ID) || DEFAULT_CODEX_CLIENT_ID,
+ claudeClientId: parseOptionalSecret(env.CLAUDE_CLIENT_ID) || DEFAULT_CLAUDE_CLIENT_ID,
+ claudeClientSecret: parseOptionalSecret(env.CLAUDE_CLIENT_SECRET),
+ geminiCliClientId: parseOptionalSecret(env.GEMINI_CLI_CLIENT_ID) || DEFAULT_GEMINI_CLI_CLIENT_ID,
+ geminiCliClientSecret: parseOptionalSecret(env.GEMINI_CLI_CLIENT_SECRET) || DEFAULT_GEMINI_CLI_CLIENT_SECRET,
systemProxyUrl: env.SYSTEM_PROXY_URL || '',
accountCredentialSecret: env.ACCOUNT_CREDENTIAL_SECRET || env.AUTH_TOKEN || 'change-me-admin-token',
checkinCron: env.CHECKIN_CRON || '0 8 * * *',
+ checkinScheduleMode: (env.CHECKIN_SCHEDULE_MODE || 'cron').trim().toLowerCase() === 'interval'
+ ? 'interval' as const
+ : 'cron' as const,
+ checkinIntervalHours: Math.min(24, Math.max(1, Math.trunc(parseNumber(env.CHECKIN_INTERVAL_HOURS, 6)))),
balanceRefreshCron: env.BALANCE_REFRESH_CRON || '0 * * * *',
+ logCleanupCron: env.LOG_CLEANUP_CRON || '0 6 * * *',
+ logCleanupConfigured: false,
+ logCleanupUsageLogsEnabled: parseBoolean(env.LOG_CLEANUP_USAGE_LOGS_ENABLED, false),
+ logCleanupProgramLogsEnabled: parseBoolean(env.LOG_CLEANUP_PROGRAM_LOGS_ENABLED, false),
+ logCleanupRetentionDays: Math.max(1, Math.trunc(parseNumber(env.LOG_CLEANUP_RETENTION_DAYS, 30))),
webhookUrl: env.WEBHOOK_URL || '',
barkUrl: env.BARK_URL || '',
webhookEnabled: parseBoolean(env.WEBHOOK_ENABLED, true),
@@ -56,8 +84,11 @@ export function buildConfig(env: NodeJS.ProcessEnv) {
serverChanEnabled: parseBoolean(env.SERVERCHAN_ENABLED, true),
serverChanKey: env.SERVERCHAN_KEY || '',
telegramEnabled: parseBoolean(env.TELEGRAM_ENABLED, false),
+ telegramApiBaseUrl: 'https://api.telegram.org',
telegramBotToken: env.TELEGRAM_BOT_TOKEN || '',
telegramChatId: env.TELEGRAM_CHAT_ID || '',
+ telegramUseSystemProxy: parseBoolean(env.TELEGRAM_USE_SYSTEM_PROXY, false),
+ telegramMessageThreadId: (env.TELEGRAM_MESSAGE_THREAD_ID || '').trim(),
smtpEnabled: parseBoolean(env.SMTP_ENABLED, false),
smtpHost: env.SMTP_HOST || '',
smtpPort: parseInt(env.SMTP_PORT || '587'),
@@ -77,8 +108,28 @@ export function buildConfig(env: NodeJS.ProcessEnv) {
requestBodyLimit: DEFAULT_REQUEST_BODY_LIMIT,
routingFallbackUnitCost: Math.max(1e-6, parseNumber(env.ROUTING_FALLBACK_UNIT_COST, 1)),
tokenRouterCacheTtlMs: Math.max(100, Math.trunc(parseNumber(env.TOKEN_ROUTER_CACHE_TTL_MS, 1_500))),
+ proxyMaxChannelAttempts: Math.max(1, Math.trunc(parseNumber(env.PROXY_MAX_CHANNEL_ATTEMPTS, 3))),
+ proxyStickySessionEnabled: parseBoolean(env.PROXY_STICKY_SESSION_ENABLED, true),
+ proxyStickySessionTtlMs: Math.max(30_000, Math.trunc(parseNumber(env.PROXY_STICKY_SESSION_TTL_MS, 30 * 60 * 1000))),
+ proxySessionChannelConcurrencyLimit: Math.max(0, Math.trunc(parseNumber(env.PROXY_SESSION_CHANNEL_CONCURRENCY_LIMIT, 2))),
+ proxySessionChannelQueueWaitMs: Math.max(0, Math.trunc(parseNumber(env.PROXY_SESSION_CHANNEL_QUEUE_WAIT_MS, 1_500))),
+ proxySessionChannelLeaseTtlMs: Math.max(5_000, Math.trunc(parseNumber(env.PROXY_SESSION_CHANNEL_LEASE_TTL_MS, 90_000))),
+ proxySessionChannelLeaseKeepaliveMs: Math.max(1_000, Math.trunc(parseNumber(env.PROXY_SESSION_CHANNEL_LEASE_KEEPALIVE_MS, 15_000))),
+ codexUpstreamWebsocketEnabled: parseBoolean(env.CODEX_UPSTREAM_WEBSOCKET_ENABLED, false),
proxyLogRetentionDays: Math.max(0, Math.trunc(parseNumber(env.PROXY_LOG_RETENTION_DAYS, 30))),
proxyLogRetentionPruneIntervalMinutes: Math.max(1, Math.trunc(parseNumber(env.PROXY_LOG_RETENTION_PRUNE_INTERVAL_MINUTES, 30))),
+ proxyFileRetentionDays: Math.max(0, Math.trunc(parseNumber(env.PROXY_FILE_RETENTION_DAYS, 30))),
+ proxyFileRetentionPruneIntervalMinutes: Math.max(1, Math.trunc(parseNumber(env.PROXY_FILE_RETENTION_PRUNE_INTERVAL_MINUTES, 60))),
+ proxyErrorKeywords: parseCsvList(env.PROXY_ERROR_KEYWORDS),
+ proxyEmptyContentFailEnabled: parseBoolean(env.PROXY_EMPTY_CONTENT_FAIL, false),
+ globalBlockedBrands: [] as string[],
+ globalAllowedModels: [] as string[],
+ codexResponsesWebsocketBeta: parseOptionalSecret(env.CODEX_RESPONSES_WEBSOCKET_BETA) || 'responses_websockets=2026-02-06',
+ codexHeaderDefaults: {
+ userAgent: parseOptionalSecret(env.CODEX_HEADER_DEFAULTS_USER_AGENT),
+ betaFeatures: parseOptionalSecret(env.CODEX_HEADER_DEFAULTS_BETA_FEATURES),
+ },
+ payloadRules: normalizePayloadRulesConfig(parseJsonValue(env.PAYLOAD_RULES_JSON || env.PAYLOAD_RULES)),
routingWeights: {
baseWeightFactor: parseNumber(env.BASE_WEIGHT_FACTOR, 0.5),
valueScoreFactor: parseNumber(env.VALUE_SCORE_FACTOR, 0.5),
@@ -96,6 +147,7 @@ export function buildFastifyOptions(
): FastifyServerOptions {
return {
logger: true,
+ trustProxy: true,
bodyLimit: appConfig.requestBodyLimit,
};
}
diff --git a/src/server/db/accountTokenSchemaCompatibility.ts b/src/server/db/accountTokenSchemaCompatibility.ts
new file mode 100644
index 00000000..256ab2ea
--- /dev/null
+++ b/src/server/db/accountTokenSchemaCompatibility.ts
@@ -0,0 +1,73 @@
+export type AccountTokenSchemaDialect = 'sqlite' | 'mysql' | 'postgres';
+
+export interface AccountTokenSchemaInspector {
+ dialect: AccountTokenSchemaDialect;
+ tableExists(table: string): Promise;
+ columnExists(table: string, column: string): Promise;
+ execute(sqlText: string): Promise;
+}
+
+export type AccountTokenColumnCompatibilitySpec = {
+ table: 'account_tokens';
+ column: string;
+ addSql: Record;
+};
+
+export const ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS: AccountTokenColumnCompatibilitySpec[] = [
+ {
+ table: 'account_tokens',
+ column: 'token_group',
+ addSql: {
+ sqlite: 'ALTER TABLE account_tokens ADD COLUMN token_group text;',
+ mysql: 'ALTER TABLE `account_tokens` ADD COLUMN `token_group` TEXT NULL',
+ postgres: 'ALTER TABLE "account_tokens" ADD COLUMN "token_group" TEXT',
+ },
+ },
+ {
+ table: 'account_tokens',
+ column: 'value_status',
+ addSql: {
+ sqlite: "ALTER TABLE account_tokens ADD COLUMN value_status text NOT NULL DEFAULT 'ready';",
+ mysql: "ALTER TABLE `account_tokens` ADD COLUMN `value_status` VARCHAR(191) NOT NULL DEFAULT 'ready'",
+ postgres: "ALTER TABLE \"account_tokens\" ADD COLUMN \"value_status\" TEXT NOT NULL DEFAULT 'ready'",
+ },
+ },
+];
+
+function normalizeSchemaErrorMessage(error: unknown): string {
+ if (typeof error === 'object' && error && 'message' in error) {
+ return String((error as { message?: unknown }).message || '');
+ }
+ return String(error || '');
+}
+
+function isDuplicateColumnError(error: unknown): boolean {
+ const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
+ return lowered.includes('duplicate column')
+ || lowered.includes('already exists')
+ || lowered.includes('duplicate column name');
+}
+
+async function executeAddColumn(inspector: AccountTokenSchemaInspector, sqlText: string): Promise {
+ try {
+ await inspector.execute(sqlText);
+ } catch (error) {
+ if (!isDuplicateColumnError(error)) {
+ throw error;
+ }
+ }
+}
+
+export async function ensureAccountTokenSchemaCompatibility(inspector: AccountTokenSchemaInspector): Promise {
+ for (const spec of ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS) {
+ const hasTable = await inspector.tableExists(spec.table);
+ if (!hasTable) {
+ continue;
+ }
+
+ const hasColumn = await inspector.columnExists(spec.table, spec.column);
+ if (!hasColumn) {
+ await executeAddColumn(inspector, spec.addSql[inspector.dialect]);
+ }
+ }
+}
diff --git a/src/server/db/generated/fixtures/2026-03-14-baseline.schemaContract.json b/src/server/db/generated/fixtures/2026-03-14-baseline.schemaContract.json
new file mode 100644
index 00000000..648bf4bf
--- /dev/null
+++ b/src/server/db/generated/fixtures/2026-03-14-baseline.schemaContract.json
@@ -0,0 +1,1120 @@
+{
+ "tables": {
+ "account_tokens": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "token": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "source": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": "'manual'",
+ "primaryKey": false
+ },
+ "enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "is_default": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "token_group": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "accounts": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "site_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "username": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "access_token": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "api_token": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "balance": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "balance_used": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "quota": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "unit_cost": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "value_score": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "status": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": "'active'",
+ "primaryKey": false
+ },
+ "checkin_enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "last_checkin_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "last_balance_refresh": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "extra_config": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "is_pinned": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "sort_order": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ }
+ }
+ },
+ "checkin_logs": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "status": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "message": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "reward": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "events": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "type": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "title": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "message": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "level": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": "'info'",
+ "primaryKey": false
+ },
+ "read": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "related_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "related_type": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "model_availability": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "available": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "latency_ms": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "checked_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "proxy_logs": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "route_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "channel_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_requested": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_actual": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "status": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "http_status": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "latency_ms": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "prompt_tokens": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "completion_tokens": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "total_tokens": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "estimated_cost": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "error_message": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "retry_count": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "billing_details": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "route_channels": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "route_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "token_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "priority": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "weight": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "10",
+ "primaryKey": false
+ },
+ "enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "manual_override": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "success_count": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "fail_count": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "total_latency_ms": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "total_cost": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "last_used_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "last_fail_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "cooldown_until": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "settings": {
+ "columns": {
+ "key": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "value": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "site_disabled_models": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "site_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "sites": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "url": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "platform": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "status": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": "'active'",
+ "primaryKey": false
+ },
+ "api_key": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "is_pinned": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "sort_order": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ }
+ }
+ },
+ "token_model_availability": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "token_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "available": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "latency_ms": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "checked_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "token_routes": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "model_pattern": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_mapping": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ }
+ },
+ "indexes": [
+ {
+ "name": "account_tokens_account_enabled_idx",
+ "table": "account_tokens",
+ "columns": [
+ "account_id",
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "account_tokens_account_id_idx",
+ "table": "account_tokens",
+ "columns": [
+ "account_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "account_tokens_enabled_idx",
+ "table": "account_tokens",
+ "columns": [
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "accounts_site_id_idx",
+ "table": "accounts",
+ "columns": [
+ "site_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "accounts_site_status_idx",
+ "table": "accounts",
+ "columns": [
+ "site_id",
+ "status"
+ ],
+ "unique": false
+ },
+ {
+ "name": "accounts_status_idx",
+ "table": "accounts",
+ "columns": [
+ "status"
+ ],
+ "unique": false
+ },
+ {
+ "name": "checkin_logs_account_created_at_idx",
+ "table": "checkin_logs",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "checkin_logs_created_at_idx",
+ "table": "checkin_logs",
+ "columns": [
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "checkin_logs_status_idx",
+ "table": "checkin_logs",
+ "columns": [
+ "status"
+ ],
+ "unique": false
+ },
+ {
+ "name": "events_created_at_idx",
+ "table": "events",
+ "columns": [
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "events_read_created_at_idx",
+ "table": "events",
+ "columns": [
+ "read",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "events_type_created_at_idx",
+ "table": "events",
+ "columns": [
+ "type",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "model_availability_account_available_idx",
+ "table": "model_availability",
+ "columns": [
+ "account_id",
+ "available"
+ ],
+ "unique": false
+ },
+ {
+ "name": "model_availability_account_model_unique",
+ "table": "model_availability",
+ "columns": [
+ "account_id",
+ "model_name"
+ ],
+ "unique": true
+ },
+ {
+ "name": "model_availability_model_name_idx",
+ "table": "model_availability",
+ "columns": [
+ "model_name"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_account_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_model_actual_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "model_actual",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_status_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "status",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_account_id_idx",
+ "table": "route_channels",
+ "columns": [
+ "account_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_route_enabled_idx",
+ "table": "route_channels",
+ "columns": [
+ "route_id",
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_route_id_idx",
+ "table": "route_channels",
+ "columns": [
+ "route_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_route_token_idx",
+ "table": "route_channels",
+ "columns": [
+ "route_id",
+ "token_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_token_id_idx",
+ "table": "route_channels",
+ "columns": [
+ "token_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "site_disabled_models_site_id_idx",
+ "table": "site_disabled_models",
+ "columns": [
+ "site_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "site_disabled_models_site_model_unique",
+ "table": "site_disabled_models",
+ "columns": [
+ "site_id",
+ "model_name"
+ ],
+ "unique": true
+ },
+ {
+ "name": "sites_status_idx",
+ "table": "sites",
+ "columns": [
+ "status"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_model_availability_available_idx",
+ "table": "token_model_availability",
+ "columns": [
+ "available"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_model_availability_model_name_idx",
+ "table": "token_model_availability",
+ "columns": [
+ "model_name"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_model_availability_token_available_idx",
+ "table": "token_model_availability",
+ "columns": [
+ "token_id",
+ "available"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_model_availability_token_model_unique",
+ "table": "token_model_availability",
+ "columns": [
+ "token_id",
+ "model_name"
+ ],
+ "unique": true
+ },
+ {
+ "name": "token_routes_enabled_idx",
+ "table": "token_routes",
+ "columns": [
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_routes_model_pattern_idx",
+ "table": "token_routes",
+ "columns": [
+ "model_pattern"
+ ],
+ "unique": false
+ }
+ ],
+ "uniques": [
+ {
+ "name": "model_availability_account_model_unique",
+ "table": "model_availability",
+ "columns": [
+ "account_id",
+ "model_name"
+ ]
+ },
+ {
+ "name": "site_disabled_models_site_model_unique",
+ "table": "site_disabled_models",
+ "columns": [
+ "site_id",
+ "model_name"
+ ]
+ },
+ {
+ "name": "token_model_availability_token_model_unique",
+ "table": "token_model_availability",
+ "columns": [
+ "token_id",
+ "model_name"
+ ]
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "account_tokens",
+ "columns": [
+ "account_id"
+ ],
+ "referencedTable": "accounts",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "accounts",
+ "columns": [
+ "site_id"
+ ],
+ "referencedTable": "sites",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "checkin_logs",
+ "columns": [
+ "account_id"
+ ],
+ "referencedTable": "accounts",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "model_availability",
+ "columns": [
+ "account_id"
+ ],
+ "referencedTable": "accounts",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "route_channels",
+ "columns": [
+ "account_id"
+ ],
+ "referencedTable": "accounts",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "route_channels",
+ "columns": [
+ "route_id"
+ ],
+ "referencedTable": "token_routes",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "route_channels",
+ "columns": [
+ "token_id"
+ ],
+ "referencedTable": "account_tokens",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "SET NULL"
+ },
+ {
+ "table": "site_disabled_models",
+ "columns": [
+ "site_id"
+ ],
+ "referencedTable": "sites",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "token_model_availability",
+ "columns": [
+ "token_id"
+ ],
+ "referencedTable": "account_tokens",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ }
+ ]
+}
diff --git a/src/server/db/generated/mysql.bootstrap.sql b/src/server/db/generated/mysql.bootstrap.sql
new file mode 100644
index 00000000..b00926bf
--- /dev/null
+++ b/src/server/db/generated/mysql.bootstrap.sql
@@ -0,0 +1,70 @@
+CREATE TABLE IF NOT EXISTS `sites` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `platform` TEXT NOT NULL, `status` VARCHAR(191) NOT NULL DEFAULT 'active', `api_key` TEXT, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `is_pinned` BOOLEAN DEFAULT false, `sort_order` INT DEFAULT 0, `proxy_url` TEXT, `use_system_proxy` BOOLEAN DEFAULT false, `custom_headers` JSON, `external_checkin_url` TEXT, `global_weight` DOUBLE DEFAULT 1);
+CREATE TABLE IF NOT EXISTS `accounts` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `site_id` INT NOT NULL, `username` TEXT, `access_token` TEXT NOT NULL, `api_token` TEXT, `balance` DOUBLE DEFAULT 0, `balance_used` DOUBLE DEFAULT 0, `quota` DOUBLE DEFAULT 0, `unit_cost` DOUBLE, `value_score` DOUBLE DEFAULT 0, `status` VARCHAR(191) DEFAULT 'active', `checkin_enabled` BOOLEAN DEFAULT true, `last_checkin_at` VARCHAR(191), `last_balance_refresh` VARCHAR(191), `extra_config` JSON, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `is_pinned` BOOLEAN DEFAULT false, `sort_order` INT DEFAULT 0, `oauth_provider` TEXT, `oauth_account_key` TEXT, `oauth_project_id` TEXT, FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS `account_tokens` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `account_id` INT NOT NULL, `name` TEXT NOT NULL, `token` TEXT NOT NULL, `source` VARCHAR(191) DEFAULT 'manual', `enabled` BOOLEAN DEFAULT true, `is_default` BOOLEAN DEFAULT false, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `token_group` TEXT, `value_status` VARCHAR(191) NOT NULL DEFAULT 'ready', FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS `checkin_logs` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `account_id` INT NOT NULL, `status` TEXT NOT NULL, `message` TEXT, `reward` TEXT, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS `downstream_api_keys` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` TEXT NOT NULL, `key` TEXT NOT NULL, `description` TEXT, `enabled` BOOLEAN DEFAULT true, `expires_at` VARCHAR(191), `max_cost` DOUBLE, `used_cost` DOUBLE DEFAULT 0, `max_requests` INT, `used_requests` INT DEFAULT 0, `supported_models` JSON, `allowed_route_ids` JSON, `site_weight_multipliers` JSON, `last_used_at` VARCHAR(191), `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `group_name` TEXT, `tags` TEXT);
+CREATE TABLE IF NOT EXISTS `events` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `type` TEXT NOT NULL, `title` TEXT NOT NULL, `message` TEXT, `level` VARCHAR(191) NOT NULL DEFAULT 'info', `read` BOOLEAN DEFAULT false, `related_id` INT, `related_type` TEXT, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')));
+CREATE TABLE IF NOT EXISTS `model_availability` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `account_id` INT NOT NULL, `model_name` TEXT NOT NULL, `available` BOOLEAN, `latency_ms` INT, `checked_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `is_manual` BOOLEAN DEFAULT false, FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS `proxy_files` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `public_id` TEXT NOT NULL, `owner_type` TEXT NOT NULL, `owner_id` TEXT NOT NULL, `filename` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `purpose` TEXT, `byte_size` INT NOT NULL, `sha256` TEXT NOT NULL, `content_base64` TEXT NOT NULL, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `deleted_at` VARCHAR(191));
+CREATE TABLE IF NOT EXISTS `proxy_logs` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `route_id` INT, `channel_id` INT, `account_id` INT, `model_requested` TEXT, `model_actual` TEXT, `status` TEXT, `http_status` INT, `latency_ms` INT, `prompt_tokens` INT, `completion_tokens` INT, `total_tokens` INT, `estimated_cost` DOUBLE, `error_message` TEXT, `retry_count` INT DEFAULT 0, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `billing_details` JSON, `downstream_api_key_id` INT, `client_family` TEXT, `client_app_id` TEXT, `client_app_name` TEXT, `client_confidence` TEXT);
+CREATE TABLE IF NOT EXISTS `proxy_video_tasks` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `public_id` TEXT NOT NULL, `upstream_video_id` TEXT NOT NULL, `site_url` TEXT NOT NULL, `token_value` TEXT NOT NULL, `requested_model` TEXT, `actual_model` TEXT, `channel_id` INT, `account_id` INT, `status_snapshot` JSON, `upstream_response_meta` JSON, `last_upstream_status` INT, `last_polled_at` VARCHAR(191), `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')));
+CREATE TABLE IF NOT EXISTS `token_routes` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `model_pattern` TEXT NOT NULL, `model_mapping` JSON, `enabled` BOOLEAN DEFAULT true, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `updated_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `display_name` TEXT, `display_icon` TEXT, `decision_snapshot` JSON, `decision_refreshed_at` VARCHAR(191), `routing_strategy` VARCHAR(191) DEFAULT 'weighted', `route_mode` VARCHAR(191) DEFAULT 'pattern');
+CREATE TABLE IF NOT EXISTS `route_channels` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `route_id` INT NOT NULL, `account_id` INT NOT NULL, `token_id` INT, `source_model` TEXT, `priority` INT DEFAULT 0, `weight` INT DEFAULT 10, `enabled` BOOLEAN DEFAULT true, `manual_override` BOOLEAN DEFAULT false, `success_count` INT DEFAULT 0, `fail_count` INT DEFAULT 0, `total_latency_ms` INT DEFAULT 0, `total_cost` DOUBLE DEFAULT 0, `last_used_at` VARCHAR(191), `last_selected_at` VARCHAR(191), `last_fail_at` VARCHAR(191), `consecutive_fail_count` INT NOT NULL DEFAULT 0, `cooldown_level` INT NOT NULL DEFAULT 0, `cooldown_until` VARCHAR(191), FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE, FOREIGN KEY (`route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`token_id`) REFERENCES `account_tokens`(`id`) ON DELETE SET NULL);
+CREATE TABLE IF NOT EXISTS `route_group_sources` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `group_route_id` INT NOT NULL, `source_route_id` INT NOT NULL, FOREIGN KEY (`group_route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE, FOREIGN KEY (`source_route_id`) REFERENCES `token_routes`(`id`) ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS `settings` (`key` VARCHAR(191) NOT NULL PRIMARY KEY, `value` TEXT);
+CREATE TABLE IF NOT EXISTS `site_announcements` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `site_id` INT NOT NULL, `platform` TEXT NOT NULL, `source_key` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `level` VARCHAR(191) NOT NULL DEFAULT 'info', `source_url` TEXT, `starts_at` VARCHAR(191), `ends_at` VARCHAR(191), `upstream_created_at` VARCHAR(191), `upstream_updated_at` VARCHAR(191), `first_seen_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `last_seen_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), `read_at` VARCHAR(191), `dismissed_at` VARCHAR(191), `raw_payload` TEXT, FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS `site_disabled_models` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `site_id` INT NOT NULL, `model_name` TEXT NOT NULL, `created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS `token_model_availability` (`id` INT AUTO_INCREMENT NOT NULL PRIMARY KEY, `token_id` INT NOT NULL, `model_name` TEXT NOT NULL, `available` BOOLEAN, `latency_ms` INT, `checked_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')), FOREIGN KEY (`token_id`) REFERENCES `account_tokens`(`id`) ON DELETE CASCADE);
+CREATE UNIQUE INDEX `downstream_api_keys_key_unique` ON `downstream_api_keys` (`key`(191));
+CREATE UNIQUE INDEX `model_availability_account_model_unique` ON `model_availability` (`account_id`, `model_name`(191));
+CREATE UNIQUE INDEX `proxy_files_public_id_unique` ON `proxy_files` (`public_id`(191));
+CREATE UNIQUE INDEX `proxy_video_tasks_public_id_unique` ON `proxy_video_tasks` (`public_id`(191));
+CREATE UNIQUE INDEX `route_group_sources_group_source_unique` ON `route_group_sources` (`group_route_id`, `source_route_id`);
+CREATE UNIQUE INDEX `site_announcements_site_source_key_unique` ON `site_announcements` (`site_id`, `source_key`(191));
+CREATE UNIQUE INDEX `site_disabled_models_site_model_unique` ON `site_disabled_models` (`site_id`, `model_name`(191));
+CREATE UNIQUE INDEX `sites_platform_url_unique` ON `sites` (`platform`(191), `url`(191));
+CREATE UNIQUE INDEX `token_model_availability_token_model_unique` ON `token_model_availability` (`token_id`, `model_name`(191));
+CREATE INDEX `account_tokens_account_enabled_idx` ON `account_tokens` (`account_id`, `enabled`);
+CREATE INDEX `account_tokens_account_id_idx` ON `account_tokens` (`account_id`);
+CREATE INDEX `account_tokens_enabled_idx` ON `account_tokens` (`enabled`);
+CREATE INDEX `accounts_oauth_identity_idx` ON `accounts` (`oauth_provider`(191), `oauth_account_key`(191), `oauth_project_id`(191));
+CREATE INDEX `accounts_oauth_provider_idx` ON `accounts` (`oauth_provider`(191));
+CREATE INDEX `accounts_site_id_idx` ON `accounts` (`site_id`);
+CREATE INDEX `accounts_site_status_idx` ON `accounts` (`site_id`, `status`(191));
+CREATE INDEX `accounts_status_idx` ON `accounts` (`status`(191));
+CREATE INDEX `checkin_logs_account_created_at_idx` ON `checkin_logs` (`account_id`, `created_at`);
+CREATE INDEX `checkin_logs_created_at_idx` ON `checkin_logs` (`created_at`);
+CREATE INDEX `checkin_logs_status_idx` ON `checkin_logs` (`status`(191));
+CREATE INDEX `downstream_api_keys_enabled_idx` ON `downstream_api_keys` (`enabled`);
+CREATE INDEX `downstream_api_keys_expires_at_idx` ON `downstream_api_keys` (`expires_at`);
+CREATE INDEX `downstream_api_keys_name_idx` ON `downstream_api_keys` (`name`(191));
+CREATE INDEX `events_created_at_idx` ON `events` (`created_at`);
+CREATE INDEX `events_read_created_at_idx` ON `events` (`read`, `created_at`);
+CREATE INDEX `events_type_created_at_idx` ON `events` (`type`(191), `created_at`);
+CREATE INDEX `model_availability_account_available_idx` ON `model_availability` (`account_id`, `available`);
+CREATE INDEX `model_availability_model_name_idx` ON `model_availability` (`model_name`(191));
+CREATE INDEX `proxy_files_owner_lookup_idx` ON `proxy_files` (`owner_type`(191), `owner_id`(191), `deleted_at`);
+CREATE INDEX `proxy_logs_account_created_at_idx` ON `proxy_logs` (`account_id`, `created_at`);
+CREATE INDEX `proxy_logs_client_app_id_created_at_idx` ON `proxy_logs` (`client_app_id`(191), `created_at`);
+CREATE INDEX `proxy_logs_client_family_created_at_idx` ON `proxy_logs` (`client_family`(191), `created_at`);
+CREATE INDEX `proxy_logs_created_at_idx` ON `proxy_logs` (`created_at`);
+CREATE INDEX `proxy_logs_downstream_api_key_created_at_idx` ON `proxy_logs` (`downstream_api_key_id`, `created_at`);
+CREATE INDEX `proxy_logs_model_actual_created_at_idx` ON `proxy_logs` (`model_actual`(191), `created_at`);
+CREATE INDEX `proxy_logs_status_created_at_idx` ON `proxy_logs` (`status`(191), `created_at`);
+CREATE INDEX `proxy_video_tasks_created_at_idx` ON `proxy_video_tasks` (`created_at`);
+CREATE INDEX `proxy_video_tasks_upstream_video_id_idx` ON `proxy_video_tasks` (`upstream_video_id`(191));
+CREATE INDEX `route_channels_account_id_idx` ON `route_channels` (`account_id`);
+CREATE INDEX `route_channels_route_enabled_idx` ON `route_channels` (`route_id`, `enabled`);
+CREATE INDEX `route_channels_route_id_idx` ON `route_channels` (`route_id`);
+CREATE INDEX `route_channels_route_token_idx` ON `route_channels` (`route_id`, `token_id`);
+CREATE INDEX `route_channels_token_id_idx` ON `route_channels` (`token_id`);
+CREATE INDEX `route_group_sources_source_route_id_idx` ON `route_group_sources` (`source_route_id`);
+CREATE INDEX `site_announcements_read_at_idx` ON `site_announcements` (`read_at`);
+CREATE INDEX `site_announcements_site_id_first_seen_at_idx` ON `site_announcements` (`site_id`, `first_seen_at`);
+CREATE INDEX `site_disabled_models_site_id_idx` ON `site_disabled_models` (`site_id`);
+CREATE INDEX `sites_status_idx` ON `sites` (`status`(191));
+CREATE INDEX `token_model_availability_available_idx` ON `token_model_availability` (`available`);
+CREATE INDEX `token_model_availability_model_name_idx` ON `token_model_availability` (`model_name`(191));
+CREATE INDEX `token_model_availability_token_available_idx` ON `token_model_availability` (`token_id`, `available`);
+CREATE INDEX `token_routes_enabled_idx` ON `token_routes` (`enabled`);
+CREATE INDEX `token_routes_model_pattern_idx` ON `token_routes` (`model_pattern`(191));
diff --git a/src/server/db/generated/mysql.upgrade.sql b/src/server/db/generated/mysql.upgrade.sql
new file mode 100644
index 00000000..39c10184
--- /dev/null
+++ b/src/server/db/generated/mysql.upgrade.sql
@@ -0,0 +1 @@
+-- no schema changes detected for mysql
diff --git a/src/server/db/generated/postgres.bootstrap.sql b/src/server/db/generated/postgres.bootstrap.sql
new file mode 100644
index 00000000..f188938a
--- /dev/null
+++ b/src/server/db/generated/postgres.bootstrap.sql
@@ -0,0 +1,70 @@
+CREATE TABLE IF NOT EXISTS "sites" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "name" TEXT NOT NULL, "url" TEXT NOT NULL, "platform" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'active', "api_key" TEXT, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "is_pinned" BOOLEAN DEFAULT false, "sort_order" INTEGER DEFAULT 0, "proxy_url" TEXT, "use_system_proxy" BOOLEAN DEFAULT false, "custom_headers" JSONB, "external_checkin_url" TEXT, "global_weight" DOUBLE PRECISION DEFAULT 1);
+CREATE TABLE IF NOT EXISTS "accounts" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "site_id" INTEGER NOT NULL, "username" TEXT, "access_token" TEXT NOT NULL, "api_token" TEXT, "balance" DOUBLE PRECISION DEFAULT 0, "balance_used" DOUBLE PRECISION DEFAULT 0, "quota" DOUBLE PRECISION DEFAULT 0, "unit_cost" DOUBLE PRECISION, "value_score" DOUBLE PRECISION DEFAULT 0, "status" TEXT DEFAULT 'active', "checkin_enabled" BOOLEAN DEFAULT true, "last_checkin_at" TEXT, "last_balance_refresh" TEXT, "extra_config" JSONB, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "is_pinned" BOOLEAN DEFAULT false, "sort_order" INTEGER DEFAULT 0, "oauth_provider" TEXT, "oauth_account_key" TEXT, "oauth_project_id" TEXT, FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS "account_tokens" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "account_id" INTEGER NOT NULL, "name" TEXT NOT NULL, "token" TEXT NOT NULL, "source" TEXT DEFAULT 'manual', "enabled" BOOLEAN DEFAULT true, "is_default" BOOLEAN DEFAULT false, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "token_group" TEXT, "value_status" TEXT NOT NULL DEFAULT 'ready', FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS "checkin_logs" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "account_id" INTEGER NOT NULL, "status" TEXT NOT NULL, "message" TEXT, "reward" TEXT, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS "downstream_api_keys" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "name" TEXT NOT NULL, "key" TEXT NOT NULL, "description" TEXT, "enabled" BOOLEAN DEFAULT true, "expires_at" TEXT, "max_cost" DOUBLE PRECISION, "used_cost" DOUBLE PRECISION DEFAULT 0, "max_requests" INTEGER, "used_requests" INTEGER DEFAULT 0, "supported_models" JSONB, "allowed_route_ids" JSONB, "site_weight_multipliers" JSONB, "last_used_at" TEXT, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "group_name" TEXT, "tags" TEXT);
+CREATE TABLE IF NOT EXISTS "events" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "type" TEXT NOT NULL, "title" TEXT NOT NULL, "message" TEXT, "level" TEXT NOT NULL DEFAULT 'info', "read" BOOLEAN DEFAULT false, "related_id" INTEGER, "related_type" TEXT, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'));
+CREATE TABLE IF NOT EXISTS "model_availability" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "account_id" INTEGER NOT NULL, "model_name" TEXT NOT NULL, "available" BOOLEAN, "latency_ms" INTEGER, "checked_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "is_manual" BOOLEAN DEFAULT false, FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS "proxy_files" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "public_id" TEXT NOT NULL, "owner_type" TEXT NOT NULL, "owner_id" TEXT NOT NULL, "filename" TEXT NOT NULL, "mime_type" TEXT NOT NULL, "purpose" TEXT, "byte_size" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "content_base64" TEXT NOT NULL, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "deleted_at" TEXT);
+CREATE TABLE IF NOT EXISTS "proxy_logs" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "route_id" INTEGER, "channel_id" INTEGER, "account_id" INTEGER, "model_requested" TEXT, "model_actual" TEXT, "status" TEXT, "http_status" INTEGER, "latency_ms" INTEGER, "prompt_tokens" INTEGER, "completion_tokens" INTEGER, "total_tokens" INTEGER, "estimated_cost" DOUBLE PRECISION, "error_message" TEXT, "retry_count" INTEGER DEFAULT 0, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "billing_details" JSONB, "downstream_api_key_id" INTEGER, "client_family" TEXT, "client_app_id" TEXT, "client_app_name" TEXT, "client_confidence" TEXT);
+CREATE TABLE IF NOT EXISTS "proxy_video_tasks" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "public_id" TEXT NOT NULL, "upstream_video_id" TEXT NOT NULL, "site_url" TEXT NOT NULL, "token_value" TEXT NOT NULL, "requested_model" TEXT, "actual_model" TEXT, "channel_id" INTEGER, "account_id" INTEGER, "status_snapshot" JSONB, "upstream_response_meta" JSONB, "last_upstream_status" INTEGER, "last_polled_at" TEXT, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'));
+CREATE TABLE IF NOT EXISTS "token_routes" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "model_pattern" TEXT NOT NULL, "model_mapping" JSONB, "enabled" BOOLEAN DEFAULT true, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "updated_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "display_name" TEXT, "display_icon" TEXT, "decision_snapshot" JSONB, "decision_refreshed_at" TEXT, "routing_strategy" TEXT DEFAULT 'weighted', "route_mode" TEXT DEFAULT 'pattern');
+CREATE TABLE IF NOT EXISTS "route_channels" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "route_id" INTEGER NOT NULL, "account_id" INTEGER NOT NULL, "token_id" INTEGER, "source_model" TEXT, "priority" INTEGER DEFAULT 0, "weight" INTEGER DEFAULT 10, "enabled" BOOLEAN DEFAULT true, "manual_override" BOOLEAN DEFAULT false, "success_count" INTEGER DEFAULT 0, "fail_count" INTEGER DEFAULT 0, "total_latency_ms" INTEGER DEFAULT 0, "total_cost" DOUBLE PRECISION DEFAULT 0, "last_used_at" TEXT, "last_selected_at" TEXT, "last_fail_at" TEXT, "consecutive_fail_count" INTEGER NOT NULL DEFAULT 0, "cooldown_level" INTEGER NOT NULL DEFAULT 0, "cooldown_until" TEXT, FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE, FOREIGN KEY ("route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, FOREIGN KEY ("token_id") REFERENCES "account_tokens"("id") ON DELETE SET NULL);
+CREATE TABLE IF NOT EXISTS "route_group_sources" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "group_route_id" INTEGER NOT NULL, "source_route_id" INTEGER NOT NULL, FOREIGN KEY ("group_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, FOREIGN KEY ("source_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS "settings" ("key" TEXT NOT NULL PRIMARY KEY, "value" TEXT);
+CREATE TABLE IF NOT EXISTS "site_announcements" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "site_id" INTEGER NOT NULL, "platform" TEXT NOT NULL, "source_key" TEXT NOT NULL, "title" TEXT NOT NULL, "content" TEXT NOT NULL, "level" TEXT NOT NULL DEFAULT 'info', "source_url" TEXT, "starts_at" TEXT, "ends_at" TEXT, "upstream_created_at" TEXT, "upstream_updated_at" TEXT, "first_seen_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "last_seen_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), "read_at" TEXT, "dismissed_at" TEXT, "raw_payload" TEXT, FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS "site_disabled_models" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "site_id" INTEGER NOT NULL, "model_name" TEXT NOT NULL, "created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE CASCADE);
+CREATE TABLE IF NOT EXISTS "token_model_availability" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "token_id" INTEGER NOT NULL, "model_name" TEXT NOT NULL, "available" BOOLEAN, "latency_ms" INTEGER, "checked_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS'), FOREIGN KEY ("token_id") REFERENCES "account_tokens"("id") ON DELETE CASCADE);
+CREATE UNIQUE INDEX "downstream_api_keys_key_unique" ON "downstream_api_keys" ("key");
+CREATE UNIQUE INDEX "model_availability_account_model_unique" ON "model_availability" ("account_id", "model_name");
+CREATE UNIQUE INDEX "proxy_files_public_id_unique" ON "proxy_files" ("public_id");
+CREATE UNIQUE INDEX "proxy_video_tasks_public_id_unique" ON "proxy_video_tasks" ("public_id");
+CREATE UNIQUE INDEX "route_group_sources_group_source_unique" ON "route_group_sources" ("group_route_id", "source_route_id");
+CREATE UNIQUE INDEX "site_announcements_site_source_key_unique" ON "site_announcements" ("site_id", "source_key");
+CREATE UNIQUE INDEX "site_disabled_models_site_model_unique" ON "site_disabled_models" ("site_id", "model_name");
+CREATE UNIQUE INDEX "sites_platform_url_unique" ON "sites" ("platform", "url");
+CREATE UNIQUE INDEX "token_model_availability_token_model_unique" ON "token_model_availability" ("token_id", "model_name");
+CREATE INDEX "account_tokens_account_enabled_idx" ON "account_tokens" ("account_id", "enabled");
+CREATE INDEX "account_tokens_account_id_idx" ON "account_tokens" ("account_id");
+CREATE INDEX "account_tokens_enabled_idx" ON "account_tokens" ("enabled");
+CREATE INDEX "accounts_oauth_identity_idx" ON "accounts" ("oauth_provider", "oauth_account_key", "oauth_project_id");
+CREATE INDEX "accounts_oauth_provider_idx" ON "accounts" ("oauth_provider");
+CREATE INDEX "accounts_site_id_idx" ON "accounts" ("site_id");
+CREATE INDEX "accounts_site_status_idx" ON "accounts" ("site_id", "status");
+CREATE INDEX "accounts_status_idx" ON "accounts" ("status");
+CREATE INDEX "checkin_logs_account_created_at_idx" ON "checkin_logs" ("account_id", "created_at");
+CREATE INDEX "checkin_logs_created_at_idx" ON "checkin_logs" ("created_at");
+CREATE INDEX "checkin_logs_status_idx" ON "checkin_logs" ("status");
+CREATE INDEX "downstream_api_keys_enabled_idx" ON "downstream_api_keys" ("enabled");
+CREATE INDEX "downstream_api_keys_expires_at_idx" ON "downstream_api_keys" ("expires_at");
+CREATE INDEX "downstream_api_keys_name_idx" ON "downstream_api_keys" ("name");
+CREATE INDEX "events_created_at_idx" ON "events" ("created_at");
+CREATE INDEX "events_read_created_at_idx" ON "events" ("read", "created_at");
+CREATE INDEX "events_type_created_at_idx" ON "events" ("type", "created_at");
+CREATE INDEX "model_availability_account_available_idx" ON "model_availability" ("account_id", "available");
+CREATE INDEX "model_availability_model_name_idx" ON "model_availability" ("model_name");
+CREATE INDEX "proxy_files_owner_lookup_idx" ON "proxy_files" ("owner_type", "owner_id", "deleted_at");
+CREATE INDEX "proxy_logs_account_created_at_idx" ON "proxy_logs" ("account_id", "created_at");
+CREATE INDEX "proxy_logs_client_app_id_created_at_idx" ON "proxy_logs" ("client_app_id", "created_at");
+CREATE INDEX "proxy_logs_client_family_created_at_idx" ON "proxy_logs" ("client_family", "created_at");
+CREATE INDEX "proxy_logs_created_at_idx" ON "proxy_logs" ("created_at");
+CREATE INDEX "proxy_logs_downstream_api_key_created_at_idx" ON "proxy_logs" ("downstream_api_key_id", "created_at");
+CREATE INDEX "proxy_logs_model_actual_created_at_idx" ON "proxy_logs" ("model_actual", "created_at");
+CREATE INDEX "proxy_logs_status_created_at_idx" ON "proxy_logs" ("status", "created_at");
+CREATE INDEX "proxy_video_tasks_created_at_idx" ON "proxy_video_tasks" ("created_at");
+CREATE INDEX "proxy_video_tasks_upstream_video_id_idx" ON "proxy_video_tasks" ("upstream_video_id");
+CREATE INDEX "route_channels_account_id_idx" ON "route_channels" ("account_id");
+CREATE INDEX "route_channels_route_enabled_idx" ON "route_channels" ("route_id", "enabled");
+CREATE INDEX "route_channels_route_id_idx" ON "route_channels" ("route_id");
+CREATE INDEX "route_channels_route_token_idx" ON "route_channels" ("route_id", "token_id");
+CREATE INDEX "route_channels_token_id_idx" ON "route_channels" ("token_id");
+CREATE INDEX "route_group_sources_source_route_id_idx" ON "route_group_sources" ("source_route_id");
+CREATE INDEX "site_announcements_read_at_idx" ON "site_announcements" ("read_at");
+CREATE INDEX "site_announcements_site_id_first_seen_at_idx" ON "site_announcements" ("site_id", "first_seen_at");
+CREATE INDEX "site_disabled_models_site_id_idx" ON "site_disabled_models" ("site_id");
+CREATE INDEX "sites_status_idx" ON "sites" ("status");
+CREATE INDEX "token_model_availability_available_idx" ON "token_model_availability" ("available");
+CREATE INDEX "token_model_availability_model_name_idx" ON "token_model_availability" ("model_name");
+CREATE INDEX "token_model_availability_token_available_idx" ON "token_model_availability" ("token_id", "available");
+CREATE INDEX "token_routes_enabled_idx" ON "token_routes" ("enabled");
+CREATE INDEX "token_routes_model_pattern_idx" ON "token_routes" ("model_pattern");
diff --git a/src/server/db/generated/postgres.upgrade.sql b/src/server/db/generated/postgres.upgrade.sql
new file mode 100644
index 00000000..bf33dfaf
--- /dev/null
+++ b/src/server/db/generated/postgres.upgrade.sql
@@ -0,0 +1 @@
+-- no schema changes detected for postgres
diff --git a/src/server/db/generated/schemaContract.json b/src/server/db/generated/schemaContract.json
new file mode 100644
index 00000000..25db470a
--- /dev/null
+++ b/src/server/db/generated/schemaContract.json
@@ -0,0 +1,1935 @@
+{
+ "tables": {
+ "account_tokens": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "token": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "source": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": "'manual'",
+ "primaryKey": false
+ },
+ "enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "is_default": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "token_group": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "value_status": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": "'ready'",
+ "primaryKey": false
+ }
+ }
+ },
+ "accounts": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "site_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "username": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "access_token": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "api_token": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "balance": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "balance_used": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "quota": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "unit_cost": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "value_score": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "status": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": "'active'",
+ "primaryKey": false
+ },
+ "checkin_enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "last_checkin_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "last_balance_refresh": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "extra_config": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "is_pinned": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "sort_order": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "oauth_provider": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "oauth_account_key": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "oauth_project_id": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "checkin_logs": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "status": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "message": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "reward": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "downstream_api_keys": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "key": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "description": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "expires_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "max_cost": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "used_cost": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "max_requests": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "used_requests": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "supported_models": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "allowed_route_ids": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "site_weight_multipliers": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "last_used_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "group_name": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "tags": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "events": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "type": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "title": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "message": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "level": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": "'info'",
+ "primaryKey": false
+ },
+ "read": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "related_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "related_type": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "model_availability": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "available": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "latency_ms": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "checked_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "is_manual": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ }
+ }
+ },
+ "proxy_files": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "public_id": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "owner_type": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "owner_id": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "filename": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "mime_type": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "purpose": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "byte_size": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "sha256": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "content_base64": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "deleted_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "proxy_logs": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "route_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "channel_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_requested": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_actual": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "status": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "http_status": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "latency_ms": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "prompt_tokens": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "completion_tokens": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "total_tokens": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "estimated_cost": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "error_message": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "retry_count": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "billing_details": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "downstream_api_key_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "client_family": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "client_app_id": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "client_app_name": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "client_confidence": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "proxy_video_tasks": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "public_id": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "upstream_video_id": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "site_url": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "token_value": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "requested_model": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "actual_model": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "channel_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "status_snapshot": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "upstream_response_meta": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "last_upstream_status": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "last_polled_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "route_channels": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "route_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "account_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "token_id": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "source_model": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "priority": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "weight": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "10",
+ "primaryKey": false
+ },
+ "enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "manual_override": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "success_count": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "fail_count": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "total_latency_ms": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "total_cost": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "last_used_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "last_selected_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "last_fail_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "consecutive_fail_count": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "cooldown_level": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "cooldown_until": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "route_group_sources": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "group_route_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "source_route_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "settings": {
+ "columns": {
+ "key": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "value": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "site_announcements": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "site_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "platform": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "source_key": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "title": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "content": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "level": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": "'info'",
+ "primaryKey": false
+ },
+ "source_url": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "starts_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "ends_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "upstream_created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "upstream_updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "first_seen_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "last_seen_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "read_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "dismissed_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "raw_payload": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ }
+ }
+ },
+ "site_disabled_models": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "site_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "sites": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "url": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "platform": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "status": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": "'active'",
+ "primaryKey": false
+ },
+ "api_key": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "is_pinned": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "sort_order": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": "0",
+ "primaryKey": false
+ },
+ "proxy_url": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "use_system_proxy": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "false",
+ "primaryKey": false
+ },
+ "custom_headers": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "external_checkin_url": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "global_weight": {
+ "logicalType": "real",
+ "notNull": false,
+ "defaultValue": "1",
+ "primaryKey": false
+ }
+ }
+ },
+ "token_model_availability": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "token_id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_name": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "available": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "latency_ms": {
+ "logicalType": "integer",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "checked_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ }
+ }
+ },
+ "token_routes": {
+ "columns": {
+ "id": {
+ "logicalType": "integer",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": true
+ },
+ "model_pattern": {
+ "logicalType": "text",
+ "notNull": true,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "model_mapping": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "enabled": {
+ "logicalType": "boolean",
+ "notNull": false,
+ "defaultValue": "true",
+ "primaryKey": false
+ },
+ "created_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "updated_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": "datetime('now')",
+ "primaryKey": false
+ },
+ "display_name": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "display_icon": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "decision_snapshot": {
+ "logicalType": "json",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "decision_refreshed_at": {
+ "logicalType": "datetime",
+ "notNull": false,
+ "defaultValue": null,
+ "primaryKey": false
+ },
+ "routing_strategy": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": "'weighted'",
+ "primaryKey": false
+ },
+ "route_mode": {
+ "logicalType": "text",
+ "notNull": false,
+ "defaultValue": "'pattern'",
+ "primaryKey": false
+ }
+ }
+ }
+ },
+ "indexes": [
+ {
+ "name": "account_tokens_account_enabled_idx",
+ "table": "account_tokens",
+ "columns": [
+ "account_id",
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "account_tokens_account_id_idx",
+ "table": "account_tokens",
+ "columns": [
+ "account_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "account_tokens_enabled_idx",
+ "table": "account_tokens",
+ "columns": [
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "accounts_oauth_identity_idx",
+ "table": "accounts",
+ "columns": [
+ "oauth_provider",
+ "oauth_account_key",
+ "oauth_project_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "accounts_oauth_provider_idx",
+ "table": "accounts",
+ "columns": [
+ "oauth_provider"
+ ],
+ "unique": false
+ },
+ {
+ "name": "accounts_site_id_idx",
+ "table": "accounts",
+ "columns": [
+ "site_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "accounts_site_status_idx",
+ "table": "accounts",
+ "columns": [
+ "site_id",
+ "status"
+ ],
+ "unique": false
+ },
+ {
+ "name": "accounts_status_idx",
+ "table": "accounts",
+ "columns": [
+ "status"
+ ],
+ "unique": false
+ },
+ {
+ "name": "checkin_logs_account_created_at_idx",
+ "table": "checkin_logs",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "checkin_logs_created_at_idx",
+ "table": "checkin_logs",
+ "columns": [
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "checkin_logs_status_idx",
+ "table": "checkin_logs",
+ "columns": [
+ "status"
+ ],
+ "unique": false
+ },
+ {
+ "name": "downstream_api_keys_enabled_idx",
+ "table": "downstream_api_keys",
+ "columns": [
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "downstream_api_keys_expires_at_idx",
+ "table": "downstream_api_keys",
+ "columns": [
+ "expires_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "downstream_api_keys_key_unique",
+ "table": "downstream_api_keys",
+ "columns": [
+ "key"
+ ],
+ "unique": true
+ },
+ {
+ "name": "downstream_api_keys_name_idx",
+ "table": "downstream_api_keys",
+ "columns": [
+ "name"
+ ],
+ "unique": false
+ },
+ {
+ "name": "events_created_at_idx",
+ "table": "events",
+ "columns": [
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "events_read_created_at_idx",
+ "table": "events",
+ "columns": [
+ "read",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "events_type_created_at_idx",
+ "table": "events",
+ "columns": [
+ "type",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "model_availability_account_available_idx",
+ "table": "model_availability",
+ "columns": [
+ "account_id",
+ "available"
+ ],
+ "unique": false
+ },
+ {
+ "name": "model_availability_account_model_unique",
+ "table": "model_availability",
+ "columns": [
+ "account_id",
+ "model_name"
+ ],
+ "unique": true
+ },
+ {
+ "name": "model_availability_model_name_idx",
+ "table": "model_availability",
+ "columns": [
+ "model_name"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_files_owner_lookup_idx",
+ "table": "proxy_files",
+ "columns": [
+ "owner_type",
+ "owner_id",
+ "deleted_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_files_public_id_unique",
+ "table": "proxy_files",
+ "columns": [
+ "public_id"
+ ],
+ "unique": true
+ },
+ {
+ "name": "proxy_logs_account_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "account_id",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_client_app_id_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "client_app_id",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_client_family_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "client_family",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_downstream_api_key_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "downstream_api_key_id",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_model_actual_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "model_actual",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_logs_status_created_at_idx",
+ "table": "proxy_logs",
+ "columns": [
+ "status",
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_video_tasks_created_at_idx",
+ "table": "proxy_video_tasks",
+ "columns": [
+ "created_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "proxy_video_tasks_public_id_unique",
+ "table": "proxy_video_tasks",
+ "columns": [
+ "public_id"
+ ],
+ "unique": true
+ },
+ {
+ "name": "proxy_video_tasks_upstream_video_id_idx",
+ "table": "proxy_video_tasks",
+ "columns": [
+ "upstream_video_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_account_id_idx",
+ "table": "route_channels",
+ "columns": [
+ "account_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_route_enabled_idx",
+ "table": "route_channels",
+ "columns": [
+ "route_id",
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_route_id_idx",
+ "table": "route_channels",
+ "columns": [
+ "route_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_route_token_idx",
+ "table": "route_channels",
+ "columns": [
+ "route_id",
+ "token_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_channels_token_id_idx",
+ "table": "route_channels",
+ "columns": [
+ "token_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "route_group_sources_group_source_unique",
+ "table": "route_group_sources",
+ "columns": [
+ "group_route_id",
+ "source_route_id"
+ ],
+ "unique": true
+ },
+ {
+ "name": "route_group_sources_source_route_id_idx",
+ "table": "route_group_sources",
+ "columns": [
+ "source_route_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "site_announcements_read_at_idx",
+ "table": "site_announcements",
+ "columns": [
+ "read_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "site_announcements_site_id_first_seen_at_idx",
+ "table": "site_announcements",
+ "columns": [
+ "site_id",
+ "first_seen_at"
+ ],
+ "unique": false
+ },
+ {
+ "name": "site_announcements_site_source_key_unique",
+ "table": "site_announcements",
+ "columns": [
+ "site_id",
+ "source_key"
+ ],
+ "unique": true
+ },
+ {
+ "name": "site_disabled_models_site_id_idx",
+ "table": "site_disabled_models",
+ "columns": [
+ "site_id"
+ ],
+ "unique": false
+ },
+ {
+ "name": "site_disabled_models_site_model_unique",
+ "table": "site_disabled_models",
+ "columns": [
+ "site_id",
+ "model_name"
+ ],
+ "unique": true
+ },
+ {
+ "name": "sites_platform_url_unique",
+ "table": "sites",
+ "columns": [
+ "platform",
+ "url"
+ ],
+ "unique": true
+ },
+ {
+ "name": "sites_status_idx",
+ "table": "sites",
+ "columns": [
+ "status"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_model_availability_available_idx",
+ "table": "token_model_availability",
+ "columns": [
+ "available"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_model_availability_model_name_idx",
+ "table": "token_model_availability",
+ "columns": [
+ "model_name"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_model_availability_token_available_idx",
+ "table": "token_model_availability",
+ "columns": [
+ "token_id",
+ "available"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_model_availability_token_model_unique",
+ "table": "token_model_availability",
+ "columns": [
+ "token_id",
+ "model_name"
+ ],
+ "unique": true
+ },
+ {
+ "name": "token_routes_enabled_idx",
+ "table": "token_routes",
+ "columns": [
+ "enabled"
+ ],
+ "unique": false
+ },
+ {
+ "name": "token_routes_model_pattern_idx",
+ "table": "token_routes",
+ "columns": [
+ "model_pattern"
+ ],
+ "unique": false
+ }
+ ],
+ "uniques": [
+ {
+ "name": "downstream_api_keys_key_unique",
+ "table": "downstream_api_keys",
+ "columns": [
+ "key"
+ ]
+ },
+ {
+ "name": "model_availability_account_model_unique",
+ "table": "model_availability",
+ "columns": [
+ "account_id",
+ "model_name"
+ ]
+ },
+ {
+ "name": "proxy_files_public_id_unique",
+ "table": "proxy_files",
+ "columns": [
+ "public_id"
+ ]
+ },
+ {
+ "name": "proxy_video_tasks_public_id_unique",
+ "table": "proxy_video_tasks",
+ "columns": [
+ "public_id"
+ ]
+ },
+ {
+ "name": "route_group_sources_group_source_unique",
+ "table": "route_group_sources",
+ "columns": [
+ "group_route_id",
+ "source_route_id"
+ ]
+ },
+ {
+ "name": "site_announcements_site_source_key_unique",
+ "table": "site_announcements",
+ "columns": [
+ "site_id",
+ "source_key"
+ ]
+ },
+ {
+ "name": "site_disabled_models_site_model_unique",
+ "table": "site_disabled_models",
+ "columns": [
+ "site_id",
+ "model_name"
+ ]
+ },
+ {
+ "name": "sites_platform_url_unique",
+ "table": "sites",
+ "columns": [
+ "platform",
+ "url"
+ ]
+ },
+ {
+ "name": "token_model_availability_token_model_unique",
+ "table": "token_model_availability",
+ "columns": [
+ "token_id",
+ "model_name"
+ ]
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "account_tokens",
+ "columns": [
+ "account_id"
+ ],
+ "referencedTable": "accounts",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "accounts",
+ "columns": [
+ "site_id"
+ ],
+ "referencedTable": "sites",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "checkin_logs",
+ "columns": [
+ "account_id"
+ ],
+ "referencedTable": "accounts",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "model_availability",
+ "columns": [
+ "account_id"
+ ],
+ "referencedTable": "accounts",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "route_channels",
+ "columns": [
+ "account_id"
+ ],
+ "referencedTable": "accounts",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "route_channels",
+ "columns": [
+ "route_id"
+ ],
+ "referencedTable": "token_routes",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "route_channels",
+ "columns": [
+ "token_id"
+ ],
+ "referencedTable": "account_tokens",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "SET NULL"
+ },
+ {
+ "table": "route_group_sources",
+ "columns": [
+ "group_route_id"
+ ],
+ "referencedTable": "token_routes",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "route_group_sources",
+ "columns": [
+ "source_route_id"
+ ],
+ "referencedTable": "token_routes",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "site_announcements",
+ "columns": [
+ "site_id"
+ ],
+ "referencedTable": "sites",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "site_disabled_models",
+ "columns": [
+ "site_id"
+ ],
+ "referencedTable": "sites",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ },
+ {
+ "table": "token_model_availability",
+ "columns": [
+ "token_id"
+ ],
+ "referencedTable": "account_tokens",
+ "referencedColumns": [
+ "id"
+ ],
+ "onDelete": "CASCADE"
+ }
+ ]
+}
diff --git a/src/server/db/index.default-path.test.ts b/src/server/db/index.default-path.test.ts
new file mode 100644
index 00000000..bac23b87
--- /dev/null
+++ b/src/server/db/index.default-path.test.ts
@@ -0,0 +1,58 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { tmpdir } from 'node:os';
+import { resolve } from 'node:path';
+
+type DbModule = typeof import('./index.js');
+
+describe('sqlite default path resolution', () => {
+ let dbModule: DbModule | null = null;
+
+ afterEach(async () => {
+ if (dbModule) {
+ await dbModule.closeDbConnections();
+ dbModule = null;
+ }
+ delete process.env.DATA_DIR;
+ delete process.env.DB_URL;
+ vi.resetModules();
+ });
+
+ it('uses an isolated temp sqlite path under vitest when no db env is configured', async () => {
+ delete process.env.DATA_DIR;
+ delete process.env.DB_URL;
+ vi.resetModules();
+
+ dbModule = await import('./index.js');
+ const sqlitePath = dbModule.__dbProxyTestUtils.resolveSqlitePath();
+ const sharedRepoPath = resolve('./data/hub.db');
+
+ expect(sqlitePath).not.toBe(sharedRepoPath);
+ expect(sqlitePath).toContain(tmpdir());
+ expect(sqlitePath).toContain('metapi-vitest');
+ });
+
+ it('still honors explicit DATA_DIR when provided', async () => {
+ process.env.DATA_DIR = resolve(tmpdir(), 'metapi-explicit-data-dir');
+ delete process.env.DB_URL;
+ vi.resetModules();
+
+ dbModule = await import('./index.js');
+ const sqlitePath = dbModule.__dbProxyTestUtils.resolveSqlitePath();
+
+ expect(sqlitePath).toBe(resolve(process.env.DATA_DIR, 'hub.db'));
+ });
+
+ it('ignores the default repo DATA_DIR under vitest and still isolates sqlite', async () => {
+ process.env.DATA_DIR = './data';
+ delete process.env.DB_URL;
+ vi.resetModules();
+
+ dbModule = await import('./index.js');
+ const sqlitePath = dbModule.__dbProxyTestUtils.resolveSqlitePath();
+ const sharedRepoPath = resolve('./data/hub.db');
+
+ expect(sqlitePath).not.toBe(sharedRepoPath);
+ expect(sqlitePath).toContain(tmpdir());
+ expect(sqlitePath).toContain('metapi-vitest');
+ });
+});
diff --git a/src/server/db/index.proxy-wrap.test.ts b/src/server/db/index.proxy-wrap.test.ts
index 7e68a0eb..74f5d25b 100644
--- a/src/server/db/index.proxy-wrap.test.ts
+++ b/src/server/db/index.proxy-wrap.test.ts
@@ -1,7 +1,7 @@
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
-import { beforeAll, describe, expect, it, vi } from 'vitest';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
type DbModule = typeof import('./index.js');
@@ -14,6 +14,10 @@ describe('db proxy query wrapper', () => {
testUtils = dbModule.__dbProxyTestUtils;
});
+ beforeEach(() => {
+ testUtils.resetPostgresJsonTextParsersInstallStateForTests();
+ });
+
it('wraps thenable query builders and provides all/get shims', async () => {
const execute = vi.fn(async () => [{ id: 1, name: 'demo' }]);
const queryLike = {
@@ -53,5 +57,46 @@ describe('db proxy query wrapper', () => {
expect(runResult).toEqual({ changes: 3, lastInsertRowid: 9 });
expect(testUtils.shouldWrapObject(Promise.resolve())).toBe(false);
});
-});
+ it('builds mysql pool options with jsonStrings enabled', () => {
+ expect(testUtils.buildMysqlPoolOptions('mysql://root:pass@db.example.com:3306/metapi', false)).toMatchObject({
+ uri: 'mysql://root:pass@db.example.com:3306/metapi',
+ jsonStrings: true,
+ });
+ expect(testUtils.buildMysqlPoolOptions('mysql://root:pass@db.example.com:3306/metapi', true)).toMatchObject({
+ uri: 'mysql://root:pass@db.example.com:3306/metapi',
+ jsonStrings: true,
+ ssl: { rejectUnauthorized: false },
+ });
+ });
+
+ it('builds postgres pool options with ssl when requested', () => {
+ expect(testUtils.buildPostgresPoolOptions('postgres://user:pass@db.example.com:5432/metapi', false)).toEqual({
+ connectionString: 'postgres://user:pass@db.example.com:5432/metapi',
+ });
+ expect(testUtils.buildPostgresPoolOptions('postgres://user:pass@db.example.com:5432/metapi', true)).toMatchObject({
+ connectionString: 'postgres://user:pass@db.example.com:5432/metapi',
+ ssl: { rejectUnauthorized: false },
+ });
+ });
+
+ it('installs postgres JSON text parsers idempotently', () => {
+ const setTypeParser = vi.fn();
+ const fakeTypes = {
+ builtins: {
+ JSON: 114,
+ JSONB: 3802,
+ },
+ setTypeParser,
+ };
+
+ testUtils.installPostgresJsonTextParsers(fakeTypes as any);
+ testUtils.installPostgresJsonTextParsers(fakeTypes as any);
+
+ expect(setTypeParser).toHaveBeenCalledTimes(2);
+ expect(setTypeParser).toHaveBeenNthCalledWith(1, 114, 'text', expect.any(Function));
+ expect(setTypeParser).toHaveBeenNthCalledWith(2, 3802, 'text', expect.any(Function));
+ const parser = setTypeParser.mock.calls[0][2] as (value: string) => string;
+ expect(parser('{"ok":true}')).toBe('{"ok":true}');
+ });
+});
diff --git a/src/server/db/index.ts b/src/server/db/index.ts
index 40fe93cc..a9660396 100644
--- a/src/server/db/index.ts
+++ b/src/server/db/index.ts
@@ -5,12 +5,20 @@ import { drizzle as drizzleSqliteProxy } from 'drizzle-orm/sqlite-proxy';
import { drizzle as drizzleMysqlProxy } from 'drizzle-orm/mysql-proxy';
import { drizzle as drizzlePgProxy } from 'drizzle-orm/pg-proxy';
import * as schema from './schema.js';
+import {
+ installPostgresJsonTextParsers,
+ resetPostgresJsonTextParsersInstallStateForTests,
+} from './postgresJsonTextParsers.js';
import { ensureSiteSchemaCompatibility, type SiteSchemaInspector } from './siteSchemaCompatibility.js';
import { ensureRouteGroupingSchemaCompatibility } from './routeGroupingSchemaCompatibility.js';
import { ensureProxyFileSchemaCompatibility } from './proxyFileSchemaCompatibility.js';
+import { executeLegacyCompat, executeLegacyCompatSync } from './legacySchemaCompat.js';
import { config } from '../config.js';
+import { ensureRuntimeDatabaseReady } from '../runtimeDatabaseBootstrap.js';
import { mkdirSync } from 'fs';
+import { tmpdir } from 'os';
import { dirname, resolve } from 'path';
+import { threadId } from 'worker_threads';
export type RuntimeDbDialect = 'sqlite' | 'mysql' | 'postgres';
type SqlMethod = 'all' | 'get' | 'run' | 'values' | 'execute';
@@ -23,11 +31,13 @@ const TABLES_WITH_NUMERIC_ID = new Set([
'model_availability',
'token_model_availability',
'token_routes',
+ 'route_group_sources',
'route_channels',
'proxy_logs',
'proxy_video_tasks',
'proxy_files',
'downstream_api_keys',
+ 'site_announcements',
'events',
]);
@@ -37,10 +47,43 @@ let sqliteConnection: Database.Database | null = null;
let mysqlPool: mysql.Pool | null = null;
let pgPool: pg.Pool | null = null;
let proxyLogBillingDetailsColumnAvailable: boolean | null = null;
+let proxyLogDownstreamApiKeyIdColumnAvailable: boolean | null = null;
+let proxyLogClientColumnsAvailable: boolean | null = null;
+
+function buildMysqlPoolOptions(
+ connectionString = config.dbUrl,
+ sslEnabled = config.dbSsl,
+): mysql.PoolOptions {
+ const poolOptions: mysql.PoolOptions = {
+ uri: connectionString,
+ jsonStrings: true,
+ };
+ if (sslEnabled) {
+ poolOptions.ssl = { rejectUnauthorized: false };
+ }
+ return poolOptions;
+}
+
+function buildPostgresPoolOptions(
+ connectionString = config.dbUrl,
+ sslEnabled = config.dbSsl,
+): pg.PoolConfig {
+ const poolOptions: pg.PoolConfig = { connectionString };
+ if (sslEnabled) {
+ poolOptions.ssl = { rejectUnauthorized: false };
+ }
+ return poolOptions;
+}
function resolveSqlitePath(): string {
const raw = (config.dbUrl || '').trim();
- if (!raw) return resolve(`${config.dataDir}/hub.db`);
+ if (!raw) {
+ const isolatedVitestPath = resolveVitestSqlitePath();
+ if (isolatedVitestPath) {
+ return isolatedVitestPath;
+ }
+ return resolve(`${config.dataDir}/hub.db`);
+ }
if (raw === ':memory:') return raw;
if (raw.startsWith('file://')) {
const parsed = new URL(raw);
@@ -52,6 +95,41 @@ function resolveSqlitePath(): string {
return resolve(raw);
}
+function isVitestRuntime(): boolean {
+ if ((process.env.VITEST_POOL_ID || '').trim()) {
+ return true;
+ }
+ if ((process.env.VITEST_WORKER_ID || '').trim()) {
+ return true;
+ }
+ const runtimeArgs = [...process.argv, ...process.execArgv]
+ .map((value) => String(value || '').toLowerCase());
+ return runtimeArgs.some((value) => value.includes('vitest'));
+}
+
+function isDefaultRepoDataDir(value: string | undefined): boolean {
+ const trimmed = (value || '').trim();
+ if (!trimmed) return false;
+ return resolve(trimmed) === resolve('./data');
+}
+
+function resolveVitestSqlitePath(): string | null {
+ if (!isVitestRuntime()) {
+ return null;
+ }
+ if ((process.env.DB_URL || '').trim()) {
+ return null;
+ }
+ if ((process.env.DATA_DIR || '').trim() && !isDefaultRepoDataDir(process.env.DATA_DIR)) {
+ return null;
+ }
+
+ const workerTag = process.env.VITEST_POOL_ID
+ || process.env.VITEST_WORKER_ID
+ || `${process.pid}-${threadId}`;
+ return resolve(tmpdir(), `metapi-vitest-${workerTag}`, 'hub.db');
+}
+
function requireSqliteConnection(): Database.Database {
if (!sqliteConnection) {
throw new Error('SQLite connection is not initialized');
@@ -72,19 +150,33 @@ function tableColumnExists(table: string, column: string): boolean {
return rows.some((row) => row.name === column);
}
-function ensureTokenManagementSchema() {
+function tableIndexExists(indexName: string): boolean {
const sqlite = requireSqliteConnection();
+ const row = sqlite.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1")
+ .get(indexName) as { name?: string } | undefined;
+ return !!row?.name;
+}
+
+function execSqliteStatement(sqlText: string): void {
+ requireSqliteConnection().exec(sqlText);
+}
+
+function execSqliteLegacyCompat(sqlText: string): void {
+ executeLegacyCompatSync(execSqliteStatement, sqlText);
+}
+
+function ensureTokenManagementSchema() {
if (!tableExists('accounts') || !tableExists('route_channels')) {
return;
}
-
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE TABLE IF NOT EXISTS account_tokens (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
account_id integer NOT NULL,
name text NOT NULL,
token text NOT NULL,
token_group text,
+ value_status text NOT NULL DEFAULT 'ready',
source text DEFAULT 'manual',
enabled integer DEFAULT true,
is_default integer DEFAULT false,
@@ -93,16 +185,18 @@ function ensureTokenManagementSchema() {
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE cascade
);
`);
-
if (!tableColumnExists('route_channels', 'token_id')) {
- sqlite.exec('ALTER TABLE route_channels ADD COLUMN token_id integer;');
+ execSqliteLegacyCompat('ALTER TABLE route_channels ADD COLUMN token_id integer;');
}
if (!tableColumnExists('account_tokens', 'token_group')) {
- sqlite.exec('ALTER TABLE account_tokens ADD COLUMN token_group text;');
+ execSqliteLegacyCompat('ALTER TABLE account_tokens ADD COLUMN token_group text;');
+ }
+ if (!tableColumnExists('account_tokens', 'value_status')) {
+ execSqliteLegacyCompat("ALTER TABLE account_tokens ADD COLUMN value_status text NOT NULL DEFAULT 'ready';");
}
- sqlite.exec(`
+ execSqliteStatement(`
INSERT INTO account_tokens (account_id, name, token, source, enabled, is_default, created_at, updated_at)
SELECT
a.id,
@@ -117,6 +211,8 @@ function ensureTokenManagementSchema() {
WHERE
a.api_token IS NOT NULL
AND trim(a.api_token) <> ''
+ AND a.access_token IS NOT NULL
+ AND trim(a.access_token) <> ''
AND NOT EXISTS (
SELECT 1 FROM account_tokens AS t
WHERE t.account_id = a.id
@@ -124,7 +220,7 @@ function ensureTokenManagementSchema() {
);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE TABLE IF NOT EXISTS token_model_availability (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
token_id integer NOT NULL,
@@ -136,15 +232,14 @@ function ensureTokenManagementSchema() {
);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE UNIQUE INDEX IF NOT EXISTS token_model_availability_token_model_unique
ON token_model_availability(token_id, model_name);
`);
}
function ensureProxyVideoTaskSchema() {
- const sqlite = requireSqliteConnection();
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE TABLE IF NOT EXISTS proxy_video_tasks (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
public_id text NOT NULL,
@@ -164,30 +259,29 @@ function ensureProxyVideoTaskSchema() {
);
`);
if (!tableColumnExists('proxy_video_tasks', 'status_snapshot')) {
- sqlite.exec('ALTER TABLE proxy_video_tasks ADD COLUMN status_snapshot text;');
+ execSqliteLegacyCompat('ALTER TABLE proxy_video_tasks ADD COLUMN status_snapshot text;');
}
if (!tableColumnExists('proxy_video_tasks', 'upstream_response_meta')) {
- sqlite.exec('ALTER TABLE proxy_video_tasks ADD COLUMN upstream_response_meta text;');
+ execSqliteLegacyCompat('ALTER TABLE proxy_video_tasks ADD COLUMN upstream_response_meta text;');
}
if (!tableColumnExists('proxy_video_tasks', 'last_upstream_status')) {
- sqlite.exec('ALTER TABLE proxy_video_tasks ADD COLUMN last_upstream_status integer;');
+ execSqliteLegacyCompat('ALTER TABLE proxy_video_tasks ADD COLUMN last_upstream_status integer;');
}
if (!tableColumnExists('proxy_video_tasks', 'last_polled_at')) {
- sqlite.exec('ALTER TABLE proxy_video_tasks ADD COLUMN last_polled_at text;');
+ execSqliteLegacyCompat('ALTER TABLE proxy_video_tasks ADD COLUMN last_polled_at text;');
}
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE UNIQUE INDEX IF NOT EXISTS proxy_video_tasks_public_id_unique
ON proxy_video_tasks(public_id);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE INDEX IF NOT EXISTS proxy_video_tasks_upstream_video_id_idx
ON proxy_video_tasks(upstream_video_id);
`);
}
function ensureProxyFileSchema() {
- const sqlite = requireSqliteConnection();
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE TABLE IF NOT EXISTS proxy_files (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
public_id text NOT NULL,
@@ -204,27 +298,26 @@ function ensureProxyFileSchema() {
deleted_at text
);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE UNIQUE INDEX IF NOT EXISTS proxy_files_public_id_unique
ON proxy_files(public_id);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE INDEX IF NOT EXISTS proxy_files_owner_lookup_idx
ON proxy_files(owner_type, owner_id, deleted_at);
`);
}
function ensureSiteStatusSchema() {
- const sqlite = requireSqliteConnection();
if (!tableExists('sites')) {
return;
}
if (!tableColumnExists('sites', 'status')) {
- sqlite.exec(`ALTER TABLE sites ADD COLUMN status text DEFAULT 'active';`);
+ execSqliteLegacyCompat(`ALTER TABLE sites ADD COLUMN status text DEFAULT 'active';`);
}
- sqlite.exec(`
+ execSqliteStatement(`
UPDATE sites
SET status = lower(trim(status))
WHERE status IS NOT NULL
@@ -232,7 +325,7 @@ function ensureSiteStatusSchema() {
AND status != lower(trim(status));
`);
- sqlite.exec(`
+ execSqliteStatement(`
UPDATE sites
SET status = 'active'
WHERE status IS NULL
@@ -242,55 +335,61 @@ function ensureSiteStatusSchema() {
}
function ensureSiteProxySchema() {
- const sqlite = requireSqliteConnection();
if (!tableExists('sites')) {
return;
}
if (!tableColumnExists('sites', 'proxy_url')) {
- sqlite.exec(`ALTER TABLE sites ADD COLUMN proxy_url text;`);
+ execSqliteLegacyCompat(`ALTER TABLE sites ADD COLUMN proxy_url text;`);
}
}
function ensureSiteUseSystemProxySchema() {
- const sqlite = requireSqliteConnection();
if (!tableExists('sites')) {
return;
}
if (!tableColumnExists('sites', 'use_system_proxy')) {
- sqlite.exec(`ALTER TABLE sites ADD COLUMN use_system_proxy integer DEFAULT 0;`);
+ execSqliteLegacyCompat(`ALTER TABLE sites ADD COLUMN use_system_proxy integer DEFAULT 0;`);
}
- sqlite.exec(`
+ execSqliteLegacyCompat(`
UPDATE sites
SET use_system_proxy = 0
WHERE use_system_proxy IS NULL;
`);
}
+function ensureSiteCustomHeadersSchema() {
+ if (!tableExists('sites')) {
+ return;
+ }
+
+ if (!tableColumnExists('sites', 'custom_headers')) {
+ execSqliteLegacyCompat(`ALTER TABLE sites ADD COLUMN custom_headers text;`);
+ }
+}
+
function ensureSiteExternalCheckinUrlSchema() {
- const sqlite = requireSqliteConnection();
if (!tableExists('sites')) {
return;
}
if (!tableColumnExists('sites', 'external_checkin_url')) {
- sqlite.exec(`ALTER TABLE sites ADD COLUMN external_checkin_url text;`);
+ execSqliteLegacyCompat(`ALTER TABLE sites ADD COLUMN external_checkin_url text;`);
}
}
function ensureSiteGlobalWeightSchema() {
- const sqlite = requireSqliteConnection();
if (!tableExists('sites')) {
return;
}
if (!tableColumnExists('sites', 'global_weight')) {
- sqlite.exec(`ALTER TABLE sites ADD COLUMN global_weight real DEFAULT 1;`);
+ execSqliteLegacyCompat(`ALTER TABLE sites ADD COLUMN global_weight real DEFAULT 1;`);
}
- sqlite.exec(`
+ execSqliteLegacyCompat(`
UPDATE sites
SET global_weight = 1
WHERE global_weight IS NULL
@@ -311,7 +410,7 @@ function createSqliteSchemaInspector(): RuntimeSchemaInspector {
tableExists: async (table) => tableExists(table),
columnExists: async (table, column) => tableColumnExists(table, column),
execute: async (sqlText) => {
- requireSqliteConnection().exec(sqlText);
+ executeLegacyCompatSync(execSqliteStatement, sqlText);
},
};
}
@@ -335,7 +434,7 @@ function createMysqlSchemaInspector(): RuntimeSchemaInspector | null {
return Array.isArray(rows) && rows.length > 0;
},
execute: async (sqlText) => {
- await mysqlPool!.query(sqlText);
+ await executeLegacyCompat((statement) => mysqlPool!.query(statement).then(() => undefined), sqlText);
},
};
}
@@ -359,7 +458,7 @@ function createPostgresSchemaInspector(): RuntimeSchemaInspector | null {
return Number(result.rowCount || 0) > 0;
},
execute: async (sqlText) => {
- await pgPool!.query(sqlText);
+ await executeLegacyCompat((statement) => pgPool!.query(statement).then(() => undefined), sqlText);
},
};
}
@@ -393,40 +492,76 @@ export async function ensureProxyFileCompatibilityColumns(): Promise {
}
function ensureRouteGroupingSchema() {
- const sqlite = requireSqliteConnection();
if (!tableExists('token_routes') || !tableExists('route_channels')) {
return;
}
if (!tableColumnExists('token_routes', 'display_name')) {
- sqlite.exec(`ALTER TABLE token_routes ADD COLUMN display_name text;`);
+ execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN display_name text;`);
}
if (!tableColumnExists('token_routes', 'display_icon')) {
- sqlite.exec(`ALTER TABLE token_routes ADD COLUMN display_icon text;`);
+ execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN display_icon text;`);
+ }
+
+ if (!tableColumnExists('token_routes', 'route_mode')) {
+ execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN route_mode text DEFAULT 'pattern';`);
}
if (!tableColumnExists('token_routes', 'decision_snapshot')) {
- sqlite.exec(`ALTER TABLE token_routes ADD COLUMN decision_snapshot text;`);
+ execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN decision_snapshot text;`);
}
if (!tableColumnExists('token_routes', 'decision_refreshed_at')) {
- sqlite.exec(`ALTER TABLE token_routes ADD COLUMN decision_refreshed_at text;`);
+ execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN decision_refreshed_at text;`);
+ }
+
+ if (!tableColumnExists('token_routes', 'routing_strategy')) {
+ execSqliteLegacyCompat(`ALTER TABLE token_routes ADD COLUMN routing_strategy text DEFAULT 'weighted';`);
}
if (!tableColumnExists('route_channels', 'source_model')) {
- sqlite.exec(`ALTER TABLE route_channels ADD COLUMN source_model text;`);
+ execSqliteLegacyCompat(`ALTER TABLE route_channels ADD COLUMN source_model text;`);
}
+
+ if (!tableColumnExists('route_channels', 'last_selected_at')) {
+ execSqliteLegacyCompat(`ALTER TABLE route_channels ADD COLUMN last_selected_at text;`);
+ }
+
+ if (!tableColumnExists('route_channels', 'consecutive_fail_count')) {
+ execSqliteLegacyCompat(`ALTER TABLE route_channels ADD COLUMN consecutive_fail_count integer NOT NULL DEFAULT 0;`);
+ }
+
+ if (!tableColumnExists('route_channels', 'cooldown_level')) {
+ execSqliteLegacyCompat(`ALTER TABLE route_channels ADD COLUMN cooldown_level integer NOT NULL DEFAULT 0;`);
+ }
+
+ execSqliteLegacyCompat(`
+ CREATE TABLE IF NOT EXISTS route_group_sources (
+ id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ group_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade,
+ source_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade
+ );
+ `);
+ execSqliteLegacyCompat(`
+ CREATE UNIQUE INDEX IF NOT EXISTS route_group_sources_group_source_unique
+ ON route_group_sources(group_route_id, source_route_id);
+ `);
+ execSqliteLegacyCompat(`
+ CREATE INDEX IF NOT EXISTS route_group_sources_source_route_id_idx
+ ON route_group_sources(source_route_id);
+ `);
}
function ensureDownstreamApiKeySchema() {
- const sqlite = requireSqliteConnection();
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE TABLE IF NOT EXISTS downstream_api_keys (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
name text NOT NULL,
key text NOT NULL,
description text,
+ group_name text,
+ tags text,
enabled integer DEFAULT true,
expires_at text,
max_cost real,
@@ -442,37 +577,90 @@ function ensureDownstreamApiKeySchema() {
);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE UNIQUE INDEX IF NOT EXISTS downstream_api_keys_key_unique
ON downstream_api_keys(key);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE INDEX IF NOT EXISTS downstream_api_keys_name_idx
ON downstream_api_keys(name);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE INDEX IF NOT EXISTS downstream_api_keys_enabled_idx
ON downstream_api_keys(enabled);
`);
- sqlite.exec(`
+ execSqliteLegacyCompat(`
CREATE INDEX IF NOT EXISTS downstream_api_keys_expires_at_idx
ON downstream_api_keys(expires_at);
`);
+
+ if (!tableColumnExists('downstream_api_keys', 'group_name')) {
+ execSqliteLegacyCompat('ALTER TABLE downstream_api_keys ADD COLUMN group_name text;');
+ }
+
+ if (!tableColumnExists('downstream_api_keys', 'tags')) {
+ execSqliteLegacyCompat('ALTER TABLE downstream_api_keys ADD COLUMN tags text;');
+ }
}
function ensureProxyLogBillingDetailsSchema() {
- const sqlite = requireSqliteConnection();
if (!tableExists('proxy_logs')) {
return;
}
if (!tableColumnExists('proxy_logs', 'billing_details')) {
- sqlite.exec('ALTER TABLE proxy_logs ADD COLUMN billing_details text;');
+ execSqliteLegacyCompat('ALTER TABLE proxy_logs ADD COLUMN billing_details text;');
}
proxyLogBillingDetailsColumnAvailable = true;
}
+function ensureProxyLogDownstreamApiKeyIdSchema() {
+ if (!tableExists('proxy_logs')) {
+ return;
+ }
+
+ if (!tableColumnExists('proxy_logs', 'downstream_api_key_id')) {
+ execSqliteLegacyCompat('ALTER TABLE proxy_logs ADD COLUMN downstream_api_key_id integer;');
+ }
+
+ proxyLogDownstreamApiKeyIdColumnAvailable = true;
+}
+
+function ensureProxyLogClientSchema() {
+ if (!tableExists('proxy_logs')) {
+ return;
+ }
+
+ if (!tableColumnExists('proxy_logs', 'client_family')) {
+ execSqliteLegacyCompat('ALTER TABLE proxy_logs ADD COLUMN client_family text;');
+ }
+ if (!tableColumnExists('proxy_logs', 'client_app_id')) {
+ execSqliteLegacyCompat('ALTER TABLE proxy_logs ADD COLUMN client_app_id text;');
+ }
+ if (!tableColumnExists('proxy_logs', 'client_app_name')) {
+ execSqliteLegacyCompat('ALTER TABLE proxy_logs ADD COLUMN client_app_name text;');
+ }
+ if (!tableColumnExists('proxy_logs', 'client_confidence')) {
+ execSqliteLegacyCompat('ALTER TABLE proxy_logs ADD COLUMN client_confidence text;');
+ }
+
+ if (!tableIndexExists('proxy_logs_client_app_id_created_at_idx')) {
+ execSqliteLegacyCompat(`
+ CREATE INDEX IF NOT EXISTS proxy_logs_client_app_id_created_at_idx
+ ON proxy_logs(client_app_id, created_at);
+ `);
+ }
+ if (!tableIndexExists('proxy_logs_client_family_created_at_idx')) {
+ execSqliteLegacyCompat(`
+ CREATE INDEX IF NOT EXISTS proxy_logs_client_family_created_at_idx
+ ON proxy_logs(client_family, created_at);
+ `);
+ }
+
+ proxyLogClientColumnsAvailable = true;
+}
+
function normalizeSchemaErrorMessage(error: unknown): string {
if (typeof error === 'object' && error && 'message' in error) {
return String((error as { message?: unknown }).message || '');
@@ -485,6 +673,14 @@ function isDuplicateColumnError(error: unknown): boolean {
return lowered.includes('duplicate column') || lowered.includes('already exists');
}
+function isDuplicateIndexError(error: unknown): boolean {
+ const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
+ return lowered.includes('duplicate key name')
+ || lowered.includes('already exists')
+ || lowered.includes('relation')
+ || lowered.includes('duplicate index');
+}
+
export async function hasProxyLogBillingDetailsColumn(): Promise {
if (proxyLogBillingDetailsColumnAvailable !== null) {
return proxyLogBillingDetailsColumnAvailable;
@@ -527,10 +723,16 @@ export async function ensureProxyLogBillingDetailsColumn(): Promise {
try {
if (runtimeDbDialect === 'mysql') {
if (!mysqlPool) return false;
- await mysqlPool.query('ALTER TABLE `proxy_logs` ADD COLUMN `billing_details` TEXT NULL');
+ await executeLegacyCompat(
+ (statement) => mysqlPool!.query(statement).then(() => undefined),
+ 'ALTER TABLE `proxy_logs` ADD COLUMN `billing_details` TEXT NULL',
+ );
} else {
if (!pgPool) return false;
- await pgPool.query('ALTER TABLE "proxy_logs" ADD COLUMN "billing_details" TEXT');
+ await executeLegacyCompat(
+ (statement) => pgPool!.query(statement).then(() => undefined),
+ 'ALTER TABLE "proxy_logs" ADD COLUMN "billing_details" TEXT',
+ );
}
proxyLogBillingDetailsColumnAvailable = true;
return true;
@@ -545,8 +747,249 @@ export async function ensureProxyLogBillingDetailsColumn(): Promise {
}
}
+export async function hasProxyLogDownstreamApiKeyIdColumn(): Promise {
+ if (proxyLogDownstreamApiKeyIdColumnAvailable !== null) {
+ return proxyLogDownstreamApiKeyIdColumnAvailable;
+ }
+
+ if (runtimeDbDialect === 'sqlite') {
+ proxyLogDownstreamApiKeyIdColumnAvailable = tableExists('proxy_logs')
+ && tableColumnExists('proxy_logs', 'downstream_api_key_id');
+ return proxyLogDownstreamApiKeyIdColumnAvailable;
+ }
+
+ if (runtimeDbDialect === 'mysql') {
+ if (!mysqlPool) return false;
+ const [rows] = await mysqlPool.query('SHOW COLUMNS FROM `proxy_logs` LIKE ?', ['downstream_api_key_id']);
+ proxyLogDownstreamApiKeyIdColumnAvailable = Array.isArray(rows) && rows.length > 0;
+ return proxyLogDownstreamApiKeyIdColumnAvailable;
+ }
+
+ if (!pgPool) return false;
+ const result = await pgPool.query(
+ 'SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1 AND column_name = $2 LIMIT 1',
+ ['proxy_logs', 'downstream_api_key_id'],
+ );
+ proxyLogDownstreamApiKeyIdColumnAvailable = Number(result.rowCount || 0) > 0;
+ return proxyLogDownstreamApiKeyIdColumnAvailable;
+}
+
+export async function ensureProxyLogDownstreamApiKeyIdColumn(): Promise {
+ if (runtimeDbDialect === 'sqlite') {
+ ensureProxyLogDownstreamApiKeyIdSchema();
+ proxyLogDownstreamApiKeyIdColumnAvailable = tableExists('proxy_logs')
+ && tableColumnExists('proxy_logs', 'downstream_api_key_id');
+ return proxyLogDownstreamApiKeyIdColumnAvailable;
+ }
+
+ if (await hasProxyLogDownstreamApiKeyIdColumn()) {
+ return true;
+ }
+
+ try {
+ if (runtimeDbDialect === 'mysql') {
+ if (!mysqlPool) return false;
+ await executeLegacyCompat(
+ (statement) => mysqlPool!.query(statement).then(() => undefined),
+ 'ALTER TABLE `proxy_logs` ADD COLUMN `downstream_api_key_id` INT NULL',
+ );
+ } else {
+ if (!pgPool) return false;
+ await executeLegacyCompat(
+ (statement) => pgPool!.query(statement).then(() => undefined),
+ 'ALTER TABLE "proxy_logs" ADD COLUMN "downstream_api_key_id" INTEGER',
+ );
+ }
+ proxyLogDownstreamApiKeyIdColumnAvailable = true;
+ return true;
+ } catch (error) {
+ if (isDuplicateColumnError(error)) {
+ proxyLogDownstreamApiKeyIdColumnAvailable = true;
+ return true;
+ }
+ proxyLogDownstreamApiKeyIdColumnAvailable = false;
+ console.warn('[db] failed to ensure proxy_logs.downstream_api_key_id column', error);
+ return false;
+ }
+}
+
+async function hasMysqlIndex(indexName: string): Promise {
+ if (!mysqlPool) return false;
+ const [rows] = await mysqlPool.query(
+ 'SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ? LIMIT 1',
+ ['proxy_logs', indexName],
+ );
+ return Array.isArray(rows) && rows.length > 0;
+}
+
+async function hasPostgresIndex(indexName: string): Promise {
+ if (!pgPool) return false;
+ const result = await pgPool.query(
+ 'SELECT 1 FROM pg_indexes WHERE schemaname = current_schema() AND tablename = $1 AND indexname = $2 LIMIT 1',
+ ['proxy_logs', indexName],
+ );
+ return Number(result.rowCount || 0) > 0;
+}
+
+export async function hasProxyLogClientColumns(): Promise {
+ if (proxyLogClientColumnsAvailable !== null) {
+ return proxyLogClientColumnsAvailable;
+ }
+
+ const requiredColumns = [
+ 'client_family',
+ 'client_app_id',
+ 'client_app_name',
+ 'client_confidence',
+ ];
+
+ if (runtimeDbDialect === 'sqlite') {
+ proxyLogClientColumnsAvailable = tableExists('proxy_logs')
+ && requiredColumns.every((columnName) => tableColumnExists('proxy_logs', columnName));
+ return proxyLogClientColumnsAvailable;
+ }
+
+ if (runtimeDbDialect === 'mysql') {
+ if (!mysqlPool) return false;
+ const [rows] = await mysqlPool.query(
+ 'SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name IN (?, ?, ?, ?)',
+ ['proxy_logs', ...requiredColumns],
+ ) as [Array<{ column_name?: string }>, unknown];
+ const available = new Set(
+ Array.isArray(rows)
+ ? rows.map((row) => String(row?.column_name || '').trim().toLowerCase()).filter(Boolean)
+ : [],
+ );
+ proxyLogClientColumnsAvailable = requiredColumns.every((columnName) => available.has(columnName));
+ return proxyLogClientColumnsAvailable;
+ }
+
+ if (!pgPool) return false;
+ const result = await pgPool.query(
+ 'SELECT column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1 AND column_name = ANY($2::text[])',
+ ['proxy_logs', requiredColumns],
+ );
+ const available = new Set(
+ result.rows.map((row) => String((row as { column_name?: string }).column_name || '').trim().toLowerCase()).filter(Boolean),
+ );
+ proxyLogClientColumnsAvailable = requiredColumns.every((columnName) => available.has(columnName));
+ return proxyLogClientColumnsAvailable;
+}
+
+export async function ensureProxyLogClientColumns(): Promise {
+ const requiredColumns = [
+ { name: 'client_family', sqliteType: 'text', mysqlType: 'TEXT NULL', postgresType: 'TEXT' },
+ { name: 'client_app_id', sqliteType: 'text', mysqlType: 'TEXT NULL', postgresType: 'TEXT' },
+ { name: 'client_app_name', sqliteType: 'text', mysqlType: 'TEXT NULL', postgresType: 'TEXT' },
+ { name: 'client_confidence', sqliteType: 'text', mysqlType: 'TEXT NULL', postgresType: 'TEXT' },
+ ];
+ const requiredIndexes = [
+ {
+ name: 'proxy_logs_client_app_id_created_at_idx',
+ sqliteSql: 'CREATE INDEX IF NOT EXISTS proxy_logs_client_app_id_created_at_idx ON proxy_logs(client_app_id, created_at);',
+ mysqlSql: 'CREATE INDEX `proxy_logs_client_app_id_created_at_idx` ON `proxy_logs` (`client_app_id`(191), `created_at`(191))',
+ postgresSql: 'CREATE INDEX "proxy_logs_client_app_id_created_at_idx" ON "proxy_logs" ("client_app_id", "created_at")',
+ },
+ {
+ name: 'proxy_logs_client_family_created_at_idx',
+ sqliteSql: 'CREATE INDEX IF NOT EXISTS proxy_logs_client_family_created_at_idx ON proxy_logs(client_family, created_at);',
+ mysqlSql: 'CREATE INDEX `proxy_logs_client_family_created_at_idx` ON `proxy_logs` (`client_family`(191), `created_at`(191))',
+ postgresSql: 'CREATE INDEX "proxy_logs_client_family_created_at_idx" ON "proxy_logs" ("client_family", "created_at")',
+ },
+ ];
+
+ if (runtimeDbDialect === 'sqlite') {
+ ensureProxyLogClientSchema();
+ proxyLogClientColumnsAvailable = tableExists('proxy_logs')
+ && requiredColumns.every((column) => tableColumnExists('proxy_logs', column.name));
+ return proxyLogClientColumnsAvailable;
+ }
+
+ if (await hasProxyLogClientColumns()) {
+ for (const requiredIndex of requiredIndexes) {
+ const indexExists = runtimeDbDialect === 'mysql'
+ ? await hasMysqlIndex(requiredIndex.name)
+ : await hasPostgresIndex(requiredIndex.name);
+ if (indexExists) continue;
+ try {
+ if (runtimeDbDialect === 'mysql') {
+ if (!mysqlPool) return false;
+ await executeLegacyCompat(
+ (statement) => mysqlPool!.query(statement).then(() => undefined),
+ requiredIndex.mysqlSql,
+ );
+ } else {
+ if (!pgPool) return false;
+ await executeLegacyCompat(
+ (statement) => pgPool!.query(statement).then(() => undefined),
+ requiredIndex.postgresSql,
+ );
+ }
+ } catch (error) {
+ if (!isDuplicateIndexError(error)) {
+ console.warn(`[db] failed to ensure ${requiredIndex.name}`, error);
+ }
+ }
+ }
+ return true;
+ }
+
+ try {
+ if (runtimeDbDialect === 'mysql') {
+ if (!mysqlPool) return false;
+ for (const column of requiredColumns) {
+ const [rows] = await mysqlPool.query('SHOW COLUMNS FROM `proxy_logs` LIKE ?', [column.name]);
+ if (Array.isArray(rows) && rows.length > 0) continue;
+ await executeLegacyCompat(
+ (statement) => mysqlPool!.query(statement).then(() => undefined),
+ `ALTER TABLE \`proxy_logs\` ADD COLUMN \`${column.name}\` ${column.mysqlType}`,
+ );
+ }
+ for (const requiredIndex of requiredIndexes) {
+ if (await hasMysqlIndex(requiredIndex.name)) continue;
+ await executeLegacyCompat(
+ (statement) => mysqlPool!.query(statement).then(() => undefined),
+ requiredIndex.mysqlSql,
+ );
+ }
+ } else {
+ if (!pgPool) return false;
+ for (const column of requiredColumns) {
+ const result = await pgPool.query(
+ 'SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1 AND column_name = $2 LIMIT 1',
+ ['proxy_logs', column.name],
+ );
+ if (Number(result.rowCount || 0) > 0) continue;
+ await executeLegacyCompat(
+ (statement) => pgPool!.query(statement).then(() => undefined),
+ `ALTER TABLE "proxy_logs" ADD COLUMN "${column.name}" ${column.postgresType}`,
+ );
+ }
+ for (const requiredIndex of requiredIndexes) {
+ if (await hasPostgresIndex(requiredIndex.name)) continue;
+ await executeLegacyCompat(
+ (statement) => pgPool!.query(statement).then(() => undefined),
+ requiredIndex.postgresSql,
+ );
+ }
+ }
+ proxyLogClientColumnsAvailable = true;
+ return true;
+ } catch (error) {
+ if (isDuplicateColumnError(error) || isDuplicateIndexError(error)) {
+ proxyLogClientColumnsAvailable = await hasProxyLogClientColumns();
+ return proxyLogClientColumnsAvailable;
+ }
+ proxyLogClientColumnsAvailable = false;
+ console.warn('[db] failed to ensure proxy_logs client columns', error);
+ return false;
+ }
+}
+
function resetSchemaCapabilityCache() {
proxyLogBillingDetailsColumnAvailable = null;
+ proxyLogDownstreamApiKeyIdColumnAvailable = null;
+ proxyLogClientColumnsAvailable = null;
}
async function sqliteProxyQuery(sqlText: string, params: unknown[], method: SqlMethod) {
@@ -801,11 +1244,13 @@ function initSqliteDb() {
ensureSiteStatusSchema();
ensureSiteProxySchema();
ensureSiteUseSystemProxySchema();
+ ensureSiteCustomHeadersSchema();
ensureSiteExternalCheckinUrlSchema();
ensureSiteGlobalWeightSchema();
ensureRouteGroupingSchema();
ensureDownstreamApiKeySchema();
ensureProxyLogBillingDetailsSchema();
+ ensureProxyLogClientSchema();
ensureProxyVideoTaskSchema();
ensureProxyFileSchema();
@@ -822,11 +1267,7 @@ function initMysqlDb(): AppDb {
if (!config.dbUrl) {
throw new Error('DB_URL is required when DB_TYPE=mysql');
}
- const poolOptions: mysql.PoolOptions = { uri: config.dbUrl };
- if (config.dbSsl) {
- poolOptions.ssl = { rejectUnauthorized: false };
- }
- mysqlPool = mysql.createPool(poolOptions);
+ mysqlPool = mysql.createPool(buildMysqlPoolOptions());
const rawDb = drizzleMysqlProxy(
(sqlText, params, method) => mysqlProxyQuery(mysqlPool!, sqlText, params, method as SqlMethod),
@@ -858,10 +1299,8 @@ function initPostgresDb(): AppDb {
if (!config.dbUrl) {
throw new Error('DB_URL is required when DB_TYPE=postgres');
}
- const poolOptions: pg.PoolConfig = { connectionString: config.dbUrl };
- if (config.dbSsl) {
- poolOptions.ssl = { rejectUnauthorized: false };
- }
+ installPostgresJsonTextParsers();
+ const poolOptions = buildPostgresPoolOptions();
pgPool = new pg.Pool(poolOptions);
const rawDb = drizzlePgProxy(
@@ -938,6 +1377,11 @@ export async function switchRuntimeDatabase(nextDialect: RuntimeDbDialect, nextD
try {
activeDb = initDb();
+ await ensureRuntimeDatabaseReady({
+ dialect: nextDialect,
+ connectionString: nextDbUrl,
+ ssl: config.dbSsl,
+ });
} catch (error) {
await closeDbConnections();
runtimeDbDialect = previousDialect;
@@ -952,4 +1396,12 @@ export async function switchRuntimeDatabase(nextDialect: RuntimeDbDialect, nextD
export const __dbProxyTestUtils = {
wrapQueryLike,
shouldWrapObject,
+ resolveSqlitePath,
+ resolveVitestSqlitePath,
+ buildMysqlPoolOptions,
+ buildPostgresPoolOptions,
+ installPostgresJsonTextParsers,
+ ensurePostgresJsonTextParsers: installPostgresJsonTextParsers,
+ resetPostgresJsonTextParsersInstallStateForTests,
+ pg,
};
diff --git a/src/server/db/legacySchemaCompat.architecture.test.ts b/src/server/db/legacySchemaCompat.architecture.test.ts
new file mode 100644
index 00000000..568a4fe2
--- /dev/null
+++ b/src/server/db/legacySchemaCompat.architecture.test.ts
@@ -0,0 +1,18 @@
+import { readFileSync } from 'node:fs';
+import { describe, expect, it } from 'vitest';
+
+function readSource(relativePath: string): string {
+ return readFileSync(new URL(relativePath, import.meta.url), 'utf8');
+}
+
+describe('legacySchemaCompat architecture boundaries', () => {
+ it('derives feature-owned legacy mutations from compatibility specs instead of hardcoding a second full whitelist', () => {
+ const source = readSource('./legacySchemaCompat.ts');
+
+ expect(source).toContain('ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS');
+ expect(source).toContain('PROXY_FILE_COLUMN_COMPATIBILITY_SPECS');
+ expect(source).toContain('ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS');
+ expect(source).toContain('SITE_COLUMN_COMPATIBILITY_SPECS');
+ expect(source).toContain('SITE_TABLE_COMPATIBILITY_SPECS');
+ });
+});
diff --git a/src/server/db/legacySchemaCompat.test.ts b/src/server/db/legacySchemaCompat.test.ts
new file mode 100644
index 00000000..6cd7ce5b
--- /dev/null
+++ b/src/server/db/legacySchemaCompat.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, it } from 'vitest';
+import { classifyLegacyCompatMutation } from './legacySchemaCompat.js';
+
+describe('legacy schema compat boundary', () => {
+ it('allows only explicitly registered legacy upgrade shims', () => {
+ expect(classifyLegacyCompatMutation('ALTER TABLE proxy_logs ADD COLUMN billing_details text;')).toBe('legacy');
+ expect(classifyLegacyCompatMutation('ALTER TABLE proxy_logs ADD COLUMN client_app_id text;')).toBe('legacy');
+ expect(classifyLegacyCompatMutation('CREATE INDEX proxy_logs_client_app_id_created_at_idx ON proxy_logs(client_app_id, created_at);')).toBe('legacy');
+ expect(classifyLegacyCompatMutation('UPDATE "sites" SET "use_system_proxy" = FALSE WHERE "use_system_proxy" IS NULL')).toBe('legacy');
+ expect(classifyLegacyCompatMutation('ALTER TABLE sites ADD COLUMN brand_new_column text;')).toBe('forbidden');
+ expect(classifyLegacyCompatMutation('UPDATE "sites" SET "brand_new_column" = 1')).toBe('forbidden');
+ });
+});
diff --git a/src/server/db/legacySchemaCompat.ts b/src/server/db/legacySchemaCompat.ts
new file mode 100644
index 00000000..57b68451
--- /dev/null
+++ b/src/server/db/legacySchemaCompat.ts
@@ -0,0 +1,191 @@
+import {
+ ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS,
+ ensureAccountTokenSchemaCompatibility,
+ type AccountTokenSchemaInspector,
+} from './accountTokenSchemaCompatibility.js';
+import {
+ ensureProxyFileSchemaCompatibility,
+ PROXY_FILE_COLUMN_COMPATIBILITY_SPECS,
+ PROXY_FILE_INDEX_COMPATIBILITY_SPECS,
+ PROXY_FILE_TABLE_COMPATIBILITY_SPECS,
+ type ProxyFileSchemaInspector,
+} from './proxyFileSchemaCompatibility.js';
+import {
+ ensureRouteGroupingSchemaCompatibility,
+ ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS,
+ ROUTE_GROUPING_TABLE_COMPATIBILITY_SPECS,
+ type RouteGroupingSchemaInspector,
+} from './routeGroupingSchemaCompatibility.js';
+import {
+ ensureSharedIndexSchemaCompatibility,
+ SHARED_INDEX_COMPATIBILITY_SPECS,
+ type SharedIndexSchemaInspector,
+} from './sharedIndexSchemaCompatibility.js';
+import {
+ ensureSiteSchemaCompatibility,
+ SITE_COLUMN_COMPATIBILITY_SPECS,
+ SITE_TABLE_COMPATIBILITY_SPECS,
+ type SiteSchemaInspector,
+} from './siteSchemaCompatibility.js';
+
+export type LegacySchemaCompatClassification = 'legacy' | 'forbidden';
+
+export interface LegacySchemaCompatInspector extends
+ SiteSchemaInspector,
+ RouteGroupingSchemaInspector,
+ ProxyFileSchemaInspector,
+ AccountTokenSchemaInspector,
+ SharedIndexSchemaInspector {}
+
+const BOOTSTRAP_OWNED_LEGACY_TABLES = [
+ 'account_tokens',
+ 'token_model_availability',
+ 'proxy_video_tasks',
+ 'downstream_api_keys',
+];
+
+const BOOTSTRAP_OWNED_LEGACY_COLUMNS = [
+ 'sites.status',
+ 'route_channels.token_id',
+ 'proxy_video_tasks.status_snapshot',
+ 'proxy_video_tasks.upstream_response_meta',
+ 'proxy_video_tasks.last_upstream_status',
+ 'proxy_video_tasks.last_polled_at',
+ 'downstream_api_keys.group_name',
+ 'downstream_api_keys.tags',
+ 'proxy_logs.billing_details',
+ 'proxy_logs.client_family',
+ 'proxy_logs.client_app_id',
+ 'proxy_logs.client_app_name',
+ 'proxy_logs.client_confidence',
+ 'proxy_logs.downstream_api_key_id',
+];
+
+const BOOTSTRAP_OWNED_LEGACY_INDEXES = [
+ 'token_model_availability_token_model_unique',
+ 'proxy_video_tasks_public_id_unique',
+ 'proxy_video_tasks_upstream_video_id_idx',
+ 'downstream_api_keys_key_unique',
+ 'downstream_api_keys_name_idx',
+ 'downstream_api_keys_enabled_idx',
+ 'downstream_api_keys_expires_at_idx',
+ 'proxy_logs_client_app_id_created_at_idx',
+ 'proxy_logs_client_family_created_at_idx',
+ 'proxy_logs_downstream_api_key_created_at_idx',
+];
+
+function normalizeSqlText(sqlText: string): string {
+ return sqlText.trim().replace(/\s+/g, ' ').toLowerCase();
+}
+
+function extractIndexName(sqlText: string): string | null {
+ const match = normalizeSqlText(sqlText).match(
+ /^create (?:unique )?index(?: if not exists)? [`"]?([a-z0-9_]+)[`"]?/i,
+ );
+ return match?.[1] ?? null;
+}
+
+const LEGACY_COMPAT_TABLES = new Set([
+ ...SITE_TABLE_COMPATIBILITY_SPECS.map((spec) => spec.table),
+ ...ROUTE_GROUPING_TABLE_COMPATIBILITY_SPECS.map((spec) => spec.table),
+ ...PROXY_FILE_TABLE_COMPATIBILITY_SPECS.map((spec) => spec.table),
+ ...BOOTSTRAP_OWNED_LEGACY_TABLES,
+]);
+
+const LEGACY_COMPAT_COLUMNS = new Set([
+ ...SITE_COLUMN_COMPATIBILITY_SPECS.map((spec) => `sites.${spec.column}`),
+ ...ACCOUNT_TOKEN_COLUMN_COMPATIBILITY_SPECS.map((spec) => `${spec.table}.${spec.column}`),
+ ...ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS.map((spec) => `${spec.table}.${spec.column}`),
+ ...PROXY_FILE_COLUMN_COMPATIBILITY_SPECS.map((spec) => `${spec.table}.${spec.column}`),
+ ...BOOTSTRAP_OWNED_LEGACY_COLUMNS,
+]);
+
+const LEGACY_COMPAT_INDEXES = new Set([
+ ...SITE_TABLE_COMPATIBILITY_SPECS.flatMap((spec) => spec.postCreateSql ? Object.values(spec.postCreateSql) : [])
+ .flat()
+ .map((sqlText) => extractIndexName(sqlText))
+ .filter((indexName): indexName is string => Boolean(indexName)),
+ ...ROUTE_GROUPING_TABLE_COMPATIBILITY_SPECS.flatMap((spec) => Object.values(spec.createSql))
+ .flat()
+ .map((sqlText) => extractIndexName(sqlText))
+ .filter((indexName): indexName is string => Boolean(indexName)),
+ ...PROXY_FILE_INDEX_COMPATIBILITY_SPECS.map((spec) => spec.indexName),
+ ...BOOTSTRAP_OWNED_LEGACY_INDEXES,
+ ...SHARED_INDEX_COMPATIBILITY_SPECS.map((spec) => spec.indexName),
+]);
+
+const LEGACY_COMPAT_UPDATES = new Set(
+ SITE_COLUMN_COMPATIBILITY_SPECS
+ .flatMap((spec) => spec.normalizeSql ? Object.values(spec.normalizeSql) : [])
+ .map((sqlText) => normalizeSqlText(sqlText)),
+);
+
+export function classifyLegacyCompatMutation(sqlText: string): LegacySchemaCompatClassification {
+ const normalized = normalizeSqlText(sqlText);
+
+ if (LEGACY_COMPAT_UPDATES.has(normalized)) {
+ return 'legacy';
+ }
+
+ const createTableMatch = normalized.match(/^create table if not exists [`"]?([a-z0-9_]+)[`"]?/i);
+ if (createTableMatch) {
+ return LEGACY_COMPAT_TABLES.has(createTableMatch[1]) ? 'legacy' : 'forbidden';
+ }
+
+ const alterTableMatch = normalized.match(
+ /^alter table [`"]?([a-z0-9_]+)[`"]? add column [`"]?([a-z0-9_]+)[`"]?/i,
+ );
+ if (alterTableMatch) {
+ const [, tableName, columnName] = alterTableMatch;
+ return LEGACY_COMPAT_COLUMNS.has(`${tableName}.${columnName}`) ? 'legacy' : 'forbidden';
+ }
+
+ const createIndexMatch = normalized.match(
+ /^create (?:unique )?index(?: if not exists)? [`"]?([a-z0-9_]+)[`"]?/i,
+ );
+ if (createIndexMatch) {
+ return LEGACY_COMPAT_INDEXES.has(createIndexMatch[1]) ? 'legacy' : 'forbidden';
+ }
+
+ return 'forbidden';
+}
+
+function assertLegacyCompatMutation(sqlText: string): void {
+ if (classifyLegacyCompatMutation(sqlText) === 'forbidden') {
+ throw new Error(`Forbidden legacy schema mutation: ${sqlText}`);
+ }
+}
+
+export async function executeLegacyCompat(
+ execute: (sqlText: string) => Promise,
+ sqlText: string,
+): Promise {
+ assertLegacyCompatMutation(sqlText);
+ await execute(sqlText);
+}
+
+export function executeLegacyCompatSync(
+ execute: (sqlText: string) => void,
+ sqlText: string,
+): void {
+ assertLegacyCompatMutation(sqlText);
+ execute(sqlText);
+}
+
+function wrapLegacyCompatInspector(inspector: LegacySchemaCompatInspector): LegacySchemaCompatInspector {
+ return {
+ ...inspector,
+ execute: async (sqlText: string) => {
+ await executeLegacyCompat((statement) => inspector.execute(statement), sqlText);
+ },
+ };
+}
+
+export async function ensureLegacySchemaCompatibility(inspector: LegacySchemaCompatInspector): Promise {
+ const wrappedInspector = wrapLegacyCompatInspector(inspector);
+ await ensureSiteSchemaCompatibility(wrappedInspector);
+ await ensureRouteGroupingSchemaCompatibility(wrappedInspector);
+ await ensureProxyFileSchemaCompatibility(wrappedInspector);
+ await ensureAccountTokenSchemaCompatibility(wrappedInspector);
+ await ensureSharedIndexSchemaCompatibility(wrappedInspector);
+}
diff --git a/src/server/db/migrate.test.ts b/src/server/db/migrate.test.ts
index 419257ad..08695c5a 100644
--- a/src/server/db/migrate.test.ts
+++ b/src/server/db/migrate.test.ts
@@ -1,6 +1,7 @@
import Database from 'better-sqlite3';
import { afterEach, describe, expect, it, vi } from 'vitest';
-import { mkdtempSync, readFileSync } from 'node:fs';
+import { createHash } from 'node:crypto';
+import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -29,6 +30,26 @@ function applyMigrationSql(sqlite: Database.Database, sqlText: string) {
}
}
+function recordAppliedMigrations(
+ sqlite: Database.Database,
+ journalEntries: MigrationJournalEntry[],
+) {
+ sqlite.exec(`
+ CREATE TABLE IF NOT EXISTS "__drizzle_migrations" (
+ id SERIAL PRIMARY KEY,
+ hash text NOT NULL,
+ created_at numeric
+ )
+ `);
+
+ const insert = sqlite.prepare('INSERT INTO "__drizzle_migrations" ("hash", "created_at") VALUES (?, ?)');
+ for (const entry of journalEntries) {
+ const sqlText = readFileSync(join(migrationsDir, `${entry.tag}.sql`), 'utf8');
+ const hash = createHash('sha256').update(sqlText).digest('hex');
+ insert.run(hash, entry.when);
+ }
+}
+
describe('sqlite migrate bootstrap', () => {
afterEach(() => {
delete process.env.DATA_DIR;
@@ -67,4 +88,380 @@ describe('sqlite migrate bootstrap', () => {
verified.close();
});
+
+ it('recovers from duplicate-column errors for single-statement migrations', async () => {
+ const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-recover-'));
+ process.env.DATA_DIR = dataDir;
+ vi.resetModules();
+
+ const migrateModule = await import('./migrate.js');
+ const { __migrateTestUtils } = migrateModule;
+
+ const sqlite = new Database(':memory:');
+ sqlite.exec(`
+ CREATE TABLE account_tokens (
+ id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ token_group text
+ );
+ `);
+
+ const tempMigrationsDir = mkdtempSync(join(tmpdir(), 'metapi-migration-files-'));
+ mkdirSync(join(tempMigrationsDir, 'meta'), { recursive: true });
+
+ writeFileSync(
+ join(tempMigrationsDir, 'meta', '_journal.json'),
+ JSON.stringify({
+ entries: [
+ {
+ tag: '0007_account_token_group',
+ when: 1772500000000,
+ },
+ ],
+ }),
+ );
+
+ writeFileSync(
+ join(tempMigrationsDir, '0007_account_token_group.sql'),
+ 'ALTER TABLE `account_tokens` ADD `token_group` text;\n',
+ );
+
+ const duplicateColumnError = new Error(
+ "DrizzleError: Failed to run the query 'ALTER TABLE `account_tokens` ADD `token_group` text;\n' duplicate column name: token_group",
+ );
+
+ const recovered = __migrateTestUtils.tryRecoverDuplicateColumnMigrationError(
+ sqlite,
+ tempMigrationsDir,
+ duplicateColumnError,
+ );
+
+ expect(recovered).toBe(true);
+
+ const applied = sqlite
+ .prepare('SELECT hash, created_at FROM __drizzle_migrations')
+ .all() as Array<{ hash: string; created_at: number }>;
+
+ expect(applied).toHaveLength(1);
+ expect(Number(applied[0].created_at)).toBe(1772500000000);
+
+ sqlite.close();
+ });
+
+ it('recovers when duplicate-column message appears only in error cause', async () => {
+ const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-recover-cause-'));
+ process.env.DATA_DIR = dataDir;
+ vi.resetModules();
+
+ const migrateModule = await import('./migrate.js');
+ const { __migrateTestUtils } = migrateModule;
+
+ const sqlite = new Database(':memory:');
+ sqlite.exec(`
+ CREATE TABLE account_tokens (
+ id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ token_group text
+ );
+ `);
+
+ const tempMigrationsDir = mkdtempSync(join(tmpdir(), 'metapi-migration-files-cause-'));
+ mkdirSync(join(tempMigrationsDir, 'meta'), { recursive: true });
+
+ writeFileSync(
+ join(tempMigrationsDir, 'meta', '_journal.json'),
+ JSON.stringify({
+ entries: [
+ {
+ tag: '0007_account_token_group',
+ when: 1772500000001,
+ },
+ ],
+ }),
+ );
+
+ writeFileSync(
+ join(tempMigrationsDir, '0007_account_token_group.sql'),
+ 'ALTER TABLE `account_tokens` ADD `token_group` text;\n',
+ );
+
+ const drizzleLikeError = {
+ message: "DrizzleError: Failed to run the query 'ALTER TABLE `account_tokens` ADD `token_group` text;\n'",
+ cause: {
+ message: 'SqliteError: duplicate column name: token_group',
+ },
+ };
+
+ const recovered = __migrateTestUtils.tryRecoverDuplicateColumnMigrationError(
+ sqlite,
+ tempMigrationsDir,
+ drizzleLikeError,
+ );
+
+ expect(recovered).toBe(true);
+
+ const applied = sqlite
+ .prepare('SELECT hash, created_at FROM __drizzle_migrations')
+ .all() as Array<{ hash: string; created_at: number }>;
+
+ expect(applied).toHaveLength(1);
+ expect(Number(applied[0].created_at)).toBe(1772500000001);
+
+ sqlite.close();
+ });
+
+ it('recovers duplicate-column errors when the failed SQL contains quoted literals', async () => {
+ const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-recover-quoted-'));
+ process.env.DATA_DIR = dataDir;
+ vi.resetModules();
+
+ const migrateModule = await import('./migrate.js');
+ const { __migrateTestUtils } = migrateModule;
+
+ const sqlite = new Database(':memory:');
+ sqlite.exec(`
+ CREATE TABLE account_tokens (
+ id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ value_status text DEFAULT 'ready' NOT NULL
+ );
+ `);
+
+ const tempMigrationsDir = mkdtempSync(join(tmpdir(), 'metapi-migration-files-quoted-'));
+ mkdirSync(join(tempMigrationsDir, 'meta'), { recursive: true });
+
+ writeFileSync(
+ join(tempMigrationsDir, 'meta', '_journal.json'),
+ JSON.stringify({
+ entries: [
+ {
+ tag: '0012_account_token_value_status',
+ when: 1773665311013,
+ },
+ ],
+ }),
+ );
+
+ writeFileSync(
+ join(tempMigrationsDir, '0012_account_token_value_status.sql'),
+ "ALTER TABLE `account_tokens` ADD `value_status` text DEFAULT 'ready' NOT NULL;\n",
+ );
+
+ const duplicateColumnError = new Error(
+ "DrizzleError: Failed to run the query 'ALTER TABLE `account_tokens` ADD `value_status` text DEFAULT 'ready' NOT NULL;\n' duplicate column name: value_status",
+ );
+
+ const recovered = __migrateTestUtils.tryRecoverDuplicateColumnMigrationError(
+ sqlite,
+ tempMigrationsDir,
+ duplicateColumnError,
+ );
+
+ expect(recovered).toBe(true);
+
+ const applied = sqlite
+ .prepare('SELECT hash, created_at FROM __drizzle_migrations')
+ .all() as Array<{ hash: string; created_at: number }>;
+
+ expect(applied).toHaveLength(1);
+ expect(Number(applied[0].created_at)).toBe(1773665311013);
+
+ sqlite.close();
+ });
+
+ it('recovers duplicate-column errors inside multi-statement migrations by replaying the full migration', async () => {
+ const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-recover-multi-'));
+ process.env.DATA_DIR = dataDir;
+ vi.resetModules();
+
+ const migrateModule = await import('./migrate.js');
+ const { __migrateTestUtils } = migrateModule;
+
+ const sqlite = new Database(':memory:');
+ sqlite.exec(`
+ CREATE TABLE account_tokens (
+ id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ token_group text
+ );
+ `);
+
+ const tempMigrationsDir = mkdtempSync(join(tmpdir(), 'metapi-migration-files-multi-'));
+ mkdirSync(join(tempMigrationsDir, 'meta'), { recursive: true });
+
+ writeFileSync(
+ join(tempMigrationsDir, 'meta', '_journal.json'),
+ JSON.stringify({
+ entries: [
+ {
+ tag: '0009_model_availability_is_manual',
+ when: 1772600000000,
+ },
+ ],
+ }),
+ );
+
+ writeFileSync(
+ join(tempMigrationsDir, '0009_model_availability_is_manual.sql'),
+ [
+ 'CREATE TABLE IF NOT EXISTS `downstream_api_keys` (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL);',
+ 'ALTER TABLE `account_tokens` ADD `token_group` text;',
+ ].join('\n--> statement-breakpoint\n'),
+ );
+
+ const duplicateColumnError = new Error(
+ "DrizzleError: Failed to run the query 'ALTER TABLE `account_tokens` ADD `token_group` text;\n' duplicate column name: token_group",
+ );
+
+ const recovered = __migrateTestUtils.tryRecoverDuplicateColumnMigrationError(
+ sqlite,
+ tempMigrationsDir,
+ duplicateColumnError,
+ );
+
+ expect(recovered).toBe(true);
+
+ const createdTable = sqlite
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'downstream_api_keys'")
+ .get() as { name?: string } | undefined;
+ const applied = sqlite
+ .prepare('SELECT hash, created_at FROM __drizzle_migrations')
+ .all() as Array<{ hash: string; created_at: number }>;
+
+ expect(createdTable?.name).toBe('downstream_api_keys');
+ expect(applied).toHaveLength(1);
+ expect(Number(applied[0].created_at)).toBe(1772600000000);
+
+ sqlite.close();
+ });
+
+ it('replays missing migrations before marking a duplicate-column migration as applied', async () => {
+ const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-partial-journal-'));
+ const dbPath = join(dataDir, 'hub.db');
+ const sqlite = new Database(dbPath);
+ const journalEntries = readMigrationJournalEntries();
+ const missingTags = new Set([
+ '0006_site_disabled_models',
+ '0007_account_token_group',
+ '0008_sqlite_schema_backfill',
+ '0009_model_availability_is_manual',
+ '0010_proxy_logs_downstream_api_key',
+ '0011_downstream_api_key_metadata',
+ '0012_account_token_value_status',
+ '0013_oauth_multi_provider',
+ ]);
+ const appliedEntries = journalEntries.filter((entry) => !missingTags.has(entry.tag));
+
+ for (const entry of appliedEntries) {
+ const sqlText = readFileSync(join(migrationsDir, `${entry.tag}.sql`), 'utf8');
+ applyMigrationSql(sqlite, sqlText);
+ }
+ recordAppliedMigrations(sqlite, appliedEntries);
+
+ // Simulate legacy compatibility code adding token_group before the formal 0007 migration ran.
+ sqlite.exec('ALTER TABLE account_tokens ADD COLUMN token_group text;');
+ sqlite.close();
+
+ process.env.DATA_DIR = dataDir;
+ vi.resetModules();
+
+ await expect(import('./migrate.js')).resolves.toMatchObject({
+ runSqliteMigrations: expect.any(Function),
+ });
+
+ const verified = new Database(dbPath, { readonly: true });
+ const appliedRows = verified
+ .prepare('SELECT created_at FROM __drizzle_migrations ORDER BY created_at ASC')
+ .all() as Array<{ created_at: number }>;
+ const disabledModelsTable = verified
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'site_disabled_models'")
+ .get() as { name?: string } | undefined;
+ const downstreamApiKeysTable = verified
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'downstream_api_keys'")
+ .get() as { name?: string } | undefined;
+
+ expect(disabledModelsTable?.name).toBe('site_disabled_models');
+ expect(downstreamApiKeysTable?.name).toBe('downstream_api_keys');
+ expect(appliedRows.map((row) => Number(row.created_at))).toEqual(
+ journalEntries.map((entry) => entry.when),
+ );
+
+ verified.close();
+ });
+
+ it('deduplicates legacy duplicate sites before applying the oauth site unique index', async () => {
+ const dataDir = mkdtempSync(join(tmpdir(), 'metapi-migrate-duplicate-sites-'));
+ const dbPath = join(dataDir, 'hub.db');
+ const sqlite = new Database(dbPath);
+ const journalEntries = readMigrationJournalEntries();
+ const appliedEntries = journalEntries.filter((entry) => entry.tag !== '0013_oauth_multi_provider');
+
+ for (const entry of appliedEntries) {
+ const sqlText = readFileSync(join(migrationsDir, `${entry.tag}.sql`), 'utf8');
+ applyMigrationSql(sqlite, sqlText);
+ }
+ recordAppliedMigrations(sqlite, appliedEntries);
+
+ sqlite.exec(`
+ INSERT INTO sites (id, name, url, platform, status, is_pinned, sort_order, global_weight)
+ VALUES
+ (101, 'Primary Codex', 'https://chatgpt.com/backend-api/codex', 'codex', 'active', 0, 0, 1),
+ (202, 'Duplicate Codex', 'https://chatgpt.com/backend-api/codex', 'codex', 'disabled', 1, 9, 3);
+
+ INSERT INTO accounts (site_id, username, access_token, status, checkin_enabled)
+ VALUES
+ (101, 'first@example.com', 'token-a', 'active', 0),
+ (202, 'second@example.com', 'token-b', 'disabled', 0);
+
+ INSERT INTO site_disabled_models (site_id, model_name)
+ VALUES
+ (101, 'gpt-5'),
+ (202, 'gpt-5'),
+ (202, 'gpt-5-mini');
+ `);
+
+ sqlite.close();
+
+ process.env.DATA_DIR = dataDir;
+ vi.resetModules();
+
+ await expect(import('./migrate.js')).resolves.toMatchObject({
+ runSqliteMigrations: expect.any(Function),
+ });
+
+ const verified = new Database(dbPath, { readonly: true });
+ const sites = verified
+ .prepare('SELECT id, name, url, platform, status, is_pinned, sort_order, global_weight FROM sites ORDER BY id ASC')
+ .all() as Array<{
+ id: number;
+ name: string;
+ url: string;
+ platform: string;
+ status: string;
+ is_pinned: number;
+ sort_order: number;
+ global_weight: number;
+ }>;
+ const accounts = verified
+ .prepare('SELECT username, site_id FROM accounts ORDER BY username ASC')
+ .all() as Array<{ username: string; site_id: number }>;
+ const disabledModels = verified
+ .prepare('SELECT site_id, model_name FROM site_disabled_models ORDER BY site_id ASC, model_name ASC')
+ .all() as Array<{ site_id: number; model_name: string }>;
+
+ expect(sites).toEqual([
+ expect.objectContaining({
+ id: 101,
+ name: 'Primary Codex',
+ url: 'https://chatgpt.com/backend-api/codex',
+ platform: 'codex',
+ }),
+ ]);
+ expect(accounts).toEqual([
+ { username: 'first@example.com', site_id: 101 },
+ { username: 'second@example.com', site_id: 101 },
+ ]);
+ expect(disabledModels).toEqual([
+ { site_id: 101, model_name: 'gpt-5' },
+ { site_id: 101, model_name: 'gpt-5-mini' },
+ ]);
+
+ verified.close();
+ });
});
diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts
index 1237f4a7..a2758d44 100644
--- a/src/server/db/migrate.ts
+++ b/src/server/db/migrate.ts
@@ -26,7 +26,21 @@ type MigrationRecord = {
hash: string;
};
-const VERIFIED_BOOTSTRAP_TAG = '0004_sorting_preferences';
+type RecoveryMigrationRecord = MigrationRecord & {
+ tag: string;
+};
+
+type RecoveryMigration = RecoveryMigrationRecord & {
+ statements: string[];
+};
+
+type LegacySiteRow = {
+ id: number;
+ platform: string;
+ url: string;
+};
+
+const VERIFIED_BOOTSTRAP_TAG = '0012_account_token_value_status';
const VERIFIED_SCHEMA_MARKERS: SchemaMarker[] = [
{ table: 'sites' },
{ table: 'settings' },
@@ -43,8 +57,22 @@ const VERIFIED_SCHEMA_MARKERS: SchemaMarker[] = [
{ table: 'sites', column: 'sort_order' },
{ table: 'accounts', column: 'is_pinned' },
{ table: 'accounts', column: 'sort_order' },
+ // 0006: site_disabled_models table
+ { table: 'site_disabled_models' },
+ // 0007: token_group column on account_tokens
+ { table: 'account_tokens', column: 'token_group' },
+ // 0009: is_manual column on model_availability
+ { table: 'model_availability', column: 'is_manual' },
+ // 0010: downstream_api_key_id column on proxy_logs
+ { table: 'proxy_logs', column: 'downstream_api_key_id' },
+ // 0011: downstream key metadata columns
+ { table: 'downstream_api_keys', column: 'group_name' },
+ { table: 'downstream_api_keys', column: 'tags' },
+ // 0012: value_status column on account_tokens
+ { table: 'account_tokens', column: 'value_status' },
];
+
function resolveSqliteDbPath(): string {
const raw = (config.dbUrl || '').trim();
if (!raw) return resolve(`${config.dataDir}/hub.db`);
@@ -108,6 +136,422 @@ function readVerifiedMigrationRecords(migrationsFolder: string): MigrationRecord
return [];
}
+function splitMigrationStatements(sqlText: string): string[] {
+ return sqlText
+ .split('--> statement-breakpoint')
+ .map((statement) => statement.trim())
+ .filter((statement) => statement.length > 0);
+}
+
+function normalizeSqlForMatch(sqlText: string): string {
+ return sqlText
+ .replace(/[\n\r\t]+/g, ' ')
+ .replace(/["`]/g, '')
+ .replace(/\s+/g, ' ')
+ .trim()
+ .replace(/;+$/g, '')
+ .toLowerCase();
+}
+
+function extractFailedSqlFromError(error: unknown): string | null {
+ const message = normalizeSchemaErrorMessage(error);
+ const matched = message.match(/Failed to run the query '([\s\S]*?)'/i);
+ const sqlText = matched?.[1]?.trim();
+ return sqlText && sqlText.length > 0 ? sqlText : null;
+}
+
+function findMatchingSingleStatementMigration(
+ migrationsFolder: string,
+ failedSqlText: string,
+): RecoveryMigrationRecord | null {
+ const journalPath = resolve(migrationsFolder, 'meta', '_journal.json');
+ const journal = JSON.parse(readFileSync(journalPath, 'utf8')) as MigrationJournalFile;
+ const normalizedFailedSql = normalizeSqlForMatch(failedSqlText);
+
+ for (const entry of journal.entries ?? []) {
+ const migrationSql = readFileSync(resolve(migrationsFolder, `${entry.tag}.sql`), 'utf8');
+ const statements = splitMigrationStatements(migrationSql);
+ if (statements.length !== 1) {
+ continue;
+ }
+
+ if (normalizeSqlForMatch(statements[0]) !== normalizedFailedSql) {
+ continue;
+ }
+
+ return {
+ tag: entry.tag,
+ createdAt: Number(entry.when),
+ hash: createHash('sha256').update(migrationSql).digest('hex'),
+ };
+ }
+
+ return null;
+}
+
+function findMatchingMigrationByStatement(
+ migrationsFolder: string,
+ failedSqlText: string,
+): RecoveryMigrationRecord | null {
+ const normalizedFailedSql = normalizeSqlForMatch(failedSqlText);
+ const migrations = readRecoveryMigrations(migrationsFolder);
+
+ for (const migration of migrations) {
+ if (!migration.statements.some((statement) => normalizeSqlForMatch(statement) === normalizedFailedSql)) {
+ continue;
+ }
+
+ return {
+ tag: migration.tag,
+ createdAt: migration.createdAt,
+ hash: migration.hash,
+ };
+ }
+
+ return null;
+}
+
+function findMatchingMigrationByErrorMessage(
+ migrationsFolder: string,
+ error: unknown,
+): RecoveryMigrationRecord | null {
+ const normalizedErrorMessage = normalizeSqlForMatch(normalizeSchemaErrorMessage(error));
+ const migrations = readRecoveryMigrations(migrationsFolder);
+
+ for (const migration of migrations) {
+ if (!migration.statements.some((statement) => normalizedErrorMessage.includes(normalizeSqlForMatch(statement)))) {
+ continue;
+ }
+
+ return {
+ tag: migration.tag,
+ createdAt: migration.createdAt,
+ hash: migration.hash,
+ };
+ }
+
+ return null;
+}
+
+function readRecoveryMigrations(migrationsFolder: string): RecoveryMigration[] {
+ const journalPath = resolve(migrationsFolder, 'meta', '_journal.json');
+ const journal = JSON.parse(readFileSync(journalPath, 'utf8')) as MigrationJournalFile;
+
+ return (journal.entries ?? []).map((entry) => {
+ const migrationSql = readFileSync(resolve(migrationsFolder, `${entry.tag}.sql`), 'utf8');
+ return {
+ tag: entry.tag,
+ createdAt: Number(entry.when),
+ hash: createHash('sha256').update(migrationSql).digest('hex'),
+ statements: splitMigrationStatements(migrationSql),
+ };
+ });
+}
+
+function ensureDrizzleMigrationsTable(sqlite: Database.Database): void {
+ sqlite.exec(`
+ CREATE TABLE IF NOT EXISTS "__drizzle_migrations" (
+ id SERIAL PRIMARY KEY,
+ hash text NOT NULL,
+ created_at numeric
+ )
+ `);
+}
+
+function markMigrationRecordIfMissing(sqlite: Database.Database, record: MigrationRecord): boolean {
+ ensureDrizzleMigrationsTable(sqlite);
+ const existing = sqlite
+ .prepare('SELECT 1 FROM "__drizzle_migrations" WHERE "hash" = ? LIMIT 1')
+ .get(record.hash);
+ if (existing) {
+ return false;
+ }
+
+ sqlite
+ .prepare('INSERT INTO "__drizzle_migrations" ("hash", "created_at") VALUES (?, ?)')
+ .run(record.hash, record.createdAt);
+
+ return true;
+}
+
+function hasMigrationRecord(sqlite: Database.Database, record: MigrationRecord): boolean {
+ if (!tableExists(sqlite, '__drizzle_migrations')) return false;
+ const row = sqlite
+ .prepare('SELECT 1 FROM "__drizzle_migrations" WHERE "hash" = ? LIMIT 1')
+ .get(record.hash);
+ return !!row;
+}
+
+function normalizeSchemaErrorMessage(error: unknown): string {
+ if (!error || typeof error !== 'object') {
+ return String(error || '');
+ }
+
+ const collected: string[] = [];
+ let cursor: unknown = error;
+ let depth = 0;
+
+ while (cursor && typeof cursor === 'object' && depth < 8) {
+ const current = cursor as { message?: unknown; cause?: unknown };
+ if (current.message !== undefined && current.message !== null) {
+ const text = String(current.message).trim();
+ if (text.length > 0) {
+ collected.push(text);
+ }
+ }
+
+ cursor = current.cause;
+ depth += 1;
+ }
+
+ if (collected.length > 0) {
+ return collected.join(' | ');
+ }
+
+ return String(error || '');
+}
+
+function isDuplicateColumnError(error: unknown): boolean {
+ const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
+ return lowered.includes('duplicate column')
+ || lowered.includes('already exists')
+ || lowered.includes('duplicate column name');
+}
+
+function isRecoverableSchemaConflictError(error: unknown): boolean {
+ const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
+ return lowered.includes('duplicate column')
+ || lowered.includes('duplicate column name')
+ || lowered.includes('already exists');
+}
+
+function isSitesPlatformUrlUniqueConflictError(error: unknown): boolean {
+ const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
+ if (!lowered.includes('unique constraint failed: sites.platform, sites.url')) {
+ return false;
+ }
+
+ const failedSqlText = extractFailedSqlFromError(error);
+ if (!failedSqlText) {
+ return true;
+ }
+
+ return normalizeSqlForMatch(failedSqlText)
+ === normalizeSqlForMatch('CREATE UNIQUE INDEX `sites_platform_url_unique` ON `sites` (`platform`,`url`);');
+}
+
+function replayMigrationStatements(sqlite: Database.Database, statements: string[]): void {
+ for (const statement of statements) {
+ try {
+ sqlite.exec(statement);
+ } catch (error) {
+ if (isRecoverableSchemaConflictError(error)) {
+ continue;
+ }
+
+ if (isSitesPlatformUrlUniqueConflictError(error) && deduplicateLegacySitesForUniqueIndex(sqlite)) {
+ try {
+ sqlite.exec(statement);
+ continue;
+ } catch (retryError) {
+ if (isRecoverableSchemaConflictError(retryError)) {
+ continue;
+ }
+ throw retryError;
+ }
+ }
+
+ throw error;
+ }
+ }
+}
+
+function recoverMigrationSequence(
+ sqlite: Database.Database,
+ migrationsFolder: string,
+ failedMigrationTag: string,
+): boolean {
+ const migrations = readRecoveryMigrations(migrationsFolder);
+ const failedMigrationIndex = migrations.findIndex((migration) => migration.tag === failedMigrationTag);
+ if (failedMigrationIndex < 0) {
+ return false;
+ }
+
+ for (const migration of migrations.slice(0, failedMigrationIndex + 1)) {
+ if (hasMigrationRecord(sqlite, migration)) {
+ continue;
+ }
+
+ replayMigrationStatements(sqlite, migration.statements);
+ markMigrationRecordIfMissing(sqlite, migration);
+ }
+
+ return true;
+}
+
+function backfillMissingRecordedMigrations(sqlite: Database.Database, migrationsFolder: string): number {
+ if (!tableExists(sqlite, '__drizzle_migrations')) return 0;
+
+ let recoveredCount = 0;
+ for (const migration of readRecoveryMigrations(migrationsFolder)) {
+ if (hasMigrationRecord(sqlite, migration)) {
+ continue;
+ }
+
+ replayMigrationStatements(sqlite, migration.statements);
+ if (markMigrationRecordIfMissing(sqlite, migration)) {
+ recoveredCount += 1;
+ }
+ }
+
+ if (recoveredCount > 0) {
+ console.warn(`[db] Backfilled ${recoveredCount} missing drizzle migration record(s).`);
+ }
+
+ return recoveredCount;
+}
+
+function tryRecoverDuplicateColumnMigrationError(
+ sqlite: Database.Database,
+ migrationsFolder: string,
+ error: unknown,
+): boolean {
+ if (!isDuplicateColumnError(error)) {
+ return false;
+ }
+
+ const failedSqlText = extractFailedSqlFromError(error);
+ const matchedMigration = failedSqlText
+ ? findMatchingMigrationByStatement(migrationsFolder, failedSqlText)
+ ?? findMatchingMigrationByErrorMessage(migrationsFolder, error)
+ : findMatchingMigrationByErrorMessage(migrationsFolder, error);
+ if (!matchedMigration) {
+ return false;
+ }
+
+ const recovered = recoverMigrationSequence(sqlite, migrationsFolder, matchedMigration.tag);
+ if (recovered) {
+ console.warn(`[db] Recovered duplicate-column migration sequence through ${matchedMigration.tag}.`);
+ }
+ return recovered;
+}
+
+function rewriteDownstreamSiteWeightMultipliers(
+ sqlite: Database.Database,
+ siteIdMapping: Map,
+): void {
+ if (siteIdMapping.size <= 0) return;
+ if (!tableExists(sqlite, 'downstream_api_keys')) return;
+ if (!columnExists(sqlite, 'downstream_api_keys', 'site_weight_multipliers')) return;
+
+ const rows = sqlite.prepare(`
+ SELECT id, site_weight_multipliers
+ FROM downstream_api_keys
+ WHERE site_weight_multipliers IS NOT NULL
+ AND TRIM(site_weight_multipliers) <> ''
+ `).all() as Array<{ id: number; site_weight_multipliers: string | null }>;
+
+ const update = sqlite.prepare('UPDATE downstream_api_keys SET site_weight_multipliers = ? WHERE id = ?');
+ for (const row of rows) {
+ if (!row.site_weight_multipliers) continue;
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(row.site_weight_multipliers);
+ } catch {
+ continue;
+ }
+
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue;
+ const nextValue = { ...(parsed as Record) };
+ let changed = false;
+
+ for (const [fromSiteId, toSiteId] of siteIdMapping.entries()) {
+ const fromKey = String(fromSiteId);
+ const toKey = String(toSiteId);
+ if (!(fromKey in nextValue)) continue;
+ if (!(toKey in nextValue)) {
+ nextValue[toKey] = nextValue[fromKey];
+ }
+ delete nextValue[fromKey];
+ changed = true;
+ }
+
+ if (!changed) continue;
+ update.run(JSON.stringify(nextValue), row.id);
+ }
+}
+
+function deduplicateLegacySitesForUniqueIndex(sqlite: Database.Database): boolean {
+ const duplicateGroups = sqlite.prepare(`
+ SELECT platform, url
+ FROM sites
+ GROUP BY platform, url
+ HAVING COUNT(*) > 1
+ `).all() as Array<{ platform: string; url: string }>;
+
+ if (duplicateGroups.length <= 0) {
+ return false;
+ }
+
+ const selectSitesByIdentity = sqlite.prepare(`
+ SELECT id, platform, url
+ FROM sites
+ WHERE platform = ? AND url = ?
+ ORDER BY id ASC
+ `);
+ const rebindAccounts = sqlite.prepare('UPDATE accounts SET site_id = ? WHERE site_id = ?');
+ const mergeDisabledModels = sqlite.prepare(`
+ INSERT OR IGNORE INTO site_disabled_models (site_id, model_name, created_at)
+ SELECT ?, model_name, created_at
+ FROM site_disabled_models
+ WHERE site_id = ?
+ `);
+ const deleteDisabledModels = sqlite.prepare('DELETE FROM site_disabled_models WHERE site_id = ?');
+ const deleteSite = sqlite.prepare('DELETE FROM sites WHERE id = ?');
+
+ const siteIdMapping = new Map();
+
+ const transaction = sqlite.transaction(() => {
+ for (const group of duplicateGroups) {
+ const sites = selectSitesByIdentity.all(group.platform, group.url) as LegacySiteRow[];
+ if (sites.length <= 1) continue;
+
+ const canonicalSiteId = sites[0]!.id;
+ for (const site of sites.slice(1)) {
+ mergeDisabledModels.run(canonicalSiteId, site.id);
+ deleteDisabledModels.run(site.id);
+ rebindAccounts.run(canonicalSiteId, site.id);
+ siteIdMapping.set(site.id, canonicalSiteId);
+ deleteSite.run(site.id);
+ }
+ }
+
+ rewriteDownstreamSiteWeightMultipliers(sqlite, siteIdMapping);
+ });
+
+ transaction();
+ if (siteIdMapping.size > 0) {
+ console.warn(`[db] Deduplicated ${siteIdMapping.size} legacy site entries before applying sites_platform_url_unique.`);
+ }
+ return siteIdMapping.size > 0;
+}
+
+export const __migrateTestUtils = {
+ splitMigrationStatements,
+ normalizeSqlForMatch,
+ extractFailedSqlFromError,
+ findMatchingSingleStatementMigration,
+ findMatchingMigrationByStatement,
+ findMatchingMigrationByErrorMessage,
+ readRecoveryMigrations,
+ markMigrationRecordIfMissing,
+ recoverMigrationSequence,
+ tryRecoverDuplicateColumnMigrationError,
+ isSitesPlatformUrlUniqueConflictError,
+ deduplicateLegacySitesForUniqueIndex,
+};
+
function bootstrapLegacyDrizzleMigrations(sqlite: Database.Database, migrationsFolder: string): boolean {
if (hasRecordedDrizzleMigrations(sqlite)) return false;
if (!hasVerifiedLegacySchema(sqlite)) return false;
@@ -144,8 +588,24 @@ export function runSqliteMigrations(): void {
const sqlite = new Database(dbPath);
bootstrapLegacyDrizzleMigrations(sqlite, migrationsFolder);
- const db = drizzle(sqlite);
- migrate(db, { migrationsFolder });
+ backfillMissingRecordedMigrations(sqlite, migrationsFolder);
+
+ try {
+ migrate(drizzle(sqlite), { migrationsFolder });
+ } catch (error) {
+ const recoveredDuplicateColumns = tryRecoverDuplicateColumnMigrationError(sqlite, migrationsFolder, error);
+ const recoveredDuplicateSites = (
+ !recoveredDuplicateColumns
+ && isSitesPlatformUrlUniqueConflictError(error)
+ && deduplicateLegacySitesForUniqueIndex(sqlite)
+ );
+ if (!recoveredDuplicateColumns && !recoveredDuplicateSites) {
+ sqlite.close();
+ throw error;
+ }
+ migrate(drizzle(sqlite), { migrationsFolder });
+ }
+
sqlite.close();
console.log('Migration complete.');
}
diff --git a/src/server/db/postgresJsonTextParsers.ts b/src/server/db/postgresJsonTextParsers.ts
new file mode 100644
index 00000000..d5eafe7a
--- /dev/null
+++ b/src/server/db/postgresJsonTextParsers.ts
@@ -0,0 +1,23 @@
+import pg from 'pg';
+
+export type PgTypesLike = {
+ builtins: {
+ JSON: number;
+ JSONB: number;
+ };
+ setTypeParser(oid: number, format: 'text', parser: (value: string) => string): void;
+};
+
+let postgresJsonTextParsersInstalled = false;
+
+export function installPostgresJsonTextParsers(typesRegistry: PgTypesLike = pg.types as PgTypesLike): void {
+ if (postgresJsonTextParsersInstalled) return;
+ const identity = (value: string) => value;
+ typesRegistry.setTypeParser(typesRegistry.builtins.JSON, 'text', identity);
+ typesRegistry.setTypeParser(typesRegistry.builtins.JSONB, 'text', identity);
+ postgresJsonTextParsersInstalled = true;
+}
+
+export function resetPostgresJsonTextParsersInstallStateForTests(): void {
+ postgresJsonTextParsersInstalled = false;
+}
diff --git a/src/server/db/proxyFileSchemaCompatibility.test.ts b/src/server/db/proxyFileSchemaCompatibility.test.ts
index 7bbaf685..13b448ba 100644
--- a/src/server/db/proxyFileSchemaCompatibility.test.ts
+++ b/src/server/db/proxyFileSchemaCompatibility.test.ts
@@ -62,6 +62,19 @@ describe('ensureProxyFileSchemaCompatibility', () => {
expect(executedSql.some((sqlText) => ownerIndexPattern.test(sqlText))).toBe(true);
});
+ it('emits MySQL indexes compatible with generated bootstrap SQL', async () => {
+ const { inspector, executedSql } = createInspector('mysql');
+
+ await ensureProxyFileSchemaCompatibility(inspector);
+
+ expect(executedSql).toContain(
+ 'CREATE UNIQUE INDEX `proxy_files_public_id_unique` ON `proxy_files` (`public_id`(191))',
+ );
+ expect(executedSql).toContain(
+ 'CREATE INDEX `proxy_files_owner_lookup_idx` ON `proxy_files` (`owner_type`(64), `owner_id`(191), `deleted_at`(191))',
+ );
+ });
+
it('adds missing columns on existing table before ensuring indexes', async () => {
const { inspector, executedSql } = createInspector('postgres', {
hasTable: true,
diff --git a/src/server/db/proxyFileSchemaCompatibility.ts b/src/server/db/proxyFileSchemaCompatibility.ts
index 52c0a0b1..9a8e6de6 100644
--- a/src/server/db/proxyFileSchemaCompatibility.ts
+++ b/src/server/db/proxyFileSchemaCompatibility.ts
@@ -8,42 +8,57 @@ export interface ProxyFileSchemaInspector {
}
type ProxyFileColumnCompatibilitySpec = {
+ table: 'proxy_files';
column: string;
addSql: Record;
};
-const CREATE_TABLE_SQL: Record = {
- sqlite: 'CREATE TABLE IF NOT EXISTS "proxy_files" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "public_id" TEXT NOT NULL, "owner_type" TEXT NOT NULL, "owner_id" TEXT NOT NULL, "filename" TEXT NOT NULL, "mime_type" TEXT NOT NULL, "purpose" TEXT, "byte_size" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "content_base64" TEXT NOT NULL, "created_at" TEXT, "updated_at" TEXT, "deleted_at" TEXT)',
- postgres: 'CREATE TABLE IF NOT EXISTS "proxy_files" ("id" SERIAL PRIMARY KEY, "public_id" TEXT NOT NULL, "owner_type" TEXT NOT NULL, "owner_id" TEXT NOT NULL, "filename" TEXT NOT NULL, "mime_type" TEXT NOT NULL, "purpose" TEXT, "byte_size" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "content_base64" TEXT NOT NULL, "created_at" TEXT, "updated_at" TEXT, "deleted_at" TEXT)',
- mysql: 'CREATE TABLE IF NOT EXISTS `proxy_files` (`id` INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, `public_id` TEXT NOT NULL, `owner_type` TEXT NOT NULL, `owner_id` TEXT NOT NULL, `filename` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `purpose` TEXT NULL, `byte_size` INTEGER NOT NULL, `sha256` TEXT NOT NULL, `content_base64` LONGTEXT NOT NULL, `created_at` TEXT NULL, `updated_at` TEXT NULL, `deleted_at` TEXT NULL)',
+export type ProxyFileTableCompatibilitySpec = {
+ table: 'proxy_files';
+ createSql: Record;
};
-const COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
+export const PROXY_FILE_TABLE_COMPATIBILITY_SPECS: ProxyFileTableCompatibilitySpec[] = [
{
+ table: 'proxy_files',
+ createSql: {
+ sqlite: 'CREATE TABLE IF NOT EXISTS "proxy_files" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "public_id" TEXT NOT NULL, "owner_type" TEXT NOT NULL, "owner_id" TEXT NOT NULL, "filename" TEXT NOT NULL, "mime_type" TEXT NOT NULL, "purpose" TEXT, "byte_size" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "content_base64" TEXT NOT NULL, "created_at" TEXT, "updated_at" TEXT, "deleted_at" TEXT)',
+ postgres: 'CREATE TABLE IF NOT EXISTS "proxy_files" ("id" SERIAL PRIMARY KEY, "public_id" TEXT NOT NULL, "owner_type" TEXT NOT NULL, "owner_id" TEXT NOT NULL, "filename" TEXT NOT NULL, "mime_type" TEXT NOT NULL, "purpose" TEXT, "byte_size" INTEGER NOT NULL, "sha256" TEXT NOT NULL, "content_base64" TEXT NOT NULL, "created_at" TEXT, "updated_at" TEXT, "deleted_at" TEXT)',
+ mysql: 'CREATE TABLE IF NOT EXISTS `proxy_files` (`id` INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, `public_id` VARCHAR(191) NOT NULL, `owner_type` VARCHAR(64) NOT NULL, `owner_id` VARCHAR(191) NOT NULL, `filename` TEXT NOT NULL, `mime_type` VARCHAR(191) NOT NULL, `purpose` TEXT NULL, `byte_size` INTEGER NOT NULL, `sha256` VARCHAR(191) NOT NULL, `content_base64` LONGTEXT NOT NULL, `created_at` TEXT NULL, `updated_at` TEXT NULL, `deleted_at` TEXT NULL)',
+ },
+ },
+];
+
+export const PROXY_FILE_COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
+ {
+ table: 'proxy_files',
column: 'public_id',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "public_id" TEXT',
postgres: 'ALTER TABLE "proxy_files" ADD COLUMN "public_id" TEXT',
- mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `public_id` TEXT NULL',
+ mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `public_id` VARCHAR(191) NULL',
},
},
{
+ table: 'proxy_files',
column: 'owner_type',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "owner_type" TEXT',
postgres: 'ALTER TABLE "proxy_files" ADD COLUMN "owner_type" TEXT',
- mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `owner_type` TEXT NULL',
+ mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `owner_type` VARCHAR(64) NULL',
},
},
{
+ table: 'proxy_files',
column: 'owner_id',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "owner_id" TEXT',
postgres: 'ALTER TABLE "proxy_files" ADD COLUMN "owner_id" TEXT',
- mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `owner_id` TEXT NULL',
+ mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `owner_id` VARCHAR(191) NULL',
},
},
{
+ table: 'proxy_files',
column: 'filename',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "filename" TEXT',
@@ -52,14 +67,16 @@ const COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
},
},
{
+ table: 'proxy_files',
column: 'mime_type',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "mime_type" TEXT',
postgres: 'ALTER TABLE "proxy_files" ADD COLUMN "mime_type" TEXT',
- mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `mime_type` TEXT NULL',
+ mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `mime_type` VARCHAR(191) NULL',
},
},
{
+ table: 'proxy_files',
column: 'purpose',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "purpose" TEXT',
@@ -68,6 +85,7 @@ const COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
},
},
{
+ table: 'proxy_files',
column: 'byte_size',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "byte_size" INTEGER',
@@ -76,14 +94,16 @@ const COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
},
},
{
+ table: 'proxy_files',
column: 'sha256',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "sha256" TEXT',
postgres: 'ALTER TABLE "proxy_files" ADD COLUMN "sha256" TEXT',
- mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `sha256` TEXT NULL',
+ mysql: 'ALTER TABLE `proxy_files` ADD COLUMN `sha256` VARCHAR(191) NULL',
},
},
{
+ table: 'proxy_files',
column: 'content_base64',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "content_base64" TEXT',
@@ -92,6 +112,7 @@ const COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
},
},
{
+ table: 'proxy_files',
column: 'created_at',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "created_at" TEXT',
@@ -100,6 +121,7 @@ const COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
},
},
{
+ table: 'proxy_files',
column: 'updated_at',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "updated_at" TEXT',
@@ -108,6 +130,7 @@ const COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
},
},
{
+ table: 'proxy_files',
column: 'deleted_at',
addSql: {
sqlite: 'ALTER TABLE "proxy_files" ADD COLUMN "deleted_at" TEXT',
@@ -117,21 +140,33 @@ const COLUMN_COMPATIBILITY_SPECS: ProxyFileColumnCompatibilitySpec[] = [
},
];
-const CREATE_INDEX_SQL: Record = {
- sqlite: [
- 'CREATE UNIQUE INDEX IF NOT EXISTS "proxy_files_public_id_unique" ON "proxy_files" ("public_id")',
- 'CREATE INDEX IF NOT EXISTS "proxy_files_owner_lookup_idx" ON "proxy_files" ("owner_type", "owner_id", "deleted_at")',
- ],
- postgres: [
- 'CREATE UNIQUE INDEX IF NOT EXISTS "proxy_files_public_id_unique" ON "proxy_files" ("public_id")',
- 'CREATE INDEX IF NOT EXISTS "proxy_files_owner_lookup_idx" ON "proxy_files" ("owner_type", "owner_id", "deleted_at")',
- ],
- mysql: [
- 'CREATE UNIQUE INDEX `proxy_files_public_id_unique` ON `proxy_files` (`public_id`(191))',
- 'CREATE INDEX `proxy_files_owner_lookup_idx` ON `proxy_files` (`owner_type`(191), `owner_id`(191), `deleted_at`(191))',
- ],
+export type ProxyFileIndexCompatibilitySpec = {
+ table: 'proxy_files';
+ indexName: string;
+ createSql: Record;
};
+export const PROXY_FILE_INDEX_COMPATIBILITY_SPECS: ProxyFileIndexCompatibilitySpec[] = [
+ {
+ table: 'proxy_files',
+ indexName: 'proxy_files_public_id_unique',
+ createSql: {
+ sqlite: 'CREATE UNIQUE INDEX IF NOT EXISTS "proxy_files_public_id_unique" ON "proxy_files" ("public_id")',
+ postgres: 'CREATE UNIQUE INDEX IF NOT EXISTS "proxy_files_public_id_unique" ON "proxy_files" ("public_id")',
+ mysql: 'CREATE UNIQUE INDEX `proxy_files_public_id_unique` ON `proxy_files` (`public_id`(191))',
+ },
+ },
+ {
+ table: 'proxy_files',
+ indexName: 'proxy_files_owner_lookup_idx',
+ createSql: {
+ sqlite: 'CREATE INDEX IF NOT EXISTS "proxy_files_owner_lookup_idx" ON "proxy_files" ("owner_type", "owner_id", "deleted_at")',
+ postgres: 'CREATE INDEX IF NOT EXISTS "proxy_files_owner_lookup_idx" ON "proxy_files" ("owner_type", "owner_id", "deleted_at")',
+ mysql: 'CREATE INDEX `proxy_files_owner_lookup_idx` ON `proxy_files` (`owner_type`(64), `owner_id`(191), `deleted_at`(191))',
+ },
+ },
+];
+
function normalizeSchemaErrorMessage(error: unknown): string {
if (typeof error === 'object' && error && 'message' in error) {
return String((error as { message?: unknown }).message || '');
@@ -155,16 +190,17 @@ async function executeIgnoreDuplicate(inspector: ProxyFileSchemaInspector, sqlTe
}
export async function ensureProxyFileSchemaCompatibility(inspector: ProxyFileSchemaInspector): Promise {
- if (!await inspector.tableExists('proxy_files')) {
- await executeIgnoreDuplicate(inspector, CREATE_TABLE_SQL[inspector.dialect]);
+ const [tableSpec] = PROXY_FILE_TABLE_COMPATIBILITY_SPECS;
+ if (!await inspector.tableExists(tableSpec.table)) {
+ await executeIgnoreDuplicate(inspector, tableSpec.createSql[inspector.dialect]);
} else {
- for (const spec of COLUMN_COMPATIBILITY_SPECS) {
- if (await inspector.columnExists('proxy_files', spec.column)) continue;
+ for (const spec of PROXY_FILE_COLUMN_COMPATIBILITY_SPECS) {
+ if (await inspector.columnExists(spec.table, spec.column)) continue;
await executeIgnoreDuplicate(inspector, spec.addSql[inspector.dialect]);
}
}
- for (const sqlText of CREATE_INDEX_SQL[inspector.dialect]) {
- await executeIgnoreDuplicate(inspector, sqlText);
+ for (const spec of PROXY_FILE_INDEX_COMPATIBILITY_SPECS) {
+ await executeIgnoreDuplicate(inspector, spec.createSql[inspector.dialect]);
}
}
diff --git a/src/server/db/routeGroupingSchemaCompatibility.test.ts b/src/server/db/routeGroupingSchemaCompatibility.test.ts
index 3c9560e0..bf500b9f 100644
--- a/src/server/db/routeGroupingSchemaCompatibility.test.ts
+++ b/src/server/db/routeGroupingSchemaCompatibility.test.ts
@@ -8,9 +8,11 @@ function createInspector(
dialect: RouteGroupingSchemaInspector['dialect'],
options?: {
existingColumnsByTable?: Partial>;
+ existingTables?: Array<'token_routes' | 'route_channels' | 'route_group_sources'>;
},
) {
const executedSql: string[] = [];
+ const existingTables = new Set(options?.existingTables ?? ['token_routes', 'route_channels']);
const existingColumnsByTable = {
token_routes: new Set(options?.existingColumnsByTable?.token_routes ?? []),
route_channels: new Set(options?.existingColumnsByTable?.route_channels ?? []),
@@ -19,7 +21,7 @@ function createInspector(
const inspector: RouteGroupingSchemaInspector = {
dialect,
async tableExists(table) {
- return table === 'token_routes' || table === 'route_channels';
+ return existingTables.has(table as 'token_routes' | 'route_channels' | 'route_group_sources');
},
async columnExists(table, column) {
if (table === 'token_routes' || table === 'route_channels') {
@@ -37,24 +39,58 @@ function createInspector(
describe('ensureRouteGroupingSchemaCompatibility', () => {
it.each([
+ {
+ dialect: 'sqlite' as const,
+ expectedSql: [
+ 'CREATE TABLE IF NOT EXISTS route_group_sources (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, group_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade, source_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade);',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS route_group_sources_group_source_unique ON route_group_sources(group_route_id, source_route_id);',
+ 'CREATE INDEX IF NOT EXISTS route_group_sources_source_route_id_idx ON route_group_sources(source_route_id);',
+ 'ALTER TABLE token_routes ADD COLUMN display_name text;',
+ 'ALTER TABLE token_routes ADD COLUMN display_icon text;',
+ 'ALTER TABLE token_routes ADD COLUMN route_mode text DEFAULT \'pattern\';',
+ 'ALTER TABLE token_routes ADD COLUMN decision_snapshot text;',
+ 'ALTER TABLE token_routes ADD COLUMN decision_refreshed_at text;',
+ 'ALTER TABLE token_routes ADD COLUMN routing_strategy text DEFAULT \'weighted\';',
+ 'ALTER TABLE route_channels ADD COLUMN source_model text;',
+ 'ALTER TABLE route_channels ADD COLUMN last_selected_at text;',
+ 'ALTER TABLE route_channels ADD COLUMN consecutive_fail_count integer NOT NULL DEFAULT 0;',
+ 'ALTER TABLE route_channels ADD COLUMN cooldown_level integer NOT NULL DEFAULT 0;',
+ ],
+ },
{
dialect: 'postgres' as const,
expectedSql: [
+ 'CREATE TABLE IF NOT EXISTS "route_group_sources" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "group_route_id" INTEGER NOT NULL, "source_route_id" INTEGER NOT NULL, CONSTRAINT "route_group_sources_group_route_id_token_routes_id_fk" FOREIGN KEY ("group_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, CONSTRAINT "route_group_sources_source_route_id_token_routes_id_fk" FOREIGN KEY ("source_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE)',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "route_group_sources_group_source_unique" ON "route_group_sources" ("group_route_id", "source_route_id")',
+ 'CREATE INDEX IF NOT EXISTS "route_group_sources_source_route_id_idx" ON "route_group_sources" ("source_route_id")',
'ALTER TABLE "token_routes" ADD COLUMN "display_name" TEXT',
'ALTER TABLE "token_routes" ADD COLUMN "display_icon" TEXT',
+ 'ALTER TABLE "token_routes" ADD COLUMN "route_mode" TEXT DEFAULT \'pattern\'',
'ALTER TABLE "token_routes" ADD COLUMN "decision_snapshot" TEXT',
'ALTER TABLE "token_routes" ADD COLUMN "decision_refreshed_at" TEXT',
+ 'ALTER TABLE "token_routes" ADD COLUMN "routing_strategy" TEXT DEFAULT \'weighted\'',
'ALTER TABLE "route_channels" ADD COLUMN "source_model" TEXT',
+ 'ALTER TABLE "route_channels" ADD COLUMN "last_selected_at" TEXT',
+ 'ALTER TABLE "route_channels" ADD COLUMN "consecutive_fail_count" INTEGER NOT NULL DEFAULT 0',
+ 'ALTER TABLE "route_channels" ADD COLUMN "cooldown_level" INTEGER NOT NULL DEFAULT 0',
],
},
{
dialect: 'mysql' as const,
expectedSql: [
+ 'CREATE TABLE IF NOT EXISTS `route_group_sources` (`id` int AUTO_INCREMENT NOT NULL PRIMARY KEY, `group_route_id` int NOT NULL, `source_route_id` int NOT NULL, CONSTRAINT `route_group_sources_group_route_id_token_routes_id_fk` FOREIGN KEY (`group_route_id`) REFERENCES `token_routes`(`id`) ON DELETE cascade, CONSTRAINT `route_group_sources_source_route_id_token_routes_id_fk` FOREIGN KEY (`source_route_id`) REFERENCES `token_routes`(`id`) ON DELETE cascade)',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS `route_group_sources_group_source_unique` ON `route_group_sources` (`group_route_id`,`source_route_id`)',
+ 'CREATE INDEX IF NOT EXISTS `route_group_sources_source_route_id_idx` ON `route_group_sources` (`source_route_id`)',
'ALTER TABLE `token_routes` ADD COLUMN `display_name` TEXT NULL',
'ALTER TABLE `token_routes` ADD COLUMN `display_icon` TEXT NULL',
+ 'ALTER TABLE `token_routes` ADD COLUMN `route_mode` VARCHAR(32) NULL DEFAULT \'pattern\'',
'ALTER TABLE `token_routes` ADD COLUMN `decision_snapshot` TEXT NULL',
'ALTER TABLE `token_routes` ADD COLUMN `decision_refreshed_at` TEXT NULL',
+ 'ALTER TABLE `token_routes` ADD COLUMN `routing_strategy` VARCHAR(32) NULL DEFAULT \'weighted\'',
'ALTER TABLE `route_channels` ADD COLUMN `source_model` TEXT NULL',
+ 'ALTER TABLE `route_channels` ADD COLUMN `last_selected_at` TEXT NULL',
+ 'ALTER TABLE `route_channels` ADD COLUMN `consecutive_fail_count` INT NOT NULL DEFAULT 0',
+ 'ALTER TABLE `route_channels` ADD COLUMN `cooldown_level` INT NOT NULL DEFAULT 0',
],
},
])('adds missing route grouping columns for $dialect', async ({ dialect, expectedSql }) => {
@@ -68,9 +104,10 @@ describe('ensureRouteGroupingSchemaCompatibility', () => {
it('skips existing columns', async () => {
const { inspector, executedSql } = createInspector('postgres', {
existingColumnsByTable: {
- token_routes: ['display_name', 'display_icon', 'decision_snapshot', 'decision_refreshed_at'],
- route_channels: ['source_model'],
+ token_routes: ['display_name', 'display_icon', 'route_mode', 'decision_snapshot', 'decision_refreshed_at', 'routing_strategy'],
+ route_channels: ['source_model', 'last_selected_at', 'consecutive_fail_count', 'cooldown_level'],
},
+ existingTables: ['token_routes', 'route_channels', 'route_group_sources'],
});
await ensureRouteGroupingSchemaCompatibility(inspector);
diff --git a/src/server/db/routeGroupingSchemaCompatibility.ts b/src/server/db/routeGroupingSchemaCompatibility.ts
index 15e32301..4dacfb3d 100644
--- a/src/server/db/routeGroupingSchemaCompatibility.ts
+++ b/src/server/db/routeGroupingSchemaCompatibility.ts
@@ -7,13 +7,18 @@ export interface RouteGroupingSchemaInspector {
execute(sqlText: string): Promise;
}
-type RouteGroupingColumnCompatibilitySpec = {
+export type RouteGroupingColumnCompatibilitySpec = {
table: 'token_routes' | 'route_channels';
column: string;
addSql: Record;
};
-const ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS: RouteGroupingColumnCompatibilitySpec[] = [
+export type RouteGroupingTableCompatibilitySpec = {
+ table: 'route_group_sources';
+ createSql: Record;
+};
+
+export const ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS: RouteGroupingColumnCompatibilitySpec[] = [
{
table: 'token_routes',
column: 'display_name',
@@ -32,6 +37,15 @@ const ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS: RouteGroupingColumnCompatibilit
postgres: 'ALTER TABLE "token_routes" ADD COLUMN "display_icon" TEXT',
},
},
+ {
+ table: 'token_routes',
+ column: 'route_mode',
+ addSql: {
+ sqlite: 'ALTER TABLE token_routes ADD COLUMN route_mode text DEFAULT \'pattern\';',
+ mysql: 'ALTER TABLE `token_routes` ADD COLUMN `route_mode` VARCHAR(32) NULL DEFAULT \'pattern\'',
+ postgres: 'ALTER TABLE "token_routes" ADD COLUMN "route_mode" TEXT DEFAULT \'pattern\'',
+ },
+ },
{
table: 'token_routes',
column: 'decision_snapshot',
@@ -50,6 +64,15 @@ const ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS: RouteGroupingColumnCompatibilit
postgres: 'ALTER TABLE "token_routes" ADD COLUMN "decision_refreshed_at" TEXT',
},
},
+ {
+ table: 'token_routes',
+ column: 'routing_strategy',
+ addSql: {
+ sqlite: 'ALTER TABLE token_routes ADD COLUMN routing_strategy text DEFAULT \'weighted\';',
+ mysql: 'ALTER TABLE `token_routes` ADD COLUMN `routing_strategy` VARCHAR(32) NULL DEFAULT \'weighted\'',
+ postgres: 'ALTER TABLE "token_routes" ADD COLUMN "routing_strategy" TEXT DEFAULT \'weighted\'',
+ },
+ },
{
table: 'route_channels',
column: 'source_model',
@@ -59,6 +82,56 @@ const ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS: RouteGroupingColumnCompatibilit
postgres: 'ALTER TABLE "route_channels" ADD COLUMN "source_model" TEXT',
},
},
+ {
+ table: 'route_channels',
+ column: 'last_selected_at',
+ addSql: {
+ sqlite: 'ALTER TABLE route_channels ADD COLUMN last_selected_at text;',
+ mysql: 'ALTER TABLE `route_channels` ADD COLUMN `last_selected_at` TEXT NULL',
+ postgres: 'ALTER TABLE "route_channels" ADD COLUMN "last_selected_at" TEXT',
+ },
+ },
+ {
+ table: 'route_channels',
+ column: 'consecutive_fail_count',
+ addSql: {
+ sqlite: 'ALTER TABLE route_channels ADD COLUMN consecutive_fail_count integer NOT NULL DEFAULT 0;',
+ mysql: 'ALTER TABLE `route_channels` ADD COLUMN `consecutive_fail_count` INT NOT NULL DEFAULT 0',
+ postgres: 'ALTER TABLE "route_channels" ADD COLUMN "consecutive_fail_count" INTEGER NOT NULL DEFAULT 0',
+ },
+ },
+ {
+ table: 'route_channels',
+ column: 'cooldown_level',
+ addSql: {
+ sqlite: 'ALTER TABLE route_channels ADD COLUMN cooldown_level integer NOT NULL DEFAULT 0;',
+ mysql: 'ALTER TABLE `route_channels` ADD COLUMN `cooldown_level` INT NOT NULL DEFAULT 0',
+ postgres: 'ALTER TABLE "route_channels" ADD COLUMN "cooldown_level" INTEGER NOT NULL DEFAULT 0',
+ },
+ },
+];
+
+export const ROUTE_GROUPING_TABLE_COMPATIBILITY_SPECS: RouteGroupingTableCompatibilitySpec[] = [
+ {
+ table: 'route_group_sources',
+ createSql: {
+ sqlite: [
+ 'CREATE TABLE IF NOT EXISTS route_group_sources (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, group_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade, source_route_id integer NOT NULL REFERENCES token_routes(id) ON DELETE cascade);',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS route_group_sources_group_source_unique ON route_group_sources(group_route_id, source_route_id);',
+ 'CREATE INDEX IF NOT EXISTS route_group_sources_source_route_id_idx ON route_group_sources(source_route_id);',
+ ],
+ mysql: [
+ 'CREATE TABLE IF NOT EXISTS `route_group_sources` (`id` int AUTO_INCREMENT NOT NULL PRIMARY KEY, `group_route_id` int NOT NULL, `source_route_id` int NOT NULL, CONSTRAINT `route_group_sources_group_route_id_token_routes_id_fk` FOREIGN KEY (`group_route_id`) REFERENCES `token_routes`(`id`) ON DELETE cascade, CONSTRAINT `route_group_sources_source_route_id_token_routes_id_fk` FOREIGN KEY (`source_route_id`) REFERENCES `token_routes`(`id`) ON DELETE cascade)',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS `route_group_sources_group_source_unique` ON `route_group_sources` (`group_route_id`,`source_route_id`)',
+ 'CREATE INDEX IF NOT EXISTS `route_group_sources_source_route_id_idx` ON `route_group_sources` (`source_route_id`)',
+ ],
+ postgres: [
+ 'CREATE TABLE IF NOT EXISTS "route_group_sources" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY, "group_route_id" INTEGER NOT NULL, "source_route_id" INTEGER NOT NULL, CONSTRAINT "route_group_sources_group_route_id_token_routes_id_fk" FOREIGN KEY ("group_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE, CONSTRAINT "route_group_sources_source_route_id_token_routes_id_fk" FOREIGN KEY ("source_route_id") REFERENCES "token_routes"("id") ON DELETE CASCADE)',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "route_group_sources_group_source_unique" ON "route_group_sources" ("group_route_id", "source_route_id")',
+ 'CREATE INDEX IF NOT EXISTS "route_group_sources_source_route_id_idx" ON "route_group_sources" ("source_route_id")',
+ ],
+ },
+ },
];
function normalizeSchemaErrorMessage(error: unknown): string {
@@ -88,6 +161,18 @@ async function executeAddColumn(inspector: RouteGroupingSchemaInspector, sqlText
export async function ensureRouteGroupingSchemaCompatibility(inspector: RouteGroupingSchemaInspector): Promise {
const tableExistsCache = new Map();
+ for (const spec of ROUTE_GROUPING_TABLE_COMPATIBILITY_SPECS) {
+ const hasTable = await inspector.tableExists(spec.table);
+ tableExistsCache.set(spec.table, hasTable);
+ if (hasTable) {
+ continue;
+ }
+ for (const sqlText of spec.createSql[inspector.dialect]) {
+ await executeAddColumn(inspector, sqlText);
+ }
+ tableExistsCache.set(spec.table, true);
+ }
+
for (const spec of ROUTE_GROUPING_COLUMN_COMPATIBILITY_SPECS) {
let hasTable = tableExistsCache.get(spec.table);
if (hasTable === undefined) {
diff --git a/src/server/db/runtimeSchemaBootstrap.live.test.ts b/src/server/db/runtimeSchemaBootstrap.live.test.ts
new file mode 100644
index 00000000..59fd5644
--- /dev/null
+++ b/src/server/db/runtimeSchemaBootstrap.live.test.ts
@@ -0,0 +1,111 @@
+import baselineContract from './generated/fixtures/2026-03-14-baseline.schemaContract.json' with { type: 'json' };
+import currentContract from './generated/schemaContract.json' with { type: 'json' };
+import mysql from 'mysql2/promise';
+import pg from 'pg';
+import { describe, expect, it } from 'vitest';
+import { generateBootstrapSql } from './schemaArtifactGenerator.js';
+import { __schemaIntrospectionTestUtils, introspectLiveSchema } from './schemaIntrospection.js';
+import { bootstrapRuntimeDatabaseSchema } from './runtimeSchemaBootstrap.js';
+
+const mysqlRuntime = process.env.DB_PARITY_MYSQL_URL ? it : it.skip;
+const postgresRuntime = process.env.DB_PARITY_POSTGRES_URL ? it : it.skip;
+
+async function resetMySqlSchema(connectionString: string): Promise {
+ const connection = await mysql.createConnection({ uri: connectionString });
+ try {
+ await connection.query('SET FOREIGN_KEY_CHECKS = 0');
+ const [rows] = await connection.query(`
+ SELECT table_name AS table_name
+ FROM information_schema.tables
+ WHERE table_schema = DATABASE()
+ AND table_type = 'BASE TABLE'
+ `);
+ for (const row of rows as Array>) {
+ const tableName = String(row.table_name || '');
+ if (!tableName) continue;
+ await connection.query(`DROP TABLE IF EXISTS \`${tableName}\``);
+ }
+ await connection.query('SET FOREIGN_KEY_CHECKS = 1');
+ } finally {
+ await connection.end();
+ }
+}
+
+async function applyMySqlStatements(connectionString: string, statements: string[]): Promise {
+ const connection = await mysql.createConnection({ uri: connectionString });
+ try {
+ for (const statement of statements) {
+ await connection.query(statement);
+ }
+ } finally {
+ await connection.end();
+ }
+}
+
+async function resetPostgresSchema(connectionString: string): Promise {
+ const client = new pg.Client({ connectionString });
+ await client.connect();
+ try {
+ const result = await client.query(`
+ SELECT tablename
+ FROM pg_tables
+ WHERE schemaname = current_schema()
+ ORDER BY tablename ASC
+ `);
+ for (const row of result.rows as Array<{ tablename: string }>) {
+ await client.query(`DROP TABLE IF EXISTS "${row.tablename}" CASCADE`);
+ }
+ } finally {
+ await client.end();
+ }
+}
+
+async function applyPostgresStatements(connectionString: string, statements: string[]): Promise {
+ const client = new pg.Client({ connectionString });
+ await client.connect();
+ try {
+ for (const statement of statements) {
+ await client.query(statement);
+ }
+ } finally {
+ await client.end();
+ }
+}
+
+describe('runtime schema bootstrap live upgrade path', () => {
+ mysqlRuntime('upgrades mysql runtime schemas from an older live contract', async () => {
+ const connectionString = process.env.DB_PARITY_MYSQL_URL!;
+ const baselineStatements = __schemaIntrospectionTestUtils.splitSqlStatements(
+ generateBootstrapSql('mysql', baselineContract),
+ );
+
+ await resetMySqlSchema(connectionString);
+ await applyMySqlStatements(connectionString, baselineStatements);
+
+ await bootstrapRuntimeDatabaseSchema({
+ dialect: 'mysql',
+ connectionString,
+ });
+
+ const live = await introspectLiveSchema({ dialect: 'mysql', connectionString });
+ expect(live).toEqual(currentContract);
+ });
+
+ postgresRuntime('upgrades postgres runtime schemas from an older live contract', async () => {
+ const connectionString = process.env.DB_PARITY_POSTGRES_URL!;
+ const baselineStatements = __schemaIntrospectionTestUtils.splitSqlStatements(
+ generateBootstrapSql('postgres', baselineContract),
+ );
+
+ await resetPostgresSchema(connectionString);
+ await applyPostgresStatements(connectionString, baselineStatements);
+
+ await bootstrapRuntimeDatabaseSchema({
+ dialect: 'postgres',
+ connectionString,
+ });
+
+ const live = await introspectLiveSchema({ dialect: 'postgres', connectionString });
+ expect(live).toEqual(currentContract);
+ });
+});
diff --git a/src/server/db/runtimeSchemaBootstrap.test.ts b/src/server/db/runtimeSchemaBootstrap.test.ts
new file mode 100644
index 00000000..adcf1173
--- /dev/null
+++ b/src/server/db/runtimeSchemaBootstrap.test.ts
@@ -0,0 +1,304 @@
+import baselineContract from './generated/fixtures/2026-03-14-baseline.schemaContract.json' with { type: 'json' };
+import currentContract from './generated/schemaContract.json' with { type: 'json' };
+import { classifyLegacyCompatMutation } from './legacySchemaCompat.js';
+import { generateUpgradeSql } from './schemaArtifactGenerator.js';
+import type { SchemaContract, SchemaContractColumn } from './schemaContract.js';
+import { describe, expect, it } from 'vitest';
+import {
+ __runtimeSchemaBootstrapTestUtils,
+ ensureRuntimeDatabaseSchema,
+ type RuntimeSchemaClient,
+ type RuntimeSchemaDialect,
+} from './runtimeSchemaBootstrap.js';
+
+function createStubClient(dialect: RuntimeSchemaDialect, executedSql: string[]): RuntimeSchemaClient {
+ return {
+ dialect,
+ begin: async () => {},
+ commit: async () => {},
+ rollback: async () => {},
+ execute: async (sqlText: string) => {
+ if (sqlText.trim().toLowerCase().startsWith('select')) {
+ return [];
+ }
+ executedSql.push(sqlText);
+ return [];
+ },
+ queryScalar: async (sqlText: string, params: unknown[] = []) => {
+ if (sqlText.includes('information_schema') || sqlText.includes('sqlite_master') || sqlText.includes('pragma_table_info')) {
+ return 1;
+ }
+ if (params.length > 0) {
+ return 1;
+ }
+ return 0;
+ },
+ close: async () => {},
+ };
+}
+
+function makeColumn(overrides: Partial = {}): SchemaContractColumn {
+ return {
+ logicalType: 'text',
+ notNull: false,
+ defaultValue: null,
+ primaryKey: false,
+ ...overrides,
+ };
+}
+
+describe('runtime schema bootstrap', () => {
+ it.each(['mysql', 'postgres'] as const)('executes live-schema upgrade statements for %s', async (dialect) => {
+ const executedSql: string[] = [];
+ const expectedUpgradeSql = __runtimeSchemaBootstrapTestUtils.splitSqlStatements(
+ generateUpgradeSql(dialect, currentContract, baselineContract),
+ );
+
+ await ensureRuntimeDatabaseSchema(createStubClient(dialect, executedSql), {
+ currentContract,
+ liveContract: baselineContract,
+ });
+
+ expect(executedSql.slice(0, expectedUpgradeSql.length)).toEqual(expectedUpgradeSql);
+ });
+
+ it('skips external schema execution when live schema already matches the current contract', async () => {
+ const executedSql: string[] = [];
+ const expectedUpgradeSql = __runtimeSchemaBootstrapTestUtils.buildExternalUpgradeStatements(
+ 'mysql',
+ currentContract,
+ currentContract,
+ );
+
+ await ensureRuntimeDatabaseSchema(createStubClient('mysql', executedSql), {
+ currentContract,
+ liveContract: currentContract,
+ });
+
+ expect(expectedUpgradeSql).toEqual([]);
+ expect(executedSql.every((sqlText) => classifyLegacyCompatMutation(sqlText) === 'legacy')).toBe(true);
+ });
+
+ it('tolerates non-additive live-schema drift and still emits additive runtime patch statements', () => {
+ const driftedLiveContract = __runtimeSchemaBootstrapTestUtils.cloneContract(currentContract);
+
+ delete driftedLiveContract.tables.model_availability?.columns.is_manual;
+ if (driftedLiveContract.tables.sites?.columns.status) {
+ driftedLiveContract.tables.sites.columns.status.defaultValue = null;
+ }
+ driftedLiveContract.indexes = driftedLiveContract.indexes.filter((index) => index.name !== 'accounts_site_id_idx');
+ driftedLiveContract.uniques = driftedLiveContract.uniques.filter((unique) => unique.name !== 'proxy_files_public_id_unique');
+
+ const statements = __runtimeSchemaBootstrapTestUtils.buildExternalUpgradeStatements(
+ 'mysql',
+ currentContract,
+ driftedLiveContract,
+ );
+
+ expect(statements.some((sqlText) => sqlText.includes('ALTER TABLE `model_availability` ADD COLUMN `is_manual`'))).toBe(true);
+ expect(statements.some((sqlText) => sqlText.includes('CREATE INDEX `accounts_site_id_idx`'))).toBe(true);
+ expect(statements.some((sqlText) => sqlText.includes('CREATE UNIQUE INDEX `proxy_files_public_id_unique`'))).toBe(true);
+ });
+
+ it('ignores duplicate mysql index and column errors when replaying additive schema statements', async () => {
+ const executedSql: string[] = [];
+ const duplicateColumnSql = __runtimeSchemaBootstrapTestUtils.splitSqlStatements(
+ generateUpgradeSql('mysql', currentContract, baselineContract),
+ ).find((sqlText) => sqlText.includes('ALTER TABLE `model_availability` ADD COLUMN `is_manual`'));
+ const duplicateIndexSql = __runtimeSchemaBootstrapTestUtils.splitSqlStatements(
+ generateUpgradeSql('mysql', currentContract, baselineContract),
+ ).find((sqlText) => sqlText.includes('proxy_files_public_id_unique'));
+
+ expect(duplicateColumnSql).toBeDefined();
+ expect(duplicateIndexSql).toBeDefined();
+
+ await ensureRuntimeDatabaseSchema({
+ ...createStubClient('mysql', executedSql),
+ execute: async (sqlText: string) => {
+ executedSql.push(sqlText);
+ if (sqlText === duplicateColumnSql) {
+ const error = new Error("Duplicate column name 'is_manual'") as Error & { code?: string };
+ error.code = 'ER_DUP_FIELDNAME';
+ throw error;
+ }
+ if (sqlText === duplicateIndexSql) {
+ const error = new Error("Duplicate key name 'model_availability_account_model_unique'") as Error & { code?: string };
+ error.code = 'ER_DUP_KEYNAME';
+ throw error;
+ }
+ return [];
+ },
+ }, {
+ currentContract,
+ liveContract: baselineContract,
+ });
+
+ expect(executedSql).toContain(duplicateColumnSql);
+ expect(executedSql).toContain(duplicateIndexSql);
+ });
+
+ it('ignores postgres relation-already-exists errors when replaying additive schema statements', async () => {
+ const executedSql: string[] = [];
+ const targetSql = __runtimeSchemaBootstrapTestUtils.splitSqlStatements(
+ generateUpgradeSql('postgres', currentContract, baselineContract),
+ ).find((sqlText) => sqlText.includes('proxy_files_public_id_unique'));
+
+ expect(targetSql).toBeDefined();
+
+ await ensureRuntimeDatabaseSchema({
+ ...createStubClient('postgres', executedSql),
+ execute: async (sqlText: string) => {
+ executedSql.push(sqlText);
+ if (sqlText === targetSql) {
+ const error = new Error('relation "model_availability_account_model_unique" already exists') as Error & { code?: string };
+ error.code = '42P07';
+ throw error;
+ }
+ return [];
+ },
+ }, {
+ currentContract,
+ liveContract: baselineContract,
+ });
+
+ expect(executedSql).toContain(targetSql);
+ });
+
+ it('adds mysql text prefixes for new indexes when live datetime-like columns are still stored as text', async () => {
+ const executedSql: string[] = [];
+ const minimalContract: SchemaContract = {
+ tables: {
+ proxy_logs: {
+ columns: {
+ downstream_api_key_id: makeColumn({ logicalType: 'integer' }),
+ created_at: makeColumn({ logicalType: 'datetime', defaultValue: "datetime('now')" }),
+ },
+ },
+ },
+ indexes: [
+ {
+ name: 'proxy_logs_downstream_api_key_created_at_idx',
+ table: 'proxy_logs',
+ columns: ['downstream_api_key_id', 'created_at'],
+ unique: false,
+ },
+ ],
+ uniques: [],
+ foreignKeys: [],
+ };
+
+ await ensureRuntimeDatabaseSchema({
+ ...createStubClient('mysql', executedSql),
+ execute: async (sqlText: string) => {
+ if (sqlText.includes('FROM information_schema.columns')) {
+ return [[
+ {
+ table_name: 'proxy_logs',
+ column_name: 'downstream_api_key_id',
+ data_type: 'int',
+ column_type: 'int',
+ },
+ {
+ table_name: 'proxy_logs',
+ column_name: 'created_at',
+ data_type: 'text',
+ column_type: 'text',
+ },
+ ]];
+ }
+
+ executedSql.push(sqlText);
+ return [];
+ },
+ queryScalar: async (sqlText: string, params: unknown[] = []) => {
+ if (sqlText.includes('information_schema.tables')) {
+ return params[0] === 'proxy_logs' ? 1 : 0;
+ }
+ if (sqlText.includes('information_schema.columns')) {
+ return 1;
+ }
+ return 0;
+ },
+ }, {
+ currentContract: minimalContract,
+ liveContract: {
+ ...minimalContract,
+ indexes: [],
+ },
+ });
+
+ expect(executedSql).toContain(
+ 'CREATE INDEX `proxy_logs_downstream_api_key_created_at_idx` ON `proxy_logs` (`downstream_api_key_id`, `created_at`(191))',
+ );
+ });
+
+ it('does not add mysql text prefixes when live indexed text columns are varchar-backed', async () => {
+ const executedSql: string[] = [];
+ const minimalContract: SchemaContract = {
+ tables: {
+ sites: {
+ columns: {
+ platform: makeColumn({ logicalType: 'text', notNull: true }),
+ url: makeColumn({ logicalType: 'text', notNull: true }),
+ },
+ },
+ },
+ indexes: [],
+ uniques: [
+ {
+ name: 'sites_platform_url_unique',
+ table: 'sites',
+ columns: ['platform', 'url'],
+ },
+ ],
+ foreignKeys: [],
+ };
+
+ await ensureRuntimeDatabaseSchema({
+ ...createStubClient('mysql', executedSql),
+ execute: async (sqlText: string) => {
+ if (sqlText.includes('FROM information_schema.columns')) {
+ return [[
+ {
+ table_name: 'sites',
+ column_name: 'platform',
+ data_type: 'varchar',
+ column_type: 'varchar(32)',
+ },
+ {
+ table_name: 'sites',
+ column_name: 'url',
+ data_type: 'varchar',
+ column_type: 'varchar(255)',
+ },
+ ]];
+ }
+
+ executedSql.push(sqlText);
+ return [];
+ },
+ queryScalar: async (sqlText: string, params: unknown[] = []) => {
+ if (sqlText.includes('information_schema.tables')) {
+ return params[0] === 'sites' ? 1 : 0;
+ }
+ if (sqlText.includes('information_schema.columns')) {
+ return 1;
+ }
+ return 0;
+ },
+ }, {
+ currentContract: minimalContract,
+ liveContract: {
+ ...minimalContract,
+ uniques: [],
+ },
+ });
+
+ expect(executedSql).toContain(
+ 'CREATE UNIQUE INDEX `sites_platform_url_unique` ON `sites` (`platform`, `url`)',
+ );
+ expect(executedSql).not.toContain(
+ 'CREATE UNIQUE INDEX `sites_platform_url_unique` ON `sites` (`platform`(191), `url`(191))',
+ );
+ });
+});
diff --git a/src/server/db/runtimeSchemaBootstrap.ts b/src/server/db/runtimeSchemaBootstrap.ts
new file mode 100644
index 00000000..ab90c457
--- /dev/null
+++ b/src/server/db/runtimeSchemaBootstrap.ts
@@ -0,0 +1,489 @@
+import Database from 'better-sqlite3';
+import mysql from 'mysql2/promise';
+import pg from 'pg';
+import { mkdirSync, readFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import {
+ ensureLegacySchemaCompatibility,
+ type LegacySchemaCompatInspector,
+} from './legacySchemaCompat.js';
+import {
+ generateBootstrapSql,
+ generateUpgradeSql,
+ type MysqlIndexPrefixRequirementMap,
+} from './schemaArtifactGenerator.js';
+import { installPostgresJsonTextParsers } from './postgresJsonTextParsers.js';
+import { introspectLiveSchema } from './schemaIntrospection.js';
+import { resolveGeneratedSchemaContractPath, type SchemaContract } from './schemaContract.js';
+
+export type RuntimeSchemaDialect = 'sqlite' | 'mysql' | 'postgres';
+
+export interface RuntimeSchemaClient {
+ dialect: RuntimeSchemaDialect;
+ connectionString: string;
+ ssl: boolean;
+ begin(): Promise;
+ commit(): Promise;
+ rollback(): Promise;
+ execute(sqlText: string, params?: unknown[]): Promise;
+ queryScalar(sqlText: string, params?: unknown[]): Promise;
+ close(): Promise;
+}
+
+export interface RuntimeSchemaConnectionInput {
+ dialect: RuntimeSchemaDialect;
+ connectionString: string;
+ ssl?: boolean;
+}
+
+function normalizeSchemaErrorMessage(error: unknown): string {
+ if (typeof error === 'object' && error && 'message' in error) {
+ return String((error as { message?: unknown }).message || '');
+ }
+ return String(error || '');
+}
+
+function isExistingSchemaObjectError(error: unknown): boolean {
+ const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
+ const code = typeof error === 'object' && error && 'code' in error
+ ? String((error as { code?: unknown }).code || '')
+ : '';
+
+ return code === 'ER_DUP_KEYNAME'
+ || code === 'ER_DUP_FIELDNAME'
+ || code === 'ER_TABLE_EXISTS_ERROR'
+ || code === '42P07'
+ || code === '42701'
+ || code === '42710'
+ || lowered.includes('already exists')
+ || lowered.includes('duplicate column')
+ || lowered.includes('duplicate key name')
+ || lowered.includes('relation') && lowered.includes('already exists');
+}
+
+async function executeBootstrapStatement(client: RuntimeSchemaClient, sqlText: string): Promise {
+ try {
+ await client.execute(sqlText);
+ } catch (error) {
+ if (!isExistingSchemaObjectError(error)) {
+ throw error;
+ }
+ }
+}
+
+function validateIdentifier(identifier: string): string {
+ if (!/^[a-z_][a-z0-9_]*$/i.test(identifier)) {
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
+ }
+ return identifier;
+}
+
+function createLegacySchemaInspector(client: RuntimeSchemaClient): LegacySchemaCompatInspector {
+ if (client.dialect === 'sqlite') {
+ return {
+ dialect: 'sqlite',
+ tableExists: async (table) => {
+ const normalizedTable = validateIdentifier(table);
+ return (await client.queryScalar(
+ `SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = '${normalizedTable}'`,
+ )) > 0;
+ },
+ columnExists: async (table, column) => {
+ const normalizedTable = validateIdentifier(table);
+ const normalizedColumn = validateIdentifier(column);
+ return (await client.queryScalar(
+ `SELECT COUNT(*) FROM pragma_table_info('${normalizedTable}') WHERE name = '${normalizedColumn}'`,
+ )) > 0;
+ },
+ execute: async (sqlText) => {
+ await client.execute(sqlText);
+ },
+ };
+ }
+
+ if (client.dialect === 'mysql') {
+ return {
+ dialect: 'mysql',
+ tableExists: async (table) => {
+ return (await client.queryScalar(
+ 'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?',
+ [table],
+ )) > 0;
+ },
+ columnExists: async (table, column) => {
+ return (await client.queryScalar(
+ 'SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?',
+ [table, column],
+ )) > 0;
+ },
+ execute: async (sqlText) => {
+ await client.execute(sqlText);
+ },
+ };
+ }
+
+ return {
+ dialect: 'postgres',
+ tableExists: async (table) => {
+ return (await client.queryScalar(
+ 'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1',
+ [table],
+ )) > 0;
+ },
+ columnExists: async (table, column) => {
+ return (await client.queryScalar(
+ 'SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1 AND column_name = $2',
+ [table, column],
+ )) > 0;
+ },
+ execute: async (sqlText) => {
+ await client.execute(sqlText);
+ },
+ };
+}
+
+function splitSqlStatements(sqlText: string): string[] {
+ const withoutCommentLines = sqlText
+ .split(/\r?\n/g)
+ .filter((line) => !line.trim().startsWith('--'))
+ .join('\n');
+
+ return withoutCommentLines
+ .split(/;\s*(?:\r?\n|$)/g)
+ .map((statement) => statement.trim())
+ .filter((statement) => statement.length > 0);
+}
+
+function readSchemaContract(): SchemaContract {
+ return JSON.parse(readFileSync(resolveGeneratedSchemaContractPath(), 'utf8')) as SchemaContract;
+}
+
+function cloneContract(contract: SchemaContract): SchemaContract {
+ return JSON.parse(JSON.stringify(contract)) as SchemaContract;
+}
+
+function serializeColumn(column: SchemaContract['tables'][string]['columns'][string]): string {
+ return [
+ column.logicalType,
+ column.notNull ? 'not-null' : 'nullable',
+ column.defaultValue ?? 'default:null',
+ column.primaryKey ? 'pk' : 'non-pk',
+ ].join('|');
+}
+
+function serializeIndex(index: SchemaContract['indexes'][number]): string {
+ return [index.table, index.columns.join(','), index.unique ? 'unique' : 'non-unique'].join('|');
+}
+
+function serializeUnique(unique: SchemaContract['uniques'][number]): string {
+ return [unique.table, unique.columns.join(',')].join('|');
+}
+
+function serializeForeignKey(foreignKey: SchemaContract['foreignKeys'][number]): string {
+ return [
+ foreignKey.table,
+ foreignKey.columns.join(','),
+ foreignKey.referencedTable,
+ foreignKey.referencedColumns.join(','),
+ foreignKey.onDelete ?? 'null',
+ ].join('|');
+}
+
+function buildCompatibleRuntimeBaseline(
+ currentContract: SchemaContract,
+ liveContract: SchemaContract,
+): SchemaContract {
+ const baseline: SchemaContract = {
+ tables: {},
+ indexes: [],
+ uniques: [],
+ foreignKeys: [],
+ };
+
+ for (const [tableName, liveTable] of Object.entries(liveContract.tables)) {
+ const currentTable = currentContract.tables[tableName];
+ if (!currentTable) {
+ continue;
+ }
+
+ const compatibleColumns = Object.fromEntries(
+ Object.entries(liveTable.columns)
+ .filter(([columnName, liveColumn]) => {
+ const currentColumn = currentTable.columns[columnName];
+ return currentColumn && serializeColumn(currentColumn) === serializeColumn(liveColumn);
+ }),
+ );
+
+ baseline.tables[tableName] = { columns: compatibleColumns };
+ }
+
+ const currentIndexes = new Map(currentContract.indexes.map((index) => [index.name, index]));
+ baseline.indexes = liveContract.indexes
+ .filter((index) => {
+ const currentIndex = currentIndexes.get(index.name);
+ return currentIndex && serializeIndex(currentIndex) === serializeIndex(index);
+ });
+
+ const currentUniques = new Map(currentContract.uniques.map((unique) => [unique.name, unique]));
+ baseline.uniques = liveContract.uniques
+ .filter((unique) => {
+ const currentUnique = currentUniques.get(unique.name);
+ return currentUnique && serializeUnique(currentUnique) === serializeUnique(unique);
+ });
+
+ const currentForeignKeys = new Set(currentContract.foreignKeys.map(serializeForeignKey));
+ baseline.foreignKeys = liveContract.foreignKeys
+ .filter((foreignKey) => currentForeignKeys.has(serializeForeignKey(foreignKey)));
+
+ return baseline;
+}
+
+function collectIndexedColumns(contract: SchemaContract): Map> {
+ const indexedColumns = new Map>();
+
+ for (const index of [...contract.indexes, ...contract.uniques]) {
+ let columns = indexedColumns.get(index.table);
+ if (!columns) {
+ columns = new Set();
+ indexedColumns.set(index.table, columns);
+ }
+
+ for (const columnName of index.columns) {
+ columns.add(columnName);
+ }
+ }
+
+ return indexedColumns;
+}
+
+function requiresMysqlIndexPrefixForColumnType(columnType: string): boolean {
+ const normalizedType = columnType.trim().toLowerCase();
+ return normalizedType.includes('text') || normalizedType.includes('blob');
+}
+
+async function queryRuntimeRows(
+ client: RuntimeSchemaClient,
+ sqlText: string,
+ params: unknown[] = [],
+): Promise>> {
+ const result = await client.execute(sqlText, params);
+
+ if (!Array.isArray(result)) {
+ return [];
+ }
+
+ const [first] = result;
+ if (Array.isArray(first)) {
+ return first as Array>;
+ }
+
+ if (result.every((item) => typeof item === 'object' && item !== null && !Array.isArray(item))) {
+ return result as Array>;
+ }
+
+ return [];
+}
+
+async function resolveMySqlIndexPrefixRequirements(
+ client: RuntimeSchemaClient,
+ currentContract: SchemaContract,
+): Promise {
+ const indexedColumns = collectIndexedColumns(currentContract);
+ if (indexedColumns.size === 0) {
+ return {};
+ }
+
+ const rows = await queryRuntimeRows(client, `
+ SELECT
+ table_name AS table_name,
+ column_name AS column_name,
+ data_type AS data_type,
+ column_type AS column_type
+ FROM information_schema.columns
+ WHERE table_schema = DATABASE()
+ `);
+
+ const requirements: MysqlIndexPrefixRequirementMap = {};
+ for (const row of rows) {
+ const tableName = String(row.table_name || '');
+ const columnName = String(row.column_name || '');
+ const trackedColumns = indexedColumns.get(tableName);
+ if (!trackedColumns || !trackedColumns.has(columnName)) {
+ continue;
+ }
+
+ const declaredType = String(row.column_type || row.data_type || '');
+ requirements[tableName] ??= {};
+ requirements[tableName][columnName] = requiresMysqlIndexPrefixForColumnType(declaredType);
+ }
+
+ return requirements;
+}
+
+async function createPostgresClient(connectionString: string, ssl: boolean): Promise {
+ const clientOptions: pg.ClientConfig = { connectionString };
+ if (ssl) {
+ clientOptions.ssl = { rejectUnauthorized: false };
+ }
+ installPostgresJsonTextParsers();
+ const client = new pg.Client(clientOptions);
+ await client.connect();
+
+ return {
+ dialect: 'postgres',
+ connectionString,
+ ssl,
+ begin: async () => { await client.query('BEGIN'); },
+ commit: async () => { await client.query('COMMIT'); },
+ rollback: async () => { await client.query('ROLLBACK'); },
+ execute: async (sqlText, params = []) => client.query(sqlText, params),
+ queryScalar: async (sqlText, params = []) => {
+ const result = await client.query(sqlText, params);
+ const row = result.rows[0] as Record | undefined;
+ if (!row) return 0;
+ return Number(Object.values(row)[0]) || 0;
+ },
+ close: async () => { await client.end(); },
+ };
+}
+
+async function createMySqlClient(connectionString: string, ssl: boolean): Promise {
+ const connectionOptions: mysql.ConnectionOptions = { uri: connectionString };
+ if (ssl) {
+ connectionOptions.ssl = { rejectUnauthorized: false };
+ }
+ const connection = await mysql.createConnection(connectionOptions);
+
+ return {
+ dialect: 'mysql',
+ connectionString,
+ ssl,
+ begin: async () => { await connection.beginTransaction(); },
+ commit: async () => { await connection.commit(); },
+ rollback: async () => { await connection.rollback(); },
+ execute: async (sqlText, params = []) => connection.execute(sqlText, params as any[]),
+ queryScalar: async (sqlText, params = []) => {
+ const [rows] = await connection.query(sqlText, params as any[]);
+ if (!Array.isArray(rows) || rows.length === 0) return 0;
+ const row = rows[0] as Record;
+ return Number(Object.values(row)[0]) || 0;
+ },
+ close: async () => { await connection.end(); },
+ };
+}
+
+async function createSqliteClient(connectionString: string): Promise {
+ const filePath = connectionString === ':memory:' ? ':memory:' : resolve(connectionString);
+ if (filePath !== ':memory:') {
+ mkdirSync(dirname(filePath), { recursive: true });
+ }
+ const sqlite = new Database(filePath);
+ sqlite.pragma('journal_mode = WAL');
+ sqlite.pragma('foreign_keys = ON');
+
+ return {
+ dialect: 'sqlite',
+ connectionString,
+ ssl: false,
+ begin: async () => { sqlite.exec('BEGIN'); },
+ commit: async () => { sqlite.exec('COMMIT'); },
+ rollback: async () => { sqlite.exec('ROLLBACK'); },
+ execute: async (sqlText, params = []) => {
+ const lowered = sqlText.trim().toLowerCase();
+ const statement = sqlite.prepare(sqlText);
+ if (lowered.startsWith('select')) return statement.all(...params);
+ return statement.run(...params);
+ },
+ queryScalar: async (sqlText, params = []) => {
+ const row = sqlite.prepare(sqlText).get(...params) as Record | undefined;
+ if (!row) return 0;
+ return Number(Object.values(row)[0]) || 0;
+ },
+ close: async () => { sqlite.close(); },
+ };
+}
+
+export async function createRuntimeSchemaClient(input: RuntimeSchemaConnectionInput): Promise {
+ if (input.dialect === 'postgres') {
+ return createPostgresClient(input.connectionString, !!input.ssl);
+ }
+ if (input.dialect === 'mysql') {
+ return createMySqlClient(input.connectionString, !!input.ssl);
+ }
+ return createSqliteClient(input.connectionString);
+}
+
+type EnsureRuntimeDatabaseSchemaOptions = {
+ currentContract?: SchemaContract;
+ liveContract?: SchemaContract;
+};
+
+async function resolveLiveContract(client: RuntimeSchemaClient, liveContract?: SchemaContract): Promise {
+ if (liveContract) {
+ return liveContract;
+ }
+
+ return introspectLiveSchema({
+ dialect: client.dialect,
+ connectionString: client.connectionString,
+ ssl: client.ssl,
+ });
+}
+
+function buildExternalUpgradeStatements(
+ dialect: Exclude,
+ currentContract: SchemaContract,
+ liveContract: SchemaContract,
+ mysqlIndexPrefixRequirements?: MysqlIndexPrefixRequirementMap,
+): string[] {
+ const compatibleBaseline = buildCompatibleRuntimeBaseline(currentContract, liveContract);
+ return splitSqlStatements(generateUpgradeSql(dialect, currentContract, compatibleBaseline, {
+ mysqlIndexPrefixRequirements,
+ }));
+}
+
+export async function ensureRuntimeDatabaseSchema(
+ client: RuntimeSchemaClient,
+ options: EnsureRuntimeDatabaseSchemaOptions = {},
+): Promise {
+ const currentContract = options.currentContract ?? readSchemaContract();
+ let statements: string[];
+
+ if (client.dialect === 'sqlite') {
+ statements = splitSqlStatements(generateBootstrapSql('sqlite', currentContract));
+ } else {
+ const liveContract = await resolveLiveContract(client, options.liveContract);
+ const mysqlIndexPrefixRequirements = client.dialect === 'mysql'
+ ? await resolveMySqlIndexPrefixRequirements(client, currentContract)
+ : undefined;
+
+ statements = buildExternalUpgradeStatements(
+ client.dialect,
+ currentContract,
+ liveContract,
+ mysqlIndexPrefixRequirements,
+ );
+ }
+
+ for (const sqlText of statements) {
+ await executeBootstrapStatement(client, sqlText);
+ }
+
+ await ensureLegacySchemaCompatibility(createLegacySchemaInspector(client));
+}
+
+export async function bootstrapRuntimeDatabaseSchema(input: RuntimeSchemaConnectionInput): Promise {
+ const client = await createRuntimeSchemaClient(input);
+ try {
+ await ensureRuntimeDatabaseSchema(client);
+ } finally {
+ await client.close();
+ }
+}
+
+export const __runtimeSchemaBootstrapTestUtils = {
+ buildCompatibleRuntimeBaseline,
+ cloneContract,
+ splitSqlStatements,
+ buildExternalUpgradeStatements,
+};
diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts
index 621486a6..613a4ee2 100644
--- a/src/server/db/schema.ts
+++ b/src/server/db/schema.ts
@@ -6,9 +6,10 @@ export const sites = sqliteTable('sites', {
name: text('name').notNull(),
url: text('url').notNull(),
externalCheckinUrl: text('external_checkin_url'),
- platform: text('platform').notNull(), // 'new-api' | 'one-api' | 'veloera' | 'one-hub' | 'done-hub' | 'sub2api' | 'openai' | 'claude' | 'gemini'
+ platform: text('platform').notNull(), // 'new-api' | 'one-api' | 'veloera' | 'one-hub' | 'done-hub' | 'sub2api' | 'openai' | 'claude' | 'gemini' | 'codex' | 'gemini-cli' | 'antigravity'
proxyUrl: text('proxy_url'),
useSystemProxy: integer('use_system_proxy', { mode: 'boolean' }).default(false),
+ customHeaders: text('custom_headers'),
status: text('status').notNull().default('active'), // 'active' | 'disabled'
isPinned: integer('is_pinned', { mode: 'boolean' }).default(false),
sortOrder: integer('sort_order').default(0),
@@ -18,6 +19,17 @@ export const sites = sqliteTable('sites', {
updatedAt: text('updated_at').default(sql`(datetime('now'))`),
}, (table) => ({
statusIdx: index('sites_status_idx').on(table.status),
+ platformUrlUnique: uniqueIndex('sites_platform_url_unique').on(table.platform, table.url),
+}));
+
+export const siteDisabledModels = sqliteTable('site_disabled_models', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ siteId: integer('site_id').notNull().references(() => sites.id, { onDelete: 'cascade' }),
+ modelName: text('model_name').notNull(),
+ createdAt: text('created_at').default(sql`(datetime('now'))`),
+}, (table) => ({
+ siteModelUnique: uniqueIndex('site_disabled_models_site_model_unique').on(table.siteId, table.modelName),
+ siteIdIdx: index('site_disabled_models_site_id_idx').on(table.siteId),
}));
export const accounts = sqliteTable('accounts', {
@@ -37,6 +49,9 @@ export const accounts = sqliteTable('accounts', {
checkinEnabled: integer('checkin_enabled', { mode: 'boolean' }).default(true),
lastCheckinAt: text('last_checkin_at'),
lastBalanceRefresh: text('last_balance_refresh'),
+ oauthProvider: text('oauth_provider'),
+ oauthAccountKey: text('oauth_account_key'),
+ oauthProjectId: text('oauth_project_id'),
extraConfig: text('extra_config'), // JSON string
createdAt: text('created_at').default(sql`(datetime('now'))`),
updatedAt: text('updated_at').default(sql`(datetime('now'))`),
@@ -44,6 +59,8 @@ export const accounts = sqliteTable('accounts', {
siteIdIdx: index('accounts_site_id_idx').on(table.siteId),
statusIdx: index('accounts_status_idx').on(table.status),
siteStatusIdx: index('accounts_site_status_idx').on(table.siteId, table.status),
+ oauthProviderIdx: index('accounts_oauth_provider_idx').on(table.oauthProvider),
+ oauthIdentityIdx: index('accounts_oauth_identity_idx').on(table.oauthProvider, table.oauthAccountKey, table.oauthProjectId),
}));
export const accountTokens = sqliteTable('account_tokens', {
@@ -52,6 +69,7 @@ export const accountTokens = sqliteTable('account_tokens', {
name: text('name').notNull(),
token: text('token').notNull(),
tokenGroup: text('token_group'),
+ valueStatus: text('value_status').notNull().default('ready'),
source: text('source').default('manual'), // 'manual' | 'sync' | 'legacy'
enabled: integer('enabled', { mode: 'boolean' }).default(true),
isDefault: integer('is_default', { mode: 'boolean' }).default(false),
@@ -81,6 +99,7 @@ export const modelAvailability = sqliteTable('model_availability', {
accountId: integer('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }),
modelName: text('model_name').notNull(),
available: integer('available', { mode: 'boolean' }),
+ isManual: integer('is_manual', { mode: 'boolean' }).default(false),
latencyMs: integer('latency_ms'),
checkedAt: text('checked_at').default(sql`(datetime('now'))`),
}, (table) => ({
@@ -108,9 +127,11 @@ export const tokenRoutes = sqliteTable('token_routes', {
modelPattern: text('model_pattern').notNull(),
displayName: text('display_name'),
displayIcon: text('display_icon'),
+ routeMode: text('route_mode').default('pattern'),
modelMapping: text('model_mapping'), // JSON
decisionSnapshot: text('decision_snapshot'), // JSON
decisionRefreshedAt: text('decision_refreshed_at'),
+ routingStrategy: text('routing_strategy').default('weighted'),
enabled: integer('enabled', { mode: 'boolean' }).default(true),
createdAt: text('created_at').default(sql`(datetime('now'))`),
updatedAt: text('updated_at').default(sql`(datetime('now'))`),
@@ -119,6 +140,15 @@ export const tokenRoutes = sqliteTable('token_routes', {
enabledIdx: index('token_routes_enabled_idx').on(table.enabled),
}));
+export const routeGroupSources = sqliteTable('route_group_sources', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ groupRouteId: integer('group_route_id').notNull().references(() => tokenRoutes.id, { onDelete: 'cascade' }),
+ sourceRouteId: integer('source_route_id').notNull().references(() => tokenRoutes.id, { onDelete: 'cascade' }),
+}, (table) => ({
+ groupSourceUnique: uniqueIndex('route_group_sources_group_source_unique').on(table.groupRouteId, table.sourceRouteId),
+ sourceRouteIdx: index('route_group_sources_source_route_id_idx').on(table.sourceRouteId),
+}));
+
export const routeChannels = sqliteTable('route_channels', {
id: integer('id').primaryKey({ autoIncrement: true }),
routeId: integer('route_id').notNull().references(() => tokenRoutes.id, { onDelete: 'cascade' }),
@@ -134,7 +164,10 @@ export const routeChannels = sqliteTable('route_channels', {
totalLatencyMs: integer('total_latency_ms').default(0),
totalCost: real('total_cost').default(0),
lastUsedAt: text('last_used_at'),
+ lastSelectedAt: text('last_selected_at'),
lastFailAt: text('last_fail_at'),
+ consecutiveFailCount: integer('consecutive_fail_count').notNull().default(0),
+ cooldownLevel: integer('cooldown_level').notNull().default(0),
cooldownUntil: text('cooldown_until'),
}, (table) => ({
routeIdIdx: index('route_channels_route_id_idx').on(table.routeId),
@@ -149,6 +182,7 @@ export const proxyLogs = sqliteTable('proxy_logs', {
routeId: integer('route_id'),
channelId: integer('channel_id'),
accountId: integer('account_id'),
+ downstreamApiKeyId: integer('downstream_api_key_id'),
modelRequested: text('model_requested'),
modelActual: text('model_actual'),
status: text('status'), // 'success' | 'failed' | 'retried'
@@ -159,6 +193,10 @@ export const proxyLogs = sqliteTable('proxy_logs', {
totalTokens: integer('total_tokens'),
estimatedCost: real('estimated_cost'),
billingDetails: text('billing_details'),
+ clientFamily: text('client_family'),
+ clientAppId: text('client_app_id'),
+ clientAppName: text('client_app_name'),
+ clientConfidence: text('client_confidence'),
errorMessage: text('error_message'),
retryCount: integer('retry_count').default(0),
createdAt: text('created_at').default(sql`(datetime('now'))`),
@@ -167,6 +205,9 @@ export const proxyLogs = sqliteTable('proxy_logs', {
accountCreatedIdx: index('proxy_logs_account_created_at_idx').on(table.accountId, table.createdAt),
statusCreatedIdx: index('proxy_logs_status_created_at_idx').on(table.status, table.createdAt),
modelActualCreatedIdx: index('proxy_logs_model_actual_created_at_idx').on(table.modelActual, table.createdAt),
+ downstreamKeyCreatedIdx: index('proxy_logs_downstream_api_key_created_at_idx').on(table.downstreamApiKeyId, table.createdAt),
+ clientAppCreatedIdx: index('proxy_logs_client_app_id_created_at_idx').on(table.clientAppId, table.createdAt),
+ clientFamilyCreatedIdx: index('proxy_logs_client_family_created_at_idx').on(table.clientFamily, table.createdAt),
}));
export const proxyVideoTasks = sqliteTable('proxy_video_tasks', {
@@ -220,6 +261,8 @@ export const downstreamApiKeys = sqliteTable('downstream_api_keys', {
name: text('name').notNull(),
key: text('key').notNull(),
description: text('description'),
+ groupName: text('group_name'),
+ tags: text('tags'), // JSON array
enabled: integer('enabled', { mode: 'boolean' }).default(true),
expiresAt: text('expires_at'),
maxCost: real('max_cost'),
@@ -239,6 +282,30 @@ export const downstreamApiKeys = sqliteTable('downstream_api_keys', {
expiresAtIdx: index('downstream_api_keys_expires_at_idx').on(table.expiresAt),
}));
+export const siteAnnouncements = sqliteTable('site_announcements', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ siteId: integer('site_id').notNull().references(() => sites.id, { onDelete: 'cascade' }),
+ platform: text('platform').notNull(),
+ sourceKey: text('source_key').notNull(),
+ title: text('title').notNull(),
+ content: text('content').notNull(),
+ level: text('level').notNull().default('info'),
+ sourceUrl: text('source_url'),
+ startsAt: text('starts_at'),
+ endsAt: text('ends_at'),
+ upstreamCreatedAt: text('upstream_created_at'),
+ upstreamUpdatedAt: text('upstream_updated_at'),
+ firstSeenAt: text('first_seen_at').default(sql`(datetime('now'))`),
+ lastSeenAt: text('last_seen_at').default(sql`(datetime('now'))`),
+ readAt: text('read_at'),
+ dismissedAt: text('dismissed_at'),
+ rawPayload: text('raw_payload'),
+}, (table) => ({
+ siteSourceKeyUnique: uniqueIndex('site_announcements_site_source_key_unique').on(table.siteId, table.sourceKey),
+ siteIdFirstSeenAtIdx: index('site_announcements_site_id_first_seen_at_idx').on(table.siteId, table.firstSeenAt),
+ readAtIdx: index('site_announcements_read_at_idx').on(table.readAt),
+}));
+
export const events = sqliteTable('events', {
id: integer('id').primaryKey({ autoIncrement: true }),
type: text('type').notNull(), // 'checkin' | 'balance' | 'token' | 'proxy' | 'status'
diff --git a/src/server/db/schemaArtifactGenerator.test.ts b/src/server/db/schemaArtifactGenerator.test.ts
new file mode 100644
index 00000000..7a38f453
--- /dev/null
+++ b/src/server/db/schemaArtifactGenerator.test.ts
@@ -0,0 +1,173 @@
+import { readFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import { generateDialectArtifacts, generateUpgradeSql, type GeneratedDialectArtifacts } from './schemaArtifactGenerator.js';
+import type { SchemaContract, SchemaContractColumn } from './schemaContract.js';
+
+const dbDir = dirname(fileURLToPath(import.meta.url));
+const schemaContractPath = resolve(dbDir, 'generated/schemaContract.json');
+
+function readSchemaContract(): SchemaContract {
+ return JSON.parse(readFileSync(schemaContractPath, 'utf8')) as SchemaContract;
+}
+
+function makeColumn(overrides: Partial = {}): SchemaContractColumn {
+ return {
+ logicalType: 'text',
+ notNull: false,
+ defaultValue: null,
+ primaryKey: false,
+ ...overrides,
+ };
+}
+
+describe('schema artifact generator', () => {
+ it('generates bootstrap sql for mysql and postgres from the contract', () => {
+ const artifacts = generateDialectArtifacts(readSchemaContract());
+
+ expect(artifacts.mysqlBootstrap).toContain('CREATE TABLE IF NOT EXISTS `sites`');
+ expect(artifacts.mysqlBootstrap).toContain('CREATE TABLE IF NOT EXISTS `site_disabled_models`');
+ expect(artifacts.postgresBootstrap).toContain('CREATE TABLE IF NOT EXISTS "account_tokens"');
+ expect(artifacts.postgresBootstrap).toContain('"token_group"');
+ });
+
+ it('generates additive upgrade sql for newly added tables, columns, indexes, and uniques', () => {
+ const previousContract: SchemaContract = {
+ tables: {
+ sites: {
+ columns: {
+ id: makeColumn({ logicalType: 'integer', notNull: true, primaryKey: true }),
+ },
+ },
+ },
+ indexes: [],
+ uniques: [],
+ foreignKeys: [],
+ };
+ const currentContract: SchemaContract = {
+ tables: {
+ sites: {
+ columns: {
+ id: makeColumn({ logicalType: 'integer', notNull: true, primaryKey: true }),
+ status: makeColumn({ notNull: true, defaultValue: "'active'" }),
+ },
+ },
+ accounts: {
+ columns: {
+ id: makeColumn({ logicalType: 'integer', notNull: true, primaryKey: true }),
+ site_id: makeColumn({ logicalType: 'integer', notNull: true }),
+ email: makeColumn({ notNull: true }),
+ },
+ },
+ },
+ indexes: [
+ { name: 'sites_status_idx', table: 'sites', columns: ['status'], unique: false },
+ ],
+ uniques: [
+ { name: 'accounts_site_email_unique', table: 'accounts', columns: ['site_id', 'email'] },
+ ],
+ foreignKeys: [
+ {
+ table: 'accounts',
+ columns: ['site_id'],
+ referencedTable: 'sites',
+ referencedColumns: ['id'],
+ onDelete: 'cascade',
+ },
+ ],
+ };
+
+ const artifacts: GeneratedDialectArtifacts = generateDialectArtifacts(currentContract, previousContract);
+
+ expect(artifacts.mysqlUpgrade).toContain('CREATE TABLE IF NOT EXISTS `accounts`');
+ expect(artifacts.mysqlUpgrade).toContain('ALTER TABLE `sites` ADD COLUMN `status`');
+ expect(artifacts.mysqlUpgrade).toContain('CREATE INDEX `sites_status_idx` ON `sites`');
+ expect(artifacts.mysqlUpgrade).toContain('CREATE UNIQUE INDEX `accounts_site_email_unique` ON `accounts`');
+ expect(artifacts.postgresUpgrade).toContain('CREATE TABLE IF NOT EXISTS "accounts"');
+ expect(artifacts.postgresUpgrade).toContain('ALTER TABLE "sites" ADD COLUMN "status"');
+ expect(artifacts.postgresUpgrade).toContain('CREATE INDEX "sites_status_idx" ON "sites"');
+ expect(artifacts.postgresUpgrade).toContain('CREATE UNIQUE INDEX "accounts_site_email_unique" ON "accounts"');
+ });
+
+ it('orders dependent tables safely and emits executable datetime defaults for external dialects', () => {
+ const artifacts = generateDialectArtifacts(readSchemaContract());
+
+ expect(
+ artifacts.mysqlBootstrap.indexOf('CREATE TABLE IF NOT EXISTS `accounts`'),
+ ).toBeLessThan(
+ artifacts.mysqlBootstrap.indexOf('CREATE TABLE IF NOT EXISTS `account_tokens`'),
+ );
+ expect(
+ artifacts.postgresBootstrap.indexOf('CREATE TABLE IF NOT EXISTS "accounts"'),
+ ).toBeLessThan(
+ artifacts.postgresBootstrap.indexOf('CREATE TABLE IF NOT EXISTS "account_tokens"'),
+ );
+ expect(artifacts.mysqlBootstrap).toContain("`created_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'))");
+ expect(artifacts.postgresBootstrap).toContain(`"created_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS')`);
+ });
+
+ it('uses varchar for mysql text primary keys so bootstrap ddl stays executable', () => {
+ const artifacts = generateDialectArtifacts(readSchemaContract());
+
+ expect(artifacts.mysqlBootstrap).toContain('CREATE TABLE IF NOT EXISTS `settings` (`key` VARCHAR(191) NOT NULL PRIMARY KEY, `value` TEXT)');
+ expect(artifacts.mysqlBootstrap).not.toContain('CREATE TABLE IF NOT EXISTS `settings` (`key` TEXT NOT NULL PRIMARY KEY, `value` TEXT)');
+ });
+
+ it('does not add mysql text prefixes to non-text index columns', () => {
+ const artifacts = generateDialectArtifacts(readSchemaContract());
+
+ expect(artifacts.mysqlBootstrap).toContain('CREATE INDEX `checkin_logs_account_created_at_idx` ON `checkin_logs` (`account_id`, `created_at`)');
+ expect(artifacts.mysqlBootstrap).toContain('CREATE INDEX `events_read_created_at_idx` ON `events` (`read`, `created_at`)');
+ expect(artifacts.mysqlBootstrap).not.toContain('`created_at`(191)');
+ expect(artifacts.mysqlBootstrap).not.toContain('`read`(191)');
+ });
+
+ it('allows mysql upgrade generation to force prefix lengths for live text-backed columns', () => {
+ const previousContract: SchemaContract = {
+ tables: {
+ proxy_logs: {
+ columns: {
+ downstream_api_key_id: makeColumn({ logicalType: 'integer' }),
+ created_at: makeColumn({ logicalType: 'datetime', defaultValue: "datetime('now')" }),
+ },
+ },
+ },
+ indexes: [],
+ uniques: [],
+ foreignKeys: [],
+ };
+ const currentContract: SchemaContract = {
+ ...previousContract,
+ indexes: [
+ {
+ name: 'proxy_logs_downstream_api_key_created_at_idx',
+ table: 'proxy_logs',
+ columns: ['downstream_api_key_id', 'created_at'],
+ unique: false,
+ },
+ ],
+ };
+
+ const mysqlUpgrade = generateUpgradeSql('mysql', currentContract, previousContract, {
+ mysqlIndexPrefixRequirements: {
+ proxy_logs: {
+ created_at: true,
+ },
+ },
+ });
+
+ expect(mysqlUpgrade).toContain(
+ 'CREATE INDEX `proxy_logs_downstream_api_key_created_at_idx` ON `proxy_logs` (`downstream_api_key_id`, `created_at`(191))',
+ );
+ });
+
+ it('rejects destructive diffs when generating additive upgrades', () => {
+ const current = readSchemaContract();
+ const previous = structuredClone(current);
+
+ delete current.tables.sites.columns.status;
+
+ expect(() => generateDialectArtifacts(current, previous)).toThrow(/non-additive schema diff/i);
+ });
+});
diff --git a/src/server/db/schemaArtifactGenerator.ts b/src/server/db/schemaArtifactGenerator.ts
new file mode 100644
index 00000000..b0d87800
--- /dev/null
+++ b/src/server/db/schemaArtifactGenerator.ts
@@ -0,0 +1,446 @@
+import { mkdirSync, writeFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import type {
+ LogicalColumnType,
+ SchemaContract,
+ SchemaContractColumn,
+ SchemaContractForeignKey,
+ SchemaContractIndex,
+ SchemaContractUnique,
+} from './schemaContract.js';
+
+export interface GeneratedDialectArtifacts {
+ mysqlBootstrap: string;
+ postgresBootstrap: string;
+ mysqlUpgrade: string;
+ postgresUpgrade: string;
+}
+
+export type MysqlIndexPrefixRequirementMap = Record>;
+
+type Dialect = 'mysql' | 'postgres';
+type SqlDialect = 'sqlite' | Dialect;
+type SqlGenerationOptions = {
+ mysqlIndexPrefixRequirements?: MysqlIndexPrefixRequirementMap;
+};
+
+function resolveDbDir(): string {
+ return dirname(fileURLToPath(import.meta.url));
+}
+
+export function resolveGeneratedArtifactPath(filename: string): string {
+ return resolve(resolveDbDir(), 'generated', filename);
+}
+
+function quoteIdentifier(dialect: SqlDialect, identifier: string): string {
+ return dialect === 'mysql' ? `\`${identifier}\`` : `"${identifier}"`;
+}
+
+function escapeMysqlTextPrefix(columnType: LogicalColumnType): string {
+ return columnType === 'text' ? '(191)' : '';
+}
+
+function resolveMysqlIndexPrefix(
+ tableName: string,
+ columnName: string,
+ contract: SchemaContract,
+ options?: SqlGenerationOptions,
+): string {
+ const override = options?.mysqlIndexPrefixRequirements?.[tableName]?.[columnName];
+ if (override !== undefined) {
+ return override ? '(191)' : '';
+ }
+
+ const column = contract.tables[tableName]?.columns[columnName];
+ return column ? escapeMysqlTextPrefix(column.logicalType) : '';
+}
+
+function mapColumnType(dialect: SqlDialect, columnName: string, column: SchemaContractColumn): string {
+ if (dialect === 'sqlite') {
+ switch (column.logicalType) {
+ case 'boolean':
+ case 'integer':
+ return 'INTEGER';
+ case 'real':
+ return 'REAL';
+ case 'datetime':
+ case 'json':
+ case 'text':
+ default:
+ return 'TEXT';
+ }
+ }
+
+ if (dialect === 'mysql') {
+ switch (column.logicalType) {
+ case 'boolean':
+ return 'BOOLEAN';
+ case 'integer':
+ return column.primaryKey && columnName === 'id' ? 'INT AUTO_INCREMENT' : 'INT';
+ case 'real':
+ return 'DOUBLE';
+ case 'datetime':
+ return 'VARCHAR(191)';
+ case 'json':
+ return 'JSON';
+ case 'text':
+ return column.primaryKey || column.defaultValue != null ? 'VARCHAR(191)' : 'TEXT';
+ default:
+ return 'TEXT';
+ }
+ }
+
+ switch (column.logicalType) {
+ case 'boolean':
+ return 'BOOLEAN';
+ case 'integer':
+ return column.primaryKey && columnName === 'id' ? 'INTEGER GENERATED BY DEFAULT AS IDENTITY' : 'INTEGER';
+ case 'real':
+ return 'DOUBLE PRECISION';
+ case 'datetime':
+ return 'TEXT';
+ case 'json':
+ return 'JSONB';
+ case 'text':
+ default:
+ return 'TEXT';
+ }
+}
+
+function formatDefaultValue(dialect: SqlDialect, column: SchemaContractColumn): string {
+ if (column.defaultValue == null || column.primaryKey) {
+ return '';
+ }
+
+ if (column.logicalType === 'datetime') {
+ if (dialect === 'sqlite') {
+ return " DEFAULT (datetime('now'))";
+ }
+ if (dialect === 'mysql') {
+ return " DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'))";
+ }
+ return " DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS')";
+ }
+
+ if (column.logicalType === 'boolean') {
+ return ` DEFAULT ${column.defaultValue.toLowerCase()}`;
+ }
+
+ return ` DEFAULT ${column.defaultValue}`;
+}
+
+function buildColumnDefinition(
+ dialect: SqlDialect,
+ columnName: string,
+ column: SchemaContractColumn,
+): string {
+ const sqlType = mapColumnType(dialect, columnName, column);
+ const notNull = column.notNull ? ' NOT NULL' : '';
+ const defaultValue = formatDefaultValue(dialect, column);
+ const primaryKey = column.primaryKey ? ' PRIMARY KEY' : '';
+ return `${quoteIdentifier(dialect, columnName)} ${sqlType}${notNull}${defaultValue}${primaryKey}`;
+}
+
+function buildForeignKeyClause(dialect: SqlDialect, foreignKey: SchemaContractForeignKey): string {
+ const sourceColumns = foreignKey.columns.map((column) => quoteIdentifier(dialect, column)).join(', ');
+ const targetColumns = foreignKey.referencedColumns.map((column) => quoteIdentifier(dialect, column)).join(', ');
+ const onDelete = foreignKey.onDelete ? ` ON DELETE ${foreignKey.onDelete.toUpperCase()}` : '';
+ return `FOREIGN KEY (${sourceColumns}) REFERENCES ${quoteIdentifier(dialect, foreignKey.referencedTable)}(${targetColumns})${onDelete}`;
+}
+
+function sortTableNamesForCreation(contract: SchemaContract): string[] {
+ const ordered: string[] = [];
+ const visited = new Set();
+ const visiting = new Set();
+ const tableNames = Object.keys(contract.tables).sort((left, right) => left.localeCompare(right, 'en'));
+
+ const dependencies = new Map();
+ for (const tableName of tableNames) {
+ const referencedTables = contract.foreignKeys
+ .filter((foreignKey) => foreignKey.table === tableName)
+ .map((foreignKey) => foreignKey.referencedTable)
+ .filter((referencedTable) => referencedTable !== tableName && tableNames.includes(referencedTable))
+ .sort((left, right) => left.localeCompare(right, 'en'));
+ dependencies.set(tableName, [...new Set(referencedTables)]);
+ }
+
+ function visit(tableName: string): void {
+ if (visited.has(tableName) || visiting.has(tableName)) {
+ return;
+ }
+ visiting.add(tableName);
+ for (const dependency of dependencies.get(tableName) ?? []) {
+ visit(dependency);
+ }
+ visiting.delete(tableName);
+ visited.add(tableName);
+ ordered.push(tableName);
+ }
+
+ for (const tableName of tableNames) {
+ visit(tableName);
+ }
+
+ return ordered;
+}
+
+function buildCreateTableStatement(
+ dialect: SqlDialect,
+ tableName: string,
+ contract: SchemaContract,
+): string {
+ const table = contract.tables[tableName];
+ const columnEntries = Object.entries(table.columns);
+ const foreignKeys = contract.foreignKeys.filter((foreignKey) => foreignKey.table === tableName);
+ const parts = [
+ ...columnEntries.map(([columnName, column]) => buildColumnDefinition(dialect, columnName, column)),
+ ...foreignKeys.map((foreignKey) => buildForeignKeyClause(dialect, foreignKey)),
+ ];
+ return `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(dialect, tableName)} (${parts.join(', ')})`;
+}
+
+function buildUniqueIndexStatement(
+ dialect: SqlDialect,
+ uniqueIndex: SchemaContractUnique,
+ contract: SchemaContract,
+ options?: SqlGenerationOptions,
+): string {
+ const columns = uniqueIndex.columns
+ .map((columnName) => {
+ const suffix = dialect === 'mysql'
+ ? resolveMysqlIndexPrefix(uniqueIndex.table, columnName, contract, options)
+ : '';
+ return `${quoteIdentifier(dialect, columnName)}${suffix}`;
+ })
+ .join(', ');
+ return `CREATE UNIQUE INDEX ${quoteIdentifier(dialect, uniqueIndex.name)} ON ${quoteIdentifier(dialect, uniqueIndex.table)} (${columns})`;
+}
+
+function buildIndexStatement(
+ dialect: SqlDialect,
+ index: SchemaContractIndex,
+ contract: SchemaContract,
+ options?: SqlGenerationOptions,
+): string {
+ const columns = index.columns
+ .map((columnName) => {
+ const suffix = dialect === 'mysql'
+ ? resolveMysqlIndexPrefix(index.table, columnName, contract, options)
+ : '';
+ return `${quoteIdentifier(dialect, columnName)}${suffix}`;
+ })
+ .join(', ');
+ return `CREATE INDEX ${quoteIdentifier(dialect, index.name)} ON ${quoteIdentifier(dialect, index.table)} (${columns})`;
+}
+
+function serializeColumn(column: SchemaContractColumn): string {
+ return [
+ column.logicalType,
+ column.notNull ? 'not-null' : 'nullable',
+ column.defaultValue ?? 'default:null',
+ column.primaryKey ? 'pk' : 'non-pk',
+ ].join('|');
+}
+
+function serializeIndex(index: SchemaContractIndex): string {
+ return [index.table, index.columns.join(','), index.unique ? 'unique' : 'non-unique'].join('|');
+}
+
+function serializeUnique(unique: SchemaContractUnique): string {
+ return [unique.table, unique.columns.join(',')].join('|');
+}
+
+function serializeForeignKey(foreignKey: SchemaContractForeignKey): string {
+ return [
+ foreignKey.table,
+ foreignKey.columns.join(','),
+ foreignKey.referencedTable,
+ foreignKey.referencedColumns.join(','),
+ foreignKey.onDelete ?? 'null',
+ ].join('|');
+}
+
+function assertAdditiveSchemaDiff(currentContract: SchemaContract, previousContract: SchemaContract): void {
+ const violations: string[] = [];
+
+ for (const [tableName, previousTable] of Object.entries(previousContract.tables)) {
+ const currentTable = currentContract.tables[tableName];
+ if (!currentTable) {
+ violations.push(`removed table ${tableName}`);
+ continue;
+ }
+
+ for (const [columnName, previousColumn] of Object.entries(previousTable.columns)) {
+ const currentColumn = currentTable.columns[columnName];
+ if (!currentColumn) {
+ violations.push(`removed column ${tableName}.${columnName}`);
+ continue;
+ }
+
+ if (serializeColumn(currentColumn) !== serializeColumn(previousColumn)) {
+ violations.push(`changed column ${tableName}.${columnName}`);
+ }
+ }
+ }
+
+ const currentIndexes = new Map(currentContract.indexes.map((index) => [index.name, index]));
+ for (const previousIndex of previousContract.indexes) {
+ const currentIndex = currentIndexes.get(previousIndex.name);
+ if (!currentIndex) {
+ violations.push(`removed index ${previousIndex.name}`);
+ continue;
+ }
+ if (serializeIndex(currentIndex) !== serializeIndex(previousIndex)) {
+ violations.push(`changed index ${previousIndex.name}`);
+ }
+ }
+
+ const currentUniques = new Map(currentContract.uniques.map((unique) => [unique.name, unique]));
+ for (const previousUnique of previousContract.uniques) {
+ const currentUnique = currentUniques.get(previousUnique.name);
+ if (!currentUnique) {
+ violations.push(`removed unique ${previousUnique.name}`);
+ continue;
+ }
+ if (serializeUnique(currentUnique) !== serializeUnique(previousUnique)) {
+ violations.push(`changed unique ${previousUnique.name}`);
+ }
+ }
+
+ const currentForeignKeys = new Set(currentContract.foreignKeys.map(serializeForeignKey));
+ for (const previousForeignKey of previousContract.foreignKeys) {
+ if (!currentForeignKeys.has(serializeForeignKey(previousForeignKey))) {
+ violations.push(`removed foreign key ${previousForeignKey.table}(${previousForeignKey.columns.join(',')})`);
+ }
+ }
+
+ if (violations.length > 0) {
+ throw new Error(`Non-additive schema diff detected:\n- ${violations.join('\n- ')}`);
+ }
+}
+
+export function generateBootstrapSql(dialect: SqlDialect, contract: SchemaContract): string {
+ const tableStatements = sortTableNamesForCreation(contract)
+ .map((tableName) => buildCreateTableStatement(dialect, tableName, contract));
+
+ const uniqueStatements = contract.uniques
+ .slice()
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'))
+ .map((uniqueIndex) => buildUniqueIndexStatement(dialect, uniqueIndex, contract));
+
+ const knownUniqueNames = new Set(contract.uniques.map((uniqueIndex) => uniqueIndex.name));
+ const indexStatements = contract.indexes
+ .filter((index) => !knownUniqueNames.has(index.name))
+ .slice()
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'))
+ .map((index) => buildIndexStatement(dialect, index, contract));
+
+ return `${[...tableStatements, ...uniqueStatements, ...indexStatements].join(';\n')};\n`;
+}
+
+function buildAddColumnStatement(
+ dialect: SqlDialect,
+ tableName: string,
+ columnName: string,
+ column: SchemaContractColumn,
+): string {
+ return `ALTER TABLE ${quoteIdentifier(dialect, tableName)} ADD COLUMN ${buildColumnDefinition(dialect, columnName, column)}`;
+}
+
+export function generateUpgradeSql(
+ dialect: SqlDialect,
+ currentContract: SchemaContract,
+ previousContract?: SchemaContract | null,
+ options?: SqlGenerationOptions,
+): string {
+ if (!previousContract) {
+ return `-- no previous schema contract available for ${dialect} additive upgrade generation\n`;
+ }
+
+ assertAdditiveSchemaDiff(currentContract, previousContract);
+
+ const previousTableNames = new Set(Object.keys(previousContract.tables));
+ const currentTableNames = Object.keys(currentContract.tables).sort((left, right) => left.localeCompare(right, 'en'));
+ const addedTableNames = currentTableNames.filter((tableName) => !previousTableNames.has(tableName));
+ const addedTablesContract: SchemaContract = {
+ tables: Object.fromEntries(addedTableNames.map((tableName) => [tableName, currentContract.tables[tableName]])),
+ indexes: currentContract.indexes.filter((index) => addedTableNames.includes(index.table)),
+ uniques: currentContract.uniques.filter((unique) => addedTableNames.includes(unique.table)),
+ foreignKeys: currentContract.foreignKeys.filter((foreignKey) => addedTableNames.includes(foreignKey.table)),
+ };
+ const addedTableStatements = sortTableNamesForCreation(addedTablesContract)
+ .map((tableName) => buildCreateTableStatement(dialect, tableName, currentContract));
+
+ const addColumnStatements: string[] = [];
+ for (const tableName of currentTableNames) {
+ if (!previousTableNames.has(tableName)) {
+ continue;
+ }
+
+ const previousColumns = previousContract.tables[tableName]?.columns ?? {};
+ const currentColumns = currentContract.tables[tableName]?.columns ?? {};
+ for (const [columnName, column] of Object.entries(currentColumns)) {
+ if (previousColumns[columnName]) {
+ continue;
+ }
+ addColumnStatements.push(buildAddColumnStatement(dialect, tableName, columnName, column));
+ }
+ }
+
+ const previousUniqueNames = new Set(previousContract.uniques.map((unique) => unique.name));
+ const uniqueStatements = currentContract.uniques
+ .filter((unique) => !previousUniqueNames.has(unique.name))
+ .slice()
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'))
+ .map((unique) => buildUniqueIndexStatement(dialect, unique, currentContract, options));
+
+ const currentUniqueNames = new Set(currentContract.uniques.map((unique) => unique.name));
+ const previousIndexNames = new Set(previousContract.indexes.map((index) => index.name));
+ const indexStatements = currentContract.indexes
+ .filter((index) => !currentUniqueNames.has(index.name))
+ .filter((index) => !previousIndexNames.has(index.name))
+ .slice()
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'))
+ .map((index) => buildIndexStatement(dialect, index, currentContract, options));
+
+ const statements = [...addedTableStatements, ...addColumnStatements, ...uniqueStatements, ...indexStatements];
+ if (statements.length === 0) {
+ return `-- no schema changes detected for ${dialect}\n`;
+ }
+
+ return `${statements.join(';\n')};\n`;
+}
+
+export function generateDialectArtifacts(
+ contract: SchemaContract,
+ previousContract?: SchemaContract | null,
+): GeneratedDialectArtifacts {
+ return {
+ mysqlBootstrap: generateBootstrapSql('mysql', contract),
+ postgresBootstrap: generateBootstrapSql('postgres', contract),
+ mysqlUpgrade: generateUpgradeSql('mysql', contract, previousContract),
+ postgresUpgrade: generateUpgradeSql('postgres', contract, previousContract),
+ };
+}
+
+export function writeDialectArtifactFiles(
+ contract: SchemaContract,
+ previousContract?: SchemaContract | null,
+): GeneratedDialectArtifacts {
+ const artifacts = generateDialectArtifacts(contract, previousContract);
+ const artifactEntries = [
+ ['mysql.bootstrap.sql', artifacts.mysqlBootstrap],
+ ['postgres.bootstrap.sql', artifacts.postgresBootstrap],
+ ['mysql.upgrade.sql', artifacts.mysqlUpgrade],
+ ['postgres.upgrade.sql', artifacts.postgresUpgrade],
+ ] as const;
+
+ for (const [filename, content] of artifactEntries) {
+ const outputPath = resolveGeneratedArtifactPath(filename);
+ mkdirSync(dirname(outputPath), { recursive: true });
+ writeFileSync(outputPath, content, 'utf8');
+ }
+
+ return artifacts;
+}
diff --git a/src/server/db/schemaContract.test.ts b/src/server/db/schemaContract.test.ts
new file mode 100644
index 00000000..b3961fc2
--- /dev/null
+++ b/src/server/db/schemaContract.test.ts
@@ -0,0 +1,72 @@
+import { describe, expect, it } from 'vitest';
+import { buildSchemaContractFromSqliteMigrations } from './schemaContract.js';
+
+describe('schema contract generation', () => {
+ it('captures the current schema shape from sqlite migrations', () => {
+ const contract = buildSchemaContractFromSqliteMigrations();
+
+ expect(contract.tables.sites.columns.status).toMatchObject({
+ logicalType: 'text',
+ notNull: true,
+ primaryKey: false,
+ });
+ expect(contract.tables.account_tokens.columns.token_group).toBeDefined();
+ expect(contract.tables.account_tokens.columns.value_status).toMatchObject({
+ logicalType: 'text',
+ notNull: true,
+ defaultValue: "'ready'",
+ });
+ expect(contract.tables.site_disabled_models).toBeDefined();
+ expect(contract.tables.downstream_api_keys).toBeDefined();
+ expect(contract.tables.proxy_files).toBeDefined();
+ expect(contract.tables.proxy_video_tasks).toBeDefined();
+ expect(contract.tables.route_channels.columns.source_model).toBeDefined();
+ expect(contract.tables.route_channels.columns.last_selected_at).toBeDefined();
+ expect(contract.tables.route_channels.columns.consecutive_fail_count).toMatchObject({
+ logicalType: 'integer',
+ notNull: true,
+ defaultValue: '0',
+ });
+ expect(contract.tables.sites.columns.use_system_proxy).toMatchObject({
+ logicalType: 'boolean',
+ defaultValue: 'false',
+ });
+ expect(contract.tables.token_routes.columns.routing_strategy).toMatchObject({
+ logicalType: 'text',
+ defaultValue: "'weighted'",
+ });
+ expect(contract.indexes).toContainEqual(
+ expect.objectContaining({ name: 'sites_status_idx', table: 'sites', unique: false }),
+ );
+ expect(contract.uniques).toContainEqual(
+ expect.objectContaining({
+ name: 'site_disabled_models_site_model_unique',
+ table: 'site_disabled_models',
+ columns: ['site_id', 'model_name'],
+ }),
+ );
+ expect(contract.uniques).toContainEqual(
+ expect.objectContaining({
+ name: 'model_availability_account_model_unique',
+ table: 'model_availability',
+ columns: ['account_id', 'model_name'],
+ }),
+ );
+ expect(contract.foreignKeys).toContainEqual(
+ expect.objectContaining({
+ table: 'site_disabled_models',
+ columns: ['site_id'],
+ referencedTable: 'sites',
+ referencedColumns: ['id'],
+ }),
+ );
+ expect(contract.foreignKeys).toContainEqual(
+ expect.objectContaining({
+ table: 'route_channels',
+ columns: ['token_id'],
+ referencedTable: 'account_tokens',
+ referencedColumns: ['id'],
+ }),
+ );
+ });
+});
diff --git a/src/server/db/schemaContract.ts b/src/server/db/schemaContract.ts
new file mode 100644
index 00000000..00303a8d
--- /dev/null
+++ b/src/server/db/schemaContract.ts
@@ -0,0 +1,272 @@
+import Database from 'better-sqlite3';
+import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import {
+ normalizeLogicalColumnType,
+ normalizeSchemaMetadataDefaultValue,
+ type LogicalColumnType,
+} from './schemaMetadata.js';
+
+export type { LogicalColumnType } from './schemaMetadata.js';
+
+export interface SchemaContractColumn {
+ logicalType: LogicalColumnType;
+ notNull: boolean;
+ defaultValue: string | null;
+ primaryKey: boolean;
+}
+
+export interface SchemaContractTable {
+ columns: Record;
+}
+
+export interface SchemaContractIndex {
+ name: string;
+ table: string;
+ columns: string[];
+ unique: boolean;
+}
+
+export interface SchemaContractUnique {
+ name: string;
+ table: string;
+ columns: string[];
+}
+
+export interface SchemaContractForeignKey {
+ table: string;
+ columns: string[];
+ referencedTable: string;
+ referencedColumns: string[];
+ onDelete: string | null;
+}
+
+export interface SchemaContract {
+ tables: Record;
+ indexes: SchemaContractIndex[];
+ uniques: SchemaContractUnique[];
+ foreignKeys: SchemaContractForeignKey[];
+}
+
+type TableInfoRow = {
+ name: string;
+ type: string;
+ notnull: number;
+ dflt_value: string | null;
+ pk: number;
+};
+
+type IndexListRow = {
+ name: string;
+ unique: number;
+ origin: string;
+ partial: number;
+};
+
+type IndexInfoRow = {
+ seqno: number;
+ cid: number;
+ name: string;
+};
+
+type ForeignKeyRow = {
+ id: number;
+ seq: number;
+ table: string;
+ from: string;
+ to: string;
+ on_update: string;
+ on_delete: string;
+ match: string;
+};
+
+function resolveDbDir(): string {
+ return dirname(fileURLToPath(import.meta.url));
+}
+
+export function resolveMigrationsFolder(): string {
+ return resolve(resolveDbDir(), '../../../drizzle');
+}
+
+export function resolveGeneratedSchemaContractPath(): string {
+ return resolve(resolveDbDir(), 'generated/schemaContract.json');
+}
+
+function resolveMigrationFiles(migrationsFolder: string): string[] {
+ return readdirSync(migrationsFolder)
+ .filter((entry) => entry.endsWith('.sql'))
+ .sort((left, right) => left.localeCompare(right, 'en'));
+}
+
+function splitMigrationStatements(sqlText: string): string[] {
+ return sqlText
+ .split('--> statement-breakpoint')
+ .map((statement) => statement.trim())
+ .filter((statement) => statement.length > 0);
+}
+
+function applySqliteMigrations(sqlite: Database.Database, migrationsFolder: string): void {
+ for (const migrationFile of resolveMigrationFiles(migrationsFolder)) {
+ const sqlText = readFileSync(join(migrationsFolder, migrationFile), 'utf8');
+ for (const statement of splitMigrationStatements(sqlText)) {
+ sqlite.exec(statement);
+ }
+ }
+}
+
+function normalizeDefaultValue(defaultValue: string | null): string | null {
+ return normalizeSchemaMetadataDefaultValue(defaultValue);
+}
+
+function normalizeLogicalType(columnName: string, declaredType: string, defaultValue: string | null): LogicalColumnType {
+ return normalizeLogicalColumnType({
+ columnName,
+ declaredType,
+ defaultValue,
+ dialect: 'sqlite',
+ });
+}
+
+function readTables(sqlite: Database.Database): Record {
+ const tables = sqlite.prepare(`
+ SELECT name
+ FROM sqlite_master
+ WHERE type = 'table'
+ AND name NOT LIKE 'sqlite_%'
+ ORDER BY name ASC
+ `).all() as Array<{ name: string }>;
+
+ const result: Record = {};
+
+ for (const table of tables) {
+ const rows = sqlite.prepare(`PRAGMA table_info("${table.name}")`).all() as TableInfoRow[];
+ const columns = Object.fromEntries(rows.map((row) => {
+ const defaultValue = normalizeDefaultValue(row.dflt_value);
+ return [row.name, {
+ logicalType: normalizeLogicalType(row.name, row.type, defaultValue),
+ notNull: row.notnull === 1,
+ defaultValue,
+ primaryKey: row.pk > 0,
+ } satisfies SchemaContractColumn];
+ }));
+
+ result[table.name] = { columns };
+ }
+
+ return result;
+}
+
+function readIndexes(sqlite: Database.Database, tables: Record): {
+ indexes: SchemaContractIndex[];
+ uniques: SchemaContractUnique[];
+} {
+ const indexes: SchemaContractIndex[] = [];
+ const uniques: SchemaContractUnique[] = [];
+
+ for (const tableName of Object.keys(tables).sort((left, right) => left.localeCompare(right, 'en'))) {
+ const rows = sqlite.prepare(`PRAGMA index_list("${tableName}")`).all() as IndexListRow[];
+ for (const row of rows) {
+ if (!row.name || row.name.startsWith('sqlite_autoindex')) {
+ continue;
+ }
+ const indexColumns = (sqlite.prepare(`PRAGMA index_info("${row.name}")`).all() as IndexInfoRow[])
+ .sort((left, right) => left.seqno - right.seqno)
+ .map((item) => item.name)
+ .filter((name) => !!name);
+
+ const index: SchemaContractIndex = {
+ name: row.name,
+ table: tableName,
+ columns: indexColumns,
+ unique: row.unique === 1,
+ };
+ indexes.push(index);
+
+ if (row.unique === 1) {
+ uniques.push({
+ name: row.name,
+ table: tableName,
+ columns: indexColumns,
+ });
+ }
+ }
+ }
+
+ indexes.sort((left, right) => left.name.localeCompare(right.name, 'en'));
+ uniques.sort((left, right) => left.name.localeCompare(right.name, 'en'));
+
+ return { indexes, uniques };
+}
+
+function readForeignKeys(sqlite: Database.Database, tables: Record): SchemaContractForeignKey[] {
+ const foreignKeys: SchemaContractForeignKey[] = [];
+
+ for (const tableName of Object.keys(tables).sort((left, right) => left.localeCompare(right, 'en'))) {
+ const rows = sqlite.prepare(`PRAGMA foreign_key_list("${tableName}")`).all() as ForeignKeyRow[];
+ const grouped = new Map();
+
+ for (const row of rows) {
+ const existing = grouped.get(row.id);
+ if (existing) {
+ existing.columns.push(row.from);
+ existing.referencedColumns.push(row.to);
+ continue;
+ }
+
+ grouped.set(row.id, {
+ table: tableName,
+ columns: [row.from],
+ referencedTable: row.table,
+ referencedColumns: [row.to],
+ onDelete: row.on_delete || null,
+ });
+ }
+
+ foreignKeys.push(...grouped.values());
+ }
+
+ foreignKeys.sort((left, right) => {
+ const leftKey = `${left.table}:${left.columns.join(',')}`;
+ const rightKey = `${right.table}:${right.columns.join(',')}`;
+ return leftKey.localeCompare(rightKey, 'en');
+ });
+
+ return foreignKeys;
+}
+
+export function buildSchemaContractFromSqliteMigrations(migrationsFolder = resolveMigrationsFolder()): SchemaContract {
+ const sqlite = new Database(':memory:');
+ sqlite.pragma('foreign_keys = ON');
+
+ try {
+ applySqliteMigrations(sqlite, migrationsFolder);
+ const tables = readTables(sqlite);
+ const { indexes, uniques } = readIndexes(sqlite, tables);
+ const foreignKeys = readForeignKeys(sqlite, tables);
+
+ return {
+ tables,
+ indexes,
+ uniques,
+ foreignKeys,
+ };
+ } finally {
+ sqlite.close();
+ }
+}
+
+export function writeSchemaContractFile(outputPath = resolveGeneratedSchemaContractPath()): SchemaContract {
+ const contract = buildSchemaContractFromSqliteMigrations();
+ mkdirSync(dirname(outputPath), { recursive: true });
+ writeFileSync(outputPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
+ return contract;
+}
+
+export const __schemaContractTestUtils = {
+ splitMigrationStatements,
+ normalizeDefaultValue,
+ normalizeLogicalType,
+ resolveMigrationsFolder,
+};
diff --git a/src/server/db/schemaIntrospection.test.ts b/src/server/db/schemaIntrospection.test.ts
new file mode 100644
index 00000000..7f578a5d
--- /dev/null
+++ b/src/server/db/schemaIntrospection.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest';
+import { normalizeDefaultValue, normalizeSqlType, readMySqlField } from './schemaIntrospection.js';
+
+describe('schema introspection normalization', () => {
+ it('normalizes booleans consistently across dialects', () => {
+ expect(normalizeSqlType('sqlite', 'INTEGER', 'use_system_proxy')).toBe('boolean');
+ expect(normalizeSqlType('mysql', 'tinyint', 'use_system_proxy')).toBe('boolean');
+ expect(normalizeSqlType('postgres', 'boolean', 'use_system_proxy')).toBe('boolean');
+ });
+
+ it('normalizes common default values', () => {
+ expect(normalizeDefaultValue("DEFAULT 'active'")).toBe("'active'");
+ expect(normalizeDefaultValue('DEFAULT FALSE')).toBe('false');
+ expect(normalizeDefaultValue("datetime('now')")).toBe("datetime('now')");
+ });
+
+ it('reads mysql information_schema fields regardless of casing', () => {
+ expect(readMySqlField({ COLUMN_TYPE: 'varchar(191)' }, 'column_type')).toBe('varchar(191)');
+ expect(readMySqlField({ column_type: 'text' }, 'column_type')).toBe('text');
+ expect(readMySqlField({ Table_Name: 'settings' }, 'table_name')).toBe('settings');
+ });
+});
diff --git a/src/server/db/schemaIntrospection.ts b/src/server/db/schemaIntrospection.ts
new file mode 100644
index 00000000..81a64722
--- /dev/null
+++ b/src/server/db/schemaIntrospection.ts
@@ -0,0 +1,875 @@
+import Database from 'better-sqlite3';
+import { mkdtempSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
+import mysql from 'mysql2/promise';
+import { tmpdir } from 'node:os';
+import pg from 'pg';
+import { join, resolve } from 'node:path';
+import { randomUUID } from 'node:crypto';
+import {
+ generateBootstrapSql,
+ generateUpgradeSql,
+ resolveGeneratedArtifactPath,
+} from './schemaArtifactGenerator.js';
+import type {
+ SchemaContract,
+ SchemaContractColumn,
+ SchemaContractForeignKey,
+ SchemaContractIndex,
+ SchemaContractTable,
+ SchemaContractUnique,
+} from './schemaContract.js';
+import { resolveMigrationsFolder } from './schemaContract.js';
+import { installPostgresJsonTextParsers } from './postgresJsonTextParsers.js';
+import {
+ normalizeLogicalColumnType,
+ type LogicalColumnType,
+} from './schemaMetadata.js';
+
+export type SchemaIntrospectionDialect = 'sqlite' | 'mysql' | 'postgres';
+
+export interface SchemaIntrospectionInput {
+ dialect: SchemaIntrospectionDialect;
+ connectionString: string;
+ ssl?: boolean;
+}
+
+export interface MaterializeFreshSchemaOptions {
+ connectionString?: string;
+ ssl?: boolean;
+}
+
+export interface ApplyContractFixtureThenUpgradeOptions extends MaterializeFreshSchemaOptions {}
+
+type SqliteTableInfoRow = {
+ name: string;
+ type: string;
+ notnull: number;
+ dflt_value: string | null;
+ pk: number;
+};
+
+type SqliteIndexListRow = {
+ name: string;
+ unique: number;
+};
+
+type SqliteIndexInfoRow = {
+ seqno: number;
+ name: string;
+};
+
+type SqliteForeignKeyRow = {
+ id: number;
+ table: string;
+ from: string;
+ to: string;
+ on_delete: string;
+};
+
+type MySqlColumnRow = {
+ table_name: string;
+ column_name: string;
+ data_type: string;
+ column_type: string;
+ is_nullable: 'YES' | 'NO';
+ column_default: string | null;
+};
+
+type MySqlPrimaryKeyRow = {
+ table_name: string;
+ column_name: string;
+};
+
+type MySqlIndexRow = {
+ table_name: string;
+ index_name: string;
+ column_name: string;
+ non_unique: number;
+};
+
+type MySqlForeignKeyRow = {
+ table_name: string;
+ constraint_name: string;
+ column_name: string;
+ referenced_table_name: string;
+ referenced_column_name: string;
+ delete_rule: string | null;
+};
+
+type PostgresColumnRow = {
+ table_name: string;
+ column_name: string;
+ data_type: string;
+ udt_name: string;
+ is_nullable: 'YES' | 'NO';
+ column_default: string | null;
+};
+
+type PostgresPrimaryKeyRow = {
+ table_name: string;
+ column_name: string;
+};
+
+type PostgresIndexRow = {
+ table_name: string;
+ index_name: string;
+ is_unique: boolean;
+ column_name: string;
+};
+
+type PostgresForeignKeyRow = {
+ table_name: string;
+ constraint_name: string;
+ column_name: string;
+ referenced_table_name: string;
+ referenced_column_name: string;
+ delete_rule: string | null;
+};
+
+export function readMySqlField(row: Record, field: string): T | undefined {
+ const exact = row[field];
+ if (exact !== undefined) return exact as T;
+
+ const upper = row[field.toUpperCase()];
+ if (upper !== undefined) return upper as T;
+
+ const lower = row[field.toLowerCase()];
+ if (lower !== undefined) return lower as T;
+
+ const matchedKey = Object.keys(row).find((key) => key.toLowerCase() === field.toLowerCase());
+ return matchedKey ? row[matchedKey] as T : undefined;
+}
+
+function splitMigrationStatements(sqlText: string): string[] {
+ return sqlText
+ .split('--> statement-breakpoint')
+ .map((statement) => statement.trim())
+ .filter((statement) => statement.length > 0);
+}
+
+function splitSqlStatements(sqlText: string): string[] {
+ const withoutCommentLines = sqlText
+ .split(/\r?\n/g)
+ .filter((line) => !line.trim().startsWith('--'))
+ .join('\n');
+
+ return withoutCommentLines
+ .split(/;\s*(?:\r?\n|$)/g)
+ .map((statement) => statement.trim())
+ .filter((statement) => statement.length > 0);
+}
+
+function hasBalancedParentheses(value: string): boolean {
+ let depth = 0;
+ for (const char of value) {
+ if (char === '(') depth += 1;
+ if (char === ')') depth -= 1;
+ if (depth < 0) return false;
+ }
+ return depth === 0;
+}
+
+function unwrapSurroundingParentheses(value: string): string {
+ let normalized = value.trim();
+ while (normalized.startsWith('(') && normalized.endsWith(')')) {
+ const inner = normalized.slice(1, -1).trim();
+ if (!hasBalancedParentheses(inner)) {
+ break;
+ }
+ normalized = inner;
+ }
+ return normalized;
+}
+
+function normalizeDeleteRule(value: string | null | undefined): string | null {
+ if (!value) return null;
+ return value.trim().replace(/\s+/g, ' ').toUpperCase();
+}
+
+export function normalizeSqlType(
+ dialect: SchemaIntrospectionDialect,
+ declaredType: string,
+ columnName: string,
+ rawDefaultValue: string | null = null,
+): LogicalColumnType {
+ return normalizeLogicalColumnType({
+ dialect,
+ declaredType,
+ columnName,
+ defaultValue: normalizeDefaultValue(rawDefaultValue),
+ });
+}
+
+function normalizeDefaultValueForColumn(
+ rawDefaultValue: string | null | undefined,
+ logicalType: LogicalColumnType | null,
+): string | null {
+ if (rawDefaultValue == null) return null;
+
+ let normalized = String(rawDefaultValue).trim();
+ if (!normalized) return null;
+
+ normalized = normalized.replace(/^default\s+/i, '').trim();
+ normalized = unwrapSurroundingParentheses(normalized);
+ normalized = normalized.replace(/::[\w\s.\[\]"]+/g, '').trim();
+ normalized = unwrapSurroundingParentheses(normalized);
+
+ const lowered = normalized.toLowerCase();
+ if (lowered === 'null') return null;
+
+ if (logicalType === 'datetime' || lowered === 'current_timestamp' || lowered === 'current_timestamp()' || lowered === 'now()' || lowered.includes("datetime('now')")) {
+ return "datetime('now')";
+ }
+
+ if (logicalType === 'boolean') {
+ if (lowered === '1' || lowered === 'true' || lowered === "b'1'") return 'true';
+ if (lowered === '0' || lowered === 'false' || lowered === "b'0'") return 'false';
+ }
+
+ if (lowered === 'true' || lowered === 'false') return lowered;
+ if (/^-?\d+(?:\.\d+)?$/.test(normalized)) return normalized;
+ if (/^'.*'$/.test(normalized)) return normalized;
+ if (/^[a-z_][a-z0-9_]*$/i.test(normalized)) return `'${normalized}'`;
+ return normalized;
+}
+
+export function normalizeDefaultValue(rawDefaultValue: string | null | undefined): string | null {
+ return normalizeDefaultValueForColumn(rawDefaultValue, null);
+}
+
+function sortForeignKeys(foreignKeys: SchemaContractForeignKey[]): SchemaContractForeignKey[] {
+ return foreignKeys.sort((left, right) => {
+ const leftKey = `${left.table}:${left.columns.join(',')}`;
+ const rightKey = `${right.table}:${right.columns.join(',')}`;
+ return leftKey.localeCompare(rightKey, 'en');
+ });
+}
+
+function buildSqliteTables(sqlite: Database.Database): Record {
+ const tableRows = sqlite.prepare(`
+ SELECT name
+ FROM sqlite_master
+ WHERE type = 'table'
+ AND name NOT LIKE 'sqlite_%'
+ ORDER BY name ASC
+ `).all() as Array<{ name: string }>;
+
+ const tables: Record = {};
+ for (const { name: tableName } of tableRows) {
+ const rows = sqlite.prepare(`PRAGMA table_info("${tableName}")`).all() as SqliteTableInfoRow[];
+ const columns = Object.fromEntries(rows.map((row) => {
+ const logicalType = normalizeSqlType('sqlite', row.type, row.name, row.dflt_value);
+ return [row.name, {
+ logicalType,
+ notNull: row.notnull === 1,
+ defaultValue: normalizeDefaultValueForColumn(row.dflt_value, logicalType),
+ primaryKey: row.pk > 0,
+ } satisfies SchemaContractColumn];
+ }));
+ tables[tableName] = { columns };
+ }
+ return tables;
+}
+
+function buildSqliteIndexes(sqlite: Database.Database, tables: Record): {
+ indexes: SchemaContractIndex[];
+ uniques: SchemaContractUnique[];
+} {
+ const indexes: SchemaContractIndex[] = [];
+ const uniques: SchemaContractUnique[] = [];
+
+ for (const tableName of Object.keys(tables).sort((left, right) => left.localeCompare(right, 'en'))) {
+ const rows = sqlite.prepare(`PRAGMA index_list("${tableName}")`).all() as SqliteIndexListRow[];
+ for (const row of rows) {
+ if (!row.name || row.name.startsWith('sqlite_autoindex')) {
+ continue;
+ }
+
+ const columns = (sqlite.prepare(`PRAGMA index_info("${row.name}")`).all() as SqliteIndexInfoRow[])
+ .sort((left, right) => left.seqno - right.seqno)
+ .map((item) => item.name)
+ .filter(Boolean);
+
+ indexes.push({
+ name: row.name,
+ table: tableName,
+ columns,
+ unique: row.unique === 1,
+ });
+
+ if (row.unique === 1) {
+ uniques.push({
+ name: row.name,
+ table: tableName,
+ columns,
+ });
+ }
+ }
+ }
+
+ indexes.sort((left, right) => left.name.localeCompare(right.name, 'en'));
+ uniques.sort((left, right) => left.name.localeCompare(right.name, 'en'));
+ return { indexes, uniques };
+}
+
+function buildSqliteForeignKeys(sqlite: Database.Database, tables: Record): SchemaContractForeignKey[] {
+ const foreignKeys: SchemaContractForeignKey[] = [];
+
+ for (const tableName of Object.keys(tables).sort((left, right) => left.localeCompare(right, 'en'))) {
+ const rows = sqlite.prepare(`PRAGMA foreign_key_list("${tableName}")`).all() as SqliteForeignKeyRow[];
+ const grouped = new Map();
+
+ for (const row of rows) {
+ const existing = grouped.get(row.id);
+ if (existing) {
+ existing.columns.push(row.from);
+ existing.referencedColumns.push(row.to);
+ continue;
+ }
+
+ grouped.set(row.id, {
+ table: tableName,
+ columns: [row.from],
+ referencedTable: row.table,
+ referencedColumns: [row.to],
+ onDelete: normalizeDeleteRule(row.on_delete),
+ });
+ }
+
+ foreignKeys.push(...grouped.values());
+ }
+
+ return sortForeignKeys(foreignKeys);
+}
+
+async function introspectSqliteSchema(connectionString: string): Promise {
+ const sqlitePath = connectionString === ':memory:' ? ':memory:' : resolve(connectionString);
+ const sqlite = new Database(sqlitePath, { readonly: true });
+ sqlite.pragma('foreign_keys = ON');
+
+ try {
+ const tables = buildSqliteTables(sqlite);
+ const { indexes, uniques } = buildSqliteIndexes(sqlite, tables);
+ const foreignKeys = buildSqliteForeignKeys(sqlite, tables);
+ return { tables, indexes, uniques, foreignKeys };
+ } finally {
+ sqlite.close();
+ }
+}
+
+async function introspectMySqlSchema(input: SchemaIntrospectionInput): Promise {
+ const connectionOptions: mysql.ConnectionOptions = { uri: input.connectionString };
+ if (input.ssl) {
+ connectionOptions.ssl = { rejectUnauthorized: false };
+ }
+ const connection = await mysql.createConnection(connectionOptions);
+
+ try {
+ const [tableRows] = await connection.query(`
+ SELECT table_name AS table_name
+ FROM information_schema.tables
+ WHERE table_schema = DATABASE()
+ AND table_type = 'BASE TABLE'
+ ORDER BY table_name ASC
+ `);
+ const tableNames = (tableRows as Array>)
+ .map((row) => readMySqlField(row, 'table_name'))
+ .filter((tableName): tableName is string => typeof tableName === 'string');
+
+ const [primaryKeyRows] = await connection.query(`
+ SELECT
+ kcu.table_name AS table_name,
+ kcu.column_name AS column_name
+ FROM information_schema.table_constraints tc
+ JOIN information_schema.key_column_usage kcu
+ ON tc.constraint_schema = kcu.constraint_schema
+ AND tc.constraint_name = kcu.constraint_name
+ AND tc.table_name = kcu.table_name
+ WHERE tc.table_schema = DATABASE()
+ AND tc.constraint_type = 'PRIMARY KEY'
+ `);
+ const primaryKeys = new Set((primaryKeyRows as Array>).map((row) => {
+ const tableName = readMySqlField(row, 'table_name');
+ const columnName = readMySqlField(row, 'column_name');
+ return tableName && columnName ? `${tableName}.${columnName}` : null;
+ }).filter((value): value is string => value !== null));
+
+ const [columnRows] = await connection.query(`
+ SELECT
+ table_name AS table_name,
+ column_name AS column_name,
+ data_type AS data_type,
+ column_type AS column_type,
+ is_nullable AS is_nullable,
+ column_default AS column_default
+ FROM information_schema.columns
+ WHERE table_schema = DATABASE()
+ ORDER BY table_name ASC, ordinal_position ASC
+ `);
+
+ const tableMap = new Map();
+ for (const tableName of tableNames) {
+ tableMap.set(tableName, { columns: {} });
+ }
+
+ for (const row of columnRows as Array>) {
+ const tableName = readMySqlField(row, 'table_name');
+ const columnName = readMySqlField(row, 'column_name');
+ const declaredType = readMySqlField(row, 'column_type') || readMySqlField(row, 'data_type');
+ const isNullable = readMySqlField(row, 'is_nullable');
+ const columnDefault = readMySqlField(row, 'column_default') ?? null;
+ if (!tableName || !columnName || !declaredType) {
+ continue;
+ }
+
+ const logicalType = normalizeSqlType('mysql', declaredType, columnName, columnDefault);
+ tableMap.get(tableName)!.columns[columnName] = {
+ logicalType,
+ notNull: isNullable === 'NO',
+ defaultValue: primaryKeys.has(`${tableName}.${columnName}`)
+ ? null
+ : normalizeDefaultValueForColumn(columnDefault, logicalType),
+ primaryKey: primaryKeys.has(`${tableName}.${columnName}`),
+ };
+ }
+
+ const tables = Object.fromEntries(tableNames.map((tableName) => [tableName, tableMap.get(tableName)!]));
+
+ const [indexRows] = await connection.query(`
+ SELECT
+ table_name AS table_name,
+ index_name AS index_name,
+ column_name AS column_name,
+ non_unique AS non_unique
+ FROM information_schema.statistics
+ WHERE table_schema = DATABASE()
+ AND index_name <> 'PRIMARY'
+ ORDER BY table_name ASC, index_name ASC, seq_in_index ASC
+ `);
+ const indexGroups = new Map();
+ for (const row of indexRows as Array>) {
+ const tableName = readMySqlField(row, 'table_name');
+ const indexName = readMySqlField(row, 'index_name');
+ const columnName = readMySqlField(row, 'column_name');
+ const nonUnique = readMySqlField(row, 'non_unique');
+ if (!tableName || !indexName || !columnName) {
+ continue;
+ }
+
+ const key = `${tableName}:${indexName}`;
+ const existing = indexGroups.get(key);
+ if (existing) {
+ existing.columns.push(columnName);
+ continue;
+ }
+ indexGroups.set(key, {
+ name: indexName,
+ table: tableName,
+ columns: [columnName],
+ unique: Number(nonUnique) === 0,
+ });
+ }
+
+ const indexes = [...indexGroups.values()].sort((left, right) => left.name.localeCompare(right.name, 'en'));
+ const uniques = indexes
+ .filter((index) => index.unique)
+ .map((index) => ({ name: index.name, table: index.table, columns: [...index.columns] }))
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'));
+
+ const [foreignKeyRows] = await connection.query(`
+ SELECT
+ kcu.table_name AS table_name,
+ kcu.constraint_name AS constraint_name,
+ kcu.column_name AS column_name,
+ kcu.referenced_table_name AS referenced_table_name,
+ kcu.referenced_column_name AS referenced_column_name,
+ rc.delete_rule AS delete_rule
+ FROM information_schema.key_column_usage kcu
+ JOIN information_schema.referential_constraints rc
+ ON rc.constraint_schema = kcu.constraint_schema
+ AND rc.constraint_name = kcu.constraint_name
+ WHERE kcu.table_schema = DATABASE()
+ AND kcu.referenced_table_name IS NOT NULL
+ ORDER BY kcu.table_name ASC, kcu.constraint_name ASC, kcu.ordinal_position ASC
+ `);
+ const foreignKeyGroups = new Map();
+ for (const row of foreignKeyRows as Array>) {
+ const tableName = readMySqlField(row, 'table_name');
+ const constraintName = readMySqlField(row, 'constraint_name');
+ const columnName = readMySqlField(row, 'column_name');
+ const referencedTableName = readMySqlField(row, 'referenced_table_name');
+ const referencedColumnName = readMySqlField(row, 'referenced_column_name');
+ const deleteRule = readMySqlField(row, 'delete_rule') ?? null;
+ if (!tableName || !constraintName || !columnName || !referencedTableName || !referencedColumnName) {
+ continue;
+ }
+
+ const key = `${tableName}:${constraintName}`;
+ const existing = foreignKeyGroups.get(key);
+ if (existing) {
+ existing.columns.push(columnName);
+ existing.referencedColumns.push(referencedColumnName);
+ continue;
+ }
+ foreignKeyGroups.set(key, {
+ table: tableName,
+ columns: [columnName],
+ referencedTable: referencedTableName,
+ referencedColumns: [referencedColumnName],
+ onDelete: normalizeDeleteRule(deleteRule),
+ });
+ }
+
+ return {
+ tables,
+ indexes,
+ uniques,
+ foreignKeys: sortForeignKeys([...foreignKeyGroups.values()]),
+ };
+ } finally {
+ await connection.end();
+ }
+}
+
+async function introspectPostgresSchema(input: SchemaIntrospectionInput): Promise {
+ const clientOptions: pg.ClientConfig = { connectionString: input.connectionString };
+ if (input.ssl) {
+ clientOptions.ssl = { rejectUnauthorized: false };
+ }
+ installPostgresJsonTextParsers();
+ const client = new pg.Client(clientOptions);
+ await client.connect();
+
+ try {
+ const tableResult = await client.query(`
+ SELECT table_name
+ FROM information_schema.tables
+ WHERE table_schema = current_schema()
+ AND table_type = 'BASE TABLE'
+ ORDER BY table_name ASC
+ `);
+ const tableNames = tableResult.rows.map((row) => String(row.table_name));
+
+ const primaryKeyResult = await client.query(`
+ SELECT kcu.table_name, kcu.column_name
+ FROM information_schema.table_constraints tc
+ JOIN information_schema.key_column_usage kcu
+ ON tc.constraint_schema = kcu.constraint_schema
+ AND tc.constraint_name = kcu.constraint_name
+ AND tc.table_name = kcu.table_name
+ WHERE tc.table_schema = current_schema()
+ AND tc.constraint_type = 'PRIMARY KEY'
+ `);
+ const primaryKeys = new Set((primaryKeyResult.rows as PostgresPrimaryKeyRow[]).map((row) => `${row.table_name}.${row.column_name}`));
+
+ const columnResult = await client.query(`
+ SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default
+ FROM information_schema.columns
+ WHERE table_schema = current_schema()
+ ORDER BY table_name ASC, ordinal_position ASC
+ `);
+ const tableMap = new Map();
+ for (const tableName of tableNames) {
+ tableMap.set(tableName, { columns: {} });
+ }
+
+ for (const row of columnResult.rows as PostgresColumnRow[]) {
+ const logicalType = normalizeSqlType('postgres', `${row.data_type} ${row.udt_name}`, row.column_name, row.column_default);
+ tableMap.get(row.table_name)!.columns[row.column_name] = {
+ logicalType,
+ notNull: row.is_nullable === 'NO',
+ defaultValue: primaryKeys.has(`${row.table_name}.${row.column_name}`)
+ ? null
+ : normalizeDefaultValueForColumn(row.column_default, logicalType),
+ primaryKey: primaryKeys.has(`${row.table_name}.${row.column_name}`),
+ };
+ }
+
+ const tables = Object.fromEntries(tableNames.map((tableName) => [tableName, tableMap.get(tableName)!]));
+
+ const indexResult = await client.query(`
+ SELECT
+ t.relname AS table_name,
+ i.relname AS index_name,
+ ix.indisunique AS is_unique,
+ a.attname AS column_name
+ FROM pg_class t
+ JOIN pg_namespace ns ON ns.oid = t.relnamespace
+ JOIN pg_index ix ON t.oid = ix.indrelid
+ JOIN pg_class i ON i.oid = ix.indexrelid
+ JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS ord(attnum, n) ON TRUE
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ord.attnum
+ WHERE ns.nspname = current_schema()
+ AND t.relkind = 'r'
+ AND NOT ix.indisprimary
+ ORDER BY t.relname ASC, i.relname ASC, ord.n ASC
+ `);
+ const indexGroups = new Map();
+ for (const row of indexResult.rows as PostgresIndexRow[]) {
+ const key = `${row.table_name}:${row.index_name}`;
+ const existing = indexGroups.get(key);
+ if (existing) {
+ existing.columns.push(row.column_name);
+ continue;
+ }
+ indexGroups.set(key, {
+ name: row.index_name,
+ table: row.table_name,
+ columns: [row.column_name],
+ unique: !!row.is_unique,
+ });
+ }
+
+ const indexes = [...indexGroups.values()].sort((left, right) => left.name.localeCompare(right.name, 'en'));
+ const uniques = indexes
+ .filter((index) => index.unique)
+ .map((index) => ({ name: index.name, table: index.table, columns: [...index.columns] }))
+ .sort((left, right) => left.name.localeCompare(right.name, 'en'));
+
+ const foreignKeyResult = await client.query(`
+ SELECT
+ tc.table_name,
+ tc.constraint_name,
+ kcu.column_name,
+ ccu.table_name AS referenced_table_name,
+ ccu.column_name AS referenced_column_name,
+ rc.delete_rule
+ FROM information_schema.table_constraints tc
+ JOIN information_schema.key_column_usage kcu
+ ON tc.constraint_schema = kcu.constraint_schema
+ AND tc.constraint_name = kcu.constraint_name
+ AND tc.table_name = kcu.table_name
+ JOIN information_schema.referential_constraints rc
+ ON tc.constraint_schema = rc.constraint_schema
+ AND tc.constraint_name = rc.constraint_name
+ JOIN information_schema.constraint_column_usage ccu
+ ON rc.unique_constraint_schema = ccu.constraint_schema
+ AND rc.unique_constraint_name = ccu.constraint_name
+ WHERE tc.table_schema = current_schema()
+ AND tc.constraint_type = 'FOREIGN KEY'
+ ORDER BY tc.table_name ASC, tc.constraint_name ASC, kcu.ordinal_position ASC
+ `);
+ const foreignKeyGroups = new Map();
+ for (const row of foreignKeyResult.rows as PostgresForeignKeyRow[]) {
+ const key = `${row.table_name}:${row.constraint_name}`;
+ const existing = foreignKeyGroups.get(key);
+ if (existing) {
+ existing.columns.push(row.column_name);
+ existing.referencedColumns.push(row.referenced_column_name);
+ continue;
+ }
+ foreignKeyGroups.set(key, {
+ table: row.table_name,
+ columns: [row.column_name],
+ referencedTable: row.referenced_table_name,
+ referencedColumns: [row.referenced_column_name],
+ onDelete: normalizeDeleteRule(row.delete_rule),
+ });
+ }
+
+ return {
+ tables,
+ indexes,
+ uniques,
+ foreignKeys: sortForeignKeys([...foreignKeyGroups.values()]),
+ };
+ } finally {
+ await client.end();
+ }
+}
+
+export async function introspectLiveSchema(input: SchemaIntrospectionInput): Promise {
+ if (input.dialect === 'sqlite') {
+ return introspectSqliteSchema(input.connectionString);
+ }
+ if (input.dialect === 'mysql') {
+ return introspectMySqlSchema(input);
+ }
+ return introspectPostgresSchema(input);
+}
+
+function readBootstrapSql(dialect: Exclude): string {
+ const filename = dialect === 'mysql' ? 'mysql.bootstrap.sql' : 'postgres.bootstrap.sql';
+ return readFileSync(resolveGeneratedArtifactPath(filename), 'utf8');
+}
+
+async function resetMySqlSchema(connection: mysql.Connection): Promise {
+ await connection.query('SET FOREIGN_KEY_CHECKS = 0');
+ const [rows] = await connection.query(`
+ SELECT table_name AS table_name
+ FROM information_schema.tables
+ WHERE table_schema = DATABASE()
+ AND table_type = 'BASE TABLE'
+ `);
+ for (const row of rows as Array>) {
+ const tableName = readMySqlField(row, 'table_name');
+ if (!tableName) {
+ continue;
+ }
+ await connection.query(`DROP TABLE IF EXISTS \`${tableName}\``);
+ }
+ await connection.query('SET FOREIGN_KEY_CHECKS = 1');
+}
+
+async function resetPostgresSchema(client: pg.Client): Promise {
+ const result = await client.query(`
+ SELECT tablename
+ FROM pg_tables
+ WHERE schemaname = current_schema()
+ ORDER BY tablename ASC
+ `);
+ for (const row of result.rows as Array<{ tablename: string }>) {
+ await client.query(`DROP TABLE IF EXISTS "${row.tablename}" CASCADE`);
+ }
+}
+
+function applySqliteMigrations(sqlite: Database.Database): void {
+ const migrationsFolder = resolveMigrationsFolder();
+ const migrationFiles = readdirSync(migrationsFolder)
+ .filter((entry) => entry.endsWith('.sql'))
+ .sort((left, right) => left.localeCompare(right, 'en'));
+
+ for (const migrationFile of migrationFiles) {
+ const sqlText = readFileSync(join(migrationsFolder, migrationFile), 'utf8');
+ for (const statement of splitMigrationStatements(sqlText)) {
+ sqlite.exec(statement);
+ }
+ }
+}
+
+function createTemporarySqlitePath(): string {
+ const tempDir = mkdtempSync(join(tmpdir(), 'metapi-schema-parity-'));
+ return resolve(tempDir, `${randomUUID()}.db`);
+}
+
+function applySqliteStatements(sqlitePath: string, statements: string[]): void {
+ const sqlite = new Database(sqlitePath);
+ sqlite.pragma('foreign_keys = ON');
+ try {
+ for (const statement of statements) {
+ sqlite.exec(statement);
+ }
+ } finally {
+ sqlite.close();
+ }
+}
+
+async function applyMySqlStatements(
+ connectionString: string,
+ ssl: boolean | undefined,
+ statements: string[],
+ resetSchema = false,
+): Promise {
+ const connectionOptions: mysql.ConnectionOptions = { uri: connectionString };
+ if (ssl) {
+ connectionOptions.ssl = { rejectUnauthorized: false };
+ }
+ const connection = await mysql.createConnection(connectionOptions);
+ try {
+ if (resetSchema) {
+ await resetMySqlSchema(connection);
+ }
+ for (const statement of statements) {
+ await connection.query(statement);
+ }
+ } finally {
+ await connection.end();
+ }
+}
+
+async function applyPostgresStatements(
+ connectionString: string,
+ ssl: boolean | undefined,
+ statements: string[],
+ resetSchema = false,
+): Promise {
+ const clientOptions: pg.ClientConfig = { connectionString };
+ if (ssl) {
+ clientOptions.ssl = { rejectUnauthorized: false };
+ }
+ installPostgresJsonTextParsers();
+ const client = new pg.Client(clientOptions);
+ await client.connect();
+ try {
+ if (resetSchema) {
+ await resetPostgresSchema(client);
+ }
+ for (const statement of statements) {
+ await client.query(statement);
+ }
+ } finally {
+ await client.end();
+ }
+}
+
+export async function materializeFreshSchema(
+ dialect: SchemaIntrospectionDialect,
+ options: MaterializeFreshSchemaOptions = {},
+): Promise {
+ if (dialect === 'sqlite') {
+ const sqlitePath = createTemporarySqlitePath();
+ const sqlite = new Database(sqlitePath);
+ sqlite.pragma('foreign_keys = ON');
+ try {
+ applySqliteMigrations(sqlite);
+ } finally {
+ sqlite.close();
+ }
+ return sqlitePath;
+ }
+
+ if (!options.connectionString) {
+ throw new Error(`connectionString is required to materialize ${dialect} parity schema`);
+ }
+
+ const bootstrapStatements = splitSqlStatements(readBootstrapSql(dialect));
+ if (dialect === 'mysql') {
+ await applyMySqlStatements(options.connectionString, options.ssl, bootstrapStatements, true);
+ return options.connectionString;
+ }
+
+ await applyPostgresStatements(options.connectionString, options.ssl, bootstrapStatements, true);
+ return options.connectionString;
+}
+
+export async function applyContractFixtureThenUpgrade(
+ dialect: SchemaIntrospectionDialect,
+ baselineContract: SchemaContract,
+ currentContract: SchemaContract,
+ options: ApplyContractFixtureThenUpgradeOptions = {},
+): Promise {
+ const bootstrapStatements = splitSqlStatements(generateBootstrapSql(dialect, baselineContract));
+ const upgradeStatements = splitSqlStatements(generateUpgradeSql(dialect, currentContract, baselineContract));
+
+ if (dialect === 'sqlite') {
+ const sqlitePath = createTemporarySqlitePath();
+ applySqliteStatements(sqlitePath, bootstrapStatements);
+ if (upgradeStatements.length > 0) {
+ applySqliteStatements(sqlitePath, upgradeStatements);
+ }
+ return sqlitePath;
+ }
+
+ if (!options.connectionString) {
+ throw new Error(`connectionString is required to upgrade ${dialect} parity schema`);
+ }
+
+ if (dialect === 'mysql') {
+ await applyMySqlStatements(options.connectionString, options.ssl, bootstrapStatements, true);
+ if (upgradeStatements.length > 0) {
+ await applyMySqlStatements(options.connectionString, options.ssl, upgradeStatements, false);
+ }
+ return options.connectionString;
+ }
+
+ await applyPostgresStatements(options.connectionString, options.ssl, bootstrapStatements, true);
+ if (upgradeStatements.length > 0) {
+ await applyPostgresStatements(options.connectionString, options.ssl, upgradeStatements, false);
+ }
+ return options.connectionString;
+}
+
+export const __schemaIntrospectionTestUtils = {
+ splitMigrationStatements,
+ splitSqlStatements,
+};
diff --git a/src/server/db/schemaMetadata.architecture.test.ts b/src/server/db/schemaMetadata.architecture.test.ts
new file mode 100644
index 00000000..6a765381
--- /dev/null
+++ b/src/server/db/schemaMetadata.architecture.test.ts
@@ -0,0 +1,22 @@
+import { readFileSync } from 'node:fs';
+import { describe, expect, it } from 'vitest';
+
+function readSource(relativePath: string): string {
+ return readFileSync(new URL(relativePath, import.meta.url), 'utf8');
+}
+
+describe('schema metadata architecture boundaries', () => {
+ it('keeps logical column type heuristics in a shared schemaMetadata helper', () => {
+ const contractSource = readSource('./schemaContract.ts');
+ const introspectionSource = readSource('./schemaIntrospection.ts');
+
+ expect(contractSource).toContain("from './schemaMetadata.js'");
+ expect(introspectionSource).toContain("from './schemaMetadata.js'");
+
+ for (const source of [contractSource, introspectionSource]) {
+ expect(source).not.toContain('function isBooleanLikeColumn');
+ expect(source).not.toContain('function isDateTimeLikeColumn');
+ expect(source).not.toContain('function isJsonLikeColumn');
+ }
+ });
+});
diff --git a/src/server/db/schemaMetadata.ts b/src/server/db/schemaMetadata.ts
new file mode 100644
index 00000000..c630ea82
--- /dev/null
+++ b/src/server/db/schemaMetadata.ts
@@ -0,0 +1,139 @@
+export type LogicalColumnType =
+ | 'integer'
+ | 'real'
+ | 'text'
+ | 'boolean'
+ | 'datetime'
+ | 'json';
+
+export type SchemaMetadataDialect = 'sqlite' | 'mysql' | 'postgres';
+
+function isBooleanLikeColumn(columnName: string, defaultValue: string | null): boolean {
+ const normalizedColumn = columnName.toLowerCase();
+ const normalizedDefault = (defaultValue || '').trim().toLowerCase();
+ if (normalizedDefault === 'true' || normalizedDefault === 'false') {
+ return true;
+ }
+ return normalizedColumn.startsWith('is_')
+ || normalizedColumn.startsWith('use_')
+ || normalizedColumn.startsWith('has_')
+ || normalizedColumn.endsWith('_enabled')
+ || normalizedColumn.endsWith('_available')
+ || normalizedColumn === 'read'
+ || normalizedColumn === 'enabled'
+ || normalizedColumn === 'available'
+ || normalizedColumn === 'manual_override';
+}
+
+function isDateTimeLikeColumn(columnName: string, defaultValue: string | null): boolean {
+ const normalizedColumn = columnName.toLowerCase();
+ const normalizedDefault = (defaultValue || '').toLowerCase();
+ return normalizedColumn.endsWith('_at')
+ || normalizedColumn.endsWith('_until')
+ || normalizedColumn.endsWith('_refresh')
+ || normalizedDefault.includes('datetime(')
+ || normalizedDefault.includes('current_timestamp')
+ || normalizedDefault.includes('now()');
+}
+
+function isJsonLikeColumn(columnName: string): boolean {
+ const normalizedColumn = columnName.toLowerCase();
+ return normalizedColumn.endsWith('_json')
+ || normalizedColumn.includes('snapshot')
+ || normalizedColumn.includes('mapping')
+ || normalizedColumn.includes('headers')
+ || normalizedColumn.includes('config')
+ || normalizedColumn.includes('details')
+ || normalizedColumn.includes('meta')
+ || normalizedColumn.includes('models')
+ || normalizedColumn.includes('route_ids')
+ || normalizedColumn.includes('multipliers');
+}
+
+export function normalizeSchemaMetadataDefaultValue(defaultValue: string | null | undefined): string | null {
+ if (defaultValue == null) return null;
+ return String(defaultValue).trim() || null;
+}
+
+export function normalizeLogicalColumnType(input: {
+ declaredType: string;
+ columnName: string;
+ defaultValue?: string | null;
+ dialect?: SchemaMetadataDialect;
+}): LogicalColumnType {
+ const normalizedType = input.declaredType.trim().toLowerCase();
+ const normalizedDefault = normalizeSchemaMetadataDefaultValue(input.defaultValue);
+ const dialect = input.dialect;
+
+ if (dialect === 'mysql') {
+ if (normalizedType.includes('tinyint(1)') || normalizedType.includes('boolean') || normalizedType.includes('bool')) {
+ return 'boolean';
+ }
+ if (normalizedType.includes('json')) {
+ return 'json';
+ }
+ if (normalizedType.includes('timestamp') || normalizedType.includes('datetime') || normalizedType === 'date') {
+ return 'datetime';
+ }
+ if (normalizedType.includes('int')) {
+ return isBooleanLikeColumn(input.columnName, normalizedDefault) ? 'boolean' : 'integer';
+ }
+ if (normalizedType.includes('double') || normalizedType.includes('float') || normalizedType.includes('real') || normalizedType.includes('decimal')) {
+ return 'real';
+ }
+ if (normalizedType.includes('char') || normalizedType.includes('text')) {
+ if (isDateTimeLikeColumn(input.columnName, normalizedDefault)) return 'datetime';
+ if (isJsonLikeColumn(input.columnName)) return 'json';
+ return 'text';
+ }
+ }
+
+ if (dialect === 'postgres') {
+ if (normalizedType.includes('bool')) {
+ return 'boolean';
+ }
+ if (normalizedType.includes('json')) {
+ return 'json';
+ }
+ if (normalizedType.includes('timestamp') || normalizedType === 'date') {
+ return 'datetime';
+ }
+ if (normalizedType.includes('int')) {
+ return isBooleanLikeColumn(input.columnName, normalizedDefault) ? 'boolean' : 'integer';
+ }
+ if (normalizedType.includes('double') || normalizedType.includes('real') || normalizedType.includes('numeric') || normalizedType.includes('decimal')) {
+ return 'real';
+ }
+ if (normalizedType.includes('char') || normalizedType.includes('text')) {
+ if (isDateTimeLikeColumn(input.columnName, normalizedDefault)) return 'datetime';
+ if (isJsonLikeColumn(input.columnName)) return 'json';
+ return 'text';
+ }
+ }
+
+ if (normalizedType.includes('int')) {
+ return isBooleanLikeColumn(input.columnName, normalizedDefault) ? 'boolean' : 'integer';
+ }
+ if (normalizedType.includes('real') || normalizedType.includes('double') || normalizedType.includes('float') || normalizedType.includes('decimal')) {
+ return 'real';
+ }
+ if (normalizedType.includes('json')) {
+ return 'json';
+ }
+ if (normalizedType.includes('timestamp') || normalizedType.includes('datetime') || normalizedType === 'date') {
+ return 'datetime';
+ }
+ if (normalizedType.includes('text') || normalizedType.includes('char') || normalizedType.includes('clob')) {
+ if (isDateTimeLikeColumn(input.columnName, normalizedDefault)) return 'datetime';
+ if (isJsonLikeColumn(input.columnName)) return 'json';
+ return 'text';
+ }
+ if (isDateTimeLikeColumn(input.columnName, normalizedDefault)) return 'datetime';
+ return 'text';
+}
+
+export const __schemaMetadataTestUtils = {
+ isBooleanLikeColumn,
+ isDateTimeLikeColumn,
+ isJsonLikeColumn,
+};
diff --git a/src/server/db/schemaParity.live.test.ts b/src/server/db/schemaParity.live.test.ts
new file mode 100644
index 00000000..25c862a6
--- /dev/null
+++ b/src/server/db/schemaParity.live.test.ts
@@ -0,0 +1,39 @@
+import { readFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import type { SchemaContract } from './schemaContract.js';
+import { introspectLiveSchema, materializeFreshSchema } from './schemaIntrospection.js';
+
+const dbDir = dirname(fileURLToPath(import.meta.url));
+const schemaContractPath = resolve(dbDir, 'generated/schemaContract.json');
+const contract = JSON.parse(readFileSync(schemaContractPath, 'utf8')) as SchemaContract;
+
+const skipLiveSchema = process.env.DB_PARITY_SKIP_LIVE_SCHEMA === 'true';
+const sqliteParity = !skipLiveSchema && process.env.DB_PARITY_SQLITE !== 'false' ? it : it.skip;
+const mysqlParity = process.env.DB_PARITY_MYSQL_URL ? it : it.skip;
+const postgresParity = process.env.DB_PARITY_POSTGRES_URL ? it : it.skip;
+
+describe('live schema parity', () => {
+ sqliteParity('matches the contract for sqlite', async () => {
+ const sqliteUrl = await materializeFreshSchema('sqlite');
+ const live = await introspectLiveSchema({ dialect: 'sqlite', connectionString: sqliteUrl });
+ expect(live).toEqual(contract);
+ });
+
+ mysqlParity('matches the contract for mysql', async () => {
+ const mysqlUrl = await materializeFreshSchema('mysql', {
+ connectionString: process.env.DB_PARITY_MYSQL_URL!,
+ });
+ const live = await introspectLiveSchema({ dialect: 'mysql', connectionString: mysqlUrl });
+ expect(live).toEqual(contract);
+ });
+
+ postgresParity('matches the contract for postgres', async () => {
+ const postgresUrl = await materializeFreshSchema('postgres', {
+ connectionString: process.env.DB_PARITY_POSTGRES_URL!,
+ });
+ const live = await introspectLiveSchema({ dialect: 'postgres', connectionString: postgresUrl });
+ expect(live).toEqual(contract);
+ });
+});
diff --git a/src/server/db/schemaParity.test.ts b/src/server/db/schemaParity.test.ts
new file mode 100644
index 00000000..995eb49e
--- /dev/null
+++ b/src/server/db/schemaParity.test.ts
@@ -0,0 +1,102 @@
+import { existsSync, readFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import type { SchemaContract } from './schemaContract.js';
+import { SHARED_INDEX_COMPATIBILITY_SPECS } from './sharedIndexSchemaCompatibility.js';
+
+const dbDir = dirname(fileURLToPath(import.meta.url));
+const generatedDir = resolve(dbDir, 'generated');
+const supportPaths = [
+ resolve(dbDir, 'runtimeSchemaBootstrap.ts'),
+ resolve(dbDir, 'siteSchemaCompatibility.ts'),
+ resolve(dbDir, 'routeGroupingSchemaCompatibility.ts'),
+ resolve(dbDir, 'proxyFileSchemaCompatibility.ts'),
+ resolve(dbDir, 'accountTokenSchemaCompatibility.ts'),
+ resolve(dbDir, 'sharedIndexSchemaCompatibility.ts'),
+];
+const schemaContractPath = resolve(generatedDir, 'schemaContract.json');
+
+function extractAllMatches(content: string, pattern: RegExp): string[] {
+ return Array.from(content.matchAll(pattern), (match) => match[1]);
+}
+
+describe('database schema parity', () => {
+ it('keeps generated schema artifacts present', () => {
+ const artifactPaths = [
+ schemaContractPath,
+ resolve(generatedDir, 'mysql.bootstrap.sql'),
+ resolve(generatedDir, 'mysql.upgrade.sql'),
+ resolve(generatedDir, 'postgres.bootstrap.sql'),
+ resolve(generatedDir, 'postgres.upgrade.sql'),
+ ];
+
+ for (const artifactPath of artifactPaths) {
+ expect(existsSync(artifactPath), artifactPath).toBe(true);
+ expect(readFileSync(artifactPath, 'utf8').trim().length).toBeGreaterThan(0);
+ }
+ });
+
+ it('keeps runtime support modules scoped to contract-defined tables and indexes', () => {
+ const contract = JSON.parse(readFileSync(schemaContractPath, 'utf8')) as SchemaContract;
+ const supportContent = supportPaths
+ .map((filePath) => readFileSync(filePath, 'utf8'))
+ .join('\n');
+
+ const knownTables = new Set(Object.keys(contract.tables));
+ const knownIndexes = new Set([
+ ...contract.indexes.map((index) => index.name),
+ ...contract.uniques.map((unique) => unique.name),
+ ]);
+
+ const supportTables = extractAllMatches(
+ supportContent,
+ /(?:CREATE TABLE IF NOT EXISTS|ALTER TABLE|INSERT INTO)\s+["`]?([a-z_][a-z0-9_]*)["`]?/gi,
+ );
+ const supportIndexes = extractAllMatches(
+ supportContent,
+ /(?:CREATE UNIQUE INDEX(?: IF NOT EXISTS)?|CREATE INDEX(?: IF NOT EXISTS)?|indexName:\s*')["`]?([a-z_][a-z0-9_]*)/gi,
+ );
+
+ const unknownTables = [...new Set(supportTables)].filter((tableName) => !knownTables.has(tableName)).sort();
+ const unknownIndexes = [...new Set(supportIndexes)].filter((indexName) => !knownIndexes.has(indexName)).sort();
+
+ expect(unknownTables).toEqual([]);
+ expect(unknownIndexes).toEqual([]);
+ });
+
+ it('does not duplicate contract-defined indexes inside shared index compatibility specs', () => {
+ const contract = JSON.parse(readFileSync(schemaContractPath, 'utf8')) as SchemaContract;
+ const contractIndexNames = new Set([
+ ...contract.indexes.map((index) => index.name),
+ ...contract.uniques.map((unique) => unique.name),
+ ]);
+
+ const duplicatedSpecs = SHARED_INDEX_COMPATIBILITY_SPECS
+ .map((spec) => spec.indexName)
+ .filter((indexName) => contractIndexNames.has(indexName));
+
+ expect(duplicatedSpecs).toEqual([]);
+ });
+
+ it('keeps proxy_logs downstream api key schema in the generated contract artifacts', () => {
+ const contract = JSON.parse(readFileSync(schemaContractPath, 'utf8')) as SchemaContract;
+ const mysqlBootstrap = readFileSync(resolve(generatedDir, 'mysql.bootstrap.sql'), 'utf8');
+ const postgresBootstrap = readFileSync(resolve(generatedDir, 'postgres.bootstrap.sql'), 'utf8');
+
+ expect(contract.tables.proxy_logs?.columns.downstream_api_key_id?.logicalType).toBe('integer');
+ expect(contract.tables.proxy_logs?.columns.client_app_id?.logicalType).toBe('text');
+ expect(contract.tables.proxy_logs?.columns.client_family?.logicalType).toBe('text');
+ expect(contract.indexes.some((index) => index.name === 'proxy_logs_downstream_api_key_created_at_idx')).toBe(true);
+ expect(contract.indexes.some((index) => index.name === 'proxy_logs_client_app_id_created_at_idx')).toBe(true);
+ expect(contract.indexes.some((index) => index.name === 'proxy_logs_client_family_created_at_idx')).toBe(true);
+ expect(mysqlBootstrap).toContain('`downstream_api_key_id`');
+ expect(mysqlBootstrap).toContain('`proxy_logs_downstream_api_key_created_at_idx`');
+ expect(mysqlBootstrap).toContain('`client_app_id`');
+ expect(mysqlBootstrap).toContain('`proxy_logs_client_app_id_created_at_idx`');
+ expect(postgresBootstrap).toContain('"downstream_api_key_id"');
+ expect(postgresBootstrap).toContain('"proxy_logs_downstream_api_key_created_at_idx"');
+ expect(postgresBootstrap).toContain('"client_app_id"');
+ expect(postgresBootstrap).toContain('"proxy_logs_client_app_id_created_at_idx"');
+ });
+});
diff --git a/src/server/db/schemaUpgrade.live.test.ts b/src/server/db/schemaUpgrade.live.test.ts
new file mode 100644
index 00000000..fe1c8e9a
--- /dev/null
+++ b/src/server/db/schemaUpgrade.live.test.ts
@@ -0,0 +1,33 @@
+import baselineContract from './generated/fixtures/2026-03-14-baseline.schemaContract.json' with { type: 'json' };
+import currentContract from './generated/schemaContract.json' with { type: 'json' };
+import { applyContractFixtureThenUpgrade, introspectLiveSchema } from './schemaIntrospection.js';
+import { describe, expect, it } from 'vitest';
+
+const skipLiveSchema = process.env.DB_PARITY_SKIP_LIVE_SCHEMA === 'true';
+const sqliteUpgrade = !skipLiveSchema && process.env.DB_PARITY_SQLITE !== 'false' ? it : it.skip;
+const mysqlUpgrade = process.env.DB_PARITY_MYSQL_URL ? it : it.skip;
+const postgresUpgrade = process.env.DB_PARITY_POSTGRES_URL ? it : it.skip;
+
+describe('schema upgrade parity', () => {
+ sqliteUpgrade('upgrades sqlite to the current contract', async () => {
+ const sqliteUrl = await applyContractFixtureThenUpgrade('sqlite', baselineContract, currentContract);
+ const live = await introspectLiveSchema({ dialect: 'sqlite', connectionString: sqliteUrl });
+ expect(live).toEqual(currentContract);
+ });
+
+ mysqlUpgrade('upgrades mysql to the current contract', async () => {
+ const mysqlUrl = await applyContractFixtureThenUpgrade('mysql', baselineContract, currentContract, {
+ connectionString: process.env.DB_PARITY_MYSQL_URL!,
+ });
+ const live = await introspectLiveSchema({ dialect: 'mysql', connectionString: mysqlUrl });
+ expect(live).toEqual(currentContract);
+ });
+
+ postgresUpgrade('upgrades postgres to the current contract', async () => {
+ const postgresUrl = await applyContractFixtureThenUpgrade('postgres', baselineContract, currentContract, {
+ connectionString: process.env.DB_PARITY_POSTGRES_URL!,
+ });
+ const live = await introspectLiveSchema({ dialect: 'postgres', connectionString: postgresUrl });
+ expect(live).toEqual(currentContract);
+ });
+});
diff --git a/src/server/db/sharedIndexSchemaCompatibility.test.ts b/src/server/db/sharedIndexSchemaCompatibility.test.ts
new file mode 100644
index 00000000..e7ceed5c
--- /dev/null
+++ b/src/server/db/sharedIndexSchemaCompatibility.test.ts
@@ -0,0 +1,62 @@
+import { describe, expect, it } from 'vitest';
+import { ensureSharedIndexSchemaCompatibility, type SharedIndexSchemaInspector } from './sharedIndexSchemaCompatibility.js';
+
+function createInspector(
+ dialect: SharedIndexSchemaInspector['dialect'],
+ options?: {
+ existingTables?: string[];
+ },
+) {
+ const executedSql: string[] = [];
+ const existingTables = new Set(options?.existingTables ?? []);
+
+ const inspector: SharedIndexSchemaInspector = {
+ dialect,
+ async tableExists(table) {
+ return existingTables.has(table);
+ },
+ async execute(sqlText) {
+ executedSql.push(sqlText);
+ },
+ };
+
+ return { inspector, executedSql };
+}
+
+describe('ensureSharedIndexSchemaCompatibility', () => {
+ it.each([
+ 'sqlite',
+ 'mysql',
+ 'postgres',
+ ] as const)('is a no-op for %s now that contract-defined indexes come from runtime bootstrap', async (dialect) => {
+ const { inspector, executedSql } = createInspector(dialect, {
+ existingTables: [
+ 'sites',
+ 'accounts',
+ 'account_tokens',
+ 'checkin_logs',
+ 'model_availability',
+ 'token_model_availability',
+ 'token_routes',
+ 'route_channels',
+ 'proxy_logs',
+ 'proxy_video_tasks',
+ 'downstream_api_keys',
+ 'events',
+ ],
+ });
+
+ await ensureSharedIndexSchemaCompatibility(inspector);
+ expect(executedSql).toEqual([]);
+ });
+
+ it('skips indexes for tables that do not exist', async () => {
+ const { inspector, executedSql } = createInspector('postgres', {
+ existingTables: ['sites'],
+ });
+
+ await ensureSharedIndexSchemaCompatibility(inspector);
+
+ expect(executedSql).toEqual([]);
+ });
+});
diff --git a/src/server/db/sharedIndexSchemaCompatibility.ts b/src/server/db/sharedIndexSchemaCompatibility.ts
new file mode 100644
index 00000000..f7ddd456
--- /dev/null
+++ b/src/server/db/sharedIndexSchemaCompatibility.ts
@@ -0,0 +1,59 @@
+export type SharedIndexSchemaDialect = 'sqlite' | 'mysql' | 'postgres';
+
+export interface SharedIndexSchemaInspector {
+ dialect: SharedIndexSchemaDialect;
+ tableExists(table: string): Promise;
+ execute(sqlText: string): Promise;
+}
+
+type SharedIndexCompatibilitySpec = {
+ table: string;
+ indexName: string;
+ createSql: Record;
+};
+
+// Contract-defined indexes are owned by generated schema artifacts and
+// runtimeSchemaBootstrap. This legacy hook is intentionally empty so startup
+// does not maintain a second static source of index SQL.
+export const SHARED_INDEX_COMPATIBILITY_SPECS: SharedIndexCompatibilitySpec[] = [];
+
+function normalizeSchemaErrorMessage(error: unknown): string {
+ if (typeof error === 'object' && error && 'message' in error) {
+ return String((error as { message?: unknown }).message || '');
+ }
+ return String(error || '');
+}
+
+function isDuplicateSchemaError(error: unknown): boolean {
+ const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
+ return lowered.includes('already exists')
+ || lowered.includes('duplicate')
+ || lowered.includes('relation') && lowered.includes('already exists');
+}
+
+async function executeIgnoreDuplicate(inspector: SharedIndexSchemaInspector, sqlText: string): Promise {
+ try {
+ await inspector.execute(sqlText);
+ } catch (error) {
+ if (!isDuplicateSchemaError(error)) {
+ throw error;
+ }
+ }
+}
+
+export async function ensureSharedIndexSchemaCompatibility(inspector: SharedIndexSchemaInspector): Promise {
+ const tableExistsCache = new Map();
+
+ for (const spec of SHARED_INDEX_COMPATIBILITY_SPECS) {
+ let hasTable = tableExistsCache.get(spec.table);
+ if (hasTable === undefined) {
+ hasTable = await inspector.tableExists(spec.table);
+ tableExistsCache.set(spec.table, hasTable);
+ }
+ if (!hasTable) {
+ continue;
+ }
+
+ await executeIgnoreDuplicate(inspector, spec.createSql[inspector.dialect]);
+ }
+}
diff --git a/src/server/db/siteAnnouncementsSchema.test.ts b/src/server/db/siteAnnouncementsSchema.test.ts
new file mode 100644
index 00000000..29a438a6
--- /dev/null
+++ b/src/server/db/siteAnnouncementsSchema.test.ts
@@ -0,0 +1,49 @@
+import { readFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import { generateDialectArtifacts } from './schemaArtifactGenerator.js';
+import type { SchemaContract } from './schemaContract.js';
+import * as schema from './schema.js';
+
+const dbDir = dirname(fileURLToPath(import.meta.url));
+const schemaContractPath = resolve(dbDir, 'generated/schemaContract.json');
+
+function readSchemaContract(): SchemaContract {
+ return JSON.parse(readFileSync(schemaContractPath, 'utf8')) as SchemaContract;
+}
+
+describe('site announcements schema', () => {
+ it('declares the site_announcements table in the Drizzle schema', () => {
+ expect(schema.siteAnnouncements).toBeDefined();
+ });
+
+ it('keeps site_announcements in the generated schema contract', () => {
+ const contract = readSchemaContract();
+ const table = contract.tables.site_announcements;
+
+ expect(table).toBeDefined();
+ expect(table?.columns.site_id?.logicalType).toBe('integer');
+ expect(table?.columns.source_key?.logicalType).toBe('text');
+ expect(table?.columns.first_seen_at?.logicalType).toBe('datetime');
+ expect(table?.columns.last_seen_at?.logicalType).toBe('datetime');
+ expect(table?.columns.read_at?.logicalType).toBe('datetime');
+ expect(table?.columns.raw_payload?.logicalType).toBe('text');
+ expect(contract.indexes.some((index) => index.name === 'site_announcements_site_id_first_seen_at_idx')).toBe(true);
+ expect(contract.indexes.some((index) => index.name === 'site_announcements_read_at_idx')).toBe(true);
+ expect(contract.uniques.some((unique) => unique.name === 'site_announcements_site_source_key_unique')).toBe(true);
+ });
+
+ it('emits bootstrap sql for the site_announcements table', () => {
+ const artifacts = generateDialectArtifacts(readSchemaContract());
+
+ expect(artifacts.mysqlBootstrap).toContain('CREATE TABLE IF NOT EXISTS `site_announcements`');
+ expect(artifacts.mysqlBootstrap).toContain("`first_seen_at` VARCHAR(191) DEFAULT (DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'))");
+ expect(artifacts.mysqlBootstrap).toContain('`upstream_created_at` VARCHAR(191)');
+ expect(artifacts.mysqlBootstrap).toContain('`site_announcements_site_source_key_unique`');
+ expect(artifacts.postgresBootstrap).toContain('CREATE TABLE IF NOT EXISTS "site_announcements"');
+ expect(artifacts.postgresBootstrap).toContain(`"first_seen_at" TEXT DEFAULT to_char(timezone('UTC', CURRENT_TIMESTAMP), 'YYYY-MM-DD HH24:MI:SS')`);
+ expect(artifacts.postgresBootstrap).toContain('"upstream_created_at" TEXT');
+ expect(artifacts.postgresBootstrap).toContain('"site_announcements_site_source_key_unique"');
+ });
+});
diff --git a/src/server/db/siteSchemaCompatibility.test.ts b/src/server/db/siteSchemaCompatibility.test.ts
index 7a9a9974..c4f8efb6 100644
--- a/src/server/db/siteSchemaCompatibility.test.ts
+++ b/src/server/db/siteSchemaCompatibility.test.ts
@@ -5,17 +5,20 @@ function createInspector(
dialect: SiteSchemaInspector['dialect'],
options?: {
hasSitesTable?: boolean;
+ existingTables?: string[];
existingColumns?: string[];
},
) {
const executedSql: string[] = [];
const hasSitesTable = options?.hasSitesTable ?? true;
+ const existingTables = new Set(options?.existingTables ?? []);
const existingColumns = new Set(options?.existingColumns ?? []);
const inspector: SiteSchemaInspector = {
dialect,
async tableExists(table) {
- return table === 'sites' && hasSitesTable;
+ if (table === 'sites') return hasSitesTable;
+ return existingTables.has(table);
},
async columnExists(table, column) {
return table === 'sites' && existingColumns.has(column);
@@ -36,9 +39,13 @@ describe('ensureSiteSchemaCompatibility', () => {
'ALTER TABLE "sites" ADD COLUMN "proxy_url" TEXT',
'ALTER TABLE "sites" ADD COLUMN "use_system_proxy" BOOLEAN DEFAULT FALSE',
'UPDATE "sites" SET "use_system_proxy" = FALSE WHERE "use_system_proxy" IS NULL',
+ 'ALTER TABLE "sites" ADD COLUMN "custom_headers" TEXT',
'ALTER TABLE "sites" ADD COLUMN "external_checkin_url" TEXT',
'ALTER TABLE "sites" ADD COLUMN "global_weight" DOUBLE PRECISION DEFAULT 1',
'UPDATE "sites" SET "global_weight" = 1 WHERE "global_weight" IS NULL OR "global_weight" <= 0',
+ 'CREATE TABLE IF NOT EXISTS "site_disabled_models" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "site_id" INTEGER NOT NULL REFERENCES "sites"("id") ON DELETE CASCADE, "model_name" TEXT NOT NULL, "created_at" TEXT)',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "site_disabled_models_site_model_unique" ON "site_disabled_models" ("site_id", "model_name")',
+ 'CREATE INDEX IF NOT EXISTS "site_disabled_models_site_id_idx" ON "site_disabled_models" ("site_id")',
],
},
{
@@ -47,9 +54,13 @@ describe('ensureSiteSchemaCompatibility', () => {
'ALTER TABLE `sites` ADD COLUMN `proxy_url` TEXT NULL',
'ALTER TABLE `sites` ADD COLUMN `use_system_proxy` BOOLEAN DEFAULT FALSE',
'UPDATE `sites` SET `use_system_proxy` = FALSE WHERE `use_system_proxy` IS NULL',
+ 'ALTER TABLE `sites` ADD COLUMN `custom_headers` TEXT NULL',
'ALTER TABLE `sites` ADD COLUMN `external_checkin_url` TEXT NULL',
'ALTER TABLE `sites` ADD COLUMN `global_weight` DOUBLE DEFAULT 1',
'UPDATE `sites` SET `global_weight` = 1 WHERE `global_weight` IS NULL OR `global_weight` <= 0',
+ 'CREATE TABLE IF NOT EXISTS `site_disabled_models` (`id` INT AUTO_INCREMENT PRIMARY KEY, `site_id` INT NOT NULL, `model_name` VARCHAR(191) NOT NULL, `created_at` TEXT NULL, CONSTRAINT `site_disabled_models_site_fk` FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE)',
+ 'CREATE UNIQUE INDEX `site_disabled_models_site_model_unique` ON `site_disabled_models` (`site_id`, `model_name`(191))',
+ 'CREATE INDEX `site_disabled_models_site_id_idx` ON `site_disabled_models` (`site_id`)',
],
},
])('adds missing site proxy columns for $dialect', async ({ dialect, expectedSql }) => {
@@ -67,4 +78,14 @@ describe('ensureSiteSchemaCompatibility', () => {
expect(executedSql).toEqual([]);
});
+
+ it('does not recreate site_disabled_models when it already exists', async () => {
+ const { inspector, executedSql } = createInspector('mysql', {
+ existingTables: ['site_disabled_models'],
+ });
+
+ await ensureSiteSchemaCompatibility(inspector);
+
+ expect(executedSql).not.toContain('CREATE TABLE IF NOT EXISTS `site_disabled_models` (`id` INT AUTO_INCREMENT PRIMARY KEY, `site_id` INT NOT NULL, `model_name` VARCHAR(191) NOT NULL, `created_at` TEXT NULL, CONSTRAINT `site_disabled_models_site_fk` FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE)');
+ });
});
diff --git a/src/server/db/siteSchemaCompatibility.ts b/src/server/db/siteSchemaCompatibility.ts
index f90e61b7..4e08f578 100644
--- a/src/server/db/siteSchemaCompatibility.ts
+++ b/src/server/db/siteSchemaCompatibility.ts
@@ -7,13 +7,19 @@ export interface SiteSchemaInspector {
execute(sqlText: string): Promise;
}
-type SiteColumnCompatibilitySpec = {
+export type SiteColumnCompatibilitySpec = {
column: string;
addSql: Record;
normalizeSql?: Record;
};
-const SITE_COLUMN_COMPATIBILITY_SPECS: SiteColumnCompatibilitySpec[] = [
+export type SiteTableCompatibilitySpec = {
+ table: string;
+ createSql: Record;
+ postCreateSql?: Record;
+};
+
+export const SITE_COLUMN_COMPATIBILITY_SPECS: SiteColumnCompatibilitySpec[] = [
{
column: 'proxy_url',
addSql: {
@@ -35,6 +41,14 @@ const SITE_COLUMN_COMPATIBILITY_SPECS: SiteColumnCompatibilitySpec[] = [
postgres: 'UPDATE "sites" SET "use_system_proxy" = FALSE WHERE "use_system_proxy" IS NULL',
},
},
+ {
+ column: 'custom_headers',
+ addSql: {
+ sqlite: 'ALTER TABLE sites ADD COLUMN custom_headers text;',
+ mysql: 'ALTER TABLE `sites` ADD COLUMN `custom_headers` TEXT NULL',
+ postgres: 'ALTER TABLE "sites" ADD COLUMN "custom_headers" TEXT',
+ },
+ },
{
column: 'external_checkin_url',
addSql: {
@@ -58,6 +72,31 @@ const SITE_COLUMN_COMPATIBILITY_SPECS: SiteColumnCompatibilitySpec[] = [
},
];
+export const SITE_TABLE_COMPATIBILITY_SPECS: SiteTableCompatibilitySpec[] = [
+ {
+ table: 'site_disabled_models',
+ createSql: {
+ sqlite: 'CREATE TABLE IF NOT EXISTS site_disabled_models (id integer PRIMARY KEY AUTOINCREMENT NOT NULL, site_id integer NOT NULL REFERENCES sites(id) ON DELETE cascade, model_name text NOT NULL, created_at text DEFAULT (datetime(\'now\')));',
+ mysql: 'CREATE TABLE IF NOT EXISTS `site_disabled_models` (`id` INT AUTO_INCREMENT PRIMARY KEY, `site_id` INT NOT NULL, `model_name` VARCHAR(191) NOT NULL, `created_at` TEXT NULL, CONSTRAINT `site_disabled_models_site_fk` FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON DELETE CASCADE)',
+ postgres: 'CREATE TABLE IF NOT EXISTS "site_disabled_models" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "site_id" INTEGER NOT NULL REFERENCES "sites"("id") ON DELETE CASCADE, "model_name" TEXT NOT NULL, "created_at" TEXT)',
+ },
+ postCreateSql: {
+ sqlite: [
+ 'CREATE UNIQUE INDEX IF NOT EXISTS site_disabled_models_site_model_unique ON site_disabled_models (site_id, model_name);',
+ 'CREATE INDEX IF NOT EXISTS site_disabled_models_site_id_idx ON site_disabled_models (site_id);',
+ ],
+ mysql: [
+ 'CREATE UNIQUE INDEX `site_disabled_models_site_model_unique` ON `site_disabled_models` (`site_id`, `model_name`(191))',
+ 'CREATE INDEX `site_disabled_models_site_id_idx` ON `site_disabled_models` (`site_id`)',
+ ],
+ postgres: [
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "site_disabled_models_site_model_unique" ON "site_disabled_models" ("site_id", "model_name")',
+ 'CREATE INDEX IF NOT EXISTS "site_disabled_models_site_id_idx" ON "site_disabled_models" ("site_id")',
+ ],
+ },
+ },
+];
+
function normalizeSchemaErrorMessage(error: unknown): string {
if (typeof error === 'object' && error && 'message' in error) {
return String((error as { message?: unknown }).message || '');
@@ -72,6 +111,15 @@ function isDuplicateColumnError(error: unknown): boolean {
|| lowered.includes('duplicate column name');
}
+function isExistingSchemaObjectError(error: unknown): boolean {
+ const lowered = normalizeSchemaErrorMessage(error).toLowerCase();
+ return lowered.includes('duplicate column')
+ || lowered.includes('already exists')
+ || lowered.includes('duplicate column name')
+ || lowered.includes('duplicate key name')
+ || (lowered.includes('relation') && lowered.includes('already exists'));
+}
+
async function executeAddColumn(inspector: SiteSchemaInspector, sqlText: string): Promise {
try {
await inspector.execute(sqlText);
@@ -82,6 +130,16 @@ async function executeAddColumn(inspector: SiteSchemaInspector, sqlText: string)
}
}
+async function executeCreateSchemaObject(inspector: SiteSchemaInspector, sqlText: string): Promise {
+ try {
+ await inspector.execute(sqlText);
+ } catch (error) {
+ if (!isExistingSchemaObjectError(error)) {
+ throw error;
+ }
+ }
+}
+
export async function ensureSiteSchemaCompatibility(inspector: SiteSchemaInspector): Promise {
const hasSitesTable = await inspector.tableExists('sites');
if (!hasSitesTable) {
@@ -98,4 +156,15 @@ export async function ensureSiteSchemaCompatibility(inspector: SiteSchemaInspect
await inspector.execute(spec.normalizeSql[inspector.dialect]);
}
}
+
+ for (const spec of SITE_TABLE_COMPATIBILITY_SPECS) {
+ const hasTable = await inspector.tableExists(spec.table);
+ if (!hasTable) {
+ await executeCreateSchemaObject(inspector, spec.createSql[inspector.dialect]);
+ }
+
+ for (const sqlText of spec.postCreateSql?.[inspector.dialect] ?? []) {
+ await executeCreateSchemaObject(inspector, sqlText);
+ }
+ }
}
diff --git a/src/server/db/upsertSetting.ts b/src/server/db/upsertSetting.ts
new file mode 100644
index 00000000..c40424fe
--- /dev/null
+++ b/src/server/db/upsertSetting.ts
@@ -0,0 +1,41 @@
+import { eq } from 'drizzle-orm';
+import { db, runtimeDbDialect, schema } from './index.js';
+
+/**
+ * Dialect-aware upsert for the `settings` table.
+ *
+ * SQLite / PostgreSQL use `onConflictDoUpdate` while MySQL uses
+ * `onDuplicateKeyUpdate`. Drizzle exposes different builder types for each
+ * dialect, so we branch at runtime.
+ */
+export async function upsertSetting(key: string, value: unknown, txDb: typeof db = db): Promise {
+ const jsonValue = JSON.stringify(value);
+
+ if (runtimeDbDialect === 'mysql') {
+ // MySQL path – try update first, insert if not exists
+ const existing = await txDb.select({ key: schema.settings.key })
+ .from(schema.settings)
+ .where(eq(schema.settings.key, key))
+ .get();
+
+ if (existing) {
+ await txDb.update(schema.settings)
+ .set({ value: jsonValue })
+ .where(eq(schema.settings.key, key))
+ .run();
+ } else {
+ await txDb.insert(schema.settings)
+ .values({ key, value: jsonValue })
+ .run();
+ }
+ } else {
+ // SQLite / PostgreSQL path
+ await (txDb.insert(schema.settings)
+ .values({ key, value: jsonValue }) as any)
+ .onConflictDoUpdate({
+ target: schema.settings.key,
+ set: { value: jsonValue },
+ })
+ .run();
+ }
+}
diff --git a/src/server/desktop.ts b/src/server/desktop.ts
index bc844b03..02bb29b7 100644
--- a/src/server/desktop.ts
+++ b/src/server/desktop.ts
@@ -3,7 +3,7 @@ import type { FastifyInstance } from 'fastify';
const DESKTOP_HEALTH_ROUTE = '/api/desktop/health';
export function isPublicApiRoute(url: string): boolean {
- return url === DESKTOP_HEALTH_ROUTE;
+ return url === DESKTOP_HEALTH_ROUTE || url.startsWith('/api/oauth/callback/');
}
export async function registerDesktopRoutes(app: FastifyInstance) {
diff --git a/src/server/index.ts b/src/server/index.ts
index c555d8a2..8dfd6c80 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -2,6 +2,7 @@ import Fastify from 'fastify';
import cors from '@fastify/cors';
import fastifyStatic from '@fastify/static';
import { buildFastifyOptions, config } from './config.js';
+import { normalizePayloadRulesConfig } from './services/payloadRules.js';
import { authMiddleware } from './middleware/auth.js';
import { sitesRoutes } from './routes/api/sites.js';
import { accountsRoutes } from './routes/api/accounts.js';
@@ -17,20 +18,33 @@ import { taskRoutes } from './routes/api/tasks.js';
import { testRoutes } from './routes/api/test.js';
import { monitorRoutes } from './routes/api/monitor.js';
import { downstreamApiKeysRoutes } from './routes/api/downstreamApiKeys.js';
+import { oauthRoutes } from './routes/api/oauth.js';
+import { siteAnnouncementsRoutes } from './routes/api/siteAnnouncements.js';
import { proxyRoutes } from './routes/proxy/router.js';
import { startScheduler } from './services/checkinScheduler.js';
-import { startProxyLogRetentionService, stopProxyLogRetentionService } from './services/proxyLogRetentionService.js';
+import * as routeRefreshWorkflow from './services/routeRefreshWorkflow.js';
+import { startProxyFileRetentionService, stopProxyFileRetentionService } from './services/proxyFileRetentionService.js';
+import { setLegacyProxyLogRetentionFallbackEnabled, stopProxyLogRetentionService } from './services/proxyLogRetentionService.js';
import { buildStartupSummaryLines } from './services/startupInfo.js';
import { repairStoredCreatedAtValues } from './services/storedTimestampRepairService.js';
import { migrateSiteApiKeysToAccounts } from './services/siteApiKeyMigrationService.js';
import { ensureDefaultSitesSeeded } from './services/defaultSiteSeedService.js';
+import { ensureOauthIdentityBackfill } from './services/oauth/oauthIdentityBackfill.js';
+import { ensureOauthProviderSitesExist } from './services/oauth/oauthSiteRegistry.js';
+import { startOAuthLoopbackCallbackServers, stopOAuthLoopbackCallbackServers } from './services/oauth/localCallbackServer.js';
+import { startSiteAnnouncementPolling } from './services/siteAnnouncementPollingService.js';
+import { reloadBackupWebdavScheduler } from './services/backupService.js';
+import { ensureRuntimeDatabaseReady } from './runtimeDatabaseBootstrap.js';
import { isPublicApiRoute, registerDesktopRoutes } from './desktop.js';
import { existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, normalize, resolve, sep } from 'path';
+import { normalizeLogCleanupRetentionDays } from './services/logCleanupService.js';
import {
db,
ensureProxyFileCompatibilityColumns,
+ ensureProxyLogClientColumns,
+ ensureProxyLogDownstreamApiKeyIdColumn,
ensureProxyLogBillingDetailsColumn,
ensureRouteGroupingCompatibilityColumns,
ensureSiteCompatibilityColumns,
@@ -40,18 +54,6 @@ import {
type RuntimeDbDialect,
} from './db/index.js';
-let sqliteMigrationsBootstrapped = false;
-
-async function ensureSqliteRuntimeMigrations() {
- if (runtimeDbDialect !== 'sqlite') return;
- const migrateModule = await import('./db/migrate.js');
- if (sqliteMigrationsBootstrapped) {
- migrateModule.runSqliteMigrations();
- return;
- }
- sqliteMigrationsBootstrapped = true;
-}
-
function toSettingsMap(rows: Array<{ key: string; value: string }>) {
return new Map(rows.map((row) => [row.key, row.value]));
}
@@ -115,6 +117,17 @@ function extractSavedRuntimeDatabaseConfig(settingsMap: Map): {
};
}
+const LOG_CLEANUP_SETTING_KEYS = [
+ 'log_cleanup_cron',
+ 'log_cleanup_usage_logs_enabled',
+ 'log_cleanup_program_logs_enabled',
+ 'log_cleanup_retention_days',
+] as const;
+
+function hasExplicitLogCleanupSettings(settingsMap: Map): boolean {
+ return LOG_CLEANUP_SETTING_KEYS.some((key) => settingsMap.has(key));
+}
+
function applyRuntimeSettings(settingsMap: Map) {
const authToken = parseSettingFromMap(settingsMap, 'auth_token');
if (typeof authToken === 'string' && authToken) config.authToken = authToken;
@@ -125,12 +138,95 @@ function applyRuntimeSettings(settingsMap: Map) {
const systemProxyUrl = parseSettingFromMap(settingsMap, 'system_proxy_url');
if (typeof systemProxyUrl === 'string') config.systemProxyUrl = systemProxyUrl;
+ const codexUpstreamWebsocketEnabled = parseSettingFromMap(settingsMap, 'codex_upstream_websocket_enabled');
+ if (typeof codexUpstreamWebsocketEnabled === 'boolean') {
+ config.codexUpstreamWebsocketEnabled = codexUpstreamWebsocketEnabled;
+ }
+
+ const proxyErrorKeywords = parseSettingFromMap(settingsMap, 'proxy_error_keywords');
+ if (proxyErrorKeywords !== undefined) {
+ config.proxyErrorKeywords = toStringList(proxyErrorKeywords);
+ }
+
+ const proxyEmptyContentFailEnabled = parseSettingFromMap(settingsMap, 'proxy_empty_content_fail_enabled');
+ if (typeof proxyEmptyContentFailEnabled === 'boolean') {
+ config.proxyEmptyContentFailEnabled = proxyEmptyContentFailEnabled;
+ }
+
+ const globalBlockedBrands = parseSettingFromMap(settingsMap, 'global_blocked_brands');
+ if (Array.isArray(globalBlockedBrands)) {
+ config.globalBlockedBrands = globalBlockedBrands.filter((b): b is string => typeof b === 'string').map((b) => b.trim()).filter(Boolean);
+ }
+
+ const codexHeaderDefaults = parseSettingFromMap(settingsMap, 'codex_header_defaults');
+ if (codexHeaderDefaults && typeof codexHeaderDefaults === 'object') {
+ const next = codexHeaderDefaults as Record;
+ config.codexHeaderDefaults = {
+ userAgent: typeof next.userAgent === 'string'
+ ? next.userAgent.trim()
+ : (typeof next['user-agent'] === 'string' ? next['user-agent'].trim() : config.codexHeaderDefaults.userAgent),
+ betaFeatures: typeof next.betaFeatures === 'string'
+ ? next.betaFeatures.trim()
+ : (typeof next['beta-features'] === 'string' ? next['beta-features'].trim() : config.codexHeaderDefaults.betaFeatures),
+ };
+ }
+
+ if (settingsMap.has('payload_rules')) {
+ config.payloadRules = normalizePayloadRulesConfig(parseSettingFromMap(settingsMap, 'payload_rules'));
+ }
+
const checkinCron = parseSettingFromMap(settingsMap, 'checkin_cron');
if (typeof checkinCron === 'string' && checkinCron) config.checkinCron = checkinCron;
+ const checkinScheduleMode = parseSettingFromMap(settingsMap, 'checkin_schedule_mode');
+ if (checkinScheduleMode === 'cron' || checkinScheduleMode === 'interval') {
+ config.checkinScheduleMode = checkinScheduleMode;
+ }
+
+ const checkinIntervalHours = parseSettingFromMap(settingsMap, 'checkin_interval_hours');
+ if (typeof checkinIntervalHours === 'number' && Number.isFinite(checkinIntervalHours) && checkinIntervalHours >= 1 && checkinIntervalHours <= 24) {
+ config.checkinIntervalHours = Math.trunc(checkinIntervalHours);
+ }
+
const balanceRefreshCron = parseSettingFromMap(settingsMap, 'balance_refresh_cron');
if (typeof balanceRefreshCron === 'string' && balanceRefreshCron) config.balanceRefreshCron = balanceRefreshCron;
+ const logCleanupCron = parseSettingFromMap(settingsMap, 'log_cleanup_cron');
+ if (typeof logCleanupCron === 'string' && logCleanupCron) config.logCleanupCron = logCleanupCron;
+
+ const logCleanupUsageLogsEnabled = parseSettingFromMap(settingsMap, 'log_cleanup_usage_logs_enabled');
+ if (typeof logCleanupUsageLogsEnabled === 'boolean') {
+ config.logCleanupUsageLogsEnabled = logCleanupUsageLogsEnabled;
+ }
+
+ const logCleanupProgramLogsEnabled = parseSettingFromMap(settingsMap, 'log_cleanup_program_logs_enabled');
+ if (typeof logCleanupProgramLogsEnabled === 'boolean') {
+ config.logCleanupProgramLogsEnabled = logCleanupProgramLogsEnabled;
+ }
+
+ const logCleanupRetentionDays = parseSettingFromMap(settingsMap, 'log_cleanup_retention_days');
+ if (typeof logCleanupRetentionDays === 'number' && Number.isFinite(logCleanupRetentionDays) && logCleanupRetentionDays >= 1) {
+ config.logCleanupRetentionDays = normalizeLogCleanupRetentionDays(logCleanupRetentionDays);
+ }
+
+ const proxySessionChannelConcurrencyLimit = parseSettingFromMap(settingsMap, 'proxy_session_channel_concurrency_limit');
+ if (
+ typeof proxySessionChannelConcurrencyLimit === 'number'
+ && Number.isFinite(proxySessionChannelConcurrencyLimit)
+ && proxySessionChannelConcurrencyLimit >= 0
+ ) {
+ config.proxySessionChannelConcurrencyLimit = Math.trunc(proxySessionChannelConcurrencyLimit);
+ }
+
+ const proxySessionChannelQueueWaitMs = parseSettingFromMap(settingsMap, 'proxy_session_channel_queue_wait_ms');
+ if (
+ typeof proxySessionChannelQueueWaitMs === 'number'
+ && Number.isFinite(proxySessionChannelQueueWaitMs)
+ && proxySessionChannelQueueWaitMs >= 0
+ ) {
+ config.proxySessionChannelQueueWaitMs = Math.trunc(proxySessionChannelQueueWaitMs);
+ }
+
const routingWeights = parseSettingFromMap>(settingsMap, 'routing_weights');
if (routingWeights && typeof routingWeights === 'object') {
config.routingWeights = {
@@ -156,12 +252,23 @@ function applyRuntimeSettings(settingsMap: Map) {
const telegramEnabled = parseSettingFromMap(settingsMap, 'telegram_enabled');
if (typeof telegramEnabled === 'boolean') config.telegramEnabled = telegramEnabled;
+ const telegramApiBaseUrl = parseSettingFromMap(settingsMap, 'telegram_api_base_url');
+ if (typeof telegramApiBaseUrl === 'string' && telegramApiBaseUrl.trim()) {
+ config.telegramApiBaseUrl = telegramApiBaseUrl.trim().replace(/\/+$/, '');
+ }
+
const telegramBotToken = parseSettingFromMap(settingsMap, 'telegram_bot_token');
if (typeof telegramBotToken === 'string') config.telegramBotToken = telegramBotToken;
const telegramChatId = parseSettingFromMap(settingsMap, 'telegram_chat_id');
if (typeof telegramChatId === 'string') config.telegramChatId = telegramChatId;
+ const telegramUseSystemProxy = parseSettingFromMap(settingsMap, 'telegram_use_system_proxy');
+ if (typeof telegramUseSystemProxy === 'boolean') config.telegramUseSystemProxy = telegramUseSystemProxy;
+
+ const telegramMessageThreadId = parseSettingFromMap(settingsMap, 'telegram_message_thread_id');
+ if (typeof telegramMessageThreadId === 'string') config.telegramMessageThreadId = telegramMessageThreadId;
+
const smtpEnabled = parseSettingFromMap(settingsMap, 'smtp_enabled');
if (typeof smtpEnabled === 'boolean') config.smtpEnabled = smtpEnabled;
@@ -199,8 +306,12 @@ function applyRuntimeSettings(settingsMap: Map) {
}
}
-// Ensure sqlite tables exist before reading runtime settings.
-await ensureSqliteRuntimeMigrations();
+// Ensure the current runtime database is bootstrapped before reading settings.
+await ensureRuntimeDatabaseReady({
+ dialect: runtimeDbDialect,
+ connectionString: config.dbUrl,
+ ssl: config.dbSsl,
+});
// Load runtime config overrides from settings
try {
@@ -208,12 +319,27 @@ try {
const initialMap = toSettingsMap(initialRows);
const savedDbConfig = extractSavedRuntimeDatabaseConfig(initialMap);
const activeDbUrl = (config.dbUrl || '').trim();
+ const originalRuntimeConfig = {
+ dialect: runtimeDbDialect,
+ dbUrl: activeDbUrl,
+ ssl: config.dbSsl,
+ };
if (savedDbConfig && (savedDbConfig.dialect !== runtimeDbDialect || savedDbConfig.dbUrl !== activeDbUrl || savedDbConfig.ssl !== config.dbSsl)) {
try {
await switchRuntimeDatabase(savedDbConfig.dialect, savedDbConfig.dbUrl, savedDbConfig.ssl);
- await ensureSqliteRuntimeMigrations();
console.log(`Loaded runtime DB config from settings: ${savedDbConfig.dialect}`);
} catch (error) {
+ const currentDbUrl = (config.dbUrl || '').trim();
+ const switchedAway = runtimeDbDialect !== originalRuntimeConfig.dialect
+ || currentDbUrl !== originalRuntimeConfig.dbUrl
+ || config.dbSsl !== originalRuntimeConfig.ssl;
+ if (switchedAway) {
+ await switchRuntimeDatabase(
+ originalRuntimeConfig.dialect,
+ originalRuntimeConfig.dbUrl,
+ originalRuntimeConfig.ssl,
+ );
+ }
console.warn(`Failed to switch runtime DB from settings: ${(error as Error)?.message || 'unknown error'}`);
}
}
@@ -221,19 +347,31 @@ try {
await ensureSiteCompatibilityColumns();
await ensureRouteGroupingCompatibilityColumns();
await ensureProxyFileCompatibilityColumns();
+ await ensureProxyLogClientColumns();
+ await ensureProxyLogDownstreamApiKeyIdColumn();
const finalRows = await db.select().from(schema.settings).all();
const finalMap = toSettingsMap(finalRows);
applyRuntimeSettings(finalMap);
+ config.logCleanupConfigured = hasExplicitLogCleanupSettings(finalMap);
+ if (!config.logCleanupConfigured && config.proxyLogRetentionDays > 0) {
+ config.logCleanupUsageLogsEnabled = true;
+ config.logCleanupProgramLogsEnabled = false;
+ config.logCleanupRetentionDays = normalizeLogCleanupRetentionDays(config.proxyLogRetentionDays);
+ }
await ensureProxyLogBillingDetailsColumn();
await repairStoredCreatedAtValues();
await migrateSiteApiKeysToAccounts();
await ensureDefaultSitesSeeded();
+ await ensureOauthIdentityBackfill();
+ await routeRefreshWorkflow.rebuildRoutesOnly();
console.log('Loaded runtime settings overrides');
} catch (error) {
console.warn(`Failed to load runtime settings overrides: ${(error as Error)?.message || 'unknown error'}`);
}
+await ensureOauthProviderSitesExist();
+
const app = Fastify(buildFastifyOptions(config));
await app.register(cors);
@@ -256,11 +394,13 @@ await app.register(authRoutes);
await app.register(settingsRoutes);
await app.register(accountTokensRoutes);
await app.register(searchRoutes);
-await app.register(eventsRoutes);
-await app.register(taskRoutes);
+ await app.register(eventsRoutes);
+ await app.register(siteAnnouncementsRoutes);
+ await app.register(taskRoutes);
await app.register(testRoutes);
await app.register(monitorRoutes);
await app.register(downstreamApiKeysRoutes);
+await app.register(oauthRoutes);
// Register OpenAI-compatible proxy routes
await app.register(proxyRoutes);
@@ -294,9 +434,19 @@ if (existsSync(webDir)) {
// Start scheduler
await startScheduler();
-startProxyLogRetentionService();
+await reloadBackupWebdavScheduler();
+startSiteAnnouncementPolling();
+try {
+ await startOAuthLoopbackCallbackServers();
+} catch (error) {
+ console.warn(`Failed to start OAuth callback listeners: ${(error as Error)?.message || 'unknown error'}`);
+}
+setLegacyProxyLogRetentionFallbackEnabled(!config.logCleanupConfigured);
+startProxyFileRetentionService();
app.addHook('onClose', async () => {
+ stopProxyFileRetentionService();
stopProxyLogRetentionService();
+ await stopOAuthLoopbackCallbackServers();
});
// Start server
diff --git a/src/server/middleware/auth.proxy.test.ts b/src/server/middleware/auth.proxy.test.ts
new file mode 100644
index 00000000..5a959bcb
--- /dev/null
+++ b/src/server/middleware/auth.proxy.test.ts
@@ -0,0 +1,79 @@
+import Fastify from 'fastify';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+const authorizeDownstreamTokenMock = vi.fn();
+const consumeManagedKeyRequestMock = vi.fn();
+
+vi.mock('../services/downstreamApiKeyService.js', () => ({
+ authorizeDownstreamToken: (...args: unknown[]) => authorizeDownstreamTokenMock(...args),
+ consumeManagedKeyRequest: (...args: unknown[]) => consumeManagedKeyRequestMock(...args),
+}));
+
+describe('proxyAuthMiddleware', () => {
+ beforeEach(() => {
+ authorizeDownstreamTokenMock.mockReset();
+ consumeManagedKeyRequestMock.mockReset();
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('rejects missing proxy credentials', async () => {
+ const { proxyAuthMiddleware } = await import('./auth.js');
+ const app = Fastify();
+ app.addHook('onRequest', proxyAuthMiddleware);
+ app.get('/v1/ping', async () => ({ ok: true }));
+
+ const res = await app.inject({ method: 'GET', url: '/v1/ping' });
+ expect(res.statusCode).toBe(401);
+ expect(res.json()).toMatchObject({ error: expect.stringContaining('Missing Authorization') });
+ await app.close();
+ });
+
+ it('stores managed key context and consumes request usage', async () => {
+ authorizeDownstreamTokenMock.mockResolvedValue({
+ ok: true,
+ source: 'managed',
+ token: 'sk-managed-001',
+ key: { id: 12, name: 'project-key' },
+ policy: { supportedModels: ['gpt-5.2'], allowedRouteIds: [3], siteWeightMultipliers: { 1: 1.2 } },
+ });
+ consumeManagedKeyRequestMock.mockResolvedValue(undefined);
+
+ const { proxyAuthMiddleware, getProxyAuthContext, getProxyResourceOwner } = await import('./auth.js');
+ const app = Fastify();
+ app.addHook('onRequest', proxyAuthMiddleware);
+ app.get('/v1/ping', async (request) => ({
+ auth: getProxyAuthContext(request),
+ owner: getProxyResourceOwner(request),
+ }));
+
+ const res = await app.inject({
+ method: 'GET',
+ url: '/v1/ping',
+ headers: { Authorization: 'Bearer sk-managed-001' },
+ });
+
+ expect(res.statusCode).toBe(200);
+ expect(authorizeDownstreamTokenMock).toHaveBeenCalledWith('sk-managed-001');
+ expect(consumeManagedKeyRequestMock).toHaveBeenCalledWith(12);
+ expect(res.json()).toMatchObject({
+ auth: {
+ source: 'managed',
+ keyId: 12,
+ keyName: 'project-key',
+ policy: {
+ supportedModels: ['gpt-5.2'],
+ allowedRouteIds: [3],
+ siteWeightMultipliers: { 1: 1.2 },
+ },
+ },
+ owner: {
+ ownerType: 'managed_key',
+ ownerId: '12',
+ },
+ });
+ await app.close();
+ });
+});
diff --git a/src/server/middleware/requestRateLimit.ts b/src/server/middleware/requestRateLimit.ts
new file mode 100644
index 00000000..e87b2b9a
--- /dev/null
+++ b/src/server/middleware/requestRateLimit.ts
@@ -0,0 +1,85 @@
+import type { FastifyReply, FastifyRequest } from 'fastify';
+
+type RateLimitOptions = {
+ bucket: string;
+ max: number;
+ windowMs: number;
+ message?: string;
+};
+
+type RateLimitEntry = {
+ count: number;
+ resetAt: number;
+};
+
+const DEFAULT_MESSAGE = '请求过于频繁,请稍后再试';
+const rateLimitStore = new Map();
+
+function normalizeIp(rawIp: string | null | undefined): string {
+ const ip = (rawIp || '').trim();
+ if (!ip) return 'unknown';
+ if (ip.startsWith('::ffff:')) return ip.slice('::ffff:'.length).trim() || 'unknown';
+ if (ip === '::1') return '127.0.0.1';
+ return ip;
+}
+
+function extractClientIp(request: FastifyRequest): string {
+ const forwarded = request.headers['x-forwarded-for'];
+ if (Array.isArray(forwarded)) {
+ const first = forwarded.find((value) => typeof value === 'string' && value.trim().length > 0);
+ if (first) return normalizeIp(first.split(',')[0]);
+ }
+
+ if (typeof forwarded === 'string' && forwarded.trim().length > 0) {
+ return normalizeIp(forwarded.split(',')[0]);
+ }
+
+ return normalizeIp(request.ip);
+}
+
+function getRateLimitKey(bucket: string, request: FastifyRequest): string {
+ return `${bucket}:${extractClientIp(request)}`;
+}
+
+function pruneExpiredEntries(nowMs: number): void {
+ for (const [key, entry] of rateLimitStore.entries()) {
+ if (entry.resetAt > nowMs) continue;
+ rateLimitStore.delete(key);
+ }
+}
+
+export function resetRequestRateLimitStore(): void {
+ rateLimitStore.clear();
+}
+
+export function createRateLimitGuard(options: RateLimitOptions) {
+ const message = options.message || DEFAULT_MESSAGE;
+
+ return async function rateLimitGuard(request: FastifyRequest, reply: FastifyReply) {
+ const nowMs = Date.now();
+ pruneExpiredEntries(nowMs);
+
+ const key = getRateLimitKey(options.bucket, request);
+ const current = rateLimitStore.get(key);
+
+ if (!current || current.resetAt <= nowMs) {
+ rateLimitStore.set(key, {
+ count: 1,
+ resetAt: nowMs + options.windowMs,
+ });
+ return;
+ }
+
+ if (current.count >= options.max) {
+ const retryAfterSec = Math.max(1, Math.ceil((current.resetAt - nowMs) / 1000));
+ reply
+ .code(429)
+ .header('retry-after', String(retryAfterSec))
+ .send({ success: false, message });
+ return;
+ }
+
+ current.count += 1;
+ rateLimitStore.set(key, current);
+ };
+}
diff --git a/src/server/proxy-core/capabilities/conversationFileCapabilities.test.ts b/src/server/proxy-core/capabilities/conversationFileCapabilities.test.ts
new file mode 100644
index 00000000..f606dde5
--- /dev/null
+++ b/src/server/proxy-core/capabilities/conversationFileCapabilities.test.ts
@@ -0,0 +1,89 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+ rankConversationFileEndpoints,
+ resolveConversationFileEndpointCapability,
+ summarizeConversationFileInputsInOpenAiBody,
+} from './conversationFileCapabilities.js';
+
+describe('conversationFileCapabilities', () => {
+ it('summarizes image, audio, document, and remote file inputs from OpenAI bodies', () => {
+ const summary = summarizeConversationFileInputsInOpenAiBody({
+ messages: [
+ {
+ role: 'user',
+ content: [
+ { type: 'image_url', image_url: { url: 'data:image/png;base64,AAAA' } },
+ { type: 'input_audio', input_audio: { data: 'UklGRg==', format: 'wav' } },
+ {
+ type: 'file',
+ file: {
+ filename: 'brief.pdf',
+ file_data: 'JVBERi0xLjc=',
+ },
+ },
+ {
+ type: 'input_file',
+ filename: 'remote.pdf',
+ file_url: 'https://example.com/remote.pdf',
+ },
+ ],
+ },
+ ],
+ });
+
+ expect(summary).toEqual({
+ hasImage: true,
+ hasAudio: true,
+ hasDocument: true,
+ hasRemoteDocumentUrl: true,
+ });
+ });
+
+ it('describes native remote-document support for claude messages and inline-only support for gemini chat paths', () => {
+ expect(resolveConversationFileEndpointCapability({
+ sitePlatform: 'claude',
+ endpoint: 'messages',
+ })).toMatchObject({
+ image: 'native',
+ audio: 'unsupported',
+ document: 'native',
+ preservesRemoteDocumentUrl: true,
+ });
+
+ expect(resolveConversationFileEndpointCapability({
+ sitePlatform: 'gemini-cli',
+ endpoint: 'chat',
+ })).toMatchObject({
+ image: 'native',
+ audio: 'native',
+ document: 'inline_only',
+ preservesRemoteDocumentUrl: false,
+ });
+
+ expect(resolveConversationFileEndpointCapability({
+ sitePlatform: 'new-api',
+ endpoint: 'messages',
+ })).toMatchObject({
+ image: 'native',
+ audio: 'unsupported',
+ document: 'inline_only',
+ preservesRemoteDocumentUrl: false,
+ });
+ });
+
+ it('ranks document-capable endpoints ahead of lossy fallbacks', () => {
+ const ranked = rankConversationFileEndpoints({
+ sitePlatform: 'new-api',
+ requestedOrder: ['chat', 'messages', 'responses'],
+ summary: {
+ hasImage: false,
+ hasAudio: false,
+ hasDocument: true,
+ hasRemoteDocumentUrl: false,
+ },
+ });
+
+ expect(ranked).toEqual(['responses', 'messages', 'chat']);
+ });
+});
diff --git a/src/server/proxy-core/capabilities/conversationFileCapabilities.ts b/src/server/proxy-core/capabilities/conversationFileCapabilities.ts
new file mode 100644
index 00000000..bd46e90b
--- /dev/null
+++ b/src/server/proxy-core/capabilities/conversationFileCapabilities.ts
@@ -0,0 +1,268 @@
+import { inferInputFileMimeType, normalizeInputFileBlock } from '../../transformers/shared/inputFile.js';
+import { classifyConversationFileMimeType } from '../../../shared/conversationFileTypes.js';
+
+export type ConversationFileTransport = 'unsupported' | 'inline_only' | 'native';
+export type ConversationFileEndpoint = 'chat' | 'messages' | 'responses';
+
+export type ConversationFileInputSummary = {
+ hasImage: boolean;
+ hasAudio: boolean;
+ hasDocument: boolean;
+ hasRemoteDocumentUrl: boolean;
+};
+
+export type ConversationFileEndpointCapability = {
+ image: ConversationFileTransport;
+ audio: ConversationFileTransport;
+ document: ConversationFileTransport;
+ preservesRemoteDocumentUrl: boolean;
+};
+
+function isRecord(value: unknown): value is Record {
+ return !!value && typeof value === 'object' && !Array.isArray(value);
+}
+
+function asTrimmedString(value: unknown): string {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
+function appendConversationFileSummary(
+ summary: ConversationFileInputSummary,
+ item: Record,
+): void {
+ const normalizedFile = normalizeInputFileBlock(item);
+ if (normalizedFile) {
+ const mimeType = inferInputFileMimeType(normalizedFile);
+ const family = classifyConversationFileMimeType(mimeType);
+ if (family === 'image') {
+ summary.hasImage = true;
+ return;
+ }
+ if (family === 'audio') {
+ summary.hasAudio = true;
+ return;
+ }
+ summary.hasDocument = true;
+ if (normalizedFile.fileUrl) {
+ summary.hasRemoteDocumentUrl = true;
+ }
+ return;
+ }
+
+ const type = asTrimmedString(item.type).toLowerCase();
+ if (type === 'image_url' || type === 'input_image') {
+ summary.hasImage = true;
+ return;
+ }
+
+ if (type === 'input_audio') {
+ summary.hasAudio = true;
+ return;
+ }
+
+ if (Array.isArray(item.content)) {
+ for (const nested of item.content) {
+ if (isRecord(nested)) {
+ appendConversationFileSummary(summary, nested);
+ }
+ }
+ }
+}
+
+function createEmptySummary(): ConversationFileInputSummary {
+ return {
+ hasImage: false,
+ hasAudio: false,
+ hasDocument: false,
+ hasRemoteDocumentUrl: false,
+ };
+}
+
+export function summarizeConversationFileInputsInOpenAiBody(
+ body: Record,
+): ConversationFileInputSummary {
+ const summary = createEmptySummary();
+ const messages = Array.isArray(body.messages) ? body.messages : [];
+ for (const message of messages) {
+ if (!isRecord(message)) continue;
+ const content = message.content;
+ if (Array.isArray(content)) {
+ for (const item of content) {
+ if (isRecord(item)) appendConversationFileSummary(summary, item);
+ }
+ continue;
+ }
+ if (isRecord(content)) {
+ appendConversationFileSummary(summary, content);
+ }
+ }
+ return summary;
+}
+
+export function summarizeConversationFileInputsInResponsesBody(
+ body: Record,
+): ConversationFileInputSummary {
+ const summary = createEmptySummary();
+ const input = body.input;
+ if (Array.isArray(input)) {
+ for (const item of input) {
+ if (isRecord(item)) appendConversationFileSummary(summary, item);
+ }
+ return summary;
+ }
+ if (isRecord(input)) {
+ appendConversationFileSummary(summary, input);
+ }
+ return summary;
+}
+
+export function resolveConversationFileEndpointCapability(input: {
+ sitePlatform?: string;
+ endpoint: ConversationFileEndpoint;
+}): ConversationFileEndpointCapability {
+ const platform = asTrimmedString(input.sitePlatform).toLowerCase();
+ const endpoint = input.endpoint;
+
+ if (platform === 'codex') {
+ if (endpoint === 'responses') {
+ return {
+ image: 'native',
+ audio: 'native',
+ document: 'native',
+ preservesRemoteDocumentUrl: true,
+ };
+ }
+ return {
+ image: 'unsupported',
+ audio: 'unsupported',
+ document: 'unsupported',
+ preservesRemoteDocumentUrl: false,
+ };
+ }
+
+ if (platform === 'claude') {
+ if (endpoint === 'messages') {
+ return {
+ image: 'native',
+ audio: 'unsupported',
+ document: 'native',
+ preservesRemoteDocumentUrl: true,
+ };
+ }
+ return {
+ image: 'unsupported',
+ audio: 'unsupported',
+ document: 'unsupported',
+ preservesRemoteDocumentUrl: false,
+ };
+ }
+
+ if (platform === 'gemini-cli' || platform === 'antigravity') {
+ if (endpoint === 'chat') {
+ return {
+ image: 'native',
+ audio: 'native',
+ document: 'inline_only',
+ preservesRemoteDocumentUrl: false,
+ };
+ }
+ return {
+ image: 'unsupported',
+ audio: 'unsupported',
+ document: 'unsupported',
+ preservesRemoteDocumentUrl: false,
+ };
+ }
+
+ if (platform === 'gemini') {
+ if (endpoint === 'responses') {
+ return {
+ image: 'native',
+ audio: 'native',
+ document: 'native',
+ preservesRemoteDocumentUrl: true,
+ };
+ }
+ if (endpoint === 'chat') {
+ return {
+ image: 'native',
+ audio: 'native',
+ document: 'inline_only',
+ preservesRemoteDocumentUrl: false,
+ };
+ }
+ return {
+ image: 'unsupported',
+ audio: 'unsupported',
+ document: 'unsupported',
+ preservesRemoteDocumentUrl: false,
+ };
+ }
+
+ if (endpoint === 'responses') {
+ return {
+ image: 'native',
+ audio: 'native',
+ document: 'native',
+ preservesRemoteDocumentUrl: true,
+ };
+ }
+
+ if (endpoint === 'messages') {
+ return {
+ image: 'native',
+ audio: 'unsupported',
+ document: 'inline_only',
+ preservesRemoteDocumentUrl: false,
+ };
+ }
+
+ return {
+ image: 'native',
+ audio: 'native',
+ document: 'inline_only',
+ preservesRemoteDocumentUrl: false,
+ };
+}
+
+export function rankConversationFileEndpoints(input: {
+ sitePlatform?: string;
+ requestedOrder: ConversationFileEndpoint[];
+ summary: ConversationFileInputSummary;
+ preferMessagesForClaudeModel?: boolean;
+}): ConversationFileEndpoint[] {
+ if (!input.summary.hasDocument) {
+ return [...input.requestedOrder];
+ }
+
+ const isDocumentCompatible = (endpoint: ConversationFileEndpoint) => {
+ const capability = resolveConversationFileEndpointCapability({
+ sitePlatform: input.sitePlatform,
+ endpoint,
+ });
+ if (capability.document === 'unsupported') return false;
+ if (input.summary.hasRemoteDocumentUrl && !capability.preservesRemoteDocumentUrl) return false;
+ return true;
+ };
+
+ const preferredDocumentOrder = input.preferMessagesForClaudeModel === true
+ ? ['messages', 'responses', 'chat']
+ : ['responses', 'messages', 'chat'];
+
+ const supportedDocumentEndpoints = preferredDocumentOrder.filter((endpoint) => {
+ if (!input.requestedOrder.includes(endpoint as ConversationFileEndpoint)) return false;
+ return isDocumentCompatible(endpoint as ConversationFileEndpoint);
+ }) as ConversationFileEndpoint[];
+
+ if (supportedDocumentEndpoints.length <= 0) {
+ return [...input.requestedOrder];
+ }
+
+ return [
+ ...supportedDocumentEndpoints,
+ ...input.requestedOrder.filter((endpoint) => (
+ !supportedDocumentEndpoints.includes(endpoint)
+ && isDocumentCompatible(endpoint)
+ )),
+ ];
+}
diff --git a/src/server/proxy-core/cliProfiles/claudeCodeProfile.ts b/src/server/proxy-core/cliProfiles/claudeCodeProfile.ts
new file mode 100644
index 00000000..0f672187
--- /dev/null
+++ b/src/server/proxy-core/cliProfiles/claudeCodeProfile.ts
@@ -0,0 +1,56 @@
+import type { CliProfileDefinition } from './types.js';
+
+function isRecord(value: unknown): value is Record {
+ return !!value && typeof value === 'object' && !Array.isArray(value);
+}
+
+const claudeCodeUserIdPattern = /^user_[0-9a-f]{64}_account__session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+function isClaudeSurface(path: string): boolean {
+ const normalizedPath = path.trim().toLowerCase();
+ return normalizedPath === '/v1/messages'
+ || normalizedPath === '/anthropic/v1/messages'
+ || normalizedPath === '/v1/messages/count_tokens';
+}
+
+export function extractClaudeCodeSessionId(userId: string): string | null {
+ const trimmed = userId.trim();
+ if (!claudeCodeUserIdPattern.test(trimmed)) return null;
+
+ const sessionPrefix = '__session_';
+ const sessionIndex = trimmed.lastIndexOf(sessionPrefix);
+ if (sessionIndex === -1) return null;
+
+ const sessionId = trimmed.slice(sessionIndex + sessionPrefix.length).trim();
+ return sessionId || null;
+}
+
+export const claudeCodeCliProfile: CliProfileDefinition = {
+ id: 'claude_code',
+ capabilities: {
+ supportsResponsesCompact: false,
+ supportsResponsesWebsocketIncremental: false,
+ preservesContinuation: true,
+ supportsCountTokens: true,
+ echoesTurnState: false,
+ },
+ detect(input) {
+ if (!isClaudeSurface(input.downstreamPath)) return null;
+ if (!isRecord(input.body) || !isRecord(input.body.metadata)) return null;
+
+ const userId = typeof input.body.metadata.user_id === 'string'
+ ? input.body.metadata.user_id.trim()
+ : '';
+ const sessionId = userId ? extractClaudeCodeSessionId(userId) : null;
+ if (!sessionId) return null;
+
+ return {
+ id: 'claude_code',
+ sessionId,
+ traceHint: sessionId,
+ clientAppId: 'claude_code',
+ clientAppName: 'Claude Code',
+ clientConfidence: 'exact',
+ };
+ },
+};
diff --git a/src/server/proxy-core/cliProfiles/codexProfile.test.ts b/src/server/proxy-core/cliProfiles/codexProfile.test.ts
new file mode 100644
index 00000000..f8d2b75a
--- /dev/null
+++ b/src/server/proxy-core/cliProfiles/codexProfile.test.ts
@@ -0,0 +1,57 @@
+import { describe, expect, it } from 'vitest';
+import { detectCodexOfficialClientApp } from './codexProfile.js';
+
+describe('detectCodexOfficialClientApp', () => {
+ it('returns null for missing or empty headers', () => {
+ expect(detectCodexOfficialClientApp()).toBeNull();
+ expect(detectCodexOfficialClientApp({})).toBeNull();
+ });
+
+ it('detects official Codex clients from originator prefixes', () => {
+ expect(detectCodexOfficialClientApp({
+ originator: 'codex_exec',
+ })).toEqual({
+ clientAppId: 'codex_exec',
+ clientAppName: 'Codex Exec',
+ });
+ });
+
+ it('detects official Codex clients from user-agent prefixes', () => {
+ expect(detectCodexOfficialClientApp({
+ 'user-agent': 'Mozilla/5.0 codex_chatgpt_desktop/1.2.3',
+ })).toEqual({
+ clientAppId: 'codex_chatgpt_desktop',
+ clientAppName: 'Codex Desktop',
+ });
+ });
+
+ it('matches headers case-insensitively and from header arrays', () => {
+ expect(detectCodexOfficialClientApp({
+ originator: 'CODEX_EXEC',
+ })).toEqual({
+ clientAppId: 'codex_exec',
+ clientAppName: 'Codex Exec',
+ });
+ expect(detectCodexOfficialClientApp({
+ 'user-agent': 'Mozilla/5.0 CODEX_chatgpt_desktop/1.2',
+ })).toEqual({
+ clientAppId: 'codex_chatgpt_desktop',
+ clientAppName: 'Codex Desktop',
+ });
+ expect(detectCodexOfficialClientApp({
+ 'user-agent': ['Mozilla/5.0', 'codex_chatgpt_desktop/1.2'],
+ })).toEqual({
+ clientAppId: 'codex_chatgpt_desktop',
+ clientAppName: 'Codex Desktop',
+ });
+ expect(detectCodexOfficialClientApp({
+ originator: ['ignored', 'other-client'],
+ })).toBeNull();
+ });
+
+ it('returns null for non-official Codex clients', () => {
+ expect(detectCodexOfficialClientApp({
+ 'user-agent': 'OpenClaw/1.0',
+ })).toBe(null);
+ });
+});
diff --git a/src/server/proxy-core/cliProfiles/codexProfile.ts b/src/server/proxy-core/cliProfiles/codexProfile.ts
new file mode 100644
index 00000000..291f34a0
--- /dev/null
+++ b/src/server/proxy-core/cliProfiles/codexProfile.ts
@@ -0,0 +1,209 @@
+import type { CliProfileDefinition, DetectCliProfileInput } from './types.js';
+
+type CodexOfficialClientApp = {
+ clientAppId: string;
+ clientAppName: string;
+};
+
+const CODEX_OFFICIAL_CLIENT_USER_AGENT_PREFIXES = [
+ 'codex_cli_rs/',
+ 'codex_vscode/',
+ 'codex_app/',
+ 'codex_chatgpt_desktop/',
+ 'codex_atlas/',
+ 'codex_exec/',
+ 'codex_sdk_ts/',
+ 'codex ',
+];
+
+const CODEX_OFFICIAL_CLIENT_ORIGINATOR_PREFIXES = [
+ 'codex_',
+ 'codex ',
+];
+
+const CODEX_OFFICIAL_CLIENT_APP_RULES = [
+ {
+ id: 'codex_cli_rs',
+ name: 'Codex CLI',
+ userAgentPrefixes: ['codex_cli_rs/'],
+ originatorPrefixes: ['codex_cli_rs'],
+ },
+ {
+ id: 'codex_vscode',
+ name: 'Codex VSCode',
+ userAgentPrefixes: ['codex_vscode/'],
+ originatorPrefixes: ['codex_vscode'],
+ },
+ {
+ id: 'codex_app',
+ name: 'Codex App',
+ userAgentPrefixes: ['codex_app/'],
+ originatorPrefixes: ['codex_app'],
+ },
+ {
+ id: 'codex_chatgpt_desktop',
+ name: 'Codex Desktop',
+ userAgentPrefixes: ['codex_chatgpt_desktop/', 'codex desktop/'],
+ originatorPrefixes: ['codex_chatgpt_desktop', 'codex desktop'],
+ },
+ {
+ id: 'codex_atlas',
+ name: 'Codex Atlas',
+ userAgentPrefixes: ['codex_atlas/'],
+ originatorPrefixes: ['codex_atlas'],
+ },
+ {
+ id: 'codex_exec',
+ name: 'Codex Exec',
+ userAgentPrefixes: ['codex_exec/'],
+ originatorPrefixes: ['codex_exec'],
+ },
+ {
+ id: 'codex_sdk_ts',
+ name: 'Codex SDK TS',
+ userAgentPrefixes: ['codex_sdk_ts/'],
+ originatorPrefixes: ['codex_sdk_ts'],
+ },
+] as const;
+
+function headerValueToStrings(value: unknown): string[] {
+ if (typeof value === 'string') {
+ const trimmed = value.trim();
+ return trimmed ? [trimmed] : [];
+ }
+
+ if (Array.isArray(value)) {
+ const values: string[] = [];
+ for (const item of value) {
+ if (typeof item !== 'string') continue;
+ const trimmed = item.trim();
+ if (trimmed) values.push(trimmed);
+ }
+ return values;
+ }
+
+ return [];
+}
+
+function headerValueToString(value: unknown): string | null {
+ return headerValueToStrings(value)[0] || null;
+}
+
+function getHeaderValue(headers: Record | undefined, targetKey: string): string | null {
+ return getHeaderValues(headers, targetKey)[0] || null;
+}
+
+function getHeaderValues(headers: Record | undefined, targetKey: string): string[] {
+ if (!headers) return [];
+ const normalizedTarget = targetKey.trim().toLowerCase();
+ const values: string[] = [];
+
+ for (const [rawKey, rawValue] of Object.entries(headers)) {
+ if (rawKey.trim().toLowerCase() !== normalizedTarget) continue;
+ values.push(...headerValueToStrings(rawValue));
+ }
+ return values;
+}
+
+function hasHeaderPrefix(headers: Record | undefined, prefix: string): boolean {
+ if (!headers) return false;
+ const normalizedPrefix = prefix.trim().toLowerCase();
+ return Object.entries(headers).some(([rawKey, rawValue]) => {
+ const key = rawKey.trim().toLowerCase();
+ return key.startsWith(normalizedPrefix) && !!headerValueToString(rawValue);
+ });
+}
+
+function matchesHeaderPrefixes(value: string | string[] | null, prefixes: readonly string[]): boolean {
+ const values = Array.isArray(value)
+ ? value.map((item) => item.trim().toLowerCase()).filter(Boolean)
+ : [value?.trim().toLowerCase() || ''].filter(Boolean);
+ if (values.length === 0) return false;
+
+ return values.some((normalizedValue) => prefixes.some((prefix) => {
+ const normalizedPrefix = prefix.trim().toLowerCase();
+ if (!normalizedPrefix) return false;
+ return normalizedValue.startsWith(normalizedPrefix)
+ || normalizedValue.includes(normalizedPrefix);
+ }));
+}
+
+function isCodexPath(path: string): boolean {
+ const normalizedPath = path.trim().toLowerCase();
+ return normalizedPath.startsWith('/v1/responses')
+ || normalizedPath === '/v1/chat/completions'
+ || normalizedPath.startsWith('/v1/messages');
+}
+
+export function detectCodexOfficialClientApp(
+ headers?: Record,
+): CodexOfficialClientApp | null {
+ for (const rule of CODEX_OFFICIAL_CLIENT_APP_RULES) {
+ const matchesOriginator = matchesHeaderPrefixes(getHeaderValues(headers, 'originator'), rule.originatorPrefixes);
+ const matchesUserAgent = matchesHeaderPrefixes(getHeaderValues(headers, 'user-agent'), rule.userAgentPrefixes);
+ if (!matchesOriginator && !matchesUserAgent) continue;
+ return {
+ clientAppId: rule.id,
+ clientAppName: rule.name,
+ };
+ }
+ return null;
+}
+
+export function isCodexResponsesSurface(headers?: Record): boolean {
+ return isCodexRequest({
+ downstreamPath: '/v1/responses',
+ headers,
+ });
+}
+
+export function getCodexSessionId(headers?: Record): string | null {
+ return getHeaderValue(headers, 'session_id') || getHeaderValue(headers, 'session-id');
+}
+
+export function isCodexRequest(input: DetectCliProfileInput): boolean {
+ if (!isCodexPath(input.downstreamPath)) return false;
+ const headers = input.headers;
+ if (!headers) return false;
+
+ const originator = getHeaderValues(headers, 'originator');
+ if (matchesHeaderPrefixes(originator, CODEX_OFFICIAL_CLIENT_ORIGINATOR_PREFIXES)) return true;
+ if (matchesHeaderPrefixes(getHeaderValues(headers, 'user-agent'), CODEX_OFFICIAL_CLIENT_USER_AGENT_PREFIXES)) return true;
+ if (getHeaderValue(headers, 'openai-beta')) return true;
+ if (hasHeaderPrefix(headers, 'x-stainless-')) return true;
+ if (getCodexSessionId(headers)) return true;
+ if (getHeaderValue(headers, 'x-codex-turn-state')) return true;
+ return false;
+}
+
+export const codexCliProfile: CliProfileDefinition = {
+ id: 'codex',
+ capabilities: {
+ supportsResponsesCompact: true,
+ supportsResponsesWebsocketIncremental: true,
+ preservesContinuation: true,
+ supportsCountTokens: false,
+ echoesTurnState: true,
+ },
+ detect(input) {
+ if (!isCodexRequest(input)) return null;
+
+ const sessionId = getCodexSessionId(input.headers) || undefined;
+ const clientApp = detectCodexOfficialClientApp(input.headers);
+ return {
+ id: 'codex',
+ ...(sessionId ? { sessionId, traceHint: sessionId } : {}),
+ ...(clientApp
+ ? {
+ clientAppId: clientApp.clientAppId,
+ clientAppName: clientApp.clientAppName,
+ clientConfidence: 'exact' as const,
+ }
+ : {
+ clientAppId: 'codex',
+ clientAppName: 'Codex',
+ clientConfidence: 'heuristic' as const,
+ }),
+ };
+ },
+};
diff --git a/src/server/proxy-core/cliProfiles/geminiCliProfile.ts b/src/server/proxy-core/cliProfiles/geminiCliProfile.ts
new file mode 100644
index 00000000..fc9c7fcf
--- /dev/null
+++ b/src/server/proxy-core/cliProfiles/geminiCliProfile.ts
@@ -0,0 +1,40 @@
+import type { CliProfileDefinition } from './types.js';
+
+function isRecord(value: unknown): value is Record {
+ return !!value && typeof value === 'object' && !Array.isArray(value);
+}
+
+function isGeminiCliPath(path: string): boolean {
+ const normalizedPath = path.trim().toLowerCase();
+ return normalizedPath === '/v1internal:generatecontent'
+ || normalizedPath === '/v1internal:streamgeneratecontent'
+ || normalizedPath === '/v1internal:counttokens';
+}
+
+function hasGeminiCliBodyShape(body: unknown): boolean {
+ if (!isRecord(body)) return false;
+ return typeof body.model === 'string'
+ || Array.isArray(body.contents)
+ || (isRecord(body.request) && (Array.isArray(body.request.contents) || typeof body.request.model === 'string'));
+}
+
+export const geminiCliProfile: CliProfileDefinition = {
+ id: 'gemini_cli',
+ capabilities: {
+ supportsResponsesCompact: false,
+ supportsResponsesWebsocketIncremental: false,
+ preservesContinuation: false,
+ supportsCountTokens: true,
+ echoesTurnState: false,
+ },
+ detect(input) {
+ if (!isGeminiCliPath(input.downstreamPath)) return null;
+ if (input.body !== undefined && !hasGeminiCliBodyShape(input.body)) return null;
+ return {
+ id: 'gemini_cli',
+ clientAppId: 'gemini_cli',
+ clientAppName: 'Gemini CLI',
+ clientConfidence: 'exact',
+ };
+ },
+};
diff --git a/src/server/proxy-core/cliProfiles/genericProfile.ts b/src/server/proxy-core/cliProfiles/genericProfile.ts
new file mode 100644
index 00000000..8bda115c
--- /dev/null
+++ b/src/server/proxy-core/cliProfiles/genericProfile.ts
@@ -0,0 +1,17 @@
+import type { CliProfileDefinition } from './types.js';
+
+export const genericCliProfile: CliProfileDefinition = {
+ id: 'generic',
+ capabilities: {
+ supportsResponsesCompact: false,
+ supportsResponsesWebsocketIncremental: false,
+ preservesContinuation: false,
+ supportsCountTokens: false,
+ echoesTurnState: false,
+ },
+ detect() {
+ return {
+ id: 'generic',
+ };
+ },
+};
diff --git a/src/server/proxy-core/cliProfiles/registry.test.ts b/src/server/proxy-core/cliProfiles/registry.test.ts
new file mode 100644
index 00000000..89b66a22
--- /dev/null
+++ b/src/server/proxy-core/cliProfiles/registry.test.ts
@@ -0,0 +1,137 @@
+import { describe, expect, it } from 'vitest';
+
+import { detectCliProfile } from './registry.js';
+
+describe('detectCliProfile', () => {
+ it('detects Codex responses requests and exposes Codex capability flags', () => {
+ expect(detectCliProfile({
+ downstreamPath: '/v1/responses',
+ headers: {
+ originator: 'codex_cli_rs',
+ Session_id: 'codex-session-123',
+ },
+ })).toEqual({
+ id: 'codex',
+ sessionId: 'codex-session-123',
+ traceHint: 'codex-session-123',
+ clientAppId: 'codex_cli_rs',
+ clientAppName: 'Codex CLI',
+ clientConfidence: 'exact',
+ capabilities: {
+ supportsResponsesCompact: true,
+ supportsResponsesWebsocketIncremental: true,
+ preservesContinuation: true,
+ supportsCountTokens: false,
+ echoesTurnState: true,
+ },
+ });
+ });
+
+ it('treats x-codex-turn-state as a Codex marker even when session_id is absent', () => {
+ expect(detectCliProfile({
+ downstreamPath: '/v1/responses',
+ headers: {
+ 'x-codex-turn-state': 'turn-state-123',
+ },
+ })).toMatchObject({
+ id: 'codex',
+ capabilities: {
+ supportsResponsesWebsocketIncremental: true,
+ echoesTurnState: true,
+ },
+ });
+ });
+
+ it('detects broader Codex official-client headers from user-agent and originator prefixes', () => {
+ expect(detectCliProfile({
+ downstreamPath: '/v1/responses',
+ headers: {
+ 'user-agent': 'Mozilla/5.0 codex_chatgpt_desktop/1.2.3',
+ },
+ })).toMatchObject({
+ id: 'codex',
+ capabilities: {
+ supportsResponsesWebsocketIncremental: true,
+ echoesTurnState: true,
+ },
+ });
+
+ expect(detectCliProfile({
+ downstreamPath: '/v1/responses',
+ headers: {
+ originator: 'codex_exec',
+ },
+ })).toMatchObject({
+ id: 'codex',
+ capabilities: {
+ supportsResponsesWebsocketIncremental: true,
+ echoesTurnState: true,
+ },
+ });
+ });
+
+ it('detects Claude Code requests on the count_tokens surface and exposes token counting support', () => {
+ expect(detectCliProfile({
+ downstreamPath: '/v1/messages/count_tokens',
+ body: {
+ metadata: {
+ user_id: 'user_20836b5653ed68aa981604f502c0a491397f6053826a93c953423632578d38ad_account__session_f25958b8-e75c-455d-8b40-f006d87cc2a4',
+ },
+ },
+ })).toEqual({
+ id: 'claude_code',
+ sessionId: 'f25958b8-e75c-455d-8b40-f006d87cc2a4',
+ traceHint: 'f25958b8-e75c-455d-8b40-f006d87cc2a4',
+ clientAppId: 'claude_code',
+ clientAppName: 'Claude Code',
+ clientConfidence: 'exact',
+ capabilities: {
+ supportsResponsesCompact: false,
+ supportsResponsesWebsocketIncremental: false,
+ preservesContinuation: true,
+ supportsCountTokens: true,
+ echoesTurnState: false,
+ },
+ });
+ });
+
+ it('detects Gemini CLI internal routes and exposes Gemini CLI capability flags', () => {
+ expect(detectCliProfile({
+ downstreamPath: '/v1internal:countTokens',
+ body: {
+ model: 'gpt-4.1',
+ contents: [{ role: 'user', parts: [{ text: 'hello' }] }],
+ },
+ })).toEqual({
+ id: 'gemini_cli',
+ clientAppId: 'gemini_cli',
+ clientAppName: 'Gemini CLI',
+ clientConfidence: 'exact',
+ capabilities: {
+ supportsResponsesCompact: false,
+ supportsResponsesWebsocketIncremental: false,
+ preservesContinuation: false,
+ supportsCountTokens: true,
+ echoesTurnState: false,
+ },
+ });
+ });
+
+ it('falls back to generic for native Gemini routes', () => {
+ expect(detectCliProfile({
+ downstreamPath: '/v1beta/models/gemini-2.5-flash:generateContent',
+ body: {
+ contents: [{ role: 'user', parts: [{ text: 'hello' }] }],
+ },
+ })).toEqual({
+ id: 'generic',
+ capabilities: {
+ supportsResponsesCompact: false,
+ supportsResponsesWebsocketIncremental: false,
+ preservesContinuation: false,
+ supportsCountTokens: false,
+ echoesTurnState: false,
+ },
+ });
+ });
+});
diff --git a/src/server/proxy-core/cliProfiles/registry.ts b/src/server/proxy-core/cliProfiles/registry.ts
new file mode 100644
index 00000000..4f69536a
--- /dev/null
+++ b/src/server/proxy-core/cliProfiles/registry.ts
@@ -0,0 +1,38 @@
+import { claudeCodeCliProfile } from './claudeCodeProfile.js';
+import { codexCliProfile } from './codexProfile.js';
+import { geminiCliProfile } from './geminiCliProfile.js';
+import { genericCliProfile } from './genericProfile.js';
+import type { CliProfileDefinition, CliProfileId, DetectCliProfileInput, DetectedCliProfile } from './types.js';
+
+const orderedProfiles: CliProfileDefinition[] = [
+ claudeCodeCliProfile,
+ codexCliProfile,
+ geminiCliProfile,
+];
+
+export const cliProfileRegistry: Record = {
+ generic: genericCliProfile,
+ codex: codexCliProfile,
+ claude_code: claudeCodeCliProfile,
+ gemini_cli: geminiCliProfile,
+};
+
+export function getCliProfileDefinition(id: CliProfileId): CliProfileDefinition {
+ return cliProfileRegistry[id];
+}
+
+export function detectCliProfile(input: DetectCliProfileInput): DetectedCliProfile {
+ for (const profile of orderedProfiles) {
+ const detected = profile.detect(input);
+ if (!detected) continue;
+ return {
+ ...detected,
+ capabilities: profile.capabilities,
+ };
+ }
+
+ return {
+ id: genericCliProfile.id,
+ capabilities: genericCliProfile.capabilities,
+ };
+}
diff --git a/src/server/proxy-core/cliProfiles/types.ts b/src/server/proxy-core/cliProfiles/types.ts
new file mode 100644
index 00000000..5f95c67e
--- /dev/null
+++ b/src/server/proxy-core/cliProfiles/types.ts
@@ -0,0 +1,37 @@
+export type CliProfileId =
+ | 'generic'
+ | 'codex'
+ | 'claude_code'
+ | 'gemini_cli';
+
+export type CliProfileCapabilities = {
+ supportsResponsesCompact: boolean;
+ supportsResponsesWebsocketIncremental: boolean;
+ preservesContinuation: boolean;
+ supportsCountTokens: boolean;
+ echoesTurnState: boolean;
+};
+
+export type CliProfileClientConfidence = 'exact' | 'heuristic';
+
+export type DetectCliProfileInput = {
+ downstreamPath: string;
+ headers?: Record;
+ body?: unknown;
+};
+
+export type DetectedCliProfile = {
+ id: CliProfileId;
+ sessionId?: string;
+ traceHint?: string;
+ clientAppId?: string;
+ clientAppName?: string;
+ clientConfidence?: CliProfileClientConfidence;
+ capabilities: CliProfileCapabilities;
+};
+
+export type CliProfileDefinition = {
+ id: CliProfileId;
+ capabilities: CliProfileCapabilities;
+ detect(input: DetectCliProfileInput): Omit | null;
+};
diff --git a/src/server/proxy-core/conductor/DefaultProxyConductor.test.ts b/src/server/proxy-core/conductor/DefaultProxyConductor.test.ts
new file mode 100644
index 00000000..932d6c5f
--- /dev/null
+++ b/src/server/proxy-core/conductor/DefaultProxyConductor.test.ts
@@ -0,0 +1,241 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { DefaultProxyConductor } from './DefaultProxyConductor.js';
+import { terminalStreamFailure } from './streamTermination.js';
+
+const baseSelectedChannel = {
+ channel: { id: 11, routeId: 22 },
+ site: { id: 44, name: 'demo-site', url: 'https://upstream.example.com', platform: 'openai' },
+ account: { id: 33, username: 'demo-user' },
+ tokenName: 'default',
+ tokenValue: 'sk-demo',
+ actualModel: 'upstream-gpt',
+};
+
+describe('DefaultProxyConductor', () => {
+ it('returns the first selected channel when the first attempt succeeds', async () => {
+ const selectChannel = vi.fn().mockResolvedValue(baseSelectedChannel);
+ const selectNextChannel = vi.fn();
+ const recordSuccess = vi.fn().mockResolvedValue(undefined);
+ const recordFailure = vi.fn().mockResolvedValue(undefined);
+ const conductor = new DefaultProxyConductor({
+ selectChannel,
+ selectNextChannel,
+ recordSuccess,
+ recordFailure,
+ });
+ const attempt = vi.fn().mockResolvedValue({
+ ok: true,
+ response: new Response('ok', { status: 200 }),
+ latencyMs: 12,
+ cost: 0.25,
+ });
+
+ const result = await conductor.execute({
+ requestedModel: 'gpt-5.4',
+ attempt,
+ });
+
+ expect(result).toMatchObject({
+ ok: true,
+ selected: baseSelectedChannel,
+ attempts: 1,
+ });
+ expect(selectChannel).toHaveBeenCalledWith('gpt-5.4', undefined);
+ expect(selectNextChannel).not.toHaveBeenCalled();
+ expect(recordFailure).not.toHaveBeenCalled();
+ expect(recordSuccess).toHaveBeenCalledWith(11, {
+ latencyMs: 12,
+ cost: 0.25,
+ });
+ });
+
+ it('retries on the same channel when the attempt asks for a same-channel retry', async () => {
+ const selectChannel = vi.fn().mockResolvedValue(baseSelectedChannel);
+ const selectNextChannel = vi.fn();
+ const recordSuccess = vi.fn().mockResolvedValue(undefined);
+ const recordFailure = vi.fn().mockResolvedValue(undefined);
+ const conductor = new DefaultProxyConductor({
+ selectChannel,
+ selectNextChannel,
+ recordSuccess,
+ recordFailure,
+ });
+ const attempt = vi.fn()
+ .mockResolvedValueOnce({
+ ok: false,
+ action: 'retry_same_channel',
+ status: 429,
+ rawErrorText: 'rate limited',
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ response: new Response('ok', { status: 200 }),
+ });
+
+ const result = await conductor.execute({
+ requestedModel: 'gpt-5.4',
+ attempt,
+ });
+
+ expect(result).toMatchObject({
+ ok: true,
+ attempts: 2,
+ });
+ expect(selectNextChannel).not.toHaveBeenCalled();
+ expect(attempt).toHaveBeenCalledTimes(2);
+ expect(recordFailure).toHaveBeenCalledWith(11, {
+ status: 429,
+ rawErrorText: 'rate limited',
+ });
+ });
+
+ it('fails over to the next channel when the attempt asks for failover', async () => {
+ const nextSelectedChannel = {
+ ...baseSelectedChannel,
+ channel: { id: 12, routeId: 22 },
+ tokenValue: 'sk-next',
+ };
+ const selectChannel = vi.fn().mockResolvedValue(baseSelectedChannel);
+ const selectNextChannel = vi.fn().mockResolvedValue(nextSelectedChannel);
+ const recordSuccess = vi.fn().mockResolvedValue(undefined);
+ const recordFailure = vi.fn().mockResolvedValue(undefined);
+ const conductor = new DefaultProxyConductor({
+ selectChannel,
+ selectNextChannel,
+ recordSuccess,
+ recordFailure,
+ });
+ const attempt = vi.fn()
+ .mockResolvedValueOnce({
+ ok: false,
+ action: 'failover',
+ status: 503,
+ rawErrorText: 'upstream unavailable',
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ response: new Response('ok', { status: 200 }),
+ });
+
+ const result = await conductor.execute({
+ requestedModel: 'gpt-5.4',
+ attempt,
+ });
+
+ expect(result).toMatchObject({
+ ok: true,
+ selected: nextSelectedChannel,
+ attempts: 2,
+ });
+ expect(selectNextChannel).toHaveBeenCalledWith('gpt-5.4', [11], undefined);
+ expect(recordFailure).toHaveBeenCalledWith(11, {
+ status: 503,
+ rawErrorText: 'upstream unavailable',
+ });
+ expect(recordSuccess).toHaveBeenCalledWith(12, {
+ latencyMs: null,
+ cost: null,
+ });
+ });
+
+ it('refreshes auth on 401 and retries the same channel with the refreshed selection', async () => {
+ const refreshedChannel = {
+ ...baseSelectedChannel,
+ tokenValue: 'sk-refreshed',
+ };
+ const refreshAuth = vi.fn().mockResolvedValue(refreshedChannel);
+ const conductor = new DefaultProxyConductor({
+ selectChannel: vi.fn().mockResolvedValue(baseSelectedChannel),
+ selectNextChannel: vi.fn(),
+ recordSuccess: vi.fn().mockResolvedValue(undefined),
+ recordFailure: vi.fn().mockResolvedValue(undefined),
+ refreshAuth,
+ });
+ const attempt = vi.fn()
+ .mockResolvedValueOnce({
+ ok: false,
+ action: 'refresh_auth',
+ status: 401,
+ rawErrorText: 'expired token',
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ response: new Response('ok', { status: 200 }),
+ });
+
+ const result = await conductor.execute({
+ requestedModel: 'gpt-5.4',
+ attempt,
+ });
+
+ expect(result).toMatchObject({
+ ok: true,
+ selected: refreshedChannel,
+ attempts: 2,
+ });
+ expect(refreshAuth).toHaveBeenCalledWith(baseSelectedChannel, {
+ status: 401,
+ rawErrorText: 'expired token',
+ });
+ });
+
+ it('returns a no_channel result when no channel is available', async () => {
+ const conductor = new DefaultProxyConductor({
+ selectChannel: vi.fn().mockResolvedValue(null),
+ selectNextChannel: vi.fn(),
+ recordSuccess: vi.fn(),
+ recordFailure: vi.fn(),
+ previewSelectedChannel: vi.fn().mockResolvedValue(null),
+ });
+
+ expect(await conductor.previewSelectedChannel('gpt-5.4')).toBe(null);
+
+ const result = await conductor.execute({
+ requestedModel: 'gpt-5.4',
+ attempt: vi.fn(),
+ });
+
+ expect(result).toEqual({
+ ok: false,
+ reason: 'no_channel',
+ attempts: 0,
+ });
+ });
+
+ it('propagates terminal stream failures and calls the terminal failure hook', async () => {
+ const onTerminalFailure = vi.fn().mockResolvedValue(undefined);
+ const conductor = new DefaultProxyConductor({
+ selectChannel: vi.fn().mockResolvedValue(baseSelectedChannel),
+ selectNextChannel: vi.fn(),
+ recordSuccess: vi.fn(),
+ recordFailure: vi.fn().mockResolvedValue(undefined),
+ });
+ const attempt = vi.fn().mockResolvedValue({
+ ok: false,
+ ...terminalStreamFailure({
+ status: 502,
+ rawErrorText: 'stream disconnected before completion',
+ }),
+ });
+
+ const result = await conductor.execute({
+ requestedModel: 'gpt-5.4',
+ attempt,
+ onTerminalFailure,
+ });
+
+ expect(result).toEqual({
+ ok: false,
+ reason: 'terminal',
+ selected: baseSelectedChannel,
+ status: 502,
+ rawErrorText: 'stream disconnected before completion',
+ attempts: 1,
+ });
+ expect(onTerminalFailure).toHaveBeenCalledWith(baseSelectedChannel, {
+ status: 502,
+ rawErrorText: 'stream disconnected before completion',
+ });
+ });
+});
diff --git a/src/server/proxy-core/conductor/DefaultProxyConductor.ts b/src/server/proxy-core/conductor/DefaultProxyConductor.ts
new file mode 100644
index 00000000..d95ac80c
--- /dev/null
+++ b/src/server/proxy-core/conductor/DefaultProxyConductor.ts
@@ -0,0 +1,127 @@
+import {
+ failureActionOf,
+ isTerminalFailure,
+ shouldFailover,
+ shouldRefreshAuth,
+ shouldRetrySameChannel,
+} from './retryPolicy.js';
+import type { ExecuteInput, ExecuteResult, ProxyConductorDependencies, SelectedChannelLike } from './types.js';
+import { recordFailedAttempt, recordSuccessfulAttempt } from './usageHooks.js';
+
+export class DefaultProxyConductor {
+ constructor(private readonly deps: ProxyConductorDependencies) {}
+
+ async previewSelectedChannel(requestedModel: string, downstreamPolicy?: unknown): Promise {
+ if (this.deps.previewSelectedChannel) {
+ return this.deps.previewSelectedChannel(requestedModel, downstreamPolicy);
+ }
+ return this.deps.selectChannel(requestedModel, downstreamPolicy);
+ }
+
+ async execute(input: ExecuteInput): Promise {
+ const excludeChannelIds: number[] = [];
+ let attempts = 0;
+ let selected = await this.deps.selectChannel(input.requestedModel, input.downstreamPolicy);
+ if (!selected) {
+ return {
+ ok: false,
+ reason: 'no_channel',
+ attempts: 0,
+ };
+ }
+
+ while (selected) {
+ const result = await input.attempt({
+ selected,
+ attemptIndex: attempts,
+ excludeChannelIds: [...excludeChannelIds],
+ });
+ attempts += 1;
+
+ if (result.ok) {
+ await recordSuccessfulAttempt(this.deps, selected.channel.id, {
+ latencyMs: result.latencyMs ?? null,
+ cost: result.cost ?? null,
+ });
+ return {
+ ok: true,
+ selected,
+ response: result.response,
+ attempts,
+ };
+ }
+
+ const action = failureActionOf(result);
+ await recordFailedAttempt(this.deps, selected.channel.id, {
+ status: result.status,
+ rawErrorText: result.rawErrorText,
+ });
+
+ if (isTerminalFailure(action)) {
+ await input.onTerminalFailure?.(selected, {
+ status: result.status,
+ rawErrorText: result.rawErrorText,
+ });
+ return {
+ ok: false,
+ reason: 'terminal',
+ selected,
+ status: result.status,
+ rawErrorText: result.rawErrorText,
+ attempts,
+ };
+ }
+
+ if (shouldRetrySameChannel(action)) {
+ continue;
+ }
+
+ if (shouldRefreshAuth(action) && this.deps.refreshAuth) {
+ const refreshed = await this.deps.refreshAuth(selected, {
+ status: result.status,
+ rawErrorText: result.rawErrorText,
+ });
+ if (refreshed) {
+ selected = refreshed;
+ continue;
+ }
+ }
+
+ if (shouldFailover(action)) {
+ excludeChannelIds.push(selected.channel.id);
+ const next = await this.deps.selectNextChannel(
+ input.requestedModel,
+ excludeChannelIds,
+ input.downstreamPolicy,
+ );
+ if (!next) {
+ return {
+ ok: false,
+ reason: 'failed',
+ selected,
+ status: result.status,
+ rawErrorText: result.rawErrorText,
+ attempts,
+ };
+ }
+ selected = next;
+ continue;
+ }
+
+ return {
+ ok: false,
+ reason: 'failed',
+ selected,
+ status: result.status,
+ rawErrorText: result.rawErrorText,
+ attempts,
+ };
+ }
+
+ return {
+ ok: false,
+ reason: 'failed',
+ attempts,
+ };
+ }
+}
diff --git a/src/server/proxy-core/conductor/retryPolicy.ts b/src/server/proxy-core/conductor/retryPolicy.ts
new file mode 100644
index 00000000..9658d8c8
--- /dev/null
+++ b/src/server/proxy-core/conductor/retryPolicy.ts
@@ -0,0 +1,21 @@
+import type { AttemptFailure, AttemptFailureAction } from './types.js';
+
+export function failureActionOf(result: AttemptFailure): AttemptFailureAction {
+ return result.action;
+}
+
+export function shouldRetrySameChannel(action: AttemptFailureAction): boolean {
+ return action === 'retry_same_channel';
+}
+
+export function shouldRefreshAuth(action: AttemptFailureAction): boolean {
+ return action === 'refresh_auth';
+}
+
+export function shouldFailover(action: AttemptFailureAction): boolean {
+ return action === 'failover' || action === 'refresh_auth';
+}
+
+export function isTerminalFailure(action: AttemptFailureAction): boolean {
+ return action === 'terminal';
+}
diff --git a/src/server/proxy-core/conductor/streamTermination.ts b/src/server/proxy-core/conductor/streamTermination.ts
new file mode 100644
index 00000000..8bbdc2ec
--- /dev/null
+++ b/src/server/proxy-core/conductor/streamTermination.ts
@@ -0,0 +1,10 @@
+export function terminalStreamFailure(input: {
+ status?: number;
+ rawErrorText?: string;
+}) {
+ return {
+ action: 'terminal' as const,
+ ...(input.status !== undefined ? { status: input.status } : {}),
+ ...(input.rawErrorText ? { rawErrorText: input.rawErrorText } : {}),
+ };
+}
diff --git a/src/server/proxy-core/conductor/types.ts b/src/server/proxy-core/conductor/types.ts
new file mode 100644
index 00000000..fef18c89
--- /dev/null
+++ b/src/server/proxy-core/conductor/types.ts
@@ -0,0 +1,80 @@
+export type SelectedChannelLike = {
+ channel: { id: number; routeId?: number };
+ site: Record;
+ account: Record;
+ tokenName?: string;
+ tokenValue?: string;
+ actualModel?: string;
+};
+
+export type AttemptSuccess = {
+ ok: true;
+ response: Response;
+ latencyMs?: number | null;
+ cost?: number | null;
+};
+
+export type AttemptFailureAction =
+ | 'retry_same_channel'
+ | 'refresh_auth'
+ | 'failover'
+ | 'terminal'
+ | 'stop';
+
+export type AttemptFailure = {
+ ok: false;
+ action: AttemptFailureAction;
+ status?: number;
+ rawErrorText?: string;
+ error?: unknown;
+};
+
+export type AttemptResult = AttemptSuccess | AttemptFailure;
+
+export type ExecuteAttemptContext = {
+ selected: SelectedChannelLike;
+ attemptIndex: number;
+ excludeChannelIds: number[];
+};
+
+export type ProxyConductorDependencies = {
+ selectChannel: (requestedModel: string, downstreamPolicy?: unknown) => Promise;
+ previewSelectedChannel?: (requestedModel: string, downstreamPolicy?: unknown) => Promise;
+ selectNextChannel: (
+ requestedModel: string,
+ excludeChannelIds: number[],
+ downstreamPolicy?: unknown,
+ ) => Promise;
+ recordSuccess?: (channelId: number, metrics: { latencyMs: number | null; cost: number | null }) => Promise | void;
+ recordFailure?: (channelId: number, failure: { status?: number; rawErrorText?: string }) => Promise | void;
+ refreshAuth?: (
+ selected: SelectedChannelLike,
+ failure: { status?: number; rawErrorText?: string },
+ ) => Promise;
+};
+
+export type ExecuteInput = {
+ requestedModel: string;
+ downstreamPolicy?: unknown;
+ attempt: (context: ExecuteAttemptContext) => Promise;
+ onTerminalFailure?: (
+ selected: SelectedChannelLike,
+ failure: { status?: number; rawErrorText?: string },
+ ) => Promise | void;
+};
+
+export type ExecuteResult =
+ | {
+ ok: true;
+ selected: SelectedChannelLike;
+ response: Response;
+ attempts: number;
+ }
+ | {
+ ok: false;
+ reason: 'no_channel' | 'failed' | 'terminal';
+ selected?: SelectedChannelLike;
+ status?: number;
+ rawErrorText?: string;
+ attempts: number;
+ };
diff --git a/src/server/proxy-core/conductor/usageHooks.ts b/src/server/proxy-core/conductor/usageHooks.ts
new file mode 100644
index 00000000..2b8455da
--- /dev/null
+++ b/src/server/proxy-core/conductor/usageHooks.ts
@@ -0,0 +1,20 @@
+import type { ProxyConductorDependencies } from './types.js';
+
+export async function recordSuccessfulAttempt(
+ deps: ProxyConductorDependencies,
+ channelId: number,
+ metrics: { latencyMs?: number | null; cost?: number | null },
+): Promise {
+ await deps.recordSuccess?.(channelId, {
+ latencyMs: metrics.latencyMs ?? null,
+ cost: metrics.cost ?? null,
+ });
+}
+
+export async function recordFailedAttempt(
+ deps: ProxyConductorDependencies,
+ channelId: number,
+ failure: { status?: number; rawErrorText?: string },
+): Promise {
+ await deps.recordFailure?.(channelId, failure);
+}
diff --git a/src/server/proxy-core/executors/antigravityExecutor.ts b/src/server/proxy-core/executors/antigravityExecutor.ts
new file mode 100644
index 00000000..96ac1b1f
--- /dev/null
+++ b/src/server/proxy-core/executors/antigravityExecutor.ts
@@ -0,0 +1,212 @@
+import { createHash, randomUUID } from 'node:crypto';
+import { Response } from 'undici';
+import type { RuntimeDispatchInput, RuntimeExecutor, RuntimeResponse } from './types.js';
+import {
+ asTrimmedString,
+ materializeErrorResponse,
+ performFetch,
+ withRequestBody,
+} from './types.js';
+
+const ANTIGRAVITY_RUNTIME_BASE_URLS = [
+ 'https://daily-cloudcode-pa.googleapis.com',
+ 'https://daily-cloudcode-pa.sandbox.googleapis.com',
+] as const;
+
+function antigravityRequestType(modelName: string): 'image_gen' | 'agent' {
+ return modelName.includes('image') ? 'image_gen' : 'agent';
+}
+
+function generateAntigravityProjectId(): string {
+ const adjectives = ['useful', 'bright', 'swift', 'calm', 'bold'];
+ const nouns = ['signal', 'river', 'rocket', 'forest', 'bridge'];
+ const adjective = adjectives[Math.floor(Math.random() * adjectives.length)] || 'useful';
+ const noun = nouns[Math.floor(Math.random() * nouns.length)] || 'signal';
+ const suffix = Math.floor(100000 + Math.random() * 900000);
+ return `${adjective}-${noun}-${suffix}`;
+}
+
+function extractFirstUserText(value: unknown): string {
+ if (!Array.isArray(value)) return '';
+ for (const content of value) {
+ if (!content || typeof content !== 'object' || Array.isArray(content)) continue;
+ const record = content as Record;
+ if (asTrimmedString(record.role) !== 'user') continue;
+ const parts = Array.isArray(record.parts) ? record.parts : [];
+ for (const part of parts) {
+ if (!part || typeof part !== 'object' || Array.isArray(part)) continue;
+ const text = asTrimmedString((part as Record).text);
+ if (text) return text;
+ }
+ }
+ return '';
+}
+
+function generateStableAntigravitySessionId(payload: Record): string {
+ const firstUserText = extractFirstUserText(
+ payload.request && typeof payload.request === 'object' && !Array.isArray(payload.request)
+ ? (payload.request as Record).contents
+ : undefined,
+ );
+ if (!firstUserText) {
+ return `-${BigInt(`0x${randomUUID().replace(/-/g, '').slice(0, 16)}`).toString()}`;
+ }
+ const digest = createHash('sha256').update(firstUserText).digest('hex').slice(0, 16);
+ const bigint = BigInt(`0x${digest}`) & BigInt('0x7fffffffffffffff');
+ return `-${bigint.toString()}`;
+}
+
+function renameParametersJsonSchema(value: unknown): unknown {
+ if (Array.isArray(value)) {
+ return value.map((item) => renameParametersJsonSchema(item));
+ }
+ if (!value || typeof value !== 'object') return value;
+
+ const input = value as Record;
+ const output: Record = {};
+ for (const [key, entry] of Object.entries(input)) {
+ const nextKey = key === 'parametersJsonSchema' ? 'parameters' : key;
+ output[nextKey] = renameParametersJsonSchema(entry);
+ }
+ return output;
+}
+
+function deleteNestedMaxOutputTokens(payload: Record): void {
+ const request = payload.request;
+ if (!request || typeof request !== 'object' || Array.isArray(request)) return;
+ const generationConfig = (request as Record).generationConfig;
+ if (!generationConfig || typeof generationConfig !== 'object' || Array.isArray(generationConfig)) return;
+ delete (generationConfig as Record