From de3dc8792057037dd913769504936ccfd7c9ce4c Mon Sep 17 00:00:00 2001 From: chengchengzhou7 <131637042+chengchengzhou7@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:47:40 +0800 Subject: [PATCH] =?UTF-8?q?release(main):=20=E5=87=86=E5=A4=87=20v0.1.0=20?= =?UTF-8?q?=E5=80=99=E9=80=89=E7=89=88=E6=9C=AC=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add desktop project plan * feat(desktop): implement phase 1 tauri client * refactor(client): unify web and desktop release workflow * feat(desktop): implement phase 2 native capabilities * 完善桌面端交互体验与发布检查 * 完善桌面端界面、发布检查与邮箱域名同步 * fix(deploy): 修复数据库初始化复用旧镜像 * fix(deploy): 增加部署更新实时进度 * fix(auth): 支持无邮箱后缀时手动输入 * feat(desktop): 稳定桌面端界面与文件操作反馈 - 重构 DesktopPreferences 为分栏式设置面板,整合连接、外观、通知、更新与诊断信息分区,并补充过渡动效与暗色主题样式 - DesktopLayout 侧边栏导航分组支持展开折叠,调整管理/项目区块顺序并统一图标与标题 - 新增 fileTaskFeedback 工具,统一 pickFiles/saveFile/openFile 的成功/取消提示,替换审计导出、权限日志、附件、文档、线程、项目配置等处的直接调用 - desktopUpdateManager 暴露更新状态快照与状态变更监听,区分检查中、安装中、已推迟、失败等状态 - DesktopServerSettings 增加连接诊断信息(检查时间、健康地址、耗时、HTTP 状态) - unified-page.css 与 ProjectMilestones 引入 CSS 变量以适配暗色主题 - WebLayout 将服务器设置入口改为打开系统偏好面板,管理菜单中邮件服务归入系统设置分组 - ProfileSettings 移除已迁入偏好面板的桌面端专属区块 - 补充 Layout.desktop 布局与偏好面板契约测试 * feat(desktop): 支持桌面端三十天免登录 * 完善桌面端发布稳定化门禁 * 完善桌面端端到端回归收口 * 补齐桌面端附件文件流回归 * 完善桌面端回归与安全边界复审 * 完善桌面体验与系统通知收口 * feat(desktop): 收口桌面工作台视觉与活动反馈 * 优化桌面端界面布局 * style: 优化个人中心和偏好设置弹窗样式,重构工作入口为精致分屏布局并移除首字徽标 * 功能(桌面端):增加在线辅助本地缓存 * 优化桌面端标签导航与后台交互 * ci: 新增 Windows 桌面端内测构建 * fix: 修复桌面检查脚本的 Windows 路径判断 * test: 兼容 Windows 换行的桌面布局断言 * test: 兼容 Windows 换行的路由断言 * ci: 修复 Windows 内测构建配置传参 * ci: 避免 Windows 安装器构建交互等待 * 修复桌面端界面显示与稳定性问题 * feat(网页端): 完善登录后工作台与项目管理体验 * docs(desktop): 精简桌面端约束入口 * feat(admin): 完善审计访问上下文与后台布局 * fix(git): 跟踪原生图标资源 * fix(web): 修正工作台端侧标识 * feat(监控): 完善系统监控、登录状态与访问审计能力 * 修复(权限管理):统一 PM 系统导航与权限校验 * feat(工作台): 优化入口布局与连接安全状态 * feat(桌面与监控): 完善工作台导航和登录活动定位 - 优化桌面标签、上下文标题、前进后退、导航栏隐藏和原生菜单体验 - 补充登录会话 IP 采集、地理位置回退、管理端展示及数据库迁移 - 更新桌面发布检查、运维文档和前后端测试覆盖 * 功能(文档与桌面):完善文件预览下载与客户端构建基线 - 保存文档版本原始文件名,规范下载响应并持久化上传目录\n- 增加 PDF.js 预览、桌面保存打开流程及统一错误反馈\n- 统一 Node.js 22.13 构建基线并收紧临时文件权限门禁\n- 补充迁移、单元测试、发布检查与运维文档 * 功能(文档预览):集成 ONLYOFFICE 安全只读预览与工作台体验 新增 ONLYOFFICE 配置签名、内部内容接口、容器编排与反向代理。 打通网页端和桌面端独立预览工作区,完善文档入口、布局及帮助体验。 补充桌面安全发布门禁、开发脚本、使用文档和前后端测试。 * feat(collaboration): 完善在线文档协作与通知闭环 - 新增协作文件夹、文件、不可变修订、成员、会话、回调回执、编辑申请与分享链接数据模型。 - 补齐新建、导入、复制、下载、回收站、恢复、成员授权、所有权转让及文件级权限接口。 - 接入 ONLYOFFICE 共同编辑、历史版本预览与恢复、修订另存副本、导出下载审计和幂等回调保存。 - 增加编辑权限申请、审批通知、项目提醒聚合、通知 Feed、已读处理及历史待办数据回填。 - 支持公开分享的查看或编辑模式、有效期、密码哈希、失败锁定、短时访问凭证与固定分享地址。 - 增加协作者导出、申请编辑、工作表结构保护和所有权管理策略,并纳入项目接口权限矩阵。 - 新增协作文件库、编辑工作区、公开分享页、下载与另存为对话框,以及导航、路由和权限入口。 - 统一网页端与桌面端通知布局,增加沉浸式工作区和浏览器、Tauri 双端全屏能力。 - 扩展运行时文件下载适配、Tauri 环境识别和原生全屏命令,继续保持业务代码运行时边界。 - 加固 ONLYOFFICE 消息桥的同源下载、签名地址隔离和保存为能力校验,并更新桌面发布检查。 - 增加连续数据库迁移、50MB 上传限制、OnlyOffice 中文文案与开发启动路由校验。 - 补充协作、通知、权限、路由、运行时、布局和 OnlyOffice 相关测试及模块说明文档。 * refactor(frontend): 按需加载页面并清理未使用代码 - 将业务页面路由统一改为动态导入,拆分首屏入口与各功能模块构建产物。 - 将网页端和桌面端布局改为异步组件,避免两套平台布局同时进入初始包。 - 新增 Element Plus 按需安装入口,并通过全局配置组件统一注入中文语言包。 - 提取 API 运行时钩子,在应用启动时注入项目清理、令牌续期和认证失效退出能力。 - 将权限监控面板及地图资源改为延迟加载,补充地图加载状态、失败提示和切换竞态保护。 - 删除已被现有工作流替代的项目成员、接口权限、中心绑定、培训表单及旧项目首页等页面。 - 清理废弃的快捷操作、项目选择、用户选择、FAQ 表单、风险占位组件和旧地图辅助模块。 - 移除未使用的 API 方法、类型、字典、状态机、展示工具、样式和项目详情编辑逻辑。 - 开启 TypeScript 未使用变量与参数检查,并同步收紧相关测试和组件暴露类型。 - 移除未使用的 updater、date-fns 和 Sass 前端依赖,更新锁文件并删除旧 CSS 清洗插件。 - 更新路由、Axios、ETMF、通知、权限监控和桌面布局测试以覆盖重构后的边界。 * fix(审计): 移除共享库审计与预览噪声 * 功能(提醒):统一项目提醒中心与桌面通知链路 增加通用提醒状态、数据库迁移和定时同步,覆盖风险时效、文件回执、项目里程碑、访视窗口与协作申请。 新增网页端和桌面端提醒中心、真实投递诊断与固定隐私通知正文,并补齐登录来源聚合、测试和说明文档。 * feat(deploy): 默认安装 ONLYOFFICE 标准组件 * build(release): 加固 v0.1.0 桌面发布链路 (#3) --- .../workflows/desktop-release-candidate.yml | 259 ++++++++- AGENTS.md | 6 +- ...desktop-release-stabilization-checklist.md | 28 +- docs/guides/branch-maintenance-sop-zh.md | 8 +- docs/guides/client-release.md | 80 ++- frontend/package-lock.json | 503 +++++++++++------- frontend/package.json | 1 + .../scripts/create-desktop-update-feed.mjs | 125 +++-- .../verify-desktop-release-readiness.mjs | 67 ++- frontend/scripts/verify-desktop-release.mjs | 16 + .../scripts/verify-desktop-update-feed.mjs | 27 +- frontend/scripts/verify-release-build-env.mjs | 43 +- frontend/src/utils/contentDisposition.test.ts | 15 +- frontend/src/utils/contentDisposition.ts | 8 + .../src/views/documents/DocumentDetail.vue | 19 +- .../views/ia/RiskIssueMonitoringVisits.vue | 13 +- 16 files changed, 895 insertions(+), 323 deletions(-) diff --git a/.github/workflows/desktop-release-candidate.yml b/.github/workflows/desktop-release-candidate.yml index b0237d00..d09b2b45 100644 --- a/.github/workflows/desktop-release-candidate.yml +++ b/.github/workflows/desktop-release-candidate.yml @@ -14,6 +14,10 @@ on: permissions: contents: read +concurrency: + group: desktop-release-candidate-${{ github.ref }} + cancel-in-progress: false + jobs: macos-release-candidate: name: Signed macOS release candidate @@ -67,6 +71,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Audit dependencies + run: npm audit + - name: Check release build metadata and signing environment run: npm run release:env:check @@ -94,37 +101,255 @@ jobs: - name: Build Web artifact run: npm run build - - name: Build signed Universal macOS artifacts + - name: Build signed and notarized Universal macOS artifacts run: npm run desktop:build:macos-release -- --ci - - name: Create desktop update feed + - name: Verify and stage macOS artifacts shell: bash run: | - if [[ -z "${DESKTOP_UPDATE_BASE_URL}" ]]; then - echo "DESKTOP_UPDATE_BASE_URL or workflow input artifact_base_url is required." - exit 1 - fi - + app="$(find src-tauri/target -path '*/release/bundle/macos/*.app' -print -quit)" artifact="$(find src-tauri/target -path '*/release/bundle/macos/*.app.tar.gz' -print -quit)" - if [[ -z "${artifact}" ]]; then - echo "No macOS updater artifact was produced." + dmg="$(find src-tauri/target -path '*/release/bundle/dmg/*.dmg' -print -quit)" + if [[ -z "${app}" || -z "${artifact}" || -z "${dmg}" || ! -s "${artifact}.sig" ]]; then + echo "Signed macOS app, updater artifact/signature, and DMG are all required." exit 1 fi - include_args=() - dmg="$(find src-tauri/target -path '*/release/bundle/dmg/*.dmg' -print -quit)" - if [[ -n "${dmg}" ]]; then - include_args+=(--include "${dmg}") + codesign --verify --deep --strict --verbose=2 "${app}" + spctl --assess --type execute --verbose=2 "${app}" + xcrun stapler validate "${app}" + xcrun stapler validate "${dmg}" + + stage="src-tauri/target/desktop-release-macos" + mkdir -p "${stage}" + cp "${artifact}" "${artifact}.sig" "${dmg}" "${stage}/" + + - name: Upload signed macOS candidate artifacts + uses: actions/upload-artifact@v4 + with: + name: ctms-desktop-macos-${{ github.ref_name }} + path: frontend/src-tauri/target/desktop-release-macos/* + if-no-files-found: error + + windows-release-candidate: + name: Signed Windows release candidate + runs-on: windows-latest + defaults: + run: + working-directory: frontend + env: + VITE_BUILD_CHANNEL: release + VITE_BUILD_COMMIT: ${{ github.sha }} + RELEASE_BUILD: "true" + REQUIRE_WINDOWS_SIGNING: "true" + DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + WINDOWS_TIMESTAMP_URL: ${{ vars.WINDOWS_TIMESTAMP_URL }} + steps: + - name: Checkout release source + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22.13" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Enforce release tag context + shell: pwsh + run: | + if ($env:GITHUB_REF_TYPE -ne "tag") { + throw "Signed desktop release candidates must run from a vX.Y.Z tag." + } + $expectedTag = "v$(node -p "require('./package.json').version")" + if ($env:GITHUB_REF_NAME -ne $expectedTag) { + throw "Release tag $env:GITHUB_REF_NAME does not match package version $expectedTag." + } + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install dependencies + run: npm ci + + - name: Audit dependencies + run: npm audit + + - name: Check release build metadata and signing environment + run: npm run release:env:check + + - name: Check signed desktop release readiness + run: npm run desktop:release-readiness:check + + - name: Check synchronized client version + run: npm run version:check + + - name: Check runtime boundary + run: npm run runtime:check + + - name: Check desktop release and security gate + run: npm run desktop:release:check + + - name: Check UI contract + run: npm run ui:contract + + - name: Type check + run: npm run type-check + + - name: Unit tests + run: npm run test:unit + + - name: Build Web artifact + run: npm run build + + - name: Import Windows code-signing certificate + shell: pwsh + run: | + $pfxPath = Join-Path $env:RUNNER_TEMP "ctms-windows-signing.pfx" + $encodedCertificate = $env:WINDOWS_CERTIFICATE ` + -replace "-----BEGIN [^-]+-----", "" ` + -replace "-----END [^-]+-----", "" ` + -replace "\s", "" + [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($encodedCertificate)) + $password = ConvertTo-SecureString $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force + $importedCertificates = Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\CurrentUser\My" -Password $password -Exportable:$false + $certificate = $importedCertificates | Where-Object { $_.HasPrivateKey } | Select-Object -First 1 + if (-not $certificate -or -not $certificate.HasPrivateKey) { + throw "The imported Windows code-signing certificate is missing its private key." + } + $now = Get-Date + if ($certificate.NotBefore -gt $now -or $certificate.NotAfter -le $now) { + throw "The Windows code-signing certificate is not currently valid." + } + $codeSigningEku = $certificate.EnhancedKeyUsageList | Where-Object { $_.ObjectId.Value -eq "1.3.6.1.5.5.7.3.3" } + if (-not $codeSigningEku) { + throw "The Windows certificate does not contain the Code Signing enhanced key usage." + } + "WINDOWS_CERTIFICATE_THUMBPRINT=$($certificate.Thumbprint)" | Out-File -FilePath $env:GITHUB_ENV -Append + Remove-Item $pfxPath -Force + + - name: Write signed Windows Tauri config + shell: pwsh + run: | + $config = @{ + bundle = @{ + windows = @{ + certificateThumbprint = $env:WINDOWS_CERTIFICATE_THUMBPRINT + digestAlgorithm = "sha256" + timestampUrl = $env:WINDOWS_TIMESTAMP_URL + tsp = $true + } + } + } | ConvertTo-Json -Depth 8 + Set-Content -Path "tauri.windows.release.conf.json" -Value $config -Encoding utf8 + Get-Content "tauri.windows.release.conf.json" + + - name: Build signed Windows NSIS artifacts + timeout-minutes: 30 + run: npm run desktop:build:windows-release -- --config tauri.windows.release.conf.json --ci + + - name: Verify Authenticode and stage Windows artifacts + shell: pwsh + run: | + $bundleDir = "src-tauri/target/release/bundle/nsis" + $installers = @(Get-ChildItem -Path $bundleDir -Filter "*.exe" -File) + $updaters = @(Get-ChildItem -Path $bundleDir -Filter "*.nsis.zip" -File) + $appExecutables = @(Get-ChildItem -Path "src-tauri/target/release" -Filter "*.exe" -File) + if ($installers.Count -ne 1 -or $updaters.Count -ne 1 -or $appExecutables.Count -eq 0) { + throw "Exactly one NSIS installer/updater and at least one application executable are required." + } + + $signaturePath = "$($updaters[0].FullName).sig" + if (-not (Test-Path $signaturePath) -or (Get-Item $signaturePath).Length -eq 0) { + throw "The Windows updater signature is missing or empty." + } + + foreach ($executable in @($appExecutables + $installers)) { + $signature = Get-AuthenticodeSignature -FilePath $executable.FullName + if ($signature.Status -ne "Valid" -or -not $signature.SignerCertificate -or -not $signature.TimeStamperCertificate) { + throw "Authenticode signature or RFC 3161 timestamp is invalid for $($executable.FullName): $($signature.StatusMessage)" + } + } + + $stage = "src-tauri/target/desktop-release-windows" + New-Item -ItemType Directory -Path $stage -Force | Out-Null + Copy-Item $installers[0].FullName, $updaters[0].FullName, $signaturePath -Destination $stage + + - name: Remove Windows signing certificate + if: always() + shell: pwsh + run: | + if ($env:WINDOWS_CERTIFICATE_THUMBPRINT) { + Remove-Item "Cert:\CurrentUser\My\$env:WINDOWS_CERTIFICATE_THUMBPRINT" -Force -ErrorAction SilentlyContinue + } + Remove-Item (Join-Path $env:RUNNER_TEMP "ctms-windows-signing.pfx") -Force -ErrorAction SilentlyContinue + Remove-Item "tauri.windows.release.conf.json" -Force -ErrorAction SilentlyContinue + + - name: Upload signed Windows candidate artifacts + uses: actions/upload-artifact@v4 + with: + name: ctms-desktop-windows-${{ github.ref_name }} + path: frontend/src-tauri/target/desktop-release-windows/* + if-no-files-found: error + + aggregate-release-candidate: + name: Verify combined desktop release directory + needs: + - macos-release-candidate + - windows-release-candidate + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + env: + DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }} + steps: + - name: Checkout release source + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22.13" + + - name: Download signed macOS candidate artifacts + uses: actions/download-artifact@v4 + with: + name: ctms-desktop-macos-${{ github.ref_name }} + path: frontend/src-tauri/target/desktop-release-input/macos + + - name: Download signed Windows candidate artifacts + uses: actions/download-artifact@v4 + with: + name: ctms-desktop-windows-${{ github.ref_name }} + path: frontend/src-tauri/target/desktop-release-input/windows + + - name: Create combined desktop update feed + shell: bash + run: | + mapfile -t mac_artifacts < <(find src-tauri/target/desktop-release-input/macos -name '*.app.tar.gz' -type f) + mapfile -t dmgs < <(find src-tauri/target/desktop-release-input/macos -name '*.dmg' -type f) + mapfile -t windows_artifacts < <(find src-tauri/target/desktop-release-input/windows -name '*.nsis.zip' -type f) + mapfile -t installers < <(find src-tauri/target/desktop-release-input/windows -name '*.exe' -type f) + if [[ ${#mac_artifacts[@]} -ne 1 || ${#dmgs[@]} -ne 1 || ${#windows_artifacts[@]} -ne 1 || ${#installers[@]} -ne 1 ]]; then + echo "Exactly one macOS updater/DMG and one Windows updater/installer are required." + exit 1 fi npm run desktop:update-feed:create -- \ - --artifact "${artifact}" \ - "${include_args[@]}" \ + --artifact "${mac_artifacts[0]}" \ + --platform-artifact "windows-x86_64=${windows_artifacts[0]}" \ + --include "${dmgs[0]}" \ + --include "${installers[0]}" \ --output-dir src-tauri/target/desktop-release-feed \ --base-url "${DESKTOP_UPDATE_BASE_URL}" - - name: Verify desktop update feed - run: npm run desktop:update-feed:check -- --feed src-tauri/target/desktop-release-feed/latest.json --artifacts-dir src-tauri/target/desktop-release-feed --base-url "${DESKTOP_UPDATE_BASE_URL}" + - name: Verify combined desktop update feed + run: npm run desktop:update-feed:check -- --feed src-tauri/target/desktop-release-feed/latest.json --artifacts-dir src-tauri/target/desktop-release-feed --base-url "${DESKTOP_UPDATE_BASE_URL}" --require-platform windows-x86_64 - name: Upload verified desktop release directory uses: actions/upload-artifact@v4 diff --git a/AGENTS.md b/AGENTS.md index b738f502..85b048c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ 桌面端项目计划书已完成并移除;当前桌面端执行约束以本文件为准。桌面端任务包括但不限于 Tauri、macOS、Windows、桌面打包、桌面存储、文件集成、系统通知、自动更新和桌面端安全边界。历史设计只作追溯参考,见 `docs/desktop-phase-1-design.md`、`docs/desktop-phase-2-design.md`、`docs/audits/desktop-release-stabilization-checklist.md`。 -桌面端当前不是空白初始化项目。Tauri 基线、macOS 在线桌面壳和第二阶段原生能力主体已经形成;后续工作限于修复、稳定化、体验收口、发布准备、在线辅助缓存和 Windows 兼容验证。不得重新按第一阶段空白项目初始化 Tauri,不得绕过现有 `frontend/src/runtime/` 适配层直接在业务模块中使用 Tauri API。 +桌面端当前不是空白初始化项目。Tauri 基线、macOS 在线桌面壳和第二阶段原生能力主体已经形成;后续工作限于修复、稳定化、体验收口、发布准备、在线辅助缓存、Windows 兼容验证和已批准的 Windows 正式发布。不得重新按第一阶段空白项目初始化 Tauri,不得绕过现有 `frontend/src/runtime/` 适配层直接在业务模块中使用 Tauri API。 除非用户明确要求先调整本文件中的约束,否则不要实现离线功能、本地业务权威数据存储、内嵌后端服务、离线同步、本地优先工作流或新的阶段性桌面产品线。在线辅助本地缓存只允许作为已认证在线客户端的体验加速能力;处理桌面本地缓存、请求去重、条件请求、缓存诊断或缓存清理相关任务时,必须阅读并遵守 `docs/desktop-local-cache-plan.md`。 @@ -14,7 +14,7 @@ - 未完成后端 token 校验和 `/me` 身份确认前,不得展示业务缓存;登出、切换服务器、切换用户、401/403、权限上下文变化或缓存 schema 变化时必须清理或失效相关缓存。 - 新增或调整 Tauri command、capability、CSP、updater、凭据、文件、通知、本地缓存持久化或底层存储能力时,必须同步评估 `frontend/scripts/verify-desktop-release.mjs`、`npm run runtime:check` 和桌面发布检查清单是否需要更新。 - token、附件下载凭据和敏感业务信息不得写入 URL、日志、系统通知正文或明文浏览器存储。 -- Windows 仍只作为第二阶段兼容性验证目标;未获明确批准前不发布正式 Windows 安装包。Windows 内测构建只能使用 `.github/workflows/desktop-windows-internal.yml` 的手动验证入口,不得生成生产 `latest.json`、updater feed 或正式发布制品。 +- Windows x64 NSIS 已获准作为正式桌面发布目标;正式制品必须由 `.github/workflows/desktop-release-candidate.yml` 从与 macOS/Web 相同的 `vX.Y.Z` tag 和 SHA 构建,使用组织 Windows 代码签名证书、RFC 3161 时间戳和 updater 私钥,并通过 Authenticode 与 updater feed 校验。`.github/workflows/desktop-windows-internal.yml` 仍只用于分支上的无签名兼容性验证,不得生成生产 `latest.json`、updater feed 或正式发布制品。 ## 分支与发布治理 @@ -57,4 +57,4 @@ npm run desktop:build:app 本地缓存相关变更至少执行 `runtime:check`、`desktop:release:check`、`ui:contract`、`type-check`、`test:unit` 和 `build`;若新增 Tauri command、capability、CSP 或底层持久化存储,还需执行 `desktop:build:app` 并在真实桌面 App 中验证缓存清理和诊断入口。 -正式桌面发布构建仍必须使用组织批准的 updater 签名私钥和 Apple 签名/公证流程;未签名或 ad-hoc 构建只能作为内部验证构建描述。 +正式桌面发布构建必须使用组织批准的 updater 签名私钥;macOS 必须完成 Apple 签名/公证,Windows 必须完成组织代码签名、RFC 3161 时间戳和 Authenticode 校验。未签名或 ad-hoc 构建只能作为内部验证构建描述。 diff --git a/docs/audits/desktop-release-stabilization-checklist.md b/docs/audits/desktop-release-stabilization-checklist.md index b0a25131..de5f7d78 100644 --- a/docs/audits/desktop-release-stabilization-checklist.md +++ b/docs/audits/desktop-release-stabilization-checklist.md @@ -1,8 +1,8 @@ # CTMS Desktop Release Stabilization Checklist 状态: `active` -适用范围: Web 与 macOS Desktop 统一客户端发布 -最后更新: `2026-07-14` +适用范围: Web、macOS Desktop 与 Windows x64 Desktop 统一客户端发布 +最后更新: `2026-07-17` 本清单用于第一、二阶段桌面端能力完成后的准发布稳定化。当前允许按 `docs/desktop-local-cache-plan.md` 引入在线辅助本地缓存,但不引入离线登录、离线写入、本地业务权威数据、内嵌后端服务或离线同步。 @@ -30,10 +30,12 @@ npm run desktop:build:app - [ ] `VITE_BUILD_CHANNEL=release` 和 `VITE_BUILD_COMMIT=` 由 CI 注入,且 `npm run release:env:check` 通过。 - [ ] 在正式 release tag 和签名环境中执行 `npm run desktop:release-readiness:check`,确认 tag、构建元数据、签名/公证变量、updater 私钥和生产 artifact HTTPS 基址齐备。 - [ ] macOS app 已签名和公证。 +- [ ] Windows 应用和 NSIS 安装器已使用组织证书签名并带有效 RFC 3161 时间戳,`Get-AuthenticodeSignature` 对应用和安装器均返回 `Valid`。 - [ ] updater `.sig` 使用组织 CI secret 或密钥库中的私钥生成,私钥未进入仓库。 - [ ] 设置 `TAURI_SIGNING_PRIVATE_KEY`、`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 Apple 签名/公证变量后,以 `REQUIRE_DESKTOP_SIGNING=true` 再次执行 `npm run release:env:check`,随后执行 `npm run desktop:build:macos-release -- --ci`。 -- [ ] 正式 updater feed 先执行 `npm run desktop:update-feed:create -- --artifact --base-url --output-dir ` 生成 `latest.json` 与 `SHA256SUMS.txt`。 -- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed /latest.json --artifacts-dir `,并确认 checksum manifest、updater artifact、`.sig` 和 `latest.json` 均通过校验。 +- [ ] 设置 Windows PFX、密码、`WINDOWS_TIMESTAMP_URL` 和 updater 私钥后,以 `REQUIRE_WINDOWS_SIGNING=true` 执行 readiness,再执行 `npm run desktop:build:windows-release -- --ci`。 +- [ ] 正式 updater feed 使用 `--artifact ` 和 `--platform-artifact windows-x86_64=` 汇总生成同一个 `latest.json` 与 `SHA256SUMS.txt`。 +- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed /latest.json --artifacts-dir --require-platform windows-x86_64`,并确认两个平台的 updater artifact、`.sig`、安装包、checksum manifest 和 `latest.json` 均通过校验。 - [ ] 不可变制品先上传,`latest.json` 最后原子替换;若 feed 校验未通过,不替换线上 `latest.json`。 - [ ] Web 与 Desktop 制品记录同一产品版本、Git 标签和完整提交 SHA。 @@ -68,7 +70,7 @@ npm run desktop:build:app - [ ] updater capability 不直接暴露给 WebView,自动更新只走受控 Tauri command。 - [ ] 更新弹窗 release notes 过滤 URL、token 查询参数和 Authorization/Bearer 形态文本。 - [ ] CI release 候选 workflow 包含 version/runtime/desktop/ui/type/unit/build/desktop app smoke 门禁。 -- [ ] signed macOS release candidate workflow 只允许从 `vX.Y.Z` tag 运行,并包含签名环境检查、Universal macOS 构建、update feed 生成、checksum 校验和 verified release directory 上传。 +- [ ] signed Desktop release candidate workflow 只允许从 `vX.Y.Z` tag 运行;macOS 和 Windows 原生 job 分别完成签名验证,再由聚合 job 生成包含 `darwin-aarch64`、`darwin-x86_64` 和 `windows-x86_64` 的 feed、checksum 清单和 verified release directory。 人工复审还必须确认: @@ -261,3 +263,19 @@ npm run desktop:build:app 真实 `.app` 仍需人工验证:红色按钮真正关闭主窗口后 Dock 重建、菜单顺序、个人中心自身关闭按钮、编辑菜单文本输入行为、自定义快捷键即时同步,以及系统明暗模式切换时的标题栏一致性。 本轮未引入以下条件性 Swift 能力:Quick Look 需先确认附件审阅频率和 Windows 降级语义;Touch ID 会改变现有 30 天会话恢复体验,需先确定凭据访问控制策略;通知点击回跳需先定义固定、无敏感信息的动作和目标页面。三项均不是当前发布前置条件。 + +## 12. 2026-07-17 Windows 正式发布链路记录 + +经发布负责人明确批准,Windows x64 NSIS 从内部兼容性验证目标提升为正式桌面发布目标。本轮完成: + +- 正式 Desktop candidate workflow 拆分为 macOS 签名/公证、Windows 代码签名和跨平台聚合三个 job;两端都只接受与客户端版本一致的 `vX.Y.Z` tag。 +- Windows job 从组织 secret 导入 Base64 PFX,动态配置证书指纹、SHA-256、RFC 3161 时间戳,构建 NSIS installer/updater,并校验应用与安装器的 Authenticode signer 和 timestamp certificate。 +- 聚合 job 只在两个原生 job 均通过后生成统一 `latest.json`,同时包含 Universal macOS 的两个平台键和 `windows-x86_64`,并校验所有制品、`.sig` 与 checksum。 +- 无签名 `.github/workflows/desktop-windows-internal.yml` 继续保持 branch-only 内部验证语义,不生成正式 updater feed。 +- npm 依赖锁文件已升级至当前安全修复版本,生产和完整依赖审计均为 0 个已知漏洞;Axios 新 header 类型通过受控字符串归一化适配并新增单元测试。 + +创建正式 tag 前仍必须由发布负责人确认: + +- GitHub Actions 已配置 updater、Apple 与 Windows 签名 secrets,以及 versioned artifact base URL 和 Windows timestamp URL。 +- 组织 Windows 证书允许 PFX 导入 CI;若为硬件或云托管密钥,先将 workflow 切换为经审计的 Tauri `signCommand` provider。 +- macOS/Windows 原生 candidate job、联合 feed 校验、真实安装/升级和生产下载源上传全部通过。 diff --git a/docs/guides/branch-maintenance-sop-zh.md b/docs/guides/branch-maintenance-sop-zh.md index 9151fa12..98f2819a 100644 --- a/docs/guides/branch-maintenance-sop-zh.md +++ b/docs/guides/branch-maintenance-sop-zh.md @@ -106,8 +106,7 @@ npm run desktop:build:app npm run desktop:build:app ``` -正式桌面发布构建仍必须设置 updater 签名私钥和 Apple 签名/公证变量后,先执行 -`npm run desktop:release-readiness:check`,再执行 `npm run desktop:build:macos-release -- --ci`。 +正式桌面发布构建必须从同一正式 tag 分别在 macOS 和 Windows 原生 CI job 执行。两端都必须设置 updater 签名私钥;macOS 设置 Apple 签名/公证变量并以 `REQUIRE_DESKTOP_SIGNING=true` 执行 readiness 与 Universal 构建,Windows 设置 PFX 证书、密码和 RFC 3161 时间戳变量并以 `REQUIRE_WINDOWS_SIGNING=true` 执行 readiness、签名 NSIS 构建和 Authenticode 校验。两个平台通过后才能汇总正式 updater feed。 后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。 @@ -207,6 +206,7 @@ dev -> main - 前后端测试通过 - 网页端构建通过 - macOS 桌面端构建通过 +- Windows x64 NSIS 兼容性构建通过;正式发布时签名和时间戳验证通过 - 数据库迁移经过验证 - 已知风险和回滚方式已记录 @@ -281,7 +281,7 @@ main -> release - 回归测试通过 - 数据库迁移和回滚方案确认 - 网页端生产构建通过 -- 桌面端生产构建通过 +- macOS 与 Windows 桌面端生产构建均从候选 tag 通过 - 发布说明完成 - 生产配置和密钥不在仓库中 - 正式版本号已经统一 @@ -295,7 +295,7 @@ git tag -a v1.2.0 -m "CTMS v1.2.0" git push origin v1.2.0 ``` -网页端和桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。 +网页端、macOS 和 Windows 桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。macOS 签名/公证、Windows 代码签名/RFC 3161 时间戳以及两个平台的 updater 签名制品必须全部验证通过后,才能分发安装包并最后原子替换生产 `latest.json`。 发布记录至少包含: diff --git a/docs/guides/client-release.md b/docs/guides/client-release.md index dcd9326e..23fee5ae 100644 --- a/docs/guides/client-release.md +++ b/docs/guides/client-release.md @@ -66,8 +66,8 @@ Manual edits that leave these files inconsistent fail CI. ## Stabilization Gates Client builds require Node.js 22.13.0 or newer. The development frontend container, -Web CI, macOS release candidates, Windows internal validation, and the production -Web image use the same Node 22.13 baseline. +Web CI, macOS and Windows release candidates, Windows internal validation, and the +production Web image use the same Node 22.13 baseline. Client release candidates must pass the shared Web checks and the Desktop release/security gate before promotion: @@ -85,8 +85,9 @@ npm run build npm run desktop:build:app ``` -`release:env:check` verifies build channel and commit metadata, and can be -made strict for signed Desktop builds with `REQUIRE_DESKTOP_SIGNING=true`. +`release:env:check` verifies build channel and commit metadata, and becomes +platform-strict with `REQUIRE_DESKTOP_SIGNING=true` on macOS or +`REQUIRE_WINDOWS_SIGNING=true` on Windows. `desktop:release:check` statically verifies the Tauri bundle, updater public key, CSP, capability scopes, command allowlist, query-token ban, generic system notification boundary, CI gate coverage, and secure session token boundary. The @@ -96,7 +97,7 @@ full manual release, security, regression, and Desktop UX checklist lives in ## Promotion 1. Merge feature branches into `dev`. -2. Require the shared client/Web and macOS Desktop CI jobs to pass. +2. Require the shared client/Web and macOS/Windows Desktop CI jobs to pass. 3. Promote the accepted scope from `dev` to `main`. 4. Set the release version and complete regression testing on `main`. 5. Promote `main` to `release`. @@ -130,11 +131,11 @@ for `localhost`, `127.0.0.1`, or `::1`. The release pipeline must: 1. build from the accepted release tag and commit; -2. build macOS Universal desktop artifacts; -3. sign and notarize the macOS app; -4. produce updater artifacts and `.sig` files with the updater private key; -5. generate `latest.json` and a checksum manifest with `npm run desktop:update-feed:create`; -6. verify the feed with `npm run desktop:update-feed:check -- --feed /latest.json --artifacts-dir `; +2. build the Universal macOS app/DMG and Windows x64 NSIS installer from that tag; +3. sign and notarize macOS, and Authenticode-sign Windows with an RFC 3161 timestamp; +4. produce macOS `.app.tar.gz` and Windows `.nsis.zip` updater artifacts plus their `.sig` files with the shared updater private key; +5. generate one combined `latest.json` and checksum manifest with `npm run desktop:update-feed:create`; +6. verify all macOS and Windows entries with `npm run desktop:update-feed:check -- --feed /latest.json --artifacts-dir --require-platform windows-x86_64`; 7. upload immutable artifacts first; 8. atomically replace `latest.json` last. @@ -142,6 +143,10 @@ For Universal macOS artifacts, `latest.json` must provide both `darwin-aarch64` and `darwin-x86_64` entries pointing at the same Universal update package. +The same feed must also contain `windows-x86_64`, pointing to the signed NSIS +`.nsis.zip` updater package. The Windows `.exe` installer is distributed next +to the updater package but is not used as the updater URL. + The signed release candidate workflow lives at `.github/workflows/desktop-release-candidate.yml`. It must be run from a matching `vX.Y.Z` tag and produces a verified release directory as a GitHub @@ -149,21 +154,30 @@ artifact. That artifact is still only a release candidate; the release owner must upload immutable files to the production download origin and replace `latest.json` atomically after validation. -## Windows Build Readiness +## Windows Release and Internal Validation -Second-phase Windows work is limited to CI compatibility validation. A -`windows-latest` x64 NSIS build may be produced for verification, but it is not -a formal deliverable until Windows code signing and release support are -approved. +Windows x64 NSIS is an approved formal Desktop target. Formal Windows builds +run in `.github/workflows/desktop-release-candidate.yml` from the same exact +`vX.Y.Z` tag and SHA as Web and macOS. They must: -Windows validation must cover: +- import a Base64-encoded PFX from `WINDOWS_CERTIFICATE` using + `WINDOWS_CERTIFICATE_PASSWORD`; +- configure the imported certificate thumbprint, SHA-256 digest, + `WINDOWS_TIMESTAMP_URL`, and RFC 3161 timestamp mode dynamically in CI; +- set `REQUIRE_WINDOWS_SIGNING=true` and use the updater signing secrets; +- verify the application and installer with `Get-AuthenticodeSignature`, + including signer and timestamp certificates; +- publish the signed NSIS `.exe`, `.nsis.zip`, and `.nsis.zip.sig` into the + combined verified Desktop release directory. + +Windows release validation also covers: - WebView2 runtime prerequisite behavior; - user-level installer assumptions; - Windows Credential Manager session storage; - path handling and temporary file cleanup; - notification and updater compilation; -- future code-signing requirements. +- code-signing trust, timestamp validity, and updater signature verification. The manual internal Windows validation workflow lives at `.github/workflows/desktop-windows-internal.yml`. It is `workflow_dispatch` @@ -173,7 +187,24 @@ and Desktop safety gates, builds an unsigned NSIS installer with updater artifacts disabled, and uploads the `.exe` plus `SHA256SUMS.txt` as a GitHub Actions artifact. This workflow is for internal compatibility verification only; it must not generate `latest.json`, update feeds, signed release -directories, or formal Windows release artifacts. +directories, or formal Windows release artifacts. A successful internal build +does not substitute for the signed tag-only release workflow. + +The formal workflow requires these organization settings: + +- secrets: `TAURI_SIGNING_PRIVATE_KEY`, + `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`, `WINDOWS_CERTIFICATE`, and + `WINDOWS_CERTIFICATE_PASSWORD`; +- variable: `WINDOWS_TIMESTAMP_URL`; +- shared versioned HTTPS artifact prefix: `DESKTOP_UPDATE_BASE_URL` or the + manual workflow input. + +The checked-in workflow implements the exportable PFX path. Confirm that the +organization's certificate policy permits an exportable CI certificate before +provisioning it. If the selected CA provides only hardware- or cloud-backed +keys, replace the PFX import with a reviewed Tauri `signCommand` integration +(for example, the organization's Azure signing provider) while retaining the +same Authenticode and timestamp verification gates. ## Required Checks @@ -197,11 +228,12 @@ export REQUIRE_DESKTOP_SIGNING=true npm run release:env:check npm run desktop:release-readiness:check npm run desktop:build:macos-release -- --ci -npm run desktop:update-feed:create -- --artifact --base-url --output-dir -npm run desktop:update-feed:check -- --feed /latest.json --artifacts-dir +npm run desktop:update-feed:create -- --artifact --platform-artifact windows-x86_64= --include --include --base-url --output-dir +npm run desktop:update-feed:check -- --feed /latest.json --artifacts-dir --require-platform windows-x86_64 ``` -The Desktop build must run on macOS for the current first-phase target. A signed -or notarized public release additionally requires the Apple credentials defined -by the release owner. A formal second-phase desktop release also requires the -updater signing key; unsigned internal builds are not formal distributions. +The macOS and Windows signed builds run in their native CI jobs. macOS requires +the Apple signing/notarization credentials defined by the release owner; +Windows requires the PFX certificate/password and timestamp URL above. Both use +the same updater signing key and are aggregated only after both native jobs +pass. Unsigned internal builds are not formal distributions. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5dea880c..feed2449 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1001,9 +1001,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", - "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -1015,9 +1015,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", - "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -1028,10 +1028,24 @@ "android" ] }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", - "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -1043,9 +1057,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", - "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -1057,9 +1071,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", - "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -1071,13 +1085,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", - "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1085,13 +1102,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", - "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1099,13 +1119,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", - "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1113,11 +1136,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.5", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1125,13 +1153,33 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", - "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1139,13 +1187,33 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", - "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1153,13 +1221,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", - "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1167,13 +1238,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", - "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1181,13 +1255,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", - "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1195,13 +1272,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", - "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1209,9 +1289,26 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", - "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -1219,13 +1316,13 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", - "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -1237,9 +1334,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", - "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -1251,9 +1348,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", - "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -1265,9 +1362,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", - "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1279,9 +1376,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", - "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -1589,7 +1686,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1634,15 +1733,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -1651,13 +1750,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1688,9 +1787,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1701,13 +1800,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -1716,13 +1815,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -1731,9 +1830,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1744,13 +1843,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -2002,12 +2101,40 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, "node_modules/balanced-match": { @@ -2181,7 +2308,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2239,21 +2365,15 @@ "license": "MIT" }, "node_modules/echarts": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", - "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", + "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", "license": "Apache-2.0", "dependencies": { "tslib": "2.3.0", - "zrender": "6.0.0" + "zrender": "6.1.0" } }, - "node_modules/echarts/node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "license": "0BSD" - }, "node_modules/editorconfig": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", @@ -2432,7 +2552,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -2467,14 +2589,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -2591,7 +2715,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2781,11 +2907,15 @@ } }, "node_modules/lodash": { - "version": "4.17.21", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash-unified": { @@ -2876,7 +3006,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/muggle-string": { @@ -2887,7 +3016,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -3030,9 +3161,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -3063,7 +3194,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "funding": [ { "type": "opencollective", @@ -3080,7 +3213,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3096,8 +3229,13 @@ "license": "ISC" }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -3110,11 +3248,13 @@ } }, "node_modules/rollup": { - "version": "4.53.5", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -3124,45 +3264,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.5", - "@rollup/rollup-android-arm64": "4.53.5", - "@rollup/rollup-darwin-arm64": "4.53.5", - "@rollup/rollup-darwin-x64": "4.53.5", - "@rollup/rollup-freebsd-arm64": "4.53.5", - "@rollup/rollup-freebsd-x64": "4.53.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", - "@rollup/rollup-linux-arm-musleabihf": "4.53.5", - "@rollup/rollup-linux-arm64-gnu": "4.53.5", - "@rollup/rollup-linux-arm64-musl": "4.53.5", - "@rollup/rollup-linux-loong64-gnu": "4.53.5", - "@rollup/rollup-linux-ppc64-gnu": "4.53.5", - "@rollup/rollup-linux-riscv64-gnu": "4.53.5", - "@rollup/rollup-linux-riscv64-musl": "4.53.5", - "@rollup/rollup-linux-s390x-gnu": "4.53.5", - "@rollup/rollup-linux-x64-gnu": "4.53.5", - "@rollup/rollup-linux-x64-musl": "4.53.5", - "@rollup/rollup-openharmony-arm64": "4.53.5", - "@rollup/rollup-win32-arm64-msvc": "4.53.5", - "@rollup/rollup-win32-ia32-msvc": "4.53.5", - "@rollup/rollup-win32-x64-gnu": "4.53.5", - "@rollup/rollup-win32-x64-msvc": "4.53.5", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, - "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", - "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -3498,6 +3627,12 @@ "node": ">=18" } }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/typescript": { "version": "5.9.3", "devOptional": true, @@ -3516,13 +3651,13 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -3614,20 +3749,20 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -3657,8 +3792,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, @@ -3976,9 +4111,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "dev": true, "license": "MIT", "engines": { @@ -4015,19 +4150,13 @@ "license": "MIT" }, "node_modules/zrender": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", - "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz", + "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==", "license": "BSD-3-Clause", "dependencies": { "tslib": "2.3.0" } - }, - "node_modules/zrender/node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "license": "0BSD" } } } diff --git a/frontend/package.json b/frontend/package.json index 514a54ad..ca3a14c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "desktop:build": "tauri build", "desktop:build:app": "tauri build --config '{\"bundle\":{\"createUpdaterArtifacts\":false}}' --bundles app", "desktop:build:macos-release": "tauri build --target universal-apple-darwin --bundles app,dmg", + "desktop:build:windows-release": "tauri build --bundles nsis", "desktop:update-feed:create": "node scripts/create-desktop-update-feed.mjs", "desktop:bundle:dmg": "tauri build --bundles dmg", "desktop:update-feed:check": "node scripts/verify-desktop-update-feed.mjs", diff --git a/frontend/scripts/create-desktop-update-feed.mjs b/frontend/scripts/create-desktop-update-feed.mjs index 119b872b..d5d70b46 100644 --- a/frontend/scripts/create-desktop-update-feed.mjs +++ b/frontend/scripts/create-desktop-update-feed.mjs @@ -1,6 +1,6 @@ +import { createHash } from "node:crypto"; import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises"; import { basename, resolve } from "node:path"; -import { createHash } from "node:crypto"; import { fileURLToPath } from "node:url"; const frontendDir = fileURLToPath(new URL("../", import.meta.url)); @@ -25,7 +25,7 @@ const assert = (condition, message) => { if (!condition) fail(message); }; -const artifactPath = value("--artifact") || process.env.DESKTOP_UPDATE_ARTIFACT; +const macosArtifactPath = value("--artifact") || process.env.DESKTOP_UPDATE_ARTIFACT; const outputDir = resolve( frontendDir, value("--output-dir") || process.env.DESKTOP_UPDATE_OUTPUT_DIR || "src-tauri/target/release/desktop-update-feed", @@ -34,18 +34,40 @@ const baseUrlRaw = value("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL; const pubDate = value("--date") || process.env.DESKTOP_UPDATE_PUB_DATE || new Date().toISOString(); const notes = value("--notes") || process.env.DESKTOP_UPDATE_NOTES; const includes = values("--include").map((path) => resolve(frontendDir, path)); +const platformArtifactSpecs = values("--platform-artifact"); +const platformPattern = /^(?:darwin|windows|linux)-(?:x86_64|aarch64|i686|armv7)$/; -assert(Boolean(artifactPath), "--artifact or DESKTOP_UPDATE_ARTIFACT is required."); +assert(Boolean(macosArtifactPath), "--artifact or DESKTOP_UPDATE_ARTIFACT is required for the Universal macOS updater artifact."); assert(Boolean(baseUrlRaw), "--base-url or DESKTOP_UPDATE_BASE_URL is required."); -const resolvedArtifactPath = artifactPath ? resolve(frontendDir, artifactPath) : undefined; -const artifactName = resolvedArtifactPath ? basename(resolvedArtifactPath) : undefined; -const signaturePath = resolvedArtifactPath ? `${resolvedArtifactPath}.sig` : undefined; -const uploadFiles = []; +const platformArtifacts = new Map(); +if (macosArtifactPath) { + const resolvedMacosArtifact = resolve(frontendDir, macosArtifactPath); + platformArtifacts.set("darwin-aarch64", resolvedMacosArtifact); + platformArtifacts.set("darwin-x86_64", resolvedMacosArtifact); +} + +for (const spec of platformArtifactSpecs) { + const separator = spec.indexOf("="); + if (separator <= 0 || separator === spec.length - 1) { + fail(`--platform-artifact must use -=: ${spec}`); + continue; + } + + const platform = spec.slice(0, separator); + const path = spec.slice(separator + 1); + assert(platformPattern.test(platform), `Unsupported updater platform key: ${platform}`); + assert(!platformArtifacts.has(platform), `Updater platform is configured more than once: ${platform}`); + if (platformPattern.test(platform) && !platformArtifacts.has(platform)) { + platformArtifacts.set(platform, resolve(frontendDir, path)); + } +} const readRequiredFile = async (path, description) => { try { - return await readFile(path); + const data = await readFile(path); + assert(data.length > 0, `${description} must not be empty: ${path}`); + return data; } catch (error) { fail(`${description} cannot be read: ${path} (${error.message})`); return undefined; @@ -72,20 +94,16 @@ const normalizedBaseUrl = () => { } }; -const copyIntoOutput = async (sourcePath) => { - const destination = resolve(outputDir, basename(sourcePath)); - if (sourcePath !== destination) { - await copyFile(sourcePath, destination); - } - return destination; -}; - -if (resolvedArtifactPath && signaturePath) { - await readRequiredFile(resolvedArtifactPath, "Updater artifact"); +const artifactSignatures = new Map(); +const uniqueArtifactPaths = [...new Set(platformArtifacts.values())]; +for (const artifactPath of uniqueArtifactPaths) { + const signaturePath = `${artifactPath}.sig`; + await readRequiredFile(artifactPath, "Updater artifact"); const signature = await readRequiredFile(signaturePath, "Updater artifact signature"); if (signature) { const signatureText = signature.toString("utf8").trim(); - assert(signatureText.length > 80, "Updater artifact signature is unexpectedly short."); + assert(signatureText.length > 80, `Updater artifact signature is unexpectedly short: ${signaturePath}`); + artifactSignatures.set(artifactPath, signatureText); } } @@ -98,56 +116,75 @@ for (const includePath of includes) { } } +for (const [platform, artifactPath] of platformArtifacts) { + if (platform.startsWith("darwin-")) { + assert(artifactPath.endsWith(".app.tar.gz"), `${platform} must use a macOS .app.tar.gz updater artifact.`); + } + if (platform === "windows-x86_64") { + assert(artifactPath.endsWith(".nsis.zip"), "windows-x86_64 must use an NSIS .nsis.zip updater artifact."); + } +} + +const uploadSources = [ + ...uniqueArtifactPaths.flatMap((path) => [path, `${path}.sig`]), + ...includes, +]; +const sourceByName = new Map(); +for (const sourcePath of uploadSources) { + const name = basename(sourcePath); + const existing = sourceByName.get(name); + assert(!existing || existing === sourcePath, `Release files must have unique basenames: ${name}`); + sourceByName.set(name, sourcePath); +} + const baseUrl = normalizedBaseUrl(); -if (failures.length === 0 && resolvedArtifactPath && signaturePath && artifactName && baseUrl) { +if (failures.length === 0 && baseUrl) { await mkdir(outputDir, { recursive: true }); - const artifactUrl = new URL(artifactName, baseUrl).toString(); - assert(!artifactUrl.endsWith("/latest.json"), "Updater artifact URL must not point at latest.json."); - assert(!/[?&]token=/i.test(new URL(artifactUrl).search), "Updater artifact URL must not include token query parameters."); + const platforms = {}; + for (const [platform, artifactPath] of platformArtifacts) { + const artifactUrl = new URL(basename(artifactPath), baseUrl).toString(); + assert(!artifactUrl.endsWith("/latest.json"), "Updater artifact URL must not point at latest.json."); + assert(!/[?&]token=/i.test(new URL(artifactUrl).search), "Updater artifact URL must not include token query parameters."); + platforms[platform] = { + signature: artifactSignatures.get(artifactPath), + url: artifactUrl, + }; + } - const signature = (await readFile(signaturePath, "utf8")).trim(); const latest = { version: packageInfo.version, pub_date: pubDate, - platforms: { - "darwin-aarch64": { - signature, - url: artifactUrl, - }, - "darwin-x86_64": { - signature, - url: artifactUrl, - }, - }, + platforms, }; - if (notes) { latest.notes = notes; } - uploadFiles.push(await copyIntoOutput(resolvedArtifactPath)); - uploadFiles.push(await copyIntoOutput(signaturePath)); - for (const includePath of includes) { - uploadFiles.push(await copyIntoOutput(includePath)); + const copiedFiles = []; + for (const sourcePath of sourceByName.values()) { + const destination = resolve(outputDir, basename(sourcePath)); + if (sourcePath !== destination) { + await copyFile(sourcePath, destination); + } + copiedFiles.push(destination); } const latestPath = resolve(outputDir, "latest.json"); await writeFile(latestPath, `${JSON.stringify(latest, null, 2)}\n`); - uploadFiles.push(latestPath); + copiedFiles.push(latestPath); - const uniqueFiles = [...new Map(uploadFiles.map((path) => [basename(path), path])).values()]; const checksumLines = []; - for (const filePath of uniqueFiles) { + for (const filePath of copiedFiles) { checksumLines.push(`${await sha256(filePath)} ${basename(filePath)}`); } const checksumPath = resolve(outputDir, "SHA256SUMS.txt"); await writeFile(checksumPath, `${checksumLines.join("\n")}\n`); console.log(`Desktop update feed created in ${outputDir}`); - console.log(` - ${uniqueFiles.map((path) => basename(path)).join("\n - ")}`); - console.log(` - SHA256SUMS.txt`); + console.log(` - ${copiedFiles.map((path) => basename(path)).join("\n - ")}`); + console.log(" - SHA256SUMS.txt"); } if (failures.length > 0) { diff --git a/frontend/scripts/verify-desktop-release-readiness.mjs b/frontend/scripts/verify-desktop-release-readiness.mjs index 6bd46edf..0989eccb 100644 --- a/frontend/scripts/verify-desktop-release-readiness.mjs +++ b/frontend/scripts/verify-desktop-release-readiness.mjs @@ -11,13 +11,11 @@ const failures = []; const env = process.env; const fullShaPattern = /^[0-9a-f]{40}$/i; const expectedTag = `v${packageInfo.version}`; -const requiredSecretLikeEnv = [ - "TAURI_SIGNING_PRIVATE_KEY", - "TAURI_SIGNING_PRIVATE_KEY_PASSWORD", - "APPLE_ID", - "APPLE_PASSWORD", - "APPLE_TEAM_ID", -]; +const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true"; +const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true"; +const requiredUpdaterEnv = ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"]; +const requiredMacosEnv = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_TEAM_ID"]; +const requiredWindowsEnv = ["WINDOWS_CERTIFICATE", "WINDOWS_CERTIFICATE_PASSWORD"]; const fail = (message) => failures.push(message); const assert = (condition, message) => { @@ -65,6 +63,24 @@ const validateBaseUrl = () => { ); }; +const validateWindowsTimestampUrl = () => { + const raw = env.WINDOWS_TIMESTAMP_URL; + requireEnv("WINDOWS_TIMESTAMP_URL"); + if (!raw) return; + + let url; + try { + url = new URL(raw); + } catch (error) { + fail(`WINDOWS_TIMESTAMP_URL is invalid: ${error.message}`); + return; + } + + assert(["http:", "https:"].includes(url.protocol), "WINDOWS_TIMESTAMP_URL must use HTTP or HTTPS."); + assert(url.username === "" && url.password === "", "WINDOWS_TIMESTAMP_URL must not include credentials."); + assert(!/[?&]token=/i.test(url.search), "WINDOWS_TIMESTAMP_URL must not include token query parameters."); +}; + const headSha = gitMaybe(["rev-parse", "HEAD"]); const exactTag = gitMaybe(["describe", "--tags", "--exact-match", "HEAD"]); const status = gitMaybe(["status", "--porcelain"]); @@ -79,16 +95,39 @@ if (headSha && env.VITE_BUILD_COMMIT) { assert(env.VITE_BUILD_COMMIT === headSha, "VITE_BUILD_COMMIT must match the current release commit."); } -for (const name of requiredSecretLikeEnv) { +assert( + requiresMacosSigning || requiresWindowsSigning, + "Release readiness requires REQUIRE_DESKTOP_SIGNING=true or REQUIRE_WINDOWS_SIGNING=true.", +); +assert( + !(requiresMacosSigning && requiresWindowsSigning), + "macOS and Windows signing readiness must be checked in their native jobs.", +); + +for (const name of requiredUpdaterEnv) { requireEnv(name); } -assert( - Boolean(env.APPLE_CERTIFICATE || env.APPLE_SIGNING_IDENTITY), - "APPLE_CERTIFICATE or APPLE_SIGNING_IDENTITY must be configured for macOS signing.", -); -if (env.APPLE_CERTIFICATE) { - requireEnv("APPLE_CERTIFICATE_PASSWORD"); +if (requiresMacosSigning) { + assert(process.platform === "darwin", "Signed macOS desktop release readiness must run on macOS."); + for (const name of requiredMacosEnv) { + requireEnv(name); + } + assert( + Boolean(env.APPLE_CERTIFICATE || env.APPLE_SIGNING_IDENTITY), + "APPLE_CERTIFICATE or APPLE_SIGNING_IDENTITY must be configured for macOS signing.", + ); + if (env.APPLE_CERTIFICATE) { + requireEnv("APPLE_CERTIFICATE_PASSWORD"); + } +} + +if (requiresWindowsSigning) { + assert(process.platform === "win32", "Signed Windows desktop release readiness must run on Windows."); + for (const name of requiredWindowsEnv) { + requireEnv(name); + } + validateWindowsTimestampUrl(); } validateBaseUrl(); diff --git a/frontend/scripts/verify-desktop-release.mjs b/frontend/scripts/verify-desktop-release.mjs index b00f9997..bc9ec248 100644 --- a/frontend/scripts/verify-desktop-release.mjs +++ b/frontend/scripts/verify-desktop-release.mjs @@ -392,6 +392,7 @@ const verifyWorkflowGates = async () => { assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0."); const requiredScripts = [ "desktop:build:macos-release", + "desktop:build:windows-release", "desktop:update-feed:create", "desktop:update-feed:check", "desktop:release-readiness:check", @@ -424,15 +425,30 @@ const verifyWorkflowGates = async () => { const releaseWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-release-candidate.yml"), "utf8"); const requiredReleaseWorkflowTokens = [ + "runs-on: macos-latest", + "runs-on: windows-latest", "REQUIRE_DESKTOP_SIGNING", + "REQUIRE_WINDOWS_SIGNING", "TAURI_SIGNING_PRIVATE_KEY", "APPLE_ID", "APPLE_PASSWORD", "APPLE_TEAM_ID", + "WINDOWS_CERTIFICATE", + "WINDOWS_CERTIFICATE_PASSWORD", + "WINDOWS_TIMESTAMP_URL", + "npm audit", "npm run desktop:build:macos-release", + "npm run desktop:build:windows-release", + "Get-AuthenticodeSignature", + "EnhancedKeyUsageList", + "TimeStamperCertificate", + ".nsis.zip", "npm run desktop:update-feed:create", + "--platform-artifact", "npm run desktop:update-feed:check", + "--require-platform windows-x86_64", "npm run desktop:release-readiness:check", + "actions/download-artifact", "actions/upload-artifact", ]; diff --git a/frontend/scripts/verify-desktop-update-feed.mjs b/frontend/scripts/verify-desktop-update-feed.mjs index eeed8a64..0b1a5719 100644 --- a/frontend/scripts/verify-desktop-update-feed.mjs +++ b/frontend/scripts/verify-desktop-update-feed.mjs @@ -12,6 +12,8 @@ const optionValue = (name) => { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : undefined; }; +const optionValues = (name) => + args.reduce((result, argument, index) => (argument === name && args[index + 1] ? [...result, args[index + 1]] : result), []); const feedPath = resolve( frontendDir, @@ -19,6 +21,12 @@ const feedPath = resolve( ); const artifactDir = optionValue("--artifacts-dir") || process.env.DESKTOP_UPDATE_ARTIFACTS_DIR; const expectedBaseUrl = optionValue("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL; +const requiredPlatforms = new Set([ + "darwin-aarch64", + "darwin-x86_64", + ...optionValues("--require-platform"), +]); +const platformPattern = /^(?:darwin|windows|linux)-(?:x86_64|aarch64|i686|armv7)$/; const checksumManifestPath = optionValue("--checksum-manifest") || process.env.DESKTOP_UPDATE_CHECKSUM_MANIFEST || @@ -103,15 +111,16 @@ if (feed) { assert(normalizedFeedVersion === packageInfo.version, `latest.json version must match package version ${packageInfo.version}.`); assert(Boolean(feed.pub_date || feed.pubDate), "latest.json must include a publication date."); - assert(Boolean(darwinArm), "latest.json must include darwin-aarch64."); - assert(Boolean(darwinIntel), "latest.json must include darwin-x86_64."); + for (const platform of requiredPlatforms) { + assert(platformPattern.test(platform), `Required updater platform key is unsupported: ${platform}.`); + assert(Boolean(platforms[platform]), `latest.json must include ${platform}.`); + } - const entries = [ - ["darwin-aarch64", darwinArm], - ["darwin-x86_64", darwinIntel], - ]; + const entries = Object.entries(platforms); + assert(entries.length > 0, "latest.json must include at least one platform entry."); for (const [platform, entry] of entries) { + assert(platformPattern.test(platform), `latest.json contains an unsupported platform key: ${platform}.`); const rawUrl = entry?.url; const signature = entry?.signature; assert(typeof signature === "string" && signature.length > 80, `${platform} must include an updater signature.`); @@ -133,6 +142,12 @@ if (feed) { if (expectedBaseUrl) { assert(rawUrl.startsWith(expectedBaseUrl), `${platform} artifact URL must start with ${expectedBaseUrl}.`); } + if (platform.startsWith("darwin-")) { + assert(url.pathname.endsWith(".app.tar.gz"), `${platform} must point to a macOS .app.tar.gz updater artifact.`); + } + if (platform === "windows-x86_64") { + assert(url.pathname.endsWith(".nsis.zip"), "windows-x86_64 must point to an NSIS .nsis.zip updater artifact."); + } if (artifactDir) { const artifactPath = resolve(artifactDir, basename(url.pathname)); diff --git a/frontend/scripts/verify-release-build-env.mjs b/frontend/scripts/verify-release-build-env.mjs index 0e160b10..678b9d76 100644 --- a/frontend/scripts/verify-release-build-env.mjs +++ b/frontend/scripts/verify-release-build-env.mjs @@ -1,8 +1,10 @@ +import { execFileSync } from "node:child_process"; import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; const frontendDir = fileURLToPath(new URL("../", import.meta.url)); +const rootDir = resolve(frontendDir, ".."); const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8")); const failures = []; @@ -16,6 +18,8 @@ const commit = env.VITE_BUILD_COMMIT || "local"; const isCi = env.CI === "true" || env.GITHUB_ACTIONS === "true"; const isTagBuild = env.GITHUB_REF_TYPE === "tag"; const isReleaseBuild = env.RELEASE_BUILD === "true" || isTagBuild || channel === "release"; +const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true"; +const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true"; const fail = (message) => failures.push(message); const assert = (condition, message) => { @@ -26,6 +30,18 @@ const requireEnv = (name) => { assert(Boolean(env[name]), `${name} must be configured for signed desktop release builds.`); }; +const gitHead = () => { + try { + return execFileSync("git", ["rev-parse", "HEAD"], { + cwd: rootDir, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + return undefined; + } +}; + assert(allowedChannels.has(channel), `VITE_BUILD_CHANNEL must be one of ${[...allowedChannels].join(", ")}.`); if (isCi || isReleaseBuild) { @@ -40,12 +56,25 @@ if (env.GITHUB_SHA) { ); } +if (isReleaseBuild && fullShaPattern.test(commit)) { + const headSha = gitHead(); + assert(Boolean(headSha), "Release builds must be able to resolve the current Git commit."); + if (headSha) { + assert(commit === headSha, "VITE_BUILD_COMMIT must match the current Git HEAD for release builds."); + } +} + if (isTagBuild) { assert(env.GITHUB_REF_NAME === semverTag, `Release tag must be ${semverTag}; found ${env.GITHUB_REF_NAME || ""}.`); assert(channel === "release", "Release tag builds must set VITE_BUILD_CHANNEL=release."); } -if (env.REQUIRE_DESKTOP_SIGNING === "true") { +assert( + !(requiresMacosSigning && requiresWindowsSigning), + "macOS and Windows signing requirements must be checked in their native jobs.", +); + +if (requiresMacosSigning) { assert(process.platform === "darwin", "Signed macOS desktop release builds must run on macOS."); if (isCi) { assert(isTagBuild, "Signed desktop release candidate builds in CI must run from a release tag."); @@ -64,6 +93,18 @@ if (env.REQUIRE_DESKTOP_SIGNING === "true") { } } +if (requiresWindowsSigning) { + assert(process.platform === "win32", "Signed Windows desktop release builds must run on Windows."); + if (isCi) { + assert(isTagBuild, "Signed Windows desktop release candidate builds in CI must run from a release tag."); + } + requireEnv("TAURI_SIGNING_PRIVATE_KEY"); + requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD"); + requireEnv("WINDOWS_CERTIFICATE"); + requireEnv("WINDOWS_CERTIFICATE_PASSWORD"); + requireEnv("WINDOWS_TIMESTAMP_URL"); +} + if (failures.length > 0) { console.error(`Release build environment check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`); process.exitCode = 1; diff --git a/frontend/src/utils/contentDisposition.test.ts b/frontend/src/utils/contentDisposition.test.ts index 7efddde7..3dae0ec6 100644 --- a/frontend/src/utils/contentDisposition.test.ts +++ b/frontend/src/utils/contentDisposition.test.ts @@ -1,5 +1,18 @@ import { describe, expect, it } from "vitest"; -import { getContentDispositionFilename } from "./contentDisposition"; +import { getContentDispositionFilename, getHttpHeaderString } from "./contentDisposition"; + +describe("getHttpHeaderString", () => { + it("keeps string header values and joins string arrays", () => { + expect(getHttpHeaderString("application/pdf")).toBe("application/pdf"); + expect(getHttpHeaderString(["attachment", "filename=protocol.pdf"])).toBe("attachment, filename=protocol.pdf"); + }); + + it("rejects non-string Axios header values", () => { + expect(getHttpHeaderString(200)).toBeUndefined(); + expect(getHttpHeaderString(true)).toBeUndefined(); + expect(getHttpHeaderString({ value: "application/pdf" })).toBeUndefined(); + }); +}); describe("getContentDispositionFilename", () => { it("prefers the UTF-8 filename even when the ASCII fallback appears first", () => { diff --git a/frontend/src/utils/contentDisposition.ts b/frontend/src/utils/contentDisposition.ts index bc9403bf..c67b58f0 100644 --- a/frontend/src/utils/contentDisposition.ts +++ b/frontend/src/utils/contentDisposition.ts @@ -1,3 +1,11 @@ +export const getHttpHeaderString = (value: unknown): string | undefined => { + if (typeof value === "string") return value; + if (Array.isArray(value) && value.every((item) => typeof item === "string")) { + return value.join(", "); + } + return undefined; +}; + export const getContentDispositionFilename = (header?: string | null): string | null => { if (!header) return null; const encoded = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(header)?.[1]; diff --git a/frontend/src/views/documents/DocumentDetail.vue b/frontend/src/views/documents/DocumentDetail.vue index 1e3c4958..26a84add 100644 --- a/frontend/src/views/documents/DocumentDetail.vue +++ b/frontend/src/views/documents/DocumentDetail.vue @@ -381,7 +381,7 @@ import StateLoading from "../../components/StateLoading.vue"; import { onDesktopRefreshCurrentView } from "../../composables/useDesktopRefresh"; import { openFileWithFeedback, pickFilesWithFeedback, saveFileWithFeedback } from "../../utils/fileTaskFeedback"; import { getApiErrorMessage } from "../../utils/apiErrorMessage"; -import { getContentDispositionFilename } from "../../utils/contentDisposition"; +import { getContentDispositionFilename, getHttpHeaderString } from "../../utils/contentDisposition"; import { prepareSaveFile } from "../../runtime"; import { detectFilePreviewKind, ONLYOFFICE_FILE_EXTENSIONS } from "../../utils/officePreview"; @@ -803,7 +803,8 @@ const previewVersion = async (version: DocumentVersion) => { previewLoading.value = true; try { const response = await downloadDocumentVersion(version.id); - const contentType = response.headers?.["content-type"] || version.mime_type || "application/octet-stream"; + const contentType = + getHttpHeaderString(response.headers?.["content-type"]) || version.mime_type || "application/octet-stream"; const blob = new Blob([response.data], { type: contentType }); previewObjectUrl.value = URL.createObjectURL(blob); previewUrl.value = previewObjectUrl.value; @@ -823,8 +824,9 @@ const downloadVersion = async (version: DocumentVersion) => { return; } const response = await downloadDocumentVersion(version.id); - const contentType = response.headers?.["content-type"] || "application/octet-stream"; - const filename = getContentDispositionFilename(response.headers?.["content-disposition"]) || suggestedName; + const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream"; + const filename = + getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) || suggestedName; const blob = new Blob([response.data], { type: contentType }); await saveFileWithFeedback({ suggestedName: filename, mimeType: contentType, data: blob }, {}, destination); } catch (e: any) { ElMessage.error(await getApiErrorMessage(e, TEXT.common.messages.downloadFailed)); } @@ -840,10 +842,11 @@ const openVersion = async (version: DocumentVersion) => { } try { - const contentType = response.headers?.["content-type"] || "application/octet-stream"; - const filename = getContentDispositionFilename(response.headers?.["content-disposition"]) - || getVersionFileName(version) - || `document-${version.version_no || version.id}.bin`; + const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream"; + const filename = + getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) || + getVersionFileName(version) || + `document-${version.version_no || version.id}.bin`; await openFileWithFeedback({ suggestedName: filename, mimeType: contentType, diff --git a/frontend/src/views/ia/RiskIssueMonitoringVisits.vue b/frontend/src/views/ia/RiskIssueMonitoringVisits.vue index fa8617db..e8b86a0f 100644 --- a/frontend/src/views/ia/RiskIssueMonitoringVisits.vue +++ b/frontend/src/views/ia/RiskIssueMonitoringVisits.vue @@ -474,6 +474,7 @@ import { isApiPermissionAllowed } from "../../utils/apiPermissionValue"; import { displayDateTime } from "../../utils/display"; import { getProjectRole, isSystemAdmin } from "../../utils/roles"; import { useDrawerDirtyGuard } from "../../utils/drawerDirtyGuard"; +import { getContentDispositionFilename, getHttpHeaderString } from "../../utils/contentDisposition"; interface MonitoringIssueRow { id: string; @@ -937,13 +938,6 @@ const removeIssue = async (row: MonitoringIssueRow) => { } }; -const getFilename = (header?: string | null) => { - if (!header) return null; - const match = /filename\*=UTF-8''([^;]+)|filename="?([^\";]+)"?/i.exec(header); - if (!match) return null; - return decodeURIComponent(match[1] || match[2] || ""); -}; - const handleExportExcel = async () => { const studyId = study.currentStudy?.id; if (!studyId || !canListIssues.value) return; @@ -975,8 +969,9 @@ const handleExportExcel = async () => { } const response = await exportMonitoringVisitIssues(studyId, params); - const contentType = response.headers?.["content-type"] || "application/octet-stream"; - const filename = getFilename(response.headers?.["content-disposition"]) || "监查访视问题.xlsx"; + const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream"; + const filename = + getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) || "监查访视问题.xlsx"; const blob = new Blob([response.data], { type: contentType }); await saveFileWithFeedback( { suggestedName: filename, mimeType: contentType, data: blob },