Kaynağa Gözat

feat: windows build & sign

07akioni 1 hafta önce
ebeveyn
işleme
6aaba0c334

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md
-2026-08-25-electron-desktop-packaging-and-updates.md: 7b01ba80e31667d851642e57cbb025dfdce7b463
-2026-08-25-electron-desktop-packaging-and-updates.zh.md: a0db068b13f64ce5ef343d77a07e621304ae4d35
+2026-08-25-electron-desktop-packaging-and-updates.md: 1f3107a289f33dea36bb6b22e0240df1688216d2
+2026-08-25-electron-desktop-packaging-and-updates.zh.md: 4f340d3d6d06d127cd6a979dacc3e3178132df3f

+ 11 - 4
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md

@@ -68,9 +68,9 @@ The renderer uses `nodeIntegration: false`, `contextIsolation: true`, and `sandb
 
 The installer never mutates the active profile in place. It copies profile metadata into a transaction staging directory, applies an exact dependency change with the bundled pnpm, performs a full health check, stops the backend, moves the active profile to `rollback/profile`, moves staging into `.dsh/profiles/desktop`, and restarts. `pending.json` journals the filesystem moves so startup can complete or reverse an interrupted replacement.
 
-The packaged seed is an offline installation kit, not an executable dsh tree. It contains the release identity, initial desktop-project manifest, a descriptor and immutable tarballs for the first-party package closure rooted at dsh, lockfile, integrity inventory, and required store subset. The release build requires the Electron package and root dsh package to have the same version, creates final npm tarballs from the official source build, selects the reachable dsh and vendored packages plus the Landlock entry, and verifies the dsh tarball's `lib/desktop-host.js` entry and `config/desktop.cordis.patch.yml` overlay. The overlay is the only CLI configuration file published specifically for Desktop; example configurations remain outside the tarball. These tarballs remain the official `pnpm pack` results governed by each package's `files` manifest; Desktop does not remove published declarations or otherwise create a second package-content policy. The manifest lists every selected package as a local direct dependency, automatic peer installation is disabled, and the workspace file overrides every selected first-party name to its local tarball. The build rejects any lockfile that resolves one of those names by registry version. Bundled pnpm fetches external production dependencies from npm, performs an offline installation, checks both Desktop Host files, and removes `node_modules` before final store preparation. Requiring both files before copying the package set and after offline installation prevents a release whose Host entry loads but cannot compose its required overlay from reaching application signing.
+The packaged seed is an offline installation kit, not an executable dsh tree. It contains the release identity, initial desktop-project manifest, a descriptor and immutable tarballs for the first-party package closure rooted at dsh, lockfile, integrity inventory, and required store subset. The release build requires the Electron package and root dsh package to have the same version, creates final npm tarballs from the official source build, selects the reachable dsh and vendored packages plus the Landlock entry, and verifies the dsh tarball's `lib/desktop-host.js` entry and `config/desktop.cordis.patch.yml` overlay. The overlay is the only CLI configuration file published specifically for Desktop; example configurations remain outside the tarball. These tarballs remain the official `pnpm pack` results governed by each package's `files` manifest; Desktop does not remove published declarations or otherwise create a second package-content policy. The manifest lists every selected package as a local direct dependency, automatic peer installation is disabled, and the workspace file overrides every selected first-party name to its local tarball. The build rejects any lockfile that resolves one of those names by registry version. Bundled pnpm disables its global virtual store, materializes external production dependencies from npm without lifecycle scripts, deletes `node_modules` and every temporary pnpm cache, config, and state directory, then performs a clean offline installation from the final store alone and checks both Desktop Host files. Inventory generation follows removal of that second `node_modules` tree and temporary pnpm project registrations. Requiring both files before copying the package set and after offline installation prevents a release whose Host entry loads but cannot compose its required overlay from reaching application signing.
 
-The seed stores pnpm content in 16 deterministic uncompressed tar shards selected by normalized store path. Apple notarization inspects Mach-O code inside those archives, so macOS seed preparation signs every Mach-O content-addressed object with the release Developer ID, a secure timestamp, and hardened runtime before sharding. Signing changes the bytes: preparation writes each object at its new SHA-512 path and transactionally rewrites every base and side-effects file reference in pnpm's MessagePack SQLite index. A second offline installation proves that pnpm resolves the rewritten store; preparation then shards it, extracts the final archives, and repeats signature verification. Package paths and non-native bytes remain unchanged, and the seed retains bundled architecture variants because removing files would create a Desktop-specific package file set. Seed integrity covers the shard manifest and every archive before extraction. Startup validates archive paths, entry types, uniqueness, and counts, extracts every shard into a unique Desktop-owned staging directory, and only then merges the complete extraction into `.dsh/desktop/pnpm/store`. An interrupted merge may leave valid immutable cache content, but profile installation and activation still require pnpm integrity and the complete health check.
+The seed stores pnpm content in 16 deterministic uncompressed tar shards selected by normalized store path. Apple notarization inspects Mach-O code inside those archives, so macOS seed preparation signs every Mach-O content-addressed object with the release Developer ID, a secure timestamp, and hardened runtime before sharding. Signing changes the bytes: preparation writes each object at its new SHA-512 path and transactionally rewrites every base and side-effects file reference in pnpm's MessagePack SQLite index. A second offline installation proves that pnpm resolves the rewritten store; preparation then shards it, extracts the final archives, and repeats signature verification. Package paths and non-native bytes remain unchanged, and the seed retains bundled architecture variants because removing files would create a Desktop-specific package file set. Seed integrity covers the shard manifest and every archive before extraction. Startup validates archive paths, entry types, uniqueness, and counts, extracts every shard into a unique Desktop-owned staging directory, replaces matching immutable store files, and transactionally merges each pnpm store version's SQLite `package_index` into `.dsh/desktop/pnpm/store`. Seed records replace matching keys while records downloaded for Desktop plugins remain. An interrupted file merge may leave valid immutable cache content, but each SQLite merge is atomic, and profile installation and activation still require pnpm integrity and the complete health check.
 
 Startup requires the packaged release identity to equal Electron's application version, then compares `.dsh/profiles/desktop/desktop-release.json` and the installed dsh package with that release before launching the backend. It installs the new seed manifest and lockfile with `pnpm install --offline --frozen-lockfile --trust-lockfile` in staging. After Electron replacement, it restores every plugin bundle recorded in the active profile at its exact installed version through one offline pnpm add from the existing desktop store and metadata cache. The complete graph must pass the same health check before activation.
 
@@ -92,6 +92,8 @@ Core dsh comes only from integrity-recorded local npm tarballs inside the signed
 
 Electron artifacts are signed; macOS artifacts are notarized. Release automation must supply the application ID, macOS Developer ID qualifier, expected Team ID, and one complete notarytool credential strategy through explicit environment variables. Configuration loading rejects missing or malformed identifiers and incomplete notarization credentials, while macOS packaging requires signing so certificate discovery cannot silently select another installed identity or emit an unsigned release. Seed preparation verifies the exact Authority and Team ID plus the timestamp and hardened-runtime flags on every embedded Mach-O file. An after-sign hook performs Apple's deep strict application verification and requires the same leaf Authority and Team ID before artifact creation continues. Electron-builder then notarizes and staples the application and signs the DMG. The DMG artifact-completion hook separately notarizes and staples every DMG before requiring the configured identity, a valid ticket, and Gatekeeper acceptance; the upload event runs only after that hook succeeds. DMG blockmaps are disabled because macOS updates consume the signed ZIP, and stapling would otherwise invalidate an already-generated DMG blockmap. The custom protocol serves the installed frontend distribution plus client files named by the active module graph and rejects traversal or access outside those roots. The plugin installer API is available only to the Electron-owned management GUI and is absent from the browser application and backend RPC.
 
+Windows release packaging supplies the public EV leaf certificate named by `DSH_DESKTOP_WINDOWS_CER_FILE` to the configured SafeNet-compatible SignTool through `/f` and identifies its matching private key through the required `DSH_DESKTOP_WINDOWS_KEY_CONTAINER`. The certificate file remains outside source control, and the private key remains on the USB token. The electron-builder hook passes each artifact to the CRLF `windows-sign.cmd`, whose single SignTool invocation uses the SafeNet `/kc "[{{PIN}}]=container"` value and CSP, a SHA-256 file digest, and a DigiCert SHA-256 RFC 3161 timestamp. The hook never substitutes another SignTool and never retries a failed request. Package orchestration withholds every `DSH_DESKTOP_WINDOWS_*` field from build and seed-preparation children and passes only the certificate path, SignTool path, key container, and PIN into electron-builder. The signer supplies only validated signing fields in an otherwise scrubbed CMD environment; the CMD disables delayed expansion, clears those fields before SignTool starts, and preserves the PIN only in the required SignTool command line. Every surfaced diagnostic replaces the PIN, and only the dedicated build account and administrators may inspect the runner. The signer signs electron-builder's temporary NSIS bootstrap before enterprise Code Integrity evaluates that executable and clears a generated executable's certificate-table entry only when it points beyond the file before applying the final signature. Packaging fails before producing unsigned artifacts when the SignTool, certificate, container, PIN, token, or signature is unavailable. The custom protocol serves the installed frontend distribution plus client files named by the active module graph and rejects traversal or access outside those roots. The plugin installer API is available only to the Electron-owned management GUI and is absent from the browser application and backend RPC.
+
 Packaged applications ignore development resource and project environment overrides. Only an unpackaged Electron process can replace the Node.js binary, pnpm entry, seed, or active project.
 
 The bundled upstream Node.js and pnpm are expected to add about 35–50 MB compressed and 120–165 MB installed before the seed store subset. Architecture-specific builds must report actual component-level size deltas.
@@ -103,7 +105,7 @@ The bundled upstream Node.js and pnpm are expected to add about 35–50 MB compr
 | Shell | `apps/desktop` owns Electron windows, restricted preloads, the custom protocol, child lifecycle, project transactions, the plugin GUI, update coordination, and electron-builder configuration. |
 | Installed runtime | `@deepseek-ai/dsh/desktop-host` boots the portless desktop composition from the active project and streams API and asset responses over validated framed byte pipes. |
 | Package state | The release seed and every later mutation run through bundled Node.js and pnpm with desktop-owned store, config, cache, state, and home paths; core packages resolve from release tarballs while plugins resolve from the fixed npm registry. |
-| Qualification | macOS packaging requires the configured company identity and notary credentials, verifies every native seed object after final archive extraction, verifies the completed application signature, and requires notarization plus Gatekeeper acceptance for both the application and DMG. Windows signing, update hosting, previous-version installed-artifact tests, and platform GUI recordings remain release-environment gates. |
+| Qualification | macOS packaging requires the configured company identity and notary credentials, verifies every native seed object after final archive extraction, verifies the completed application signature, and requires notarization plus Gatekeeper acceptance for both the application and DMG. Windows packaging requires the configured public certificate, SafeNet private-key container, Token Password, and SignTool, and verifies every produced signature. Update hosting, previous-version installed-artifact tests, and platform GUI recordings remain release-environment gates. |
 
 `dev:desktop` builds the current workspace, projects the built CLI package and its dependency links into a disposable project, uses an isolated Harness home, opens the Main, Renderer, and Host debuggers, and starts unpackaged Electron without preparing release resources. Package mutation is disabled in this mode because its linked dependency graph is not a pnpm-installed desktop project. Fixed macOS arm64, macOS x64, and Windows x64 package commands pass one target through runtime preparation, seed installation, and electron-builder; each also has an unpacked-directory variant for release-path verification before installer generation.
 
@@ -123,10 +125,14 @@ The bundled upstream Node.js and pnpm are expected to add about 35–50 MB compr
 
 **Remove non-target Mach-O files from registry packages.** Architecture pruning saves a small amount of seed space, but packages can deliberately ship several architecture variants and callers can observe their installed file set. Signing every shipped Mach-O object satisfies notarization without inventing a Desktop-specific package layout.
 
+**Export the Windows EV private key in a PFX file.** The externally supplied public leaf certificate lets SignTool construct the signature while `/csp` and `/kc` locate the hardware key. The EV private key remains non-exportable on the token.
+
+**Commit a credential-bearing signing script or persist the Token Password.** A credential-bearing CMD file, `.env`, or Windows user or system environment variable leaves the Token Password recoverable at rest. The checked-in CMD contains only environment-variable references, and the packaging step accepts the password as an ephemeral runner secret.
+
 ## Consequences
 
 - A clean offline machine with no system Node.js or pnpm installs the seed into `.dsh/profiles/desktop` and starts a working dsh session.
-- The signed application inventories a fixed small set of seed store shards instead of every pnpm cache file; every Mach-O object inside the macOS shards has the release Developer ID, secure timestamp, and hardened runtime, while the installed private store retains the ordinary pnpm layout.
+- The signed application inventories a fixed small set of seed store shards instead of every pnpm cache file; every Mach-O object inside the macOS shards has the release Developer ID, secure timestamp, and hardened runtime, every Windows artifact has the configured hardware-backed EV signature, and the installed private store retains the ordinary pnpm layout.
 - `.dsh/profiles/desktop/node_modules` contains and resolves the desktop dsh package and every GUI-installed desktop plugin.
 - Every desktop pnpm operation uses the bundled executable and `.dsh/desktop/pnpm/store`; none reads user `PATH`, config, store, or profile `node_modules`.
 - The Electron-only GUI installs, removes, and updates ordinary npm plugin packages without exposing raw pnpm arguments.
@@ -138,6 +144,7 @@ The bundled upstream Node.js and pnpm are expected to add about 35–50 MB compr
 - Shared `.dsh` data rejects incompatible readers before migration or mutation.
 - No loopback listener is opened, and the sandboxed renderer cannot access arbitrary filesystem or Electron APIs.
 - Workspace development runs current built code without downloading release resources, while unpacked-package verification retains the production installation path.
+- Windows release packaging requires the validated SignTool, EV token, matching public leaf certificate, Token Password, and explicit key container; it never falls back to an unsigned artifact or an exportable key file.
 - Signed installed artifacts update successfully from the previous supported release on each release-blocking platform.
 
 ## Review decisions

+ 11 - 4
.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md

@@ -68,9 +68,9 @@ Electron 拥有保留 profile `.dsh/profiles/desktop`。其中精确的 `@deepse
 
 安装器绝不原地修改活跃 profile。它把 profile 元数据复制到事务暂存目录,使用内置 pnpm 应用精确依赖变更,执行完整健康检查,停止后端,把活跃 profile 移到 `rollback/profile`,把暂存 profile 移到 `.dsh/profiles/desktop`,然后重启。`pending.json` 记录文件系统移动,使启动过程可以完成或反转中断的替换。
 
-打包种子是离线安装包,而不是可执行 dsh 目录。它包含发布身份、初始桌面项目 manifest、以 dsh 为根的第一方包闭包描述文件及不可变 tarball、lockfile、完整性清单和所需 store 子集。发布构建要求 Electron 包与根 dsh 包使用相同版本,从正式源码构建生成最终 npm tarball,选择可达的 dsh 与 vendored 包以及 Landlock 入口,并验证 dsh tarball 中的 `lib/desktop-host.js` 入口与 `config/desktop.cordis.patch.yml` overlay。该 overlay 是唯一为了 Desktop 而发布的 CLI 配置文件;示例配置仍留在 tarball 之外。这些 tarball 保持为由各包 `files` manifest 决定内容的正式 `pnpm pack` 结果;Desktop 不删除已发布的声明文件,也不建立第二套包内容策略。manifest 把每个选中的包列为本地直接依赖,关闭对等依赖自动安装,workspace 文件再把每个选中的第一方包 override 到对应本地 tarball。构建会拒绝任何通过 registry 版本解析这些包名的 lockfile。内置 pnpm 从 npm 拉取外部生产依赖,执行离线安装,检查两个 Desktop Host 文件,并在最终准备 store 前删除 `node_modules`。在复制 package set 前与离线安装后都要求两个文件,可防止 Host 入口本身能够加载、却无法组合所需 overlay 的发布进入应用签名阶段。
+打包种子是离线安装包,而不是可执行 dsh 目录。它包含发布身份、初始桌面项目 manifest、以 dsh 为根的第一方包闭包描述文件及不可变 tarball、lockfile、完整性清单和所需 store 子集。发布构建要求 Electron 包与根 dsh 包使用相同版本,从正式源码构建生成最终 npm tarball,选择可达的 dsh 与 vendored 包以及 Landlock 入口,并验证 dsh tarball 中的 `lib/desktop-host.js` 入口与 `config/desktop.cordis.patch.yml` overlay。该 overlay 是唯一为了 Desktop 而发布的 CLI 配置文件;示例配置仍留在 tarball 之外。这些 tarball 保持为由各包 `files` manifest 决定内容的正式 `pnpm pack` 结果;Desktop 不删除已发布的声明文件,也不建立第二套包内容策略。manifest 把每个选中的包列为本地直接依赖,关闭对等依赖自动安装,workspace 文件再把每个选中的第一方包 override 到对应本地 tarball。构建会拒绝任何通过 registry 版本解析这些包名的 lockfile。内置 pnpm 关闭全局 virtual store,在禁用生命周期脚本的情况下从 npm 物化外部生产依赖,删除 `node_modules` 以及所有临时 pnpm cache、config 和 state 目录,然后只使用最终 store 执行一次干净的离线安装,并检查两个 Desktop Host 文件。生成清单前会删除第二次生成的 `node_modules` 和临时 pnpm 项目注册。在复制 package set 前与离线安装后都要求两个文件,可防止 Host 入口本身能够加载、却无法组合所需 overlay 的发布进入应用签名阶段。
 
-种子根据规范化 store 路径,把 pnpm 内容放入 16 个确定性的未压缩 tar 分片。Apple 公证会检查这些归档内的 Mach-O 代码,因此 macOS seed 在分片前会用发布 Developer ID、安全时间戳与 hardened runtime 签署每个内容寻址 Mach-O 对象。签名会改变字节:准备过程把每个对象写到新的 SHA-512 路径,并以事务方式重写 pnpm MessagePack SQLite 索引内全部基础文件和 side-effects 文件引用。第二次离线安装证明 pnpm 可以解析重写后的 store;准备过程随后完成分片、解包最终归档并再次验证签名。包路径和非原生字节保持不变;种子保留包内附带的架构变体,因为删除文件会创建 Desktop 专属的包文件集。种子完整性覆盖分片 manifest 和解包前的每个归档。启动时验证归档路径、条目类型、唯一性和数量,把所有分片解包到唯一且由 Desktop 拥有的 staging 目录,然后才把完整结果合并进 `.dsh/desktop/pnpm/store`。中断的合并可能留下有效的不可变缓存内容,但 profile 安装与激活仍必须通过 pnpm 完整性与完整健康检查。
+种子根据规范化 store 路径,把 pnpm 内容放入 16 个确定性的未压缩 tar 分片。Apple 公证会检查这些归档内的 Mach-O 代码,因此 macOS seed 在分片前会用发布 Developer ID、安全时间戳与 hardened runtime 签署每个内容寻址 Mach-O 对象。签名会改变字节:准备过程把每个对象写到新的 SHA-512 路径,并以事务方式重写 pnpm MessagePack SQLite 索引内全部基础文件和 side-effects 文件引用。第二次离线安装证明 pnpm 可以解析重写后的 store;准备过程随后完成分片、解包最终归档并再次验证签名。包路径和非原生字节保持不变;种子保留包内附带的架构变体,因为删除文件会创建 Desktop 专属的包文件集。种子完整性覆盖分片 manifest 和解包前的每个归档。启动时验证归档路径、条目类型、唯一性和数量,把所有分片解包到唯一且由 Desktop 拥有的 staging 目录,替换匹配的不可变 store 文件,并以事务方式把各 pnpm store 版本的 SQLite `package_index` 合并进 `.dsh/desktop/pnpm/store`。Seed 记录替换匹配的键,为 Desktop 插件下载的记录继续保留。中断的文件合并可能留下有效的不可变缓存内容,但每次 SQLite 合并都是原子的,profile 安装与激活仍必须通过 pnpm 完整性与完整健康检查。
 
 启动过程先要求安装包内的发布身份等于 Electron 应用版本,再在启动后端前比较 `.dsh/profiles/desktop/desktop-release.json`、已安装 dsh 包与该发布版本。它在 staging 中通过 `pnpm install --offline --frozen-lockfile --trust-lockfile` 安装新的种子 manifest 与 lockfile。Electron 替换后,启动过程再通过一次离线 pnpm add,从桌面端现有 store 与元数据缓存恢复活跃 profile 记录的每个插件 bundle 精确版本。完整依赖图必须通过同一套健康检查才能激活。
 
@@ -92,6 +92,8 @@ generic 更新服务必须一起发布元数据、安装包和 blockmap。NSIS 
 
 Electron 产物必须签名;macOS 产物必须公证。发布自动化必须通过明确的环境变量提供应用 ID、macOS Developer ID 限定名、预期 Team ID 与一套完整的 notarytool 凭据。配置加载会拒绝缺失或格式错误的标识符和不完整的公证凭据,macOS 打包还会强制签名,避免证书发现过程静默选择其他已安装身份或生成未签名发布。Seed 准备会验证每个内嵌 Mach-O 文件的精确 Authority 与 Team ID,以及时间戳和 hardened-runtime 标记。签名后钩子会执行 Apple 的深度严格应用验证,并要求同一叶证书 Authority 与 Team ID 完全匹配,验证通过后才继续生成产物。Electron-builder 随后公证应用并钉票、签署 DMG。DMG 的 artifact-completion hook 会单独公证每个 DMG 并钉票,再要求其使用配置的身份、具备有效票据并通过 Gatekeeper;只有该 hook 成功,上传事件才会执行。macOS 更新使用签名 ZIP,因此 DMG 不生成 blockmap;否则钉票会让已经生成的 DMG blockmap 失效。自定义协议提供已安装的前端分发目录和活跃模块图点名的客户端文件,并拒绝路径穿越或访问这些根目录之外的内容。插件安装器 API 只对 Electron 拥有的管理 GUI 可用,不存在于浏览器应用或后端 RPC 中。
 
+Windows 发布打包通过 `/f` 向已配置且与 SafeNet 兼容的 SignTool 提供 `DSH_DESKTOP_WINDOWS_CER_FILE` 指定的公开 EV 叶证书,并通过必需的 `DSH_DESKTOP_WINDOWS_KEY_CONTAINER` 标识匹配的私钥。证书文件保留在源码仓库之外,私钥仍留在 USB Token 上。electron-builder hook 把每个产物交给采用 CRLF 的 `windows-sign.cmd`;该 CMD 只调用一次 SignTool,并指定 SafeNet `/kc "[{{PIN}}]=容器"` 值与 CSP、SHA-256 文件摘要和 DigiCert SHA-256 RFC 3161 时间戳。hook 不会改用其他 SignTool,也不会重试失败的请求。打包编排不会把任何 `DSH_DESKTOP_WINDOWS_*` 字段传给构建与 seed 准备子进程,只会把证书路径、SignTool 路径、密钥容器和 PIN 传入 electron-builder。签名器在已清理的 CMD 环境中只提供经过校验的签名字段;CMD 会禁用延迟展开,在 SignTool 启动前清除这些字段,并仅在 SignTool 必需的命令行中保留 PIN。所有对外诊断都会替换 PIN,而且只能允许专用构建账号和管理员检查该 runner。签名器会在企业 Code Integrity 检查 electron-builder 的临时 NSIS bootstrap 前先为该可执行文件签名;对于生成的可执行文件,只有证书表条目指向文件末尾之外时,才会在最终签名前清除该条目。SignTool、证书、容器、PIN、Token 或签名不可用时,打包会在产生未签名产物前失败。自定义协议提供已安装的前端分发目录和活跃模块图点名的客户端文件,并拒绝路径穿越或访问这些根目录之外的内容。插件安装器 API 只对 Electron 持有的管理 GUI 可用,不存在于浏览器应用或后端 RPC 中。
+
 打包应用会忽略开发资源和项目环境变量覆盖。只有未打包的 Electron 进程可以替换 Node.js 可执行文件、pnpm 入口、seed 或活跃项目。
 
 在种子 store 子集之外,内置上游 Node.js 与 pnpm 预计增加约 35–50 MB 压缩体积和 120–165 MB 安装体积。分架构构建必须报告实际组件级体积增量。
@@ -103,7 +105,7 @@ Electron 产物必须签名;macOS 产物必须公证。发布自动化必须
 | 壳 | `apps/desktop` 负责 Electron 窗口、受限 preload、自定义协议、子进程生命周期、项目事务、插件 GUI、更新协调和 electron-builder 配置。 |
 | 已安装运行时 | `@deepseek-ai/dsh/desktop-host` 从活跃项目启动无端口桌面组合,并通过经过验证的分帧字节管道流式传输 API 与资源响应。 |
 | 包状态 | 发布种子和后续每次修改都通过内置 Node.js 与 pnpm 执行,并使用桌面端拥有的 store、config、cache、state 和 home 路径;核心包从发布 tarball 解析,插件从固定 npm registry 解析。 |
-| 资格验证 | macOS 打包要求已配置的公司身份与公证凭据可用,在解包最终归档后验证每个原生 seed 对象,验证完整应用签名,并要求应用和 DMG 都完成公证且通过 Gatekeeper。Windows 签名、更新托管、跨上一版本的已安装产物测试和各平台 GUI 录制仍是发布环境门槛。 |
+| 资格验证 | macOS 打包要求已配置的公司身份与公证凭据可用,在解包最终归档后验证每个原生 seed 对象,验证完整应用签名,并要求应用和 DMG 都完成公证且通过 Gatekeeper。Windows 打包要求已配置的公开证书、SafeNet 私钥容器、Token Password 与 SignTool,并验证生成的每个签名。更新托管、跨上一版本的已安装产物测试和各平台 GUI 录制仍是发布环境门槛。 |
 
 `dev:desktop` 会构建当前 workspace,把已构建 CLI 包及其依赖链接投影为一次性项目,使用隔离的 Harness home,打开 Main、Renderer 和 Host 调试器,并在不准备发布资源的情况下启动未打包 Electron。该模式的链接依赖图不是由 pnpm 安装的桌面项目,因此会禁用包修改。固定的 macOS arm64、macOS x64 与 Windows x64 打包命令会把同一目标传给运行时准备、seed 安装和 electron-builder;每条命令还提供未封装安装器的变体,用于在生成安装器前验证发布路径。
 
@@ -123,10 +125,14 @@ Electron 产物必须签名;macOS 产物必须公证。发布自动化必须
 
 **从 registry 包删除非目标 Mach-O 文件。** 架构裁剪可以节省少量 seed 空间,但包可能有意附带多个架构变体,调用方也可以观察安装后的文件集。签署每个实际携带的 Mach-O 对象,无需发明 Desktop 专属包布局就能满足公证要求。
 
+**把 Windows EV 私钥导出到 PFX 文件。** 外部提供的公开叶证书让 SignTool 构造签名,`/csp` 与 `/kc` 则定位硬件密钥。EV 私钥保持不可导出,并留在 Token 上。
+
+**提交包含凭据的签名脚本或持久保存 Token Password。** 包含凭据的 CMD 文件、`.env` 或 Windows 用户/系统环境变量都会让 Token Password 以静态形式被读取。已提交的 CMD 只包含环境变量引用,打包步骤则把密码作为 runner 临时 secret 接收。
+
 ## 结果
 
 - 没有系统 Node.js 或 pnpm 的干净离线机器把种子安装进 `.dsh/profiles/desktop`,并启动可工作的 dsh 会话。
-- 已签名应用记录固定少量的 seed store 分片,而不是记录每个 pnpm 缓存文件;macOS 分片内每个 Mach-O 对象都带有发布 Developer ID、安全时间戳与 hardened runtime,安装后的私有 store 仍保持普通 pnpm 布局。
+- 已签名应用记录固定少量的 seed store 分片,而不是记录每个 pnpm 缓存文件;macOS 分片内每个 Mach-O 对象都带有发布 Developer ID、安全时间戳与 hardened runtime,每个 Windows 产物都带有配置的硬件支持 EV 签名,安装后的私有 store 仍保持普通 pnpm 布局。
 - `.dsh/profiles/desktop/node_modules` 包含并解析桌面 dsh 包和每个 GUI 安装的桌面插件。
 - 每个桌面 pnpm 操作都使用内置可执行文件和 `.dsh/desktop/pnpm/store`;不读取用户 `PATH`、配置、store 或 profile `node_modules`。
 - Electron-only GUI 安装、删除和更新普通 npm 插件包,而不暴露原始 pnpm 参数。
@@ -138,6 +144,7 @@ Electron 产物必须签名;macOS 产物必须公证。发布自动化必须
 - 共享 `.dsh` 数据在迁移或修改前拒绝不兼容的读取方。
 - 不打开回环监听端口,沙箱渲染进程不能访问任意文件系统或 Electron API。
 - Workspace 开发无需下载发布资源即可运行当前已构建代码,未封装安装器的应用验证仍保留生产安装路径。
+- Windows 发布打包要求已验证的 SignTool、EV Token、匹配的公开叶证书、Token Password 和明确的密钥容器,绝不会回退到未签名产物或可导出的密钥文件。
 - 每个发布阻断平台上的签名已安装产物均能从上一个受支持版本成功更新。
 
 ## 评审决策

+ 3 - 3
.gitattributes

@@ -1,10 +1,10 @@
 # The repo's canonical text form is LF, enforced at checkout too: no smudge
 # boundary between working tree and repo, so byte-level gates (verify-*
 # comparisons, blob hashing, coverage offsets) see one form on every host.
-# If a file class ever genuinely needs CRLF in the working tree (.bat/.cmd
-# for cmd.exe), add a `*.bat text eol=crlf` override AFTER this line — the
-# in-repo form stays LF; CRLF becomes checkout-time presentation only.
+# Windows command scripts require CRLF in the working tree for cmd.exe; the
+# override after the default keeps the in-repo form normalized as text.
 * text=auto eol=lf
+*.cmd text eol=crlf
 
 *.pdf binary
 

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: d3c796a2b23caa789c3e511a8d8af046ca8b784a
-README.zh.md: 130173b8656225141e40463c3de958b031d7283b
+README.md: aa35c3fca95f3dd9d9409ca8b2446dbcdc58eab2
+README.zh.md: 27848b17fe1f8aa4b0afbec106a323eb354eb4d5

Dosya farkı çok büyük olduğundan ihmal edildi
+ 19 - 3
apps/desktop/README.md


Dosya farkı çok büyük olduğundan ihmal edildi
+ 19 - 3
apps/desktop/README.zh.md


+ 21 - 0
apps/desktop/electron-builder.config.mjs

@@ -5,6 +5,10 @@ import {
 } from './scripts/desktop-release-environment.mjs'
 import { notarizeMacOSDiskImageArtifact } from './scripts/notarize-macos-disk-images.mjs'
 import { verifyMacOSSignatureAfterSign } from './scripts/verify-macos-signature.mjs'
+import {
+  createWindowsTokenSigner,
+  installWindowsNsisBootstrapSigner,
+} from './scripts/windows-sign.mjs'
 
 /**
  * Create electron-builder configuration from one release environment.
@@ -16,8 +20,20 @@ export function createElectronBuilderConfig(env = process.env, hostPlatform = pr
   const appId = resolveDesktopAppId(env)
   const targetPlatform = env.DSH_DESKTOP_TARGET_PLATFORM
   const packagesMacOS = targetPlatform === 'darwin' || (targetPlatform === undefined && hostPlatform === 'darwin')
+  const packagesWindows = targetPlatform === 'win32'
   const macOSSigning = packagesMacOS ? resolveMacOSSigningEnvironment(env) : undefined
   if (packagesMacOS) resolveMacOSNotarizationEnvironment(env)
+  const windowsSigner = packagesWindows
+    ? createWindowsTokenSigner({
+        certificateFile: env.DSH_DESKTOP_WINDOWS_CER_FILE,
+        signTool: env.DSH_DESKTOP_WINDOWS_SIGNTOOL,
+        tokenPin: env.DSH_DESKTOP_WINDOWS_TOKEN_PIN,
+        keyContainer: env.DSH_DESKTOP_WINDOWS_KEY_CONTAINER,
+      })
+    : undefined
+  if (windowsSigner !== undefined) {
+    installWindowsNsisBootstrapSigner({ sign: windowsSigner })
+  }
   const publishUrl = env.DSH_DESKTOP_SHELL_UPDATE_URL
   return {
     appId,
@@ -60,6 +76,11 @@ export function createElectronBuilderConfig(env = process.env, hostPlatform = pr
       )
     },
     win: {
+      forceCodeSigning: true,
+      signtoolOptions: {
+        sign: windowsSigner,
+        signingHashAlgorithms: ['sha256'],
+      },
       target: ['nsis'],
     },
     linux: {

+ 1 - 0
apps/desktop/package.json

@@ -33,6 +33,7 @@
     "@electron/notarize": "2.5.0",
     "@types/node": "^22.20.0",
     "@types/semver": "^7.8.0",
+    "app-builder-lib": "26.15.3",
     "electron": "^44.0.0",
     "electron-builder": "^26.15.3",
     "extract-zip": "^2.0.1",

+ 29 - 7
apps/desktop/scripts/package-target.ts

@@ -10,6 +10,13 @@ const REPOSITORY_ROOT = resolve(APP_ROOT, '..', '..')
 const DSH_PACK_ROOT = join(REPOSITORY_ROOT, 'dist', 'npm')
 const VENDOR_PACK_ROOT = join(REPOSITORY_ROOT, 'dist', 'npm-vendor')
 const LANDLOCK_PACK_ROOT = join(REPOSITORY_ROOT, 'dist', 'npm-landlock')
+const WINDOWS_SIGNING_ENV_PREFIX = 'DSH_DESKTOP_WINDOWS_'
+const WINDOWS_SIGNING_ENV_NAMES = [
+  'DSH_DESKTOP_WINDOWS_CER_FILE',
+  'DSH_DESKTOP_WINDOWS_KEY_CONTAINER',
+  'DSH_DESKTOP_WINDOWS_SIGNTOOL',
+  'DSH_DESKTOP_WINDOWS_TOKEN_PIN',
+] as const
 
 /** Fixed platform and architecture identifiers exposed by package scripts. */
 export type DesktopPackageTargetName = 'mac-arm64' | 'mac-x64' | 'win-x64'
@@ -47,6 +54,16 @@ const TARGETS: Record<DesktopPackageTargetName, DesktopPackageTarget> = {
   },
 }
 
+/**
+ * Remove Windows signing configuration from package preparation subprocesses.
+ * @param environment - Packaging command environment.
+ * @returns A copy without Windows signing fields.
+ */
+export function withoutWindowsSigningEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
+  return Object.fromEntries(Object.entries(environment)
+    .filter(([name]) => !name.startsWith(WINDOWS_SIGNING_ENV_PREFIX)))
+}
+
 function isTargetName(value: string): value is DesktopPackageTargetName {
   return Object.hasOwn(TARGETS, value)
 }
@@ -149,24 +166,29 @@ function runPnpm(
 async function main(): Promise<void> {
   const invocation = parseDesktopPackageInvocation(process.argv.slice(2))
   const { target } = invocation
+  const buildEnv = withoutWindowsSigningEnvironment(process.env)
   const targetEnv: NodeJS.ProcessEnv = {
-    ...process.env,
+    ...buildEnv,
     DSH_DESKTOP_TARGET_PLATFORM: target.platform,
     DSH_DESKTOP_TARGET_ARCH: target.arch,
   }
-  await runPnpm(['run', 'build:official'], process.env, REPOSITORY_ROOT)
-  await runPnpm(['run', 'release:pack', '--family', 'dsh', '--out', DSH_PACK_ROOT], process.env, REPOSITORY_ROOT)
-  await runPnpm(['run', 'release:pack', '--family', 'vendor', '--out', VENDOR_PACK_ROOT], process.env, REPOSITORY_ROOT)
+  const electronBuilderEnv = { ...targetEnv }
+  for (const name of WINDOWS_SIGNING_ENV_NAMES) {
+    if (process.env[name] !== undefined) electronBuilderEnv[name] = process.env[name]
+  }
+  await runPnpm(['run', 'build:official'], buildEnv, REPOSITORY_ROOT)
+  await runPnpm(['run', 'release:pack', '--family', 'dsh', '--out', DSH_PACK_ROOT], buildEnv, REPOSITORY_ROOT)
+  await runPnpm(['run', 'release:pack', '--family', 'vendor', '--out', VENDOR_PACK_ROOT], buildEnv, REPOSITORY_ROOT)
   rmSync(LANDLOCK_PACK_ROOT, { recursive: true, force: true })
   mkdirSync(LANDLOCK_PACK_ROOT, { recursive: true })
-  await runPnpm(['--dir', 'native/landlock-run', 'run', 'build:ts'], process.env, REPOSITORY_ROOT)
+  await runPnpm(['--dir', 'native/landlock-run', 'run', 'build:ts'], buildEnv, REPOSITORY_ROOT)
   await runPnpm([
     '--dir',
     'native/landlock-run/packages/entry',
     'pack',
     '--pack-destination',
     LANDLOCK_PACK_ROOT,
-  ], process.env, REPOSITORY_ROOT)
+  ], buildEnv, REPOSITORY_ROOT)
   await runPnpm(['run', 'prepare:runtime'], targetEnv)
   await runPnpm(['run', 'prepare:packages'], targetEnv)
   await runPnpm(['run', 'prepare:seed'], targetEnv)
@@ -179,7 +201,7 @@ async function main(): Promise<void> {
     target.builderPlatform,
     target.builderArch,
     ...(invocation.directory ? ['--dir'] : []),
-  ], targetEnv)
+  ], electronBuilderEnv)
 }
 
 if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) await main()

+ 5 - 1
apps/desktop/scripts/prepare-seed.ts

@@ -74,6 +74,7 @@ function runPnpm(args: readonly string[]): Promise<void> {
       PNPM,
       '--config.registry=https://registry.npmjs.org/',
       `--config.store-dir=${STORE_ROOT}`,
+      '--config.enable-global-virtual-store=false',
       `--config.userconfig=${userConfig}`,
       command,
       ...commandArgs,
@@ -151,7 +152,10 @@ async function main(): Promise<void> {
       readFileSync(join(SEED_ROOT, 'pnpm-lock.yaml'), 'utf8'),
       readDesktopCorePackageSet(SEED_ROOT, release.version),
     )
-    await runPnpm(['fetch', '--prod', '--frozen-lockfile'])
+    const installedModules = join(SEED_ROOT, 'node_modules')
+    await runPnpm(['install', '--prod', '--frozen-lockfile', '--trust-lockfile', '--ignore-scripts'])
+    rmSync(installedModules, { recursive: true, force: true })
+    rmSync(PNPM_BUILD_STATE, { recursive: true, force: true })
     await verifyOfflineInstallation(release)
     const targetPlatform = process.env.DSH_DESKTOP_TARGET_PLATFORM ?? process.platform
     let signedMachOFiles: number | undefined

+ 17 - 0
apps/desktop/scripts/windows-sign.cmd

@@ -0,0 +1,17 @@
+@echo off
+setlocal DisableDelayedExpansion
+set "signTool=%DSH_DESKTOP_WINDOWS_SIGNTOOL%"
+set "certificateFile=%DSH_DESKTOP_WINDOWS_CER_FILE%"
+set "tokenPin=%DSH_DESKTOP_WINDOWS_TOKEN_PIN%"
+set "keyContainer=%DSH_DESKTOP_WINDOWS_KEY_CONTAINER%"
+set "targetFile=%DSH_DESKTOP_WINDOWS_SIGN_TARGET%"
+set "appendSignature="
+if "%DSH_DESKTOP_WINDOWS_SIGN_APPEND%"=="1" set "appendSignature=/as"
+set "DSH_DESKTOP_WINDOWS_SIGNTOOL="
+set "DSH_DESKTOP_WINDOWS_CER_FILE="
+set "DSH_DESKTOP_WINDOWS_TOKEN_PIN="
+set "DSH_DESKTOP_WINDOWS_KEY_CONTAINER="
+set "DSH_DESKTOP_WINDOWS_SIGN_TARGET="
+set "DSH_DESKTOP_WINDOWS_SIGN_APPEND="
+set "signTool=" & set "certificateFile=" & set "tokenPin=" & set "keyContainer=" & set "targetFile=" & set "appendSignature=" & "%signTool%" sign /v /fd sha256 /f "%certificateFile%" /kc "[{{%tokenPin%}}]=%keyContainer%" /csp "eToken Base Cryptographic Provider" %appendSignature% /tr http://timestamp.digicert.com /td sha256 "%targetFile%"
+exit /b %errorlevel%

+ 91 - 0
apps/desktop/scripts/windows-sign.d.mts

@@ -0,0 +1,91 @@
+/**
+ * Build the minimal CMD environment for one Electron artifact.
+ *
+ * @param environment Parent environment.
+ * @param input Validated signing identity and task.
+ * @returns Scrubbed environment plus the fields consumed and cleared by the signing CMD.
+ */
+export function buildWindowsSigningEnvironment(environment: NodeJS.ProcessEnv, input: {
+  certificateFile: string
+  signTool: string
+  path: string
+  isNest: boolean
+  tokenPin: string
+  keyContainer: string
+}): NodeJS.ProcessEnv
+
+/**
+ * Create the electron-builder hook for a hardware-backed Windows code-signing certificate.
+ *
+ * @param options Release signing configuration.
+ * @returns The signing hook.
+ */
+export function createWindowsTokenSigner(options: {
+  certificateFile?: string | undefined
+  signTool?: string | undefined
+  tokenPin?: string | undefined
+  keyContainer?: string | undefined
+  commandInterpreter?: string | undefined
+}): (
+  configuration: {
+    path: string
+    hash: string
+    isNest: boolean
+  },
+) => Promise<void>
+
+/**
+ * Remove inherited credentials before starting a signing-related subprocess.
+ *
+ * @param environment Parent environment.
+ * @returns Environment without credential-shaped names.
+ */
+export function scrubWindowsSigningEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv
+
+/**
+ * Replace a SignTool failure with a diagnostic that cannot retain its command line.
+ *
+ * @param error SignTool process failure.
+ * @param path Artifact that failed signing.
+ * @param secrets Values that must not appear in the diagnostic.
+ * @returns Sanitized signing failure without the original error as its cause.
+ */
+export function createRedactedWindowsSigningError(
+  error: unknown,
+  path: string,
+  secrets: readonly string[],
+): Error
+
+/**
+ * Clear a certificate-table entry that points beyond the end of a generated executable.
+ *
+ * @param path Executable to inspect.
+ * @returns Whether an invalid certificate-table entry was cleared.
+ */
+export function repairDanglingAuthenticodeDirectory(path: string): Promise<boolean>
+
+/**
+ * Sign electron-builder's temporary NSIS executable before enterprise code integrity evaluates it.
+ *
+ * @param options Signing hook and injectable host values.
+ * @returns Nothing.
+ */
+export function installWindowsNsisBootstrapSigner(options: {
+  sign: (configuration: {
+    path: string
+    hash: string
+    isNest: boolean
+  }) => Promise<void>
+  wineVmManager?: {
+    prototype: {
+      exec: (
+        file: string,
+        args: string[],
+        options?: { env?: NodeJS.ProcessEnv },
+        isLogOutIfDebug?: boolean,
+      ) => unknown
+    }
+  }
+  platform?: NodeJS.Platform
+  environment?: NodeJS.ProcessEnv
+}): void

+ 266 - 0
apps/desktop/scripts/windows-sign.mjs

@@ -0,0 +1,266 @@
+import { execFile } from 'node:child_process'
+import { X509Certificate } from 'node:crypto'
+import { readFileSync, realpathSync, statSync } from 'node:fs'
+import { open } from 'node:fs/promises'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { promisify } from 'node:util'
+import wineVmModule from 'app-builder-lib/out/vm/WineVm.js'
+
+const execFileAsync = promisify(execFile)
+const { WineVmManager } = wineVmModule
+const CODE_SIGNING_EKU = '1.3.6.1.5.5.7.3.3'
+const NSIS_RUN_AS_INVOKER = 'RunAsInvoker'
+const NSIS_BOOTSTRAP_PATCH = Symbol.for('@deepseek-ai/dsh-desktop/nsis-bootstrap-signing')
+const WINDOWS_SIGN_SCRIPT = 'windows-sign.cmd'
+const WINDOWS_SIGN_SCRIPT_DIRECTORY = dirname(fileURLToPath(import.meta.url))
+const PE_HEADER_READ_SIZE = 4096
+const PE32_MAGIC = 0x10B
+const PE32_PLUS_MAGIC = 0x20B
+const SENSITIVE_ENVIRONMENT_NAME = /(?:KEY|SECRET|TOKEN|PASSWORD)/iu
+const WINDOWS_SIGNING_ENVIRONMENT_PREFIX = 'DSH_DESKTOP_WINDOWS_'
+
+/**
+ * Remove inherited credentials before starting a signing-related subprocess.
+ *
+ * @param {NodeJS.ProcessEnv} environment Parent environment.
+ * @returns {NodeJS.ProcessEnv} Environment without credential-shaped names.
+ */
+export function scrubWindowsSigningEnvironment(environment) {
+  return Object.fromEntries(Object.entries(environment)
+    .filter(([name]) => !SENSITIVE_ENVIRONMENT_NAME.test(name)
+      && !name.startsWith(WINDOWS_SIGNING_ENVIRONMENT_PREFIX)))
+}
+
+function resolveTokenIdentity(input) {
+  const keyContainer = input.keyContainer?.trim()
+  if (!keyContainer) {
+    throw new Error('DSH_DESKTOP_WINDOWS_KEY_CONTAINER must contain the SafeNet private-key container name')
+  }
+  if (/["\r\n]/u.test(keyContainer)) {
+    throw new Error('DSH_DESKTOP_WINDOWS_KEY_CONTAINER cannot contain quotes or line breaks')
+  }
+  const tokenPin = input.tokenPin
+  if (tokenPin === undefined || tokenPin.length === 0) {
+    throw new Error('DSH_DESKTOP_WINDOWS_TOKEN_PIN must contain the SafeNet Token Password')
+  }
+  if (/[\]"\r\n]/u.test(tokenPin)) {
+    throw new Error('DSH_DESKTOP_WINDOWS_TOKEN_PIN cannot contain "]", quotes, or line breaks because the SafeNet key-container syntax uses them as delimiters')
+  }
+  return { keyContainer, tokenPin }
+}
+
+function resolveCertificateFile(value) {
+  const candidate = value?.trim()
+  if (!candidate) {
+    throw new Error('DSH_DESKTOP_WINDOWS_CER_FILE must identify the public X.509 leaf certificate file')
+  }
+  let path
+  let certificate
+  try {
+    path = realpathSync(candidate)
+    certificate = new X509Certificate(readFileSync(path))
+  }
+  catch {
+    throw new Error(`Windows code-signing certificate file is missing or invalid: ${candidate}`)
+  }
+  if (certificate.ca || !certificate.keyUsage?.includes(CODE_SIGNING_EKU)) {
+    throw new Error(`Windows code-signing certificate file must contain a non-CA Code Signing certificate: ${path}`)
+  }
+  return path
+}
+
+function resolveSignTool(value) {
+  const candidate = value?.trim()
+  if (!candidate) {
+    throw new Error('DSH_DESKTOP_WINDOWS_SIGNTOOL must identify the SafeNet-compatible SignTool executable')
+  }
+  let path
+  try {
+    path = realpathSync(candidate)
+    if (!statSync(path).isFile() || !path.toLowerCase().endsWith('.exe')) throw new Error('not an executable file')
+  }
+  catch {
+    throw new Error(`DSH_DESKTOP_WINDOWS_SIGNTOOL is missing or is not an executable file: ${candidate}`)
+  }
+  return path
+}
+
+function redactedSigningOutput(value, secrets) {
+  let output = Buffer.isBuffer(value) ? value.toString('utf8') : typeof value === 'string' ? value : ''
+  for (const secret of secrets) {
+    if (secret !== '') output = output.replaceAll(secret, '<redacted>')
+  }
+  return output
+}
+
+/**
+ * Replace a SignTool failure with a diagnostic that cannot retain its command line.
+ *
+ * @param {unknown} error SignTool process failure.
+ * @param {string} path Artifact that failed signing.
+ * @param {readonly string[]} secrets Values that must not appear in the diagnostic.
+ * @returns {Error} Sanitized signing failure without the original error as its cause.
+ */
+export function createRedactedWindowsSigningError(error, path, secrets) {
+  const record = error !== null && typeof error === 'object' ? error : undefined
+  const code = record !== undefined && 'code' in record
+    && (typeof record.code === 'number' || typeof record.code === 'string')
+    ? ` (exit ${String(record.code)})`
+    : ''
+  const stderr = record !== undefined && 'stderr' in record
+    ? redactedSigningOutput(record.stderr, secrets).trim()
+    : ''
+  return new Error(`Windows release signing failed for ${path}${code}${stderr === '' ? '' : `: ${stderr}`}`)
+}
+
+/**
+ * Build the minimal CMD environment for one Electron artifact.
+ *
+ * @param {NodeJS.ProcessEnv} environment Parent environment.
+ * @param {{ certificateFile: string, signTool: string, path: string, isNest: boolean, tokenPin: string, keyContainer: string }} input Validated signing identity and task.
+ * @returns {NodeJS.ProcessEnv} Scrubbed environment plus fields consumed and cleared by the signing CMD.
+ */
+export function buildWindowsSigningEnvironment(environment, input) {
+  return {
+    ...scrubWindowsSigningEnvironment(environment),
+    DSH_DESKTOP_WINDOWS_SIGNTOOL: input.signTool,
+    DSH_DESKTOP_WINDOWS_CER_FILE: input.certificateFile,
+    DSH_DESKTOP_WINDOWS_TOKEN_PIN: input.tokenPin,
+    DSH_DESKTOP_WINDOWS_KEY_CONTAINER: input.keyContainer,
+    DSH_DESKTOP_WINDOWS_SIGN_TARGET: input.path,
+    DSH_DESKTOP_WINDOWS_SIGN_APPEND: input.isNest ? '1' : '',
+  }
+}
+
+/**
+ * Create the electron-builder hook for a SafeNet-backed Windows code-signing certificate.
+ *
+ * @param {{ certificateFile?: string, signTool?: string, tokenPin?: string, keyContainer?: string, commandInterpreter?: string }} options Release signing configuration.
+ * @returns {(configuration: { path: string, hash: string, isNest: boolean }) => Promise<void>} The signing hook.
+ */
+export function createWindowsTokenSigner(options) {
+  const certificateFile = resolveCertificateFile(options.certificateFile)
+  const signTool = resolveSignTool(options.signTool)
+  const { keyContainer, tokenPin } = resolveTokenIdentity(options)
+  const commandInterpreter = options.commandInterpreter
+    ?? process.env.ComSpec
+    ?? join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'cmd.exe')
+  return async (configuration) => {
+    if (configuration.hash !== 'sha256') {
+      throw new Error(`Windows release signing requires SHA-256, received ${configuration.hash}`)
+    }
+    await repairDanglingAuthenticodeDirectory(configuration.path)
+    const secrets = [tokenPin]
+    let result
+    try {
+      result = await execFileAsync(commandInterpreter, [
+        '/d',
+        '/v:off',
+        '/c',
+        WINDOWS_SIGN_SCRIPT,
+      ], {
+        cwd: WINDOWS_SIGN_SCRIPT_DIRECTORY,
+        env: buildWindowsSigningEnvironment(process.env, {
+          certificateFile,
+          signTool,
+          path: configuration.path,
+          isNest: configuration.isNest,
+          tokenPin,
+          keyContainer,
+        }),
+        windowsHide: false,
+      })
+    }
+    catch (error) {
+      throw createRedactedWindowsSigningError(error, configuration.path, secrets)
+    }
+    const stdout = redactedSigningOutput(result.stdout, secrets)
+    const stderr = redactedSigningOutput(result.stderr, secrets)
+    if (stdout !== '') process.stdout.write(stdout)
+    if (stderr !== '') process.stderr.write(stderr)
+  }
+}
+
+/**
+ * Clear a certificate-table entry that points beyond the end of a generated executable.
+ *
+ * @param {string} path Executable to inspect.
+ * @returns {Promise<boolean>} Whether an invalid certificate-table entry was cleared.
+ */
+export async function repairDanglingAuthenticodeDirectory(path) {
+  const file = await open(path, 'r+')
+  try {
+    const { size } = await file.stat()
+    const header = Buffer.alloc(Math.min(PE_HEADER_READ_SIZE, size))
+    await file.read(header, 0, header.length, 0)
+    const directoryOffset = findDanglingAuthenticodeDirectory(header, size)
+    if (directoryOffset === undefined) return false
+    await file.write(Buffer.alloc(8), 0, 8, directoryOffset)
+    return true
+  }
+  finally {
+    await file.close()
+  }
+}
+
+/**
+ * Locate an Authenticode certificate-table entry whose declared bytes are outside the file.
+ *
+ * @param {Buffer} header Initial executable bytes.
+ * @param {number} fileSize Complete file size.
+ * @returns {number | undefined} File offset of the invalid data-directory entry.
+ */
+function findDanglingAuthenticodeDirectory(header, fileSize) {
+  if (header.length < 64 || header.toString('ascii', 0, 2) !== 'MZ') return undefined
+  const peOffset = header.readUInt32LE(60)
+  const optionalHeaderOffset = peOffset + 24
+  if (optionalHeaderOffset + 2 > header.length
+    || header.toString('ascii', peOffset, peOffset + 4) !== 'PE\0\0') return undefined
+  const magic = header.readUInt16LE(optionalHeaderOffset)
+  const dataDirectoryOffset = magic === PE32_MAGIC
+    ? optionalHeaderOffset + 96
+    : magic === PE32_PLUS_MAGIC
+      ? optionalHeaderOffset + 112
+      : undefined
+  if (dataDirectoryOffset === undefined) return undefined
+  const certificateDirectoryOffset = dataDirectoryOffset + (4 * 8)
+  if (certificateDirectoryOffset + 8 > header.length) return undefined
+  const certificateOffset = header.readUInt32LE(certificateDirectoryOffset)
+  const certificateSize = header.readUInt32LE(certificateDirectoryOffset + 4)
+  if (certificateOffset === 0 && certificateSize === 0) return undefined
+  return certificateOffset > 0
+    && certificateSize > 0
+    && certificateOffset + certificateSize <= fileSize
+    ? undefined
+    : certificateDirectoryOffset
+}
+
+/**
+ * Sign electron-builder's temporary NSIS executable before enterprise code integrity evaluates it.
+ *
+ * @param {{ sign: (configuration: { path: string, hash: string, isNest: boolean }) => Promise<void>, wineVmManager?: typeof WineVmManager, platform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv }} options Signing hook and injectable host values.
+ * @returns {void}
+ */
+export function installWindowsNsisBootstrapSigner(options) {
+  if ((options.platform ?? process.platform) !== 'win32') return
+  const prototype = (options.wineVmManager ?? WineVmManager).prototype
+  if (prototype[NSIS_BOOTSTRAP_PATCH] === true) return
+  const originalExec = prototype.exec
+  prototype.exec = async function (file, args, execOptions, isLogOutIfDebug) {
+    const isNsisBootstrap = file.toLowerCase().endsWith('.exe')
+      && execOptions?.env?.__COMPAT_LAYER === NSIS_RUN_AS_INVOKER
+    if (!isNsisBootstrap) {
+      return originalExec.call(this, file, args, execOptions, isLogOutIfDebug)
+    }
+    await options.sign({ path: file, hash: 'sha256', isNest: false })
+    return originalExec.call(this, file, args, {
+      ...execOptions,
+      env: scrubWindowsSigningEnvironment({
+        ...(options.environment ?? process.env),
+        ...execOptions.env,
+      }),
+    }, isLogOutIfDebug)
+  }
+  Object.defineProperty(prototype, NSIS_BOOTSTRAP_PATCH, { value: true })
+}

+ 3 - 3
apps/desktop/src/project-manager.ts

@@ -29,7 +29,7 @@ import {
 } from './core-package-set.ts'
 import type { DesktopPaths } from './paths.ts'
 import { parseDesktopRelease, type DesktopRelease } from './release.ts'
-import { extractPnpmStoreArchives } from './seed-store.ts'
+import { extractPnpmStoreArchives, mergePnpmStore } from './seed-store.ts'
 
 /** Files the package transaction copies between active and staging projects. */
 const DESKTOP_PROJECT_FILES = [
@@ -510,8 +510,7 @@ export class DesktopProjectManager {
     const extractedStore = join(transactionRoot, 'store')
     try {
       extractPnpmStoreArchives(seedDir, extractedStore)
-      mkdirSync(this.paths.pnpm.store, { recursive: true, mode: 0o700 })
-      cpSync(extractedStore, this.paths.pnpm.store, { recursive: true, force: false })
+      mergePnpmStore(extractedStore, this.paths.pnpm.store)
     } finally {
       removeOwnedDirectory(transactionRoot)
     }
@@ -566,6 +565,7 @@ export class DesktopProjectManager {
         this.runtime.pnpm,
         `--config.registry=${DESKTOP_REGISTRY}`,
         `--config.store-dir=${this.paths.pnpm.store}`,
+        '--config.enable-global-virtual-store=false',
         `--config.userconfig=${npmrc}`,
         command,
         ...commandArgs,

+ 52 - 0
apps/desktop/src/seed-store.ts

@@ -3,6 +3,8 @@
 import { createHash } from 'node:crypto'
 import {
   chmodSync,
+  copyFileSync,
+  cpSync,
   existsSync,
   mkdirSync,
   readdirSync,
@@ -11,6 +13,7 @@ import {
   writeFileSync,
 } from 'node:fs'
 import { join, relative, sep } from 'node:path'
+import { DatabaseSync } from 'node:sqlite'
 import { create, extract, list } from 'tar'
 
 /** Directory containing the seed's uncompressed pnpm store archives. */
@@ -21,6 +24,7 @@ export const SEED_STORE_ARCHIVE_MANIFEST = 'store-archives.json'
 
 const DEFAULT_SHARD_COUNT = 16
 const ARCHIVE_NAME_PATTERN = /^store-[0-9a-f]{2}\.tar$/u
+const STORE_VERSION_PATTERN = /^v\d+$/u
 
 interface SeedStoreArchiveRecord {
   readonly file: string
@@ -113,6 +117,54 @@ export function removePnpmProjectRegistrations(storeRoot: string): void {
   }
 }
 
+function mergeStoreIndex(source: string, destination: string): void {
+  if (!existsSync(destination)) {
+    copyFileSync(source, destination)
+    return
+  }
+  const database = new DatabaseSync(destination)
+  let attached = false
+  try {
+    database.exec('PRAGMA busy_timeout=5000')
+    database.prepare('ATTACH DATABASE ? AS seed').run(source)
+    attached = true
+    database.exec('BEGIN IMMEDIATE')
+    let committed = false
+    try {
+      database.exec('INSERT OR REPLACE INTO package_index (key, data) SELECT key, data FROM seed.package_index')
+      database.exec('COMMIT')
+      committed = true
+    } finally {
+      if (!committed) database.exec('ROLLBACK')
+    }
+  } finally {
+    if (attached) database.exec('DETACH DATABASE seed')
+    database.close()
+  }
+}
+
+/**
+ * Merge a completely extracted seed store into Desktop's persistent pnpm store.
+ * @param source - Verified temporary store extraction.
+ * @param destination - Desktop-owned persistent pnpm store.
+ */
+export function mergePnpmStore(source: string, destination: string): void {
+  mkdirSync(destination, { recursive: true, mode: 0o700 })
+  const indexPaths = readdirSync(source, { withFileTypes: true })
+    .filter(entry => entry.isDirectory() && STORE_VERSION_PATTERN.test(entry.name)
+      && existsSync(join(source, entry.name, 'index.db')))
+    .map(entry => `${entry.name}/index.db`)
+  const indexes = new Set(indexPaths)
+  cpSync(source, destination, {
+    recursive: true,
+    force: true,
+    filter: path => !indexes.has(relative(source, path).split(sep).join('/')),
+  })
+  for (const path of indexPaths) {
+    mergeStoreIndex(join(source, ...path.split('/')), join(destination, ...path.split('/')))
+  }
+}
+
 /**
  * Replace a prepared loose pnpm store with deterministic uncompressed archive shards.
  * @param seedRoot - seed directory that owns the archive output.

+ 3 - 6
apps/desktop/tests/macos-signature.spec.ts

@@ -47,15 +47,12 @@ describe('desktop macOS release signature', () => {
     expect(typeof config.artifactBuildCompleted).toBe('function')
   })
 
-  it('does not require macOS identifiers for a Windows target', async () => {
+  it('validates Windows signing without requiring macOS identifiers for a Windows target', async () => {
     const { createElectronBuilderConfig } = await import('../electron-builder.config.mjs')
-    expect(createElectronBuilderConfig({
+    expect(() => createElectronBuilderConfig({
       DSH_DESKTOP_APP_ID: RELEASE_ENVIRONMENT.DSH_DESKTOP_APP_ID,
       DSH_DESKTOP_TARGET_PLATFORM: 'win32',
-    }, 'win32').mac).toMatchObject({
-      identity: undefined,
-      forceCodeSigning: true,
-    })
+    }, 'win32')).toThrow(/DSH_DESKTOP_WINDOWS_CER_FILE/u)
   })
 
   it('accepts the configured authority and team', () => {

+ 11 - 0
apps/desktop/tests/package-target.spec.ts

@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
 import {
   parseDesktopPackageInvocation,
   resolveDesktopPackageTarget,
+  withoutWindowsSigningEnvironment,
 } from '../scripts/package-target.ts'
 
 describe('desktop package target', () => {
@@ -37,4 +38,14 @@ describe('desktop package target', () => {
     expect(() => parseDesktopPackageInvocation(['mac-arm64', 'mac-x64'], 'darwin', 'arm64'))
       .toThrow(/at most one target/u)
   })
+
+  it('keeps Windows signing fields out of build and seed preparation subprocesses', () => {
+    expect(withoutWindowsSigningEnvironment({
+      DSH_DESKTOP_WINDOWS_CER_FILE: 'C:\\release\\server.cer',
+      DSH_DESKTOP_WINDOWS_TOKEN_PIN: 'token-secret',
+      DSH_DESKTOP_WINDOWS_KEY_CONTAINER: 'container',
+      DSH_DESKTOP_WINDOWS_SIGNTOOL: 'C:\\tools\\signtool.exe',
+      DSH_DESKTOP_SHELL_UPDATE_URL: 'https://updates.example.test',
+    })).toEqual({ DSH_DESKTOP_SHELL_UPDATE_URL: 'https://updates.example.test' })
+  })
 })

+ 1 - 0
apps/desktop/tests/project-manager.spec.ts

@@ -200,6 +200,7 @@ describe('desktop project transactions', () => {
     expect(invocation.args).toContain('--offline')
     expect(invocation.args).toContain('--trust-lockfile')
     expect(invocation.args).toContain(`--config.store-dir=${paths.pnpm.store}`)
+    expect(invocation.args).toContain('--config.enable-global-virtual-store=false')
     expect(invocation.args).toContain('--config.registry=https://registry.npmjs.org/')
     expect(invocation.env.NPM_CONFIG_REGISTRY).toBe('https://registry.npmjs.org/')
     expect(invocation.env.NPM_CONFIG_STORE_DIR).toBe(paths.pnpm.store)

+ 46 - 1
apps/desktop/tests/seed-store.spec.ts

@@ -12,10 +12,12 @@ import {
 } from 'node:fs'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
+import { DatabaseSync } from 'node:sqlite'
 import { afterEach, describe, expect, it } from 'vitest'
 import {
   archivePnpmStore,
   extractPnpmStoreArchives,
+  mergePnpmStore,
   removePnpmProjectRegistrations,
   SEED_STORE_ARCHIVE_DIR,
   SEED_STORE_ARCHIVE_MANIFEST,
@@ -60,6 +62,47 @@ describe('desktop seed store cleanup', () => {
   })
 })
 
+describe('desktop seed store merge', () => {
+  it('preserves installed package records while the verified seed replaces matching records and files', () => {
+    const root = temporaryRoot()
+    const source = join(root, 'source')
+    const destination = join(root, 'destination')
+    for (const store of [source, destination]) {
+      mkdirSync(join(store, 'v11', 'files'), { recursive: true })
+      const database = new DatabaseSync(join(store, 'v11', 'index.db'))
+      database.exec('CREATE TABLE package_index (key TEXT PRIMARY KEY, data BLOB NOT NULL) WITHOUT ROWID')
+      const insert = database.prepare('INSERT INTO package_index (key, data) VALUES (?, ?)')
+      if (store === source) {
+        insert.run('seed-only', Buffer.from('seed'))
+        insert.run('shared', Buffer.from('new'))
+      } else {
+        insert.run('plugin-only', Buffer.from('plugin'))
+        insert.run('shared', Buffer.from('old'))
+      }
+      database.close()
+    }
+    writeFileSync(join(source, 'v11', 'files', 'shared'), 'new')
+    writeFileSync(join(destination, 'v11', 'files', 'shared'), 'old')
+    writeFileSync(join(destination, 'v11', 'files', 'plugin'), 'plugin')
+
+    mergePnpmStore(source, destination)
+
+    const database = new DatabaseSync(join(destination, 'v11', 'index.db'), { readOnly: true })
+    const records = database.prepare('SELECT key, data FROM package_index ORDER BY key').all() as {
+      key: string
+      data: Uint8Array
+    }[]
+    database.close()
+    expect(records.map(record => [record.key, Buffer.from(record.data).toString()])).toEqual([
+      ['plugin-only', 'plugin'],
+      ['seed-only', 'seed'],
+      ['shared', 'new'],
+    ])
+    expect(readFileSync(join(destination, 'v11', 'files', 'shared'), 'utf8')).toBe('new')
+    expect(readFileSync(join(destination, 'v11', 'files', 'plugin'), 'utf8')).toBe('plugin')
+  })
+})
+
 describe('desktop seed store archives', () => {
   it('extracts package bytes and executable modes without retaining loose seed files', () => {
     const root = temporaryRoot()
@@ -77,7 +120,9 @@ describe('desktop seed store archives', () => {
 
     expect(existsSync(store)).toBe(false)
     expect(readFileSync(join(destination, 'v10', 'files', 'package-data'), 'utf8')).toBe('package')
-    expect(statSync(join(destination, 'v10', 'files', 'native-addon')).mode & 0o111).toBe(0o111)
+    if (process.platform !== 'win32') {
+      expect(statSync(join(destination, 'v10', 'files', 'native-addon')).mode & 0o111).toBe(0o111)
+    }
   })
 
   it('produces identical shards for identical paths, bytes, and modes', () => {

+ 226 - 0
apps/desktop/tests/windows-sign.spec.ts

@@ -0,0 +1,226 @@
+import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join, resolve } from 'node:path'
+import { describe, expect, it, vi } from 'vitest'
+import {
+  buildWindowsSigningEnvironment,
+  createRedactedWindowsSigningError,
+  createWindowsTokenSigner,
+  installWindowsNsisBootstrapSigner,
+  repairDanglingAuthenticodeDirectory,
+  scrubWindowsSigningEnvironment,
+} from '../scripts/windows-sign.mjs'
+
+vi.mock('node:crypto', () => ({
+  X509Certificate: class {
+    readonly ca = false
+    readonly keyUsage = ['1.3.6.1.5.5.7.3.3']
+
+    constructor(contents: Buffer) {
+      if (contents.toString('utf8') !== 'code-signing-certificate-fixture') {
+        throw new Error('invalid test certificate')
+      }
+    }
+  },
+}))
+
+const CERTIFICATE_FILE = 'C:\\release\\server.cer'
+const SIGN_SCRIPT = resolve(import.meta.dirname, '../scripts/windows-sign.cmd')
+
+describe('Windows token signing', () => {
+  it('passes only the validated BAT fields to the signing command interpreter', () => {
+    expect(buildWindowsSigningEnvironment({
+      SystemRoot: 'C:\\Windows',
+      DSH_DESKTOP_WINDOWS_TOKEN_PIN: 'inherited-token-secret',
+      DEEPSEEK_API_KEY: 'api-secret',
+      BUILD_PASSWORD: 'build-secret',
+    }, {
+      certificateFile: CERTIFICATE_FILE,
+      signTool: 'C:\\tools\\signtool.exe',
+      path: 'C:\\release\\DeepSeek Harness.exe',
+      isNest: false,
+      tokenPin: 'token-secret!',
+      keyContainer: 'te-container',
+    })).toEqual({
+      SystemRoot: 'C:\\Windows',
+      DSH_DESKTOP_WINDOWS_SIGNTOOL: 'C:\\tools\\signtool.exe',
+      DSH_DESKTOP_WINDOWS_CER_FILE: CERTIFICATE_FILE,
+      DSH_DESKTOP_WINDOWS_TOKEN_PIN: 'token-secret!',
+      DSH_DESKTOP_WINDOWS_KEY_CONTAINER: 'te-container',
+      DSH_DESKTOP_WINDOWS_SIGN_TARGET: 'C:\\release\\DeepSeek Harness.exe',
+      DSH_DESKTOP_WINDOWS_SIGN_APPEND: '',
+    })
+  })
+
+  it('requests an appended signature only for an electron-builder nested task', () => {
+    expect(buildWindowsSigningEnvironment({}, {
+      certificateFile: CERTIFICATE_FILE,
+      signTool: 'C:\\tools\\signtool.exe',
+      path: 'C:\\release\\setup.exe',
+      isNest: true,
+      tokenPin: 'token-secret!',
+      keyContainer: 'te-container',
+    }).DSH_DESKTOP_WINDOWS_SIGN_APPEND).toBe('1')
+  })
+
+  it('keeps the verified SafeNet command in an ASCII CRLF CMD file', async () => {
+    const contents = await readFile(SIGN_SCRIPT)
+    const text = contents.toString('ascii')
+    expect(contents.every(byte => byte <= 0x7F)).toBe(true)
+    expect(text).toContain('\r\n')
+    expect(text.replaceAll('\r\n', '')).not.toContain('\n')
+    expect(text).toContain('setlocal DisableDelayedExpansion\r\n')
+    expect(text).toContain('set "DSH_DESKTOP_WINDOWS_CER_FILE="\r\n')
+    expect(text).toContain('set "DSH_DESKTOP_WINDOWS_TOKEN_PIN="\r\n')
+    expect(text).toContain('"%signTool%" sign /v /fd sha256 /f "%certificateFile%" /kc "[{{%tokenPin%}}]=%keyContainer%" /csp "eToken Base Cryptographic Provider" %appendSignature% /tr http://timestamp.digicert.com /td sha256 "%targetFile%"\r\n')
+  })
+
+  it('rejects incomplete signing identities and non-SHA-256 signing tasks', async () => {
+    const directory = await mkdtemp(join(tmpdir(), 'dsh-windows-sign-tool-'))
+    const certificateFile = join(directory, 'server.cer')
+    const signTool = join(directory, 'signtool.exe')
+    await writeFile(certificateFile, 'code-signing-certificate-fixture')
+    await writeFile(signTool, 'fixture')
+    expect(() => createWindowsTokenSigner({
+      certificateFile: undefined,
+      signTool,
+      tokenPin: 'token-secret!',
+      keyContainer: 'te-container',
+    })).toThrow(/DSH_DESKTOP_WINDOWS_CER_FILE/u)
+    expect(() => createWindowsTokenSigner({
+      certificateFile,
+      signTool: undefined,
+      tokenPin: 'token-secret!',
+      keyContainer: 'te-container',
+    })).toThrow(/DSH_DESKTOP_WINDOWS_SIGNTOOL/u)
+    const signer = createWindowsTokenSigner({
+      certificateFile,
+      signTool,
+      tokenPin: 'token-secret!',
+      keyContainer: 'te-container',
+    })
+    try {
+      expect(() => createWindowsTokenSigner({
+        certificateFile,
+        signTool,
+        tokenPin: 'token-secret!',
+      })).toThrow(/DSH_DESKTOP_WINDOWS_KEY_CONTAINER/u)
+      expect(() => createWindowsTokenSigner({
+        certificateFile,
+        signTool,
+        tokenPin: '',
+        keyContainer: 'te-container',
+      })).toThrow(/DSH_DESKTOP_WINDOWS_TOKEN_PIN/u)
+      expect(() => createWindowsTokenSigner({
+        certificateFile,
+        signTool,
+        tokenPin: 'token]secret',
+        keyContainer: 'te-container',
+      })).toThrow(/cannot contain/u)
+      await expect(signer({
+        path: 'C:\\release\\setup.exe',
+        hash: 'sha1',
+        isNest: false,
+      })).rejects.toThrow(/requires SHA-256/u)
+    }
+    finally {
+      await rm(directory, { recursive: true })
+    }
+  })
+
+  it('removes inherited credentials and redacts SignTool process failures', () => {
+    expect(scrubWindowsSigningEnvironment({
+      SystemRoot: 'C:\\Windows',
+      DSH_DESKTOP_WINDOWS_CER_FILE: 'C:\\release\\server.cer',
+      DSH_DESKTOP_WINDOWS_SIGNTOOL: 'C:\\tools\\signtool.exe',
+      DSH_DESKTOP_WINDOWS_TOKEN_PIN: 'token-secret',
+      DEEPSEEK_API_KEY: 'api-secret',
+      BUILD_PASSWORD: 'build-secret',
+    })).toEqual({ SystemRoot: 'C:\\Windows' })
+
+    const processError = Object.assign(new Error('failed'), {
+      code: 1,
+      cmd: 'signtool /kc [{{token-secret}}]=te-container',
+      stderr: 'provider rejected token-secret',
+    })
+    const failure = createRedactedWindowsSigningError(
+      processError,
+      'C:\\release\\setup.exe',
+      ['token-secret'],
+    )
+    expect(failure.message).toBe('Windows release signing failed for C:\\release\\setup.exe (exit 1): provider rejected <redacted>')
+    expect(failure.message).not.toContain('token-secret')
+    expect(failure).not.toHaveProperty('cause')
+    expect(failure).not.toHaveProperty('cmd')
+  })
+
+  it('signs the temporary NSIS executable before enterprise policy evaluates it', async () => {
+    const events: string[] = []
+    let receivedEnvironment: NodeJS.ProcessEnv | undefined
+    class FakeWineVmManager {
+      async exec(
+        file: string,
+        _args: string[],
+        options?: { env?: NodeJS.ProcessEnv },
+      ): Promise<string> {
+        events.push(`exec:${file}`)
+        receivedEnvironment = options?.env
+        return 'executed'
+      }
+    }
+    installWindowsNsisBootstrapSigner({
+      sign: async (configuration) => {
+        events.push(`sign:${configuration.path}:${configuration.hash}:${String(configuration.isNest)}`)
+      },
+      wineVmManager: FakeWineVmManager,
+      platform: 'win32',
+      environment: {
+        SystemRoot: 'C:\\Windows',
+        DSH_DESKTOP_WINDOWS_TOKEN_PIN: 'token-secret',
+      },
+    })
+
+    const result = await new FakeWineVmManager().exec('C:\\release\\setup.exe', [], {
+      env: {
+        __COMPAT_LAYER: 'RunAsInvoker',
+        BUILD_PASSWORD: 'build-secret',
+      },
+    })
+
+    expect(result).toBe('executed')
+    expect(events).toEqual([
+      'sign:C:\\release\\setup.exe:sha256:false',
+      'exec:C:\\release\\setup.exe',
+    ])
+    expect(receivedEnvironment).toEqual({
+      SystemRoot: 'C:\\Windows',
+      __COMPAT_LAYER: 'RunAsInvoker',
+    })
+  })
+
+  it('clears a certificate table inherited beyond the generated uninstaller', async () => {
+    const directory = await mkdtemp(join(tmpdir(), 'dsh-windows-sign-'))
+    const path = join(directory, 'uninstaller.exe')
+    const executable = Buffer.alloc(512)
+    const peOffset = 216
+    const optionalHeaderOffset = peOffset + 24
+    const certificateDirectoryOffset = optionalHeaderOffset + 96 + (4 * 8)
+    executable.write('MZ', 0, 'ascii')
+    executable.writeUInt32LE(peOffset, 60)
+    executable.write('PE\0\0', peOffset, 'ascii')
+    executable.writeUInt16LE(0x10B, optionalHeaderOffset)
+    executable.writeUInt32LE(600, certificateDirectoryOffset)
+    executable.writeUInt32LE(100, certificateDirectoryOffset + 4)
+    await writeFile(path, executable)
+    try {
+      await expect(repairDanglingAuthenticodeDirectory(path)).resolves.toBe(true)
+      const repaired = await readFile(path)
+      expect(repaired.readUInt32LE(certificateDirectoryOffset)).toBe(0)
+      expect(repaired.readUInt32LE(certificateDirectoryOffset + 4)).toBe(0)
+      await expect(repairDanglingAuthenticodeDirectory(path)).resolves.toBe(false)
+    }
+    finally {
+      await rm(directory, { recursive: true })
+    }
+  })
+})

+ 80 - 73
pnpm-lock.yaml

@@ -80,7 +80,7 @@ importers:
         version: 1.32.0
       mdast-util-from-markdown:
         specifier: ^2.0.3
-        version: 2.0.3
+        version: 2.0.3(supports-color@9.4.0)
       mdast-util-gfm:
         specifier: ^3.1.0
         version: 3.1.0
@@ -116,7 +116,7 @@ importers:
         version: 6.0.3
       vite-tsconfig-paths:
         specifier: ^6.1.1
-        version: 6.1.1(typescript@6.0.3)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
+        version: 6.1.1(supports-color@9.4.0)(typescript@6.0.3)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
       vitest:
         specifier: ^4.1.8
         version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(happy-dom@20.11.6)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))
@@ -503,9 +503,12 @@ importers:
       '@types/semver':
         specifier: ^7.8.0
         version: 7.8.0
+      app-builder-lib:
+        specifier: 26.15.3
+        version: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3)
       electron:
         specifier: ^44.0.0
-        version: 44.0.0
+        version: 44.0.0(supports-color@9.4.0)
       electron-builder:
         specifier: ^26.15.3
         version: 26.15.3(electron-builder-squirrel-windows@26.15.3)
@@ -571,13 +574,13 @@ importers:
         version: 8.18.1
       '@vitejs/plugin-react':
         specifier: ^4.0.0
-        version: 4.7.0(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))
+        version: 4.7.0(supports-color@9.4.0)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))
       fflate:
         specifier: ^0.8.2
         version: 0.8.3
       http-server:
         specifier: ^14.1.1
-        version: 14.1.1
+        version: 14.1.1(supports-color@9.4.0)
       playwright:
         specifier: ^1.49.0
         version: 1.61.1
@@ -2967,7 +2970,7 @@ importers:
         version: 0.16.47
       mdast-util-from-markdown:
         specifier: ^2.0.3
-        version: 2.0.3
+        version: 2.0.3(supports-color@9.4.0)
       mdast-util-gfm:
         specifier: ^3.1.0
         version: 3.1.0
@@ -6147,7 +6150,7 @@ importers:
         version: link:../../../vendor/schemastery
       compression:
         specifier: ^1.8.1
-        version: 1.8.1
+        version: 1.8.1(supports-color@9.4.0)
       negotiator:
         specifier: ^1.0.0
         version: 1.0.0
@@ -10875,7 +10878,7 @@ importers:
         version: 1.11.21
       debug:
         specifier: 4.4.3
-        version: 4.4.3
+        version: 4.4.3(supports-color@9.4.0)
       mermaid:
         specifier: 11.16.0
         version: 11.16.0
@@ -17788,20 +17791,20 @@ snapshots:
 
   '@babel/compat-data@7.29.7': {}
 
-  '@babel/core@7.29.7':
+  '@babel/core@7.29.7(supports-color@9.4.0)':
     dependencies:
       '@babel/code-frame': 7.29.7
       '@babel/generator': 7.29.7
       '@babel/helper-compilation-targets': 7.29.7
-      '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+      '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@9.4.0))
       '@babel/helpers': 7.29.7
       '@babel/parser': 7.29.7
       '@babel/template': 7.29.7
-      '@babel/traverse': 7.29.7
+      '@babel/traverse': 7.29.7(supports-color@9.4.0)
       '@babel/types': 7.29.7
       '@jridgewell/remapping': 2.3.5
       convert-source-map: 2.0.0
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -17837,17 +17840,17 @@ snapshots:
 
   '@babel/helper-module-imports@7.29.7':
     dependencies:
-      '@babel/traverse': 7.29.7
+      '@babel/traverse': 7.29.7(supports-color@9.4.0)
       '@babel/types': 7.29.7
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+  '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@9.4.0))':
     dependencies:
-      '@babel/core': 7.29.7
+      '@babel/core': 7.29.7(supports-color@9.4.0)
       '@babel/helper-module-imports': 7.29.7
       '@babel/helper-validator-identifier': 7.29.7
-      '@babel/traverse': 7.29.7
+      '@babel/traverse': 7.29.7(supports-color@9.4.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -17876,14 +17879,14 @@ snapshots:
     dependencies:
       '@babel/types': 8.0.0-rc.6
 
-  '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
+  '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@9.4.0))':
     dependencies:
-      '@babel/core': 7.29.7
+      '@babel/core': 7.29.7(supports-color@9.4.0)
       '@babel/helper-plugin-utils': 7.29.7
 
-  '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
+  '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@9.4.0))':
     dependencies:
-      '@babel/core': 7.29.7
+      '@babel/core': 7.29.7(supports-color@9.4.0)
       '@babel/helper-plugin-utils': 7.29.7
 
   '@babel/runtime@7.29.7': {}
@@ -17894,7 +17897,7 @@ snapshots:
       '@babel/parser': 7.29.7
       '@babel/types': 7.29.7
 
-  '@babel/traverse@7.29.7':
+  '@babel/traverse@7.29.7(supports-color@9.4.0)':
     dependencies:
       '@babel/code-frame': 7.29.7
       '@babel/generator': 7.29.7
@@ -17902,7 +17905,7 @@ snapshots:
       '@babel/parser': 7.29.7
       '@babel/template': 7.29.7
       '@babel/types': 7.29.7
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -18031,7 +18034,7 @@ snapshots:
 
   '@electron/get@3.1.0':
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       env-paths: 2.2.1
       fs-extra: 8.1.0
       got: 11.8.6
@@ -18043,9 +18046,9 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@electron/get@5.1.0':
+  '@electron/get@5.1.0(supports-color@9.4.0)':
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       env-paths: 3.0.0
       graceful-fs: 4.2.11
       progress: 2.0.3
@@ -18058,7 +18061,7 @@ snapshots:
 
   '@electron/notarize@2.5.0':
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       fs-extra: 9.1.0
       promise-retry: 2.0.1
     transitivePeerDependencies:
@@ -18067,7 +18070,7 @@ snapshots:
   '@electron/osx-sign@1.3.3':
     dependencies:
       compare-version: 0.1.2
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       fs-extra: 10.1.0
       isbinaryfile: 4.0.10
       minimist: 1.2.8
@@ -18078,7 +18081,7 @@ snapshots:
   '@electron/rebuild@4.2.0':
     dependencies:
       '@malept/cross-spawn-promise': 2.0.0
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       node-abi: 4.34.0
       node-api-version: 0.2.1
       node-gyp: 12.4.0
@@ -18090,7 +18093,7 @@ snapshots:
     dependencies:
       '@electron/asar': 3.4.1
       '@malept/cross-spawn-promise': 2.0.0
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       dir-compare: 4.2.0
       fs-extra: 11.4.0
       minimatch: 9.0.9
@@ -18101,7 +18104,7 @@ snapshots:
   '@electron/windows-sign@1.2.2':
     dependencies:
       cross-dirname: 0.1.0
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       fs-extra: 11.4.0
       minimist: 1.2.8
       postject: 1.0.0-alpha.6
@@ -18381,7 +18384,7 @@ snapshots:
   '@eslint/config-array@0.23.5':
     dependencies:
       '@eslint/object-schema': 3.0.5
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       minimatch: 10.2.5
     transitivePeerDependencies:
       - supports-color
@@ -18748,7 +18751,7 @@ snapshots:
 
   '@malept/flatpak-bundler@0.4.0':
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       fs-extra: 9.1.0
       lodash: 4.18.1
       tmp-promise: 3.0.3
@@ -19866,11 +19869,11 @@ snapshots:
       d3-selection: 3.0.0
       d3-transition: 3.0.1(d3-selection@3.0.0)
 
-  '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))':
+  '@vitejs/plugin-react@4.7.0(supports-color@9.4.0)(vite@6.4.3(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))':
     dependencies:
-      '@babel/core': 7.29.7
-      '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
-      '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
+      '@babel/core': 7.29.7(supports-color@9.4.0)
+      '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@9.4.0))
+      '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@9.4.0))
       '@rolldown/pluginutils': 1.0.0-beta.27
       '@types/babel__core': 7.20.5
       react-refresh: 0.17.0
@@ -20207,7 +20210,7 @@ snapshots:
       builder-util-runtime: 9.7.0
       chromium-pickle-js: 0.2.0
       ci-info: 4.3.1
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3)
       dotenv: 16.6.1
       dotenv-expand: 11.0.7
@@ -20299,7 +20302,7 @@ snapshots:
     dependencies:
       bytes: 3.1.2
       content-type: 2.0.0
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       http-errors: 2.0.1
       iconv-lite: 0.7.3
       on-finished: 2.4.1
@@ -20352,7 +20355,7 @@ snapshots:
 
   builder-util-runtime@9.7.0:
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       sax: 1.6.1
     transitivePeerDependencies:
       - supports-color
@@ -20363,7 +20366,7 @@ snapshots:
       builder-util-runtime: 9.7.0
       chalk: 4.1.2
       cross-spawn: 7.0.6
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       fs-extra: 10.1.0
       http-proxy-agent: 7.0.2
       https-proxy-agent: 7.0.6
@@ -20488,11 +20491,11 @@ snapshots:
     dependencies:
       mime-db: 1.54.0
 
-  compression@1.8.1:
+  compression@1.8.1(supports-color@9.4.0):
     dependencies:
       bytes: 3.1.2
       compressible: 2.0.18
-      debug: 2.6.9
+      debug: 2.6.9(supports-color@9.4.0)
       negotiator: 0.6.4
       on-headers: 1.1.0
       safe-buffer: 5.2.1
@@ -20746,13 +20749,17 @@ snapshots:
 
   dayjs@1.11.21: {}
 
-  debug@2.6.9:
+  debug@2.6.9(supports-color@9.4.0):
     dependencies:
       ms: 2.0.0
+    optionalDependencies:
+      supports-color: 9.4.0
 
-  debug@4.4.3:
+  debug@4.4.3(supports-color@9.4.0):
     dependencies:
       ms: 2.1.3
+    optionalDependencies:
+      supports-color: 9.4.0
 
   decimal.js@10.6.0: {}
 
@@ -20945,7 +20952,7 @@ snapshots:
   electron-winstaller@5.4.0:
     dependencies:
       '@electron/asar': 3.4.1
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       fs-extra: 7.0.1
       lodash: 4.18.1
       temp: 0.9.4
@@ -20954,10 +20961,10 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  electron@44.0.0:
+  electron@44.0.0(supports-color@9.4.0):
     dependencies:
       '@electron-internal/extract-zip': 1.0.5
-      '@electron/get': 5.1.0
+      '@electron/get': 5.1.0(supports-color@9.4.0)
       '@types/node': 24.13.3
     transitivePeerDependencies:
       - supports-color
@@ -21144,7 +21151,7 @@ snapshots:
       '@types/estree': 1.0.9
       ajv: 6.15.0
       cross-spawn: 7.0.6
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       escape-string-regexp: 4.0.0
       eslint-scope: 9.1.2
       eslint-visitor-keys: 5.0.1
@@ -21244,7 +21251,7 @@ snapshots:
       content-type: 1.0.5
       cookie: 0.7.2
       cookie-signature: 1.2.2
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       depd: 2.0.0
       encodeurl: 2.0.0
       escape-html: 1.0.3
@@ -21273,7 +21280,7 @@ snapshots:
 
   extract-zip@2.0.1:
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       get-stream: 5.2.0
       yauzl: 2.10.0
     optionalDependencies:
@@ -21334,7 +21341,7 @@ snapshots:
 
   finalhandler@2.1.1:
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       encodeurl: 2.0.0
       escape-html: 1.0.3
       on-finished: 2.4.1
@@ -21653,7 +21660,7 @@ snapshots:
   http-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.4
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -21665,7 +21672,7 @@ snapshots:
     transitivePeerDependencies:
       - debug
 
-  http-server@14.1.1:
+  http-server@14.1.1(supports-color@9.4.0):
     dependencies:
       basic-auth: 2.0.1
       chalk: 4.1.2
@@ -21676,7 +21683,7 @@ snapshots:
       mime: 1.6.0
       minimist: 1.2.8
       opener: 1.5.2
-      portfinder: 1.0.38
+      portfinder: 1.0.38(supports-color@9.4.0)
       secure-compare: 3.0.1
       union: 0.5.0
       url-join: 4.0.1
@@ -21692,7 +21699,7 @@ snapshots:
   https-proxy-agent@7.0.6:
     dependencies:
       agent-base: 7.1.4
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -22148,14 +22155,14 @@ snapshots:
       unist-util-is: 6.0.1
       unist-util-visit-parents: 6.0.2
 
-  mdast-util-from-markdown@2.0.3:
+  mdast-util-from-markdown@2.0.3(supports-color@9.4.0):
     dependencies:
       '@types/mdast': 4.0.4
       '@types/unist': 3.0.3
       decode-named-character-reference: 1.3.0
       devlop: 1.1.0
       mdast-util-to-string: 4.0.0
-      micromark: 4.0.2
+      micromark: 4.0.2(supports-color@9.4.0)
       micromark-util-decode-numeric-character-reference: 2.0.2
       micromark-util-decode-string: 2.0.1
       micromark-util-normalize-identifier: 2.0.1
@@ -22177,7 +22184,7 @@ snapshots:
     dependencies:
       '@types/mdast': 4.0.4
       devlop: 1.1.0
-      mdast-util-from-markdown: 2.0.3
+      mdast-util-from-markdown: 2.0.3(supports-color@9.4.0)
       mdast-util-to-markdown: 2.1.2
       micromark-util-normalize-identifier: 2.0.1
     transitivePeerDependencies:
@@ -22186,7 +22193,7 @@ snapshots:
   mdast-util-gfm-strikethrough@2.0.0:
     dependencies:
       '@types/mdast': 4.0.4
-      mdast-util-from-markdown: 2.0.3
+      mdast-util-from-markdown: 2.0.3(supports-color@9.4.0)
       mdast-util-to-markdown: 2.1.2
     transitivePeerDependencies:
       - supports-color
@@ -22196,7 +22203,7 @@ snapshots:
       '@types/mdast': 4.0.4
       devlop: 1.1.0
       markdown-table: 3.0.4
-      mdast-util-from-markdown: 2.0.3
+      mdast-util-from-markdown: 2.0.3(supports-color@9.4.0)
       mdast-util-to-markdown: 2.1.2
     transitivePeerDependencies:
       - supports-color
@@ -22205,14 +22212,14 @@ snapshots:
     dependencies:
       '@types/mdast': 4.0.4
       devlop: 1.1.0
-      mdast-util-from-markdown: 2.0.3
+      mdast-util-from-markdown: 2.0.3(supports-color@9.4.0)
       mdast-util-to-markdown: 2.1.2
     transitivePeerDependencies:
       - supports-color
 
   mdast-util-gfm@3.1.0:
     dependencies:
-      mdast-util-from-markdown: 2.0.3
+      mdast-util-from-markdown: 2.0.3(supports-color@9.4.0)
       mdast-util-gfm-autolink-literal: 2.0.1
       mdast-util-gfm-footnote: 2.1.0
       mdast-util-gfm-strikethrough: 2.0.0
@@ -22228,7 +22235,7 @@ snapshots:
       '@types/mdast': 4.0.4
       devlop: 1.1.0
       longest-streak: 3.1.0
-      mdast-util-from-markdown: 2.0.3
+      mdast-util-from-markdown: 2.0.3(supports-color@9.4.0)
       mdast-util-to-markdown: 2.1.2
       unist-util-remove-position: 5.0.0
     transitivePeerDependencies:
@@ -22476,10 +22483,10 @@ snapshots:
 
   micromark-util-types@2.0.2: {}
 
-  micromark@4.0.2:
+  micromark@4.0.2(supports-color@9.4.0):
     dependencies:
       '@types/debug': 4.1.13
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       decode-named-character-reference: 1.3.0
       devlop: 1.1.0
       micromark-core-commonmark: 2.0.3
@@ -22909,10 +22916,10 @@ snapshots:
       path-data-parser: 0.1.0
       points-on-curve: 0.2.0
 
-  portfinder@1.0.38:
+  portfinder@1.0.38(supports-color@9.4.0):
     dependencies:
       async: 3.2.6
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -23039,7 +23046,7 @@ snapshots:
 
   read-binary-file-arch@1.0.6:
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -23224,7 +23231,7 @@ snapshots:
 
   router@2.2.0:
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       depd: 2.0.0
       is-promise: 4.0.0
       parseurl: 1.3.3
@@ -23285,7 +23292,7 @@ snapshots:
 
   send@1.2.1:
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       encodeurl: 2.0.0
       escape-html: 1.0.3
       etag: 1.8.1
@@ -23494,7 +23501,7 @@ snapshots:
 
   sumchecker@3.0.1:
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -23751,9 +23758,9 @@ snapshots:
       '@types/unist': 3.0.3
       vfile-message: 4.0.3
 
-  vite-tsconfig-paths@6.1.1(typescript@6.0.3)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
+  vite-tsconfig-paths@6.1.1(supports-color@9.4.0)(typescript@6.0.3)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)):
     dependencies:
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@9.4.0)
       globrex: 0.1.2
       tsconfck: 3.1.6(typescript@6.0.3)
       vite: 8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor