mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
feat: mac code sign & notarize
This commit is contained in:
+2
-2
@@ -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: ad5fdb1b5b845557765ad51e51d880095c013882
|
||||
2026-08-25-electron-desktop-packaging-and-updates.zh.md: 5ace455af600144064d582d2d083f2386dd5e370
|
||||
2026-08-25-electron-desktop-packaging-and-updates.md: 789fadb670fbf37f5d8cc90c24b1e5efec6b6723
|
||||
2026-08-25-electron-desktop-packaging-and-updates.zh.md: 83a2ac99b84659f7c2365ca5bf7438716c8e55fa
|
||||
|
||||
+7
-5
@@ -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. 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 the offline installation once, checks the Host entry again, and removes `node_modules` before inventory generation.
|
||||
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. 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 the Host entry, and removes `node_modules` before final store preparation.
|
||||
|
||||
The seed stores pnpm content in 16 deterministic uncompressed tar shards selected by normalized store path. This reduces the signed application resource inventory without changing npm package bytes, lets the outer installer provide compression, and limits differential-update churn to shards containing changed paths. 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, 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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -90,7 +90,7 @@ The generic update provider publishes metadata, installers, and blockmaps togeth
|
||||
|
||||
Core dsh comes only from integrity-recorded local npm tarballs inside the signed Electron release; pnpm overrides prevent transitive core packages from falling back to a registry. Store archives are integrity-checked and fully validated in an isolated extraction directory before their files can enter writable package state. Plugin installation accepts registry package specs allowed by desktop policy but never raw pnpm commands. Exact versions, lockfile integrity, a reviewed `allowBuilds` set, user-only directory permissions, redacted diagnostics, and health checking are required before activation.
|
||||
|
||||
Electron artifacts are signed; macOS artifacts are notarized. 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -103,7 +103,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 | Production signing, notarization, 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 signing, 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.
|
||||
|
||||
@@ -121,10 +121,12 @@ The bundled upstream Node.js and pnpm are expected to add about 35–50 MB compr
|
||||
|
||||
**Install dsh and plugins into separate desktop projects.** This creates a second resolution anchor and peer-dependency fallback. One ordinary npm project already provides the required installation and resolution model.
|
||||
|
||||
**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.
|
||||
|
||||
## 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, 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, while 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.
|
||||
|
||||
+7
-5
@@ -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`。这些 tarball 保持为由各包 `files` manifest 决定内容的正式 `pnpm pack` 结果;Desktop 不删除已发布的声明文件,也不建立第二套包内容策略。manifest 把每个选中的包列为本地直接依赖,关闭对等依赖自动安装,workspace 文件再把每个选中的第一方包 override 到对应本地 tarball。构建会拒绝任何通过 registry 版本解析这些包名的 lockfile。内置 pnpm 从 npm 拉取外部生产依赖,执行一次离线安装并再次检查 Host 入口,然后在生成清单前删除 `node_modules`。
|
||||
打包种子是离线安装包,而不是可执行 dsh 目录。它包含发布身份、初始桌面项目 manifest、以 dsh 为根的第一方包闭包描述文件及不可变 tarball、lockfile、完整性清单和所需 store 子集。发布构建要求 Electron 包与根 dsh 包使用相同版本,从正式源码构建生成最终 npm tarball,选择可达的 dsh 与 vendored 包以及 Landlock 入口,并验证 dsh tarball 中的 `lib/desktop-host.js`。这些 tarball 保持为由各包 `files` manifest 决定内容的正式 `pnpm pack` 结果;Desktop 不删除已发布的声明文件,也不建立第二套包内容策略。manifest 把每个选中的包列为本地直接依赖,关闭对等依赖自动安装,workspace 文件再把每个选中的第一方包 override 到对应本地 tarball。构建会拒绝任何通过 registry 版本解析这些包名的 lockfile。内置 pnpm 从 npm 拉取外部生产依赖,执行离线安装,检查 Host 入口,并在最终准备 store 前删除 `node_modules`。
|
||||
|
||||
种子根据规范化 store 路径,把 pnpm 内容放入 16 个确定性的未压缩 tar 分片。这可以在不改变 npm 包字节的前提下减少签名应用的资源清单,让外层安装包负责压缩,并把差分更新变化限制在包含已变路径的分片中。种子完整性覆盖分片 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 目录,然后才把完整结果合并进 `.dsh/desktop/pnpm/store`。中断的合并可能留下有效的不可变缓存内容,但 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 精确版本。完整依赖图必须通过同一套健康检查才能激活。
|
||||
|
||||
@@ -90,7 +90,7 @@ generic 更新服务必须一起发布元数据、安装包和 blockmap。NSIS
|
||||
|
||||
核心 dsh 只能来自签名 Electron 发布内经过完整性记录的本地 npm tarball;pnpm overrides 防止传递核心包回退到 registry。Store 归档经过完整性检查,并在隔离的解包目录中完成全部验证,归档文件随后才能进入可写包状态。插件安装接受桌面策略允许的 registry 包 spec,但绝不接受原始 pnpm 命令。激活前必须具备精确版本、lockfile 完整性、经过评审的 `allowBuilds` 集合、仅限用户的目录权限、遮盖后的诊断和健康检查。
|
||||
|
||||
Electron 产物必须签名;macOS 产物必须公证。自定义协议提供已安装的前端分发目录和活跃模块图点名的客户端文件,并拒绝路径穿越或访问这些根目录之外的内容。插件安装器 API 只对 Electron 拥有的管理 GUI 可用,不存在于浏览器应用或后端 RPC 中。
|
||||
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 中。
|
||||
|
||||
打包应用会忽略开发资源和项目环境变量覆盖。只有未打包的 Electron 进程可以替换 Node.js 可执行文件、pnpm 入口、seed 或活跃项目。
|
||||
|
||||
@@ -103,7 +103,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 解析。 |
|
||||
| 资格验证 | 生产签名、公证、更新托管、跨上一版本的已安装产物测试和各平台 GUI 录制仍是发布环境门槛。 |
|
||||
| 资格验证 | macOS 打包要求已配置的公司身份与公证凭据可用,在解包最终归档后验证每个原生 seed 对象,验证完整应用签名,并要求应用和 DMG 都完成公证且通过 Gatekeeper。Windows 签名、更新托管、跨上一版本的已安装产物测试和各平台 GUI 录制仍是发布环境门槛。 |
|
||||
|
||||
`dev:desktop` 会构建当前 workspace,把已构建 CLI 包及其依赖链接投影为一次性项目,使用隔离的 Harness home,打开 Main、Renderer 和 Host 调试器,并在不准备发布资源的情况下启动未打包 Electron。该模式的链接依赖图不是由 pnpm 安装的桌面项目,因此会禁用包修改。固定的 macOS arm64、macOS x64 与 Windows x64 打包命令会把同一目标传给运行时准备、seed 安装和 electron-builder;每条命令还提供未封装安装器的变体,用于在生成安装器前验证发布路径。
|
||||
|
||||
@@ -121,10 +121,12 @@ Electron 产物必须签名;macOS 产物必须公证。自定义协议提供
|
||||
|
||||
**把 dsh 与插件安装到不同桌面项目。** 这会产生第二解析锚点和 peer dependency 回退。一个普通 npm 项目已经提供所需安装与解析模型。
|
||||
|
||||
**从 registry 包删除非目标 Mach-O 文件。** 架构裁剪可以节省少量 seed 空间,但包可能有意附带多个架构变体,调用方也可以观察安装后的文件集。签署每个实际携带的 Mach-O 对象,无需发明 Desktop 专属包布局就能满足公证要求。
|
||||
|
||||
## 结果
|
||||
|
||||
- 没有系统 Node.js 或 pnpm 的干净离线机器把种子安装进 `.dsh/profiles/desktop`,并启动可工作的 dsh 会话。
|
||||
- 已签名应用记录固定少量的 seed store 分片,而不是记录每个 pnpm 缓存文件;安装后的私有 store 仍保持普通 pnpm 布局。
|
||||
- 已签名应用记录固定少量的 seed store 分片,而不是记录每个 pnpm 缓存文件;macOS 分片内每个 Mach-O 对象都带有发布 Developer ID、安全时间戳与 hardened runtime,安装后的私有 store 仍保持普通 pnpm 布局。
|
||||
- `.dsh/profiles/desktop/node_modules` 包含并解析桌面 dsh 包和每个 GUI 安装的桌面插件。
|
||||
- 每个桌面 pnpm 操作都使用内置可执行文件和 `.dsh/desktop/pnpm/store`;不读取用户 `PATH`、配置、store 或 profile `node_modules`。
|
||||
- Electron-only GUI 安装、删除和更新普通 npm 插件包,而不暴露原始 pnpm 参数。
|
||||
|
||||
@@ -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: 94c6207c19441c763d30b5d885db00694e1c66d3
|
||||
README.zh.md: dd95b16c7b526599835dfdf938b9294350097adb
|
||||
README.md: 1b5be9b33101e1a9b5c00f8e4927b2dacac230f2
|
||||
README.zh.md: 04be419851d2b86457906cb4fbd4249b446a8516
|
||||
|
||||
+18
-5
@@ -11,7 +11,7 @@ The desktop application is an Electron shell around the dsh Web UI. It opens no
|
||||
| Release identity | The shell API, Web client, backend, and plugin graph are qualified as one combination; independent versions would create untested combinations and ambiguous update availability. | Electron and `@deepseek-ai/dsh` always have the same exact version. A dsh upgrade is a Desktop release, even when the shell code is unchanged. |
|
||||
| Runtime | Electron's Node.js carries Electron patches, fuses, ABI, and lifecycle constraints, while system runtimes and package-manager state are uncontrolled. | dsh runs under the bundled upstream Node.js and every package operation uses the bundled pnpm. Electron's Node.js, system Node.js, system pnpm, and user package-manager configuration are outside the execution path. |
|
||||
| Package sources | The exact dsh source build must be packageable before npm publication and install offline; plugins must remain ordinary user-selected npm packages. | The signed application carries locally packed first-party dsh packages and an offline seed store. Desktop plugins remain ordinary npm dependencies resolved from the fixed Desktop registry. |
|
||||
| Seed transport | Shipping every pnpm store file separately makes code signing inventory tens of thousands of immutable cache entries and increases update metadata, while a single compressed archive would make small package changes replace one large block range. | Packaging assigns store files to 16 deterministic uncompressed tar shards. Signing inventories the shards, the outer installer compresses them, and unchanged shards remain reusable by differential updates. |
|
||||
| Seed transport | Apple notarization inspects code inside archives; shipping every pnpm store file separately would also make the application signature inventory tens of thousands of cache entries, while one compressed archive would amplify small package changes. | macOS packaging signs every Mach-O CAS object, rewrites its pnpm hashes, and proves another offline install before assigning store files to 16 deterministic uncompressed tar shards. The outer installer compresses them, and differential updates can reuse unchanged shards. |
|
||||
| State ownership | Sharing executable dependency graphs would let CLI and Desktop change each other's dsh, Cordis, plugin, or native-module versions. | Electron exclusively owns `$DSH_HOME/profiles/desktop` and its package-manager state. CLI and Desktop share supported product data under `$DSH_HOME`, but never executable packages, plugin activation, lockfiles, or `node_modules`. |
|
||||
| Transport | A listening Web service adds port ownership, authentication, CORS, and exposure concerns; Electron and upstream Node.js also need an explicit cross-process protocol. | The application opens no Web port. `dsh-app://` carries Web assets and Fetch traffic; framed byte pipes carry bounded request and response chunks with backpressure, while Node IPC carries only child lifecycle control. |
|
||||
| Activation | Dependency resolution, lifecycle scripts, native modules, and plugin startup can fail, and a process can stop during directory replacement. | Release and plugin changes install in staging, boot a complete backend health check, and replace the active profile only after success; a journal and one rollback profile cover interrupted replacement. |
|
||||
@@ -27,7 +27,7 @@ The main dsh renderer receives only the desktop protocol marker. The separate pl
|
||||
|
||||
### Seed installation
|
||||
|
||||
The packaged seed is an installation kit, not a ready-to-run `node_modules` tree. Packaging creates the lockfile, fetches the production graph, proves one complete offline installation with the matching Desktop Host entry, and then deletes `node_modules`. The signed seed retains the release identity, local first-party tarballs and their descriptor, project metadata, lockfile, integrity inventory, and pnpm store content required to repeat that installation on the user's machine.
|
||||
The packaged seed is an installation kit, not a ready-to-run `node_modules` tree. Packaging creates the lockfile, fetches the production graph, and proves one complete offline installation with the matching Desktop Host entry. A macOS build then Developer ID signs every Mach-O object in pnpm's content-addressed store, updates every affected SHA-512 index record, and proves the rewritten store with another offline install before deleting `node_modules`. The signed seed retains the release identity, local first-party tarballs and their descriptor, project metadata, lockfile, integrity inventory, and pnpm store content required to repeat that installation on the user's machine.
|
||||
|
||||
| Seed content | Writable destination or use |
|
||||
|---|---|
|
||||
@@ -67,7 +67,18 @@ Workspace development runs the current CLI package under the invoking Node.js an
|
||||
|
||||
## Package
|
||||
|
||||
The normal packaging path is one complete command. It performs release preparation before creating the host platform's installers; a configured release build also emits update metadata. `prepare:desktop` is not a prerequisite:
|
||||
The normal packaging path is one complete command. It performs release preparation before creating the host platform's installers; a configured release build also emits update metadata. Every target requires a reverse-DNS `DSH_DESKTOP_APP_ID`. macOS targets additionally require the electron-builder certificate qualifier in `DSH_DESKTOP_MACOS_SIGNING_IDENTITY`, its 10-character Apple Team ID in `DSH_DESKTOP_MACOS_TEAM_ID`, and one complete notarytool credential strategy. The App Store Connect API-key strategy uses these variables:
|
||||
|
||||
```sh
|
||||
export DSH_DESKTOP_APP_ID='<reverse-DNS application ID>'
|
||||
export DSH_DESKTOP_MACOS_SIGNING_IDENTITY='<certificate name without the Developer ID Application prefix>'
|
||||
export DSH_DESKTOP_MACOS_TEAM_ID='<10-character Apple Team ID>'
|
||||
export APPLE_API_KEY='<absolute path to the .p8 file>'
|
||||
export APPLE_API_KEY_ID='<App Store Connect API Key ID>'
|
||||
export APPLE_API_ISSUER='<App Store Connect issuer UUID>'
|
||||
```
|
||||
|
||||
`prepare:desktop` is not a prerequisite:
|
||||
|
||||
```sh
|
||||
pnpm run package:desktop
|
||||
@@ -83,6 +94,8 @@ pnpm run package:desktop:win:x64
|
||||
|
||||
The macOS arm64 command requires Apple Silicon. The macOS x64 command runs on Intel macOS or Apple Silicon with Rosetta. The Windows x64 command requires Windows x64. Linux is not a supported Desktop release target.
|
||||
|
||||
The macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported `Developer ID Application:` prefix, and incomplete notarization credentials. macOS packaging requires the configured identity and its private key. Seed preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. Electron-builder notarizes and staples the application before packaging and signs the DMG. The DMG artifact-completion hook then notarizes and staples it before requiring its exact identity, ticket, and Gatekeeper acceptance; only after the hook succeeds can electron-builder publish the file. The private key can come from the login keychain or electron-builder's standard `CSC_LINK` input; ambient `CSC_NAME` and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>`.
|
||||
|
||||
Create a runnable application directory instead of an installer by using the matching `:dir` command, such as:
|
||||
|
||||
```sh
|
||||
@@ -98,7 +111,7 @@ pnpm run prepare:desktop
|
||||
|
||||
This diagnostic command is an alternative stopping point, not the first half of a two-command build. A later `package:desktop*` command repeats the official build and preparation so it cannot consume stale dsh packages, runtime files, or seed content.
|
||||
|
||||
Every package command performs the official repository build, packs the dsh and vendored package families, and packs the Landlock entry before preparing release resources. `prepare:packages` selects the first-party production closure rooted at `@deepseek-ai/dsh`, verifies that its tarball contains `lib/desktop-host.js`, copies the selected tarballs into the seed input, and records their sizes and SHA-512 integrity. These are the official `pnpm pack` outputs, so each package's `files` manifest controls its published contents: Desktop adds no second filter, retains published declarations such as `lib/types`, and neither strips nor adds source maps independently. Registry packages likewise retain their published package bytes in pnpm's content-addressed store. The root dsh package and Electron package must have the same version, but dsh does not need to be published to npm before the Desktop application is built. `prepare:runtime` downloads Node.js 24.17.0 from the official Node.js release service, verifies its SHA-256 entry before extraction, and executes the prepared binary on a compatible build host to verify its reported version. It copies the pnpm version declared by the desktop package and records both runtime versions in the release seed. `prepare:seed` generates local core-package mappings, uses bundled pnpm to fetch external production dependencies from npm, proves that the complete graph installs offline with the matching Host entry, removes `node_modules`, removes temporary pnpm project registrations, and replaces the loose store with 16 deterministic uncompressed tar shards before inventory generation. Later GUI plugin operations retain the local core mappings while resolving plugin packages and their external dependencies from the fixed Desktop npm registry. `electron-builder` emits platform artifacts under `apps/desktop/.desktop-build/artifacts`.
|
||||
Every package command performs the official repository build, packs the dsh and vendored package families, and packs the Landlock entry before preparing release resources. `prepare:packages` selects the first-party production closure rooted at `@deepseek-ai/dsh`, verifies that its tarball contains `lib/desktop-host.js`, copies the selected tarballs into the seed input, and records their sizes and SHA-512 integrity. These are the official `pnpm pack` outputs, so each package's `files` manifest controls its published contents: Desktop adds no second filter, retains published declarations such as `lib/types`, and neither strips nor adds source maps independently. Registry resolution, package paths, manifests, and non-native bytes remain npm-owned. For macOS, `prepare:seed` replaces each Mach-O CAS object with the company Developer ID signed bytes, writes them at their new SHA-512 paths, and transactionally rewrites every base or side-effects index reference; it preserves the package file set, including bundled architecture variants. The root dsh package and Electron package must have the same version, but dsh does not need to be published to npm before the Desktop application is built. `prepare:runtime` downloads Node.js 24.17.0 from the official Node.js release service, verifies its SHA-256 entry before extraction, and executes the prepared binary on a compatible build host to verify its reported version. It copies the pnpm version declared by the desktop package and records both runtime versions in the release seed. `prepare:seed` generates local core-package mappings, uses bundled pnpm to fetch external production dependencies from npm, proves the graph installs offline, performs the macOS rewrite when applicable, proves the rewritten store with another offline installation, removes temporary pnpm project registrations, and replaces the loose store with 16 deterministic uncompressed tar shards. It extracts those final shards and verifies every embedded macOS signature before inventory generation. Later GUI plugin operations retain the local core mappings while resolving plugin packages and their external dependencies from the fixed Desktop npm registry. `electron-builder` emits platform artifacts under `apps/desktop/.desktop-build/artifacts`.
|
||||
|
||||
An unpacked artifact contains four independent size contributors: Electron, the offline seed store shards and local dsh tarballs, the upstream Node.js and pnpm runtime, and the small shell application. The shards are uncompressed so the outer DMG, ZIP, or NSIS compressor and differential updater can operate on stable ranges. Filesystem size is not installer download size, so measure both separately. First packaged startup also extracts the seed store into `$DSH_HOME/desktop/pnpm/store` before installing the writable profile, so release qualification must measure both application and Harness-home disk use.
|
||||
|
||||
@@ -106,7 +119,7 @@ An unpacked artifact contains four independent size contributors: Electron, the
|
||||
|
||||
A packaged application checks its configured release stream ten seconds after the main window opens; the **检查更新…** menu item triggers the same check manually. An available release opens one native confirmation dialog. Accepting it downloads and verifies the signed Desktop release, stops the dsh child, and hands installation plus restart to electron-updater. The next launch reconciles the version-bound seed before reopening the product window. A build without updater configuration performs no network update request and reports that it is current.
|
||||
|
||||
Release builds set `DSH_DESKTOP_SHELL_UPDATE_URL` to the generic update server used by electron-updater. With this setting, electron-builder emits the channel metadata that must be published with the blockmaps and installers; an unconfigured local build omits that metadata. NSIS differential packages and the macOS ZIP target allow electron-updater to reuse unchanged blocks; the seed and shell still form one signed Desktop release. Code-signing and macOS notarization credentials use electron-builder's standard environment.
|
||||
Release builds set `DSH_DESKTOP_SHELL_UPDATE_URL` to the generic update server used by electron-updater. With this setting, electron-builder emits the channel metadata that must be published with the update blockmaps and installers; an unconfigured local build omits that metadata. NSIS differential packages and the macOS ZIP target allow electron-updater to reuse unchanged blocks; the manually installed DMG is notarized without a blockmap because it is not a macOS updater payload. The seed and shell still form one signed Desktop release. Windows signing and macOS notarization credentials use electron-builder's standard environment; the required Desktop release environment selects the application and macOS signature identities that the build verifies.
|
||||
|
||||
## Low-level development overrides
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| 发布身份 | 桌面壳 API、Web 客户端、后端与插件依赖图作为一个组合完成验证;独立版本会产生未经验证的组合,并让更新可用性含糊不清。 | Electron 与 `@deepseek-ai/dsh` 始终使用同一精确版本。即使桌面壳代码不变,升级 dsh 也必须发布新 Desktop 版本。 |
|
||||
| 运行时 | Electron 的 Node.js 带有 Electron 补丁、fuse、ABI 与生命周期约束,而系统运行时和用户包管理器状态不可控。 | dsh 通过内置的上游 Node.js 运行,所有包操作都使用内置 pnpm。Electron 的 Node.js、系统 Node.js、系统 pnpm 与用户的包管理器配置都不进入执行路径。 |
|
||||
| 包来源 | 必须能在发布到 npm 之前从同一次源码构建打包精确的 dsh,并支持离线安装;插件则需要保留为用户选择的普通 npm 包。 | 已签名应用携带本地打包的第一方 dsh 包与离线 seed store。桌面插件仍是从固定 Desktop registry 解析的普通 npm 依赖。 |
|
||||
| Seed 传输 | 把 pnpm store 的每个文件分别放入应用,会让代码签名记录数万个不可变缓存条目并增大更新元数据;单个压缩归档又会让很小的包变化改写一大片数据块。 | 打包按路径确定性地把 store 文件分配到 16 个未压缩 tar 分片。签名只记录分片,外层安装包负责压缩,差分更新可以复用未变化的分片。 |
|
||||
| Seed 传输 | Apple 公证会检查归档内的代码;把 pnpm store 的每个文件分别放入应用,还会让应用签名记录数万个缓存条目,而单个压缩归档会放大小幅包变更。 | macOS 打包先签署每个 Mach-O CAS 对象、重写其 pnpm 哈希并再次证明离线安装,再把 store 文件分配到 16 个确定性的未压缩 tar 分片。外层安装包负责压缩,差分更新可以复用未变化的分片。 |
|
||||
| 状态归属 | 共享可执行依赖图会让 CLI 与 Desktop 相互改变 dsh、Cordis、插件或原生模块版本。 | Electron 独占 `$DSH_HOME/profiles/desktop` 及其包管理器状态。CLI 与 Desktop 共享 `$DSH_HOME` 下受支持的产品数据,但绝不共享可执行包、插件激活、锁文件或 `node_modules`。 |
|
||||
| 通信 | 监听 Web 服务会引入端口归属、认证、CORS 与暴露风险;Electron 与上游 Node.js 之间也需要明确的跨进程协议。 | 应用不打开 Web 端口。`dsh-app://` 承载 Web 资源和 Fetch 流量;分帧字节管道以背压传输有界请求与响应分块,Node IPC 只承载子进程生命周期控制。 |
|
||||
| 激活 | 依赖解析、生命周期脚本、原生模块与插件启动都可能失败,目录替换期间进程也可能中断。 | 发布与插件变更先安装到 staging,并启动完整后端执行健康检查;只有成功后才替换活跃 profile,中断替换由事务日志和一个 rollback profile 恢复。 |
|
||||
@@ -27,7 +27,7 @@ dsh 主渲染进程只获得桌面协议标记。独立插件窗口获得结构
|
||||
|
||||
### Seed 安装
|
||||
|
||||
安装包内的 seed 是安装工具包,不是可以直接运行的 `node_modules` 目录。打包过程会生成锁文件、拉取生产依赖图、用匹配的 Desktop Host 入口完成一次完整离线安装验证,然后删除 `node_modules`。签名 seed 保留发布身份、本地第一方 tarball 及其描述文件、项目元数据、锁文件、完整性清单,以及在用户机器上重复该安装所需的 pnpm store 内容。
|
||||
安装包内的 seed 是安装工具包,不是可以直接运行的 `node_modules` 目录。打包过程会生成锁文件、拉取生产依赖图,并用匹配的 Desktop Host 入口完成一次完整离线安装验证。macOS 构建随后用 Developer ID 签署 pnpm 内容寻址 store 中的每个 Mach-O 对象,更新所有受影响的 SHA-512 索引记录,再用一次离线安装证明重写后的 store,最后删除 `node_modules`。签名 seed 保留发布身份、本地第一方 tarball 及其描述文件、项目元数据、锁文件、完整性清单,以及在用户机器上重复该安装所需的 pnpm store 内容。
|
||||
|
||||
| Seed 内容 | 可写目标或用途 |
|
||||
|---|---|
|
||||
@@ -67,7 +67,18 @@ Workspace 开发使用调用命令的 Node.js 运行当前 CLI 包,并禁用
|
||||
|
||||
## 打包
|
||||
|
||||
正常打包只需执行一条完整命令。该命令会先准备发布资源,再生成宿主平台的安装包;配置发布信息后还会生成更新元数据。无需提前执行 `prepare:desktop`:
|
||||
正常打包只需执行一条完整命令。该命令会先准备发布资源,再生成宿主平台的安装包;配置发布信息后还会生成更新元数据。所有目标都要求通过 `DSH_DESKTOP_APP_ID` 提供反向域名形式的应用 ID。macOS 目标还要求通过 `DSH_DESKTOP_MACOS_SIGNING_IDENTITY` 提供 electron-builder 证书限定名,通过 `DSH_DESKTOP_MACOS_TEAM_ID` 提供对应的 10 字符 Apple Team ID,并提供一套完整的 notarytool 凭据。App Store Connect API Key 方式使用以下变量:
|
||||
|
||||
```sh
|
||||
export DSH_DESKTOP_APP_ID='<reverse-DNS application ID>'
|
||||
export DSH_DESKTOP_MACOS_SIGNING_IDENTITY='<certificate name without the Developer ID Application prefix>'
|
||||
export DSH_DESKTOP_MACOS_TEAM_ID='<10-character Apple Team ID>'
|
||||
export APPLE_API_KEY='<absolute path to the .p8 file>'
|
||||
export APPLE_API_KEY_ID='<App Store Connect API Key ID>'
|
||||
export APPLE_API_ISSUER='<App Store Connect issuer UUID>'
|
||||
```
|
||||
|
||||
无需提前执行 `prepare:desktop`:
|
||||
|
||||
```sh
|
||||
pnpm run package:desktop
|
||||
@@ -83,6 +94,8 @@ pnpm run package:desktop:win:x64
|
||||
|
||||
macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS 或带 Rosetta 的 Apple Silicon 上运行。Windows x64 命令要求 Windows x64。Desktop 尚不支持 Linux 发布目标。
|
||||
|
||||
macOS 配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 打包要求已配置的身份及其私钥可用。Seed 准备会把该身份、安全时间戳与 hardened runtime 应用到每个内嵌 Mach-O 文件;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。Electron-builder 会在封装前公证应用并钉票,然后签署 DMG。DMG 的 artifact-completion hook 随后会公证它并钉票,再要求其身份、票据与 Gatekeeper 验证全部通过;只有 hook 成功,electron-builder 才能发布该文件。私钥可以来自登录钥匙串或 electron-builder 的标准 `CSC_LINK` 输入;环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>` 重复应用检查时,也必须提供两个 macOS 身份变量。
|
||||
|
||||
使用对应的 `:dir` 命令可以生成可直接运行的应用目录,而不是安装包,例如:
|
||||
|
||||
```sh
|
||||
@@ -98,7 +111,7 @@ pnpm run prepare:desktop
|
||||
|
||||
这条诊断命令是另一种停止位置,并非两条命令构建流程的前半段。之后执行 `package:desktop*` 时仍会重新完成正式构建与准备,避免使用陈旧的 dsh 包、运行时文件或 seed 内容。
|
||||
|
||||
每条打包命令都会先执行仓库的正式构建,打包 dsh 与 vendored 包族,并打包 Landlock 入口,然后再准备发布资源。`prepare:packages` 选择以 `@deepseek-ai/dsh` 为根的第一方生产依赖闭包,验证 dsh tarball 包含 `lib/desktop-host.js`,把选中的 tarball 复制到种子输入,并记录其大小与 SHA-512 完整性。这些 tarball 是正式的 `pnpm pack` 输出,因此各包的 `files` manifest 决定发布内容:Desktop 不增加第二套过滤规则,会保留 `lib/types` 等已发布声明,也不会独立删除或增加 source map。Registry 包同样在 pnpm 内容寻址 store 中保留其发布的包字节。根 dsh 包与 Electron 包必须使用同一版本,但构建 Desktop 应用前不再要求 dsh 已发布到 npm。`prepare:runtime` 从 Node.js 官方发行服务下载 Node.js 24.17.0,在解压前验证其 SHA-256 条目,并在兼容的构建宿主上执行准备完成的二进制文件以验证其报告版本。它复制桌面包声明的 pnpm 版本,并把两个运行时版本记录进发布种子。`prepare:seed` 生成本地核心包映射,使用内置 pnpm 从 npm 拉取外部生产依赖,证明完整依赖图可以离线安装并包含匹配的 Host 入口,删除 `node_modules` 和临时 pnpm 项目注册,再把松散 store 替换为 16 个确定性的未压缩 tar 分片,然后生成清单。后续 GUI 插件操作保留本地核心包映射,同时从固定的 Desktop npm registry 解析插件包及其外部依赖。`electron-builder` 把平台产物写到 `apps/desktop/.desktop-build/artifacts`。
|
||||
每条打包命令都会先执行仓库的正式构建,打包 dsh 与 vendored 包族,并打包 Landlock 入口,然后再准备发布资源。`prepare:packages` 选择以 `@deepseek-ai/dsh` 为根的第一方生产依赖闭包,验证 dsh tarball 包含 `lib/desktop-host.js`,把选中的 tarball 复制到种子输入,并记录其大小与 SHA-512 完整性。这些 tarball 是正式的 `pnpm pack` 输出,因此各包的 `files` manifest 决定发布内容:Desktop 不增加第二套过滤规则,会保留 `lib/types` 等已发布声明,也不会独立删除或增加 source map。Registry 解析、包路径、manifest 和非原生字节仍由 npm 管理。在 macOS 上,`prepare:seed` 会用公司 Developer ID 签名字节替换每个 Mach-O CAS 对象,把它们写到新的 SHA-512 路径,并以事务方式重写所有基础或 side-effects 索引引用;它保留完整包文件集,包括包内附带的架构变体。根 dsh 包与 Electron 包必须使用同一版本,但构建 Desktop 应用前不再要求 dsh 已发布到 npm。`prepare:runtime` 从 Node.js 官方发行服务下载 Node.js 24.17.0,在解压前验证其 SHA-256 条目,并在兼容的构建宿主上执行准备完成的二进制文件以验证其报告版本。它复制桌面包声明的 pnpm 版本,并把两个运行时版本记录进发布种子。`prepare:seed` 生成本地核心包映射,使用内置 pnpm 从 npm 拉取外部生产依赖,证明依赖图可以离线安装,在适用时执行 macOS 重写,再通过一次离线安装证明重写后的 store,删除临时 pnpm 项目注册,然后把松散 store 替换为 16 个确定性的未压缩 tar 分片。它会解包这些最终分片,并在生成清单前验证每个内嵌 macOS 签名。后续 GUI 插件操作保留本地核心包映射,同时从固定的 Desktop npm registry 解析插件包及其外部依赖。`electron-builder` 把平台产物写到 `apps/desktop/.desktop-build/artifacts`。
|
||||
|
||||
未压缩产物包含四块相互独立的体积:Electron、离线 seed store 分片与本地 dsh tarball、上游 Node.js 与 pnpm 运行时,以及很小的桌面壳应用。分片不压缩,使外层 DMG、ZIP 或 NSIS 压缩器与差分更新器可以处理稳定的数据区间。文件系统占用不等于安装包下载大小,因此必须分别测量。打包应用首次启动时还会先把 seed store 解包到 `$DSH_HOME/desktop/pnpm/store`,再安装可写 profile,因此发布验证必须同时测量应用与 Harness home 的磁盘占用。
|
||||
|
||||
@@ -106,7 +119,7 @@ pnpm run prepare:desktop
|
||||
|
||||
打包应用会在主窗口打开十秒后检查已配置的发布流;**检查更新…** 菜单项会手动触发同一检查。发现可用版本时,应用打开一个原生确认弹窗。用户确认后,应用下载并验证已签名的 Desktop 发布、停止 dsh 子进程,并把安装与重启交给 electron-updater。下次启动会先校准版本绑定的 seed,再重新打开产品窗口。没有 updater 配置的构建不会发起网络更新请求,并会报告当前已是最新版本。
|
||||
|
||||
发布构建通过 `DSH_DESKTOP_SHELL_UPDATE_URL` 配置 electron-updater 使用的 generic 更新服务。设置该变量后,electron-builder 会生成需要与 blockmap 和安装包一起发布的频道元数据;未配置的本地构建不会生成该元数据。NSIS 差分包与 macOS ZIP 目标让 electron-updater 可以复用未变化的数据块;seed 与桌面壳仍属于同一个签名 Desktop 发布。代码签名与 macOS 公证凭据使用 electron-builder 的标准环境变量。
|
||||
发布构建通过 `DSH_DESKTOP_SHELL_UPDATE_URL` 配置 electron-updater 使用的 generic 更新服务。设置该变量后,electron-builder 会生成需要与更新 blockmap 和安装包一起发布的频道元数据;未配置的本地构建不会生成该元数据。NSIS 差分包与 macOS ZIP 目标让 electron-updater 可以复用未变化的数据块;供手动安装的 DMG 经过公证,但不生成 blockmap,因为它不是 macOS updater 的载荷。Seed 与桌面壳仍属于同一个签名 Desktop 发布。Windows 签名和 macOS 公证凭据使用 electron-builder 的标准环境变量;必填 Desktop 发布环境选择构建所验证的应用身份与 macOS 签名身份。
|
||||
|
||||
## 底层开发覆盖项
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/** Electron-builder fields asserted by the Desktop release tests. */
|
||||
export interface DesktopElectronBuilderConfig {
|
||||
readonly appId: string
|
||||
readonly mac: {
|
||||
readonly identity: string | undefined
|
||||
readonly forceCodeSigning: boolean
|
||||
readonly notarize: boolean
|
||||
}
|
||||
readonly dmg: {
|
||||
readonly sign: boolean
|
||||
readonly writeUpdateInfo: boolean
|
||||
}
|
||||
readonly artifactBuildCompleted: (artifact: { readonly file: string }) => Promise<void> | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Create electron-builder configuration from one release environment.
|
||||
* @param env - Packaging environment.
|
||||
* @param hostPlatform - Build-host platform used when no explicit target is present.
|
||||
* @returns electron-builder configuration.
|
||||
*/
|
||||
export function createElectronBuilderConfig(
|
||||
env?: NodeJS.ProcessEnv,
|
||||
hostPlatform?: NodeJS.Platform,
|
||||
): DesktopElectronBuilderConfig
|
||||
|
||||
declare const electronBuilderConfig: DesktopElectronBuilderConfig
|
||||
|
||||
export default electronBuilderConfig
|
||||
@@ -1,39 +1,80 @@
|
||||
const publishUrl = process.env.DSH_DESKTOP_SHELL_UPDATE_URL
|
||||
import {
|
||||
resolveDesktopAppId,
|
||||
resolveMacOSNotarizationEnvironment,
|
||||
resolveMacOSSigningEnvironment,
|
||||
} from './scripts/desktop-release-environment.mjs'
|
||||
import { notarizeMacOSDiskImageArtifact } from './scripts/notarize-macos-disk-images.mjs'
|
||||
import { verifyMacOSSignatureAfterSign } from './scripts/verify-macos-signature.mjs'
|
||||
|
||||
export default {
|
||||
appId: 'com.deepseek.dsh',
|
||||
productName: 'DeepSeek Harness',
|
||||
artifactName: 'deepseek-harness-${version}-${os}-${arch}.${ext}',
|
||||
directories: { output: '.desktop-build/artifacts' },
|
||||
asar: true,
|
||||
files: [
|
||||
'lib/*.js',
|
||||
'lib/*.cjs',
|
||||
'renderer/**/*',
|
||||
'package.json',
|
||||
],
|
||||
extraResources: [
|
||||
{ from: '.desktop-build/runtime', to: 'runtime' },
|
||||
{ from: '.desktop-build/seed', to: 'seed' },
|
||||
],
|
||||
mac: {
|
||||
category: 'public.app-category.developer-tools',
|
||||
hardenedRuntime: true,
|
||||
target: ['dmg', 'zip'],
|
||||
},
|
||||
win: {
|
||||
target: ['nsis'],
|
||||
},
|
||||
linux: {
|
||||
category: 'Development',
|
||||
target: ['AppImage'],
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
differentialPackage: true,
|
||||
},
|
||||
publish: publishUrl === undefined || publishUrl === ''
|
||||
? null
|
||||
: [{ provider: 'generic', url: publishUrl }],
|
||||
/**
|
||||
* Create electron-builder configuration from one release environment.
|
||||
* @param {NodeJS.ProcessEnv} env - Packaging environment.
|
||||
* @param {NodeJS.Platform} hostPlatform - Build-host platform used when no explicit target is present.
|
||||
* @returns {object} electron-builder configuration.
|
||||
*/
|
||||
export function createElectronBuilderConfig(env = process.env, hostPlatform = process.platform) {
|
||||
const appId = resolveDesktopAppId(env)
|
||||
const targetPlatform = env.DSH_DESKTOP_TARGET_PLATFORM
|
||||
const packagesMacOS = targetPlatform === 'darwin' || (targetPlatform === undefined && hostPlatform === 'darwin')
|
||||
const macOSSigning = packagesMacOS ? resolveMacOSSigningEnvironment(env) : undefined
|
||||
if (packagesMacOS) resolveMacOSNotarizationEnvironment(env)
|
||||
const publishUrl = env.DSH_DESKTOP_SHELL_UPDATE_URL
|
||||
return {
|
||||
appId,
|
||||
productName: 'DeepSeek Harness',
|
||||
artifactName: 'deepseek-harness-${version}-${os}-${arch}.${ext}',
|
||||
directories: { output: '.desktop-build/artifacts' },
|
||||
asar: true,
|
||||
files: [
|
||||
'lib/*.js',
|
||||
'lib/*.cjs',
|
||||
'renderer/**/*',
|
||||
'package.json',
|
||||
],
|
||||
extraResources: [
|
||||
{ from: '.desktop-build/runtime', to: 'runtime' },
|
||||
{ from: '.desktop-build/seed', to: 'seed' },
|
||||
],
|
||||
mac: {
|
||||
category: 'public.app-category.developer-tools',
|
||||
identity: macOSSigning?.signingIdentity,
|
||||
forceCodeSigning: true,
|
||||
hardenedRuntime: true,
|
||||
notarize: true,
|
||||
target: ['dmg', 'zip'],
|
||||
},
|
||||
dmg: {
|
||||
sign: true,
|
||||
writeUpdateInfo: false,
|
||||
},
|
||||
afterSign: context => {
|
||||
if (context.electronPlatformName !== 'darwin') return
|
||||
verifyMacOSSignatureAfterSign(context, macOSSigning ?? resolveMacOSSigningEnvironment(env))
|
||||
},
|
||||
artifactBuildCompleted: artifact => {
|
||||
if (!artifact.file.endsWith('.dmg')) return
|
||||
return notarizeMacOSDiskImageArtifact(
|
||||
artifact,
|
||||
env,
|
||||
macOSSigning ?? resolveMacOSSigningEnvironment(env),
|
||||
)
|
||||
},
|
||||
win: {
|
||||
target: ['nsis'],
|
||||
},
|
||||
linux: {
|
||||
category: 'Development',
|
||||
target: ['AppImage'],
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
differentialPackage: true,
|
||||
},
|
||||
publish: publishUrl === undefined || publishUrl === ''
|
||||
? null
|
||||
: [{ provider: 'generic', url: publishUrl }],
|
||||
}
|
||||
}
|
||||
|
||||
export default createElectronBuilderConfig()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-desktop",
|
||||
"description": "Electron desktop shell for an isolated pnpm-installed dsh runtime",
|
||||
"version": "0.1.2-alpha.1",
|
||||
"version": "0.1.2-alpha.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
@@ -13,6 +14,7 @@
|
||||
"prepare:packages": "tsx scripts/prepare-package-set.ts",
|
||||
"prepare:seed": "tsx scripts/prepare-seed.ts",
|
||||
"prepare:package": "tsx scripts/package-target.ts --prepare-only",
|
||||
"verify:mac-signature": "node scripts/verify-macos-signature.mjs",
|
||||
"package": "tsx scripts/package-target.ts",
|
||||
"package:dir": "tsx scripts/package-target.ts --dir",
|
||||
"package:mac:arm64": "tsx scripts/package-target.ts mac-arm64",
|
||||
@@ -28,11 +30,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:^",
|
||||
"@electron/notarize": "2.5.0",
|
||||
"@types/node": "^22.20.0",
|
||||
"@types/semver": "^7.8.0",
|
||||
"electron": "^44.0.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"extract-zip": "^2.0.1",
|
||||
"msgpackr": "2.0.4",
|
||||
"pnpm": "11.7.0",
|
||||
"tar": "^7.5.0",
|
||||
"typescript": "^6.0.3"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/** Environment variable that supplies the Electron application identifier. */
|
||||
export const DESKTOP_APP_ID_ENV: 'DSH_DESKTOP_APP_ID'
|
||||
|
||||
/** Environment variable that supplies electron-builder's macOS certificate qualifier. */
|
||||
export const MACOS_SIGNING_IDENTITY_ENV: 'DSH_DESKTOP_MACOS_SIGNING_IDENTITY'
|
||||
|
||||
/** Environment variable that supplies the expected Apple Developer Team ID. */
|
||||
export const MACOS_TEAM_ID_ENV: 'DSH_DESKTOP_MACOS_TEAM_ID'
|
||||
|
||||
/** Public identity expected on a macOS release. */
|
||||
export interface MacOSSigningEnvironment {
|
||||
readonly signingIdentity: string
|
||||
readonly teamId: string
|
||||
}
|
||||
|
||||
/** Apple ID credentials accepted by notarytool. */
|
||||
export interface MacOSAppleIdNotarizationEnvironment {
|
||||
readonly appleId: string
|
||||
readonly appleIdPassword: string
|
||||
readonly teamId: string
|
||||
}
|
||||
|
||||
/** App Store Connect API credentials accepted by notarytool. */
|
||||
export interface MacOSApiKeyNotarizationEnvironment {
|
||||
readonly appleApiKey: string
|
||||
readonly appleApiKeyId: string
|
||||
readonly appleApiIssuer: string
|
||||
}
|
||||
|
||||
/** Keychain profile accepted by notarytool. */
|
||||
export interface MacOSKeychainNotarizationEnvironment {
|
||||
readonly keychainProfile: string
|
||||
readonly keychain?: string
|
||||
}
|
||||
|
||||
/** One complete credential strategy accepted by notarytool. */
|
||||
export type MacOSNotarizationEnvironment =
|
||||
| MacOSAppleIdNotarizationEnvironment
|
||||
| MacOSApiKeyNotarizationEnvironment
|
||||
| MacOSKeychainNotarizationEnvironment
|
||||
|
||||
/**
|
||||
* Resolve and validate the application identifier shared by every platform target.
|
||||
* @param env - Packaging environment.
|
||||
* @returns Reverse-DNS application identifier.
|
||||
*/
|
||||
export function resolveDesktopAppId(env: NodeJS.ProcessEnv): string
|
||||
|
||||
/**
|
||||
* Resolve and validate the public identity expected on a macOS release.
|
||||
* @param env - Packaging environment.
|
||||
* @returns Expected certificate qualifier and Team ID.
|
||||
*/
|
||||
export function resolveMacOSSigningEnvironment(env: NodeJS.ProcessEnv): MacOSSigningEnvironment
|
||||
|
||||
/**
|
||||
* Resolve one complete credential set accepted by Apple's notary service.
|
||||
* @param env - Packaging environment.
|
||||
* @returns Notary credentials without the submitted artifact path.
|
||||
*/
|
||||
export function resolveMacOSNotarizationEnvironment(env: NodeJS.ProcessEnv): MacOSNotarizationEnvironment
|
||||
@@ -0,0 +1,98 @@
|
||||
/** Resolve public release identifiers supplied by the packaging environment. */
|
||||
|
||||
/** Environment variable that supplies the Electron application identifier. */
|
||||
export const DESKTOP_APP_ID_ENV = 'DSH_DESKTOP_APP_ID'
|
||||
|
||||
/** Environment variable that supplies electron-builder's macOS certificate qualifier. */
|
||||
export const MACOS_SIGNING_IDENTITY_ENV = 'DSH_DESKTOP_MACOS_SIGNING_IDENTITY'
|
||||
|
||||
/** Environment variable that supplies the expected Apple Developer Team ID. */
|
||||
export const MACOS_TEAM_ID_ENV = 'DSH_DESKTOP_MACOS_TEAM_ID'
|
||||
|
||||
const APPLE_API_KEY_ENV = 'APPLE_API_KEY'
|
||||
const APPLE_API_KEY_ID_ENV = 'APPLE_API_KEY_ID'
|
||||
const APPLE_API_ISSUER_ENV = 'APPLE_API_ISSUER'
|
||||
const APPLE_ID_ENV = 'APPLE_ID'
|
||||
const APPLE_APP_SPECIFIC_PASSWORD_ENV = 'APPLE_APP_SPECIFIC_PASSWORD'
|
||||
const APPLE_TEAM_ID_ENV = 'APPLE_TEAM_ID'
|
||||
const APPLE_KEYCHAIN_ENV = 'APPLE_KEYCHAIN'
|
||||
const APPLE_KEYCHAIN_PROFILE_ENV = 'APPLE_KEYCHAIN_PROFILE'
|
||||
|
||||
/**
|
||||
* Read one required non-empty environment variable.
|
||||
* @param {NodeJS.ProcessEnv} env - Packaging environment.
|
||||
* @param {string} name - Required variable name.
|
||||
* @returns {string} Trimmed variable value.
|
||||
*/
|
||||
function requireEnvironmentValue(env, name) {
|
||||
const value = env[name]?.trim()
|
||||
if (value === undefined || value === '') {
|
||||
throw new Error(`desktop release environment: ${name} must be set to a non-empty value`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and validate the application identifier shared by every platform target.
|
||||
* @param {NodeJS.ProcessEnv} env - Packaging environment.
|
||||
* @returns {string} Reverse-DNS application identifier.
|
||||
*/
|
||||
export function resolveDesktopAppId(env) {
|
||||
const appId = requireEnvironmentValue(env, DESKTOP_APP_ID_ENV)
|
||||
if (!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/u.test(appId)) {
|
||||
throw new Error(`desktop release environment: ${DESKTOP_APP_ID_ENV} must be a reverse-DNS identifier`)
|
||||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and validate the public identity expected on a macOS release.
|
||||
* @param {NodeJS.ProcessEnv} env - Packaging environment.
|
||||
* @returns {{ signingIdentity: string, teamId: string }} Expected certificate qualifier and Team ID.
|
||||
*/
|
||||
export function resolveMacOSSigningEnvironment(env) {
|
||||
const signingIdentity = requireEnvironmentValue(env, MACOS_SIGNING_IDENTITY_ENV)
|
||||
if (signingIdentity.startsWith('Developer ID Application:')) {
|
||||
throw new Error(`desktop release environment: ${MACOS_SIGNING_IDENTITY_ENV} must omit the "Developer ID Application:" prefix`)
|
||||
}
|
||||
const teamId = requireEnvironmentValue(env, MACOS_TEAM_ID_ENV)
|
||||
if (!/^[A-Z0-9]{10}$/u.test(teamId)) {
|
||||
throw new Error(`desktop release environment: ${MACOS_TEAM_ID_ENV} must contain 10 uppercase letters or digits`)
|
||||
}
|
||||
return { signingIdentity, teamId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one complete credential set accepted by Apple's notary service.
|
||||
* @param {NodeJS.ProcessEnv} env - Packaging environment.
|
||||
* @returns {{ appleId: string, appleIdPassword: string, teamId: string } | { appleApiKey: string, appleApiKeyId: string, appleApiIssuer: string } | { keychainProfile: string, keychain?: string }} Notary credentials without the submitted artifact path.
|
||||
*/
|
||||
export function resolveMacOSNotarizationEnvironment(env) {
|
||||
const appleIdValues = [env[APPLE_ID_ENV], env[APPLE_APP_SPECIFIC_PASSWORD_ENV], env[APPLE_TEAM_ID_ENV]]
|
||||
if (appleIdValues.some(value => value !== undefined)) {
|
||||
return {
|
||||
appleId: requireEnvironmentValue(env, APPLE_ID_ENV),
|
||||
appleIdPassword: requireEnvironmentValue(env, APPLE_APP_SPECIFIC_PASSWORD_ENV),
|
||||
teamId: requireEnvironmentValue(env, APPLE_TEAM_ID_ENV),
|
||||
}
|
||||
}
|
||||
|
||||
const apiKeyValues = [env[APPLE_API_KEY_ENV], env[APPLE_API_KEY_ID_ENV], env[APPLE_API_ISSUER_ENV]]
|
||||
if (apiKeyValues.some(value => value !== undefined)) {
|
||||
return {
|
||||
appleApiKey: requireEnvironmentValue(env, APPLE_API_KEY_ENV),
|
||||
appleApiKeyId: requireEnvironmentValue(env, APPLE_API_KEY_ID_ENV),
|
||||
appleApiIssuer: requireEnvironmentValue(env, APPLE_API_ISSUER_ENV),
|
||||
}
|
||||
}
|
||||
|
||||
const keychainProfile = env[APPLE_KEYCHAIN_PROFILE_ENV]?.trim()
|
||||
if (keychainProfile !== undefined && keychainProfile !== '') {
|
||||
const keychain = env[APPLE_KEYCHAIN_ENV]?.trim()
|
||||
return keychain === undefined || keychain === ''
|
||||
? { keychainProfile }
|
||||
: { keychainProfile, keychain }
|
||||
}
|
||||
|
||||
throw new Error('desktop release environment: macOS packaging requires APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER; APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID; or APPLE_KEYCHAIN_PROFILE')
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/** Sign Mach-O content in a pnpm CAS without invalidating the store index. */
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import {
|
||||
chmodSync,
|
||||
closeSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
readSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, join, relative, sep } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { Packr } from 'msgpackr'
|
||||
import type { MacOSSigningEnvironment } from './desktop-release-environment.mjs'
|
||||
import { signMacOSSeedCode, verifyMacOSSeedCode } from './verify-macos-signature.mjs'
|
||||
|
||||
const MACH_O_MAGICS = new Set([
|
||||
'cafebabe',
|
||||
'cafebabf',
|
||||
'cefaedfe',
|
||||
'cffaedfe',
|
||||
'feedface',
|
||||
'feedfacf',
|
||||
'bebafeca',
|
||||
'bfbafeca',
|
||||
])
|
||||
const CAS_PATH_PATTERN = /^([0-9a-f]{2})\/([0-9a-f]{126})(-exec)?$/u
|
||||
const packr = new Packr({ moreTypes: true, useRecords: true })
|
||||
|
||||
interface PnpmStoreFileRecord {
|
||||
checkedAt: number
|
||||
digest: string
|
||||
mode: number
|
||||
size: number
|
||||
}
|
||||
|
||||
interface PnpmSideEffectsRecord {
|
||||
readonly added?: Map<string, PnpmStoreFileRecord>
|
||||
}
|
||||
|
||||
interface PnpmPackageIndexRecord {
|
||||
readonly algo?: string
|
||||
readonly files?: Map<string, PnpmStoreFileRecord>
|
||||
readonly sideEffects?: Map<string, PnpmSideEffectsRecord>
|
||||
}
|
||||
|
||||
interface DecodedIndexRow {
|
||||
readonly key: string
|
||||
readonly value: PnpmPackageIndexRecord
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
interface CasFile {
|
||||
readonly path: string
|
||||
readonly digest: string
|
||||
readonly executable: boolean
|
||||
}
|
||||
|
||||
interface FileReference {
|
||||
readonly row: DecodedIndexRow
|
||||
readonly record: PnpmStoreFileRecord
|
||||
}
|
||||
|
||||
/** Summary of native code rewritten in one pnpm store. */
|
||||
export interface MacOSSeedStoreSigningResult {
|
||||
readonly signedFiles: number
|
||||
readonly prunedOrphans: number
|
||||
readonly updatedIndexRows: number
|
||||
}
|
||||
|
||||
/** A signer used to make one writable Mach-O copy release-valid. */
|
||||
export type MacOSSeedCodeSigner = (path: string, identifier: string) => void
|
||||
|
||||
/** A verifier used to check one Mach-O file after packaging transport. */
|
||||
export type MacOSSeedCodeVerifier = (path: string) => void
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
function isStoreFileRecord(value: unknown): value is PnpmStoreFileRecord {
|
||||
if (!isRecord(value)) return false
|
||||
return typeof value.checkedAt === 'number'
|
||||
&& typeof value.digest === 'string'
|
||||
&& /^[0-9a-f]{128}$/u.test(value.digest)
|
||||
&& Number.isSafeInteger(value.mode)
|
||||
&& Number.isSafeInteger(value.size)
|
||||
}
|
||||
|
||||
function packageFileMaps(value: unknown, key: string): readonly Map<string, PnpmStoreFileRecord>[] {
|
||||
if (!isRecord(value)) throw new Error(`desktop seed signing: invalid pnpm index record ${key}`)
|
||||
const record = value as PnpmPackageIndexRecord
|
||||
if (record.algo !== undefined && record.algo !== 'sha512') {
|
||||
throw new Error(`desktop seed signing: unsupported pnpm index algorithm in ${key}`)
|
||||
}
|
||||
const maps: Map<string, PnpmStoreFileRecord>[] = []
|
||||
if (record.files !== undefined) {
|
||||
if (!(record.files instanceof Map)) throw new Error(`desktop seed signing: invalid pnpm file map in ${key}`)
|
||||
maps.push(record.files)
|
||||
}
|
||||
if (record.sideEffects !== undefined) {
|
||||
if (!(record.sideEffects instanceof Map)) {
|
||||
throw new Error(`desktop seed signing: invalid pnpm side-effects map in ${key}`)
|
||||
}
|
||||
for (const effect of record.sideEffects.values()) {
|
||||
if (!isRecord(effect)) throw new Error(`desktop seed signing: invalid pnpm side effect in ${key}`)
|
||||
if (effect.added === undefined) continue
|
||||
if (!(effect.added instanceof Map)) {
|
||||
throw new Error(`desktop seed signing: invalid pnpm side-effect file map in ${key}`)
|
||||
}
|
||||
maps.push(effect.added)
|
||||
}
|
||||
}
|
||||
for (const files of maps) {
|
||||
for (const file of files.values()) {
|
||||
if (!isStoreFileRecord(file)) throw new Error(`desktop seed signing: invalid pnpm file record in ${key}`)
|
||||
}
|
||||
}
|
||||
return maps
|
||||
}
|
||||
|
||||
function isExecutableMode(mode: number): boolean {
|
||||
return (mode & 0o111) !== 0
|
||||
}
|
||||
|
||||
function referenceKey(digest: string, executable: boolean): string {
|
||||
return `${digest}:${executable ? 'exec' : 'nonexec'}`
|
||||
}
|
||||
|
||||
function isMachO(path: string): boolean {
|
||||
const descriptor = openSync(path, 'r')
|
||||
try {
|
||||
const header = Buffer.alloc(4)
|
||||
return readSync(descriptor, header, 0, header.length, 0) === header.length
|
||||
&& MACH_O_MAGICS.has(header.toString('hex'))
|
||||
} finally {
|
||||
closeSync(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
function visitFiles(root: string): readonly string[] {
|
||||
const files: string[] = []
|
||||
const visit = (directory: string): void => {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name)
|
||||
if (entry.isSymbolicLink()) {
|
||||
throw new Error(`desktop seed signing: pnpm store contains a symbolic link: ${relative(root, path)}`)
|
||||
}
|
||||
if (entry.isDirectory()) visit(path)
|
||||
else if (entry.isFile()) files.push(path)
|
||||
else throw new Error(`desktop seed signing: unsupported pnpm store entry: ${relative(root, path)}`)
|
||||
}
|
||||
}
|
||||
visit(root)
|
||||
return files.sort((left, right) => left.localeCompare(right))
|
||||
}
|
||||
|
||||
function versionRoots(storeRoot: string): readonly string[] {
|
||||
return readdirSync(storeRoot, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && /^v\d+$/u.test(entry.name))
|
||||
.map(entry => join(storeRoot, entry.name))
|
||||
.filter(root => existsSync(join(root, 'files')))
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
}
|
||||
|
||||
function casFiles(versionRoot: string): readonly CasFile[] {
|
||||
const filesRoot = join(versionRoot, 'files')
|
||||
const result: CasFile[] = []
|
||||
for (const path of visitFiles(filesRoot)) {
|
||||
if (!isMachO(path)) continue
|
||||
const normalized = relative(filesRoot, path).split(sep).join('/')
|
||||
const match = CAS_PATH_PATTERN.exec(normalized)
|
||||
if (match === null) {
|
||||
throw new Error(`desktop seed signing: Mach-O content has an unsupported pnpm CAS path: ${normalized}`)
|
||||
}
|
||||
result.push({
|
||||
path,
|
||||
digest: `${match[1]}${match[2]}`,
|
||||
executable: match[3] !== undefined,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function readIndexRows(database: DatabaseSync): readonly DecodedIndexRow[] {
|
||||
const rows: DecodedIndexRow[] = []
|
||||
for (const row of database.prepare('SELECT key, data FROM package_index').iterate() as Iterable<{
|
||||
key: string
|
||||
data: Uint8Array
|
||||
}>) {
|
||||
rows.push({ key: row.key, value: packr.unpack(row.data) as PnpmPackageIndexRecord, changed: false })
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
function fileReferences(rows: readonly DecodedIndexRow[]): ReadonlyMap<string, readonly FileReference[]> {
|
||||
const references = new Map<string, FileReference[]>()
|
||||
for (const row of rows) {
|
||||
for (const files of packageFileMaps(row.value, row.key)) {
|
||||
for (const record of files.values()) {
|
||||
const key = referenceKey(record.digest, isExecutableMode(record.mode))
|
||||
const values = references.get(key) ?? []
|
||||
values.push({ row, record })
|
||||
references.set(key, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
return references
|
||||
}
|
||||
|
||||
function writeCasFile(path: string, body: Buffer, mode: number): void {
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
try {
|
||||
writeFileSync(path, body, { flag: 'wx', mode })
|
||||
} catch (error) {
|
||||
if (!isRecord(error) || error.code !== 'EEXIST' || !readFileSync(path).equals(body)) throw error
|
||||
}
|
||||
chmodSync(path, mode)
|
||||
}
|
||||
|
||||
function signedCasPath(versionRoot: string, digest: string, executable: boolean): string {
|
||||
return join(
|
||||
versionRoot,
|
||||
'files',
|
||||
digest.slice(0, 2),
|
||||
`${digest.slice(2)}${executable ? '-exec' : ''}`,
|
||||
)
|
||||
}
|
||||
|
||||
function rewriteVersionStore(
|
||||
versionRoot: string,
|
||||
appId: string,
|
||||
signer: MacOSSeedCodeSigner,
|
||||
): MacOSSeedStoreSigningResult {
|
||||
const databasePath = join(versionRoot, 'index.db')
|
||||
if (!existsSync(databasePath)) {
|
||||
throw new Error(`desktop seed signing: pnpm store has no package index: ${databasePath}`)
|
||||
}
|
||||
const database = new DatabaseSync(databasePath)
|
||||
const workRoot = mkdtempSync(join(tmpdir(), 'dsh-desktop-seed-signing-'))
|
||||
const obsoleteFiles = new Set<string>()
|
||||
let signedFiles = 0
|
||||
let prunedOrphans = 0
|
||||
let rows: readonly DecodedIndexRow[] = []
|
||||
try {
|
||||
rows = readIndexRows(database)
|
||||
const references = fileReferences(rows)
|
||||
for (const file of casFiles(versionRoot)) {
|
||||
const body = readFileSync(file.path)
|
||||
const actualDigest = createHash('sha512').update(body).digest('hex')
|
||||
if (actualDigest !== file.digest) {
|
||||
throw new Error(`desktop seed signing: pnpm CAS digest mismatch at ${file.path}`)
|
||||
}
|
||||
const fileReferences = references.get(referenceKey(file.digest, file.executable)) ?? []
|
||||
if (fileReferences.length === 0) {
|
||||
obsoleteFiles.add(file.path)
|
||||
prunedOrphans += 1
|
||||
continue
|
||||
}
|
||||
const temporary = join(workRoot, `${signedFiles.toString().padStart(4, '0')}-${basename(file.path)}`)
|
||||
copyFileSync(file.path, temporary)
|
||||
chmodSync(temporary, 0o755)
|
||||
signer(temporary, `${appId}.seed.${file.digest.slice(0, 32)}`)
|
||||
const signedBody = readFileSync(temporary)
|
||||
if (!isMachO(temporary)) {
|
||||
throw new Error(`desktop seed signing: signer produced non-Mach-O content for ${file.path}`)
|
||||
}
|
||||
const signedDigest = createHash('sha512').update(signedBody).digest('hex')
|
||||
const mode = file.executable ? 0o755 : 0o644
|
||||
const destination = signedCasPath(versionRoot, signedDigest, file.executable)
|
||||
writeCasFile(destination, signedBody, mode)
|
||||
const checkedAt = Date.now()
|
||||
for (const reference of fileReferences) {
|
||||
reference.record.checkedAt = checkedAt
|
||||
reference.record.digest = signedDigest
|
||||
reference.record.mode = mode
|
||||
reference.record.size = signedBody.length
|
||||
reference.row.changed = true
|
||||
}
|
||||
if (destination !== file.path) obsoleteFiles.add(file.path)
|
||||
signedFiles += 1
|
||||
}
|
||||
const changedRows = rows.filter(row => row.changed)
|
||||
database.exec('BEGIN IMMEDIATE')
|
||||
let committed = false
|
||||
try {
|
||||
const statement = database.prepare('INSERT OR REPLACE INTO package_index (key, data) VALUES (?, ?)')
|
||||
for (const row of changedRows) statement.run(row.key, packr.pack(row.value))
|
||||
database.exec('COMMIT')
|
||||
committed = true
|
||||
} finally {
|
||||
if (!committed) database.exec('ROLLBACK')
|
||||
}
|
||||
for (const path of obsoleteFiles) unlinkSync(path)
|
||||
database.exec('VACUUM')
|
||||
return { signedFiles, prunedOrphans, updatedIndexRows: changedRows.length }
|
||||
} finally {
|
||||
database.close()
|
||||
rmSync(workRoot, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace every Mach-O CAS object with a Developer ID signed object and update pnpm's SHA-512 index.
|
||||
* @param storeRoot - Loose pnpm store prepared for the packaged seed.
|
||||
* @param appId - Electron application ID used as the signing identifier prefix.
|
||||
* @param expected - Company Developer ID identity and Team ID.
|
||||
* @param signer - Injectable code signer used by focused tests.
|
||||
* @returns Counts for release diagnostics.
|
||||
*/
|
||||
export function signMacOSSeedStore(
|
||||
storeRoot: string,
|
||||
appId: string,
|
||||
expected: MacOSSigningEnvironment,
|
||||
signer: MacOSSeedCodeSigner = (path, identifier) => {
|
||||
signMacOSSeedCode(path, identifier, expected)
|
||||
},
|
||||
): MacOSSeedStoreSigningResult {
|
||||
const roots = versionRoots(storeRoot)
|
||||
if (roots.length === 0) throw new Error(`desktop seed signing: no pnpm store versions found in ${storeRoot}`)
|
||||
return roots.map(root => rewriteVersionStore(root, appId, signer)).reduce((total, current) => ({
|
||||
signedFiles: total.signedFiles + current.signedFiles,
|
||||
prunedOrphans: total.prunedOrphans + current.prunedOrphans,
|
||||
updatedIndexRows: total.updatedIndexRows + current.updatedIndexRows,
|
||||
}), { signedFiles: 0, prunedOrphans: 0, updatedIndexRows: 0 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that every Mach-O CAS object has the expected Developer ID, timestamp, and hardened runtime.
|
||||
* @param storeRoot - Loose or extracted pnpm store.
|
||||
* @param expected - Company Developer ID identity and Team ID.
|
||||
* @param verifier - Injectable signature verifier used by focused tests.
|
||||
* @returns Number of verified Mach-O files.
|
||||
*/
|
||||
export function verifyMacOSSeedStore(
|
||||
storeRoot: string,
|
||||
expected: MacOSSigningEnvironment,
|
||||
verifier: MacOSSeedCodeVerifier = (path) => { verifyMacOSSeedCode(path, expected) },
|
||||
): number {
|
||||
let count = 0
|
||||
for (const root of versionRoots(storeRoot)) {
|
||||
for (const file of casFiles(root)) {
|
||||
verifier(file.path)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { NotarizeOptions } from '@electron/notarize'
|
||||
import type { MacOSSigningEnvironment } from './desktop-release-environment.mjs'
|
||||
|
||||
/** Completed electron-builder artifact needed for disk-image notarization. */
|
||||
export interface DesktopBuildArtifact {
|
||||
readonly file: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit one generated DMG to Apple, staple its ticket, and verify Gatekeeper acceptance.
|
||||
* @param artifact - Completed electron-builder artifact.
|
||||
* @param env - Packaging environment.
|
||||
* @param expected - Public release identity.
|
||||
* @param submit - Notary submission implementation.
|
||||
* @param verify - Disk-image qualification implementation.
|
||||
*/
|
||||
export function notarizeMacOSDiskImageArtifact(
|
||||
artifact: DesktopBuildArtifact,
|
||||
env: NodeJS.ProcessEnv,
|
||||
expected: MacOSSigningEnvironment,
|
||||
submit?: (options: NotarizeOptions) => Promise<void>,
|
||||
verify?: (path: string, expected: MacOSSigningEnvironment) => void,
|
||||
): Promise<void>
|
||||
@@ -0,0 +1,30 @@
|
||||
/** Notarize and qualify macOS disk images after electron-builder creates them. */
|
||||
|
||||
import { notarize } from '@electron/notarize'
|
||||
import { rmSync } from 'node:fs'
|
||||
import { resolveMacOSNotarizationEnvironment } from './desktop-release-environment.mjs'
|
||||
import { verifyMacOSDiskImage } from './verify-macos-signature.mjs'
|
||||
|
||||
/**
|
||||
* Submit one generated DMG to Apple, staple its ticket, and verify Gatekeeper acceptance.
|
||||
* @param {{ file: string }} artifact - Completed electron-builder artifact.
|
||||
* @param {NodeJS.ProcessEnv} env - Packaging environment.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @param {(options: object) => Promise<void>} submit - Notary submission implementation.
|
||||
* @param {(path: string, expected: object) => void} verify - Disk-image qualification implementation.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function notarizeMacOSDiskImageArtifact(
|
||||
artifact,
|
||||
env,
|
||||
expected,
|
||||
submit = notarize,
|
||||
verify = verifyMacOSDiskImage,
|
||||
) {
|
||||
if (!artifact.file.endsWith('.dmg')) return
|
||||
rmSync(`${artifact.file}.blockmap`, { force: true })
|
||||
const credentials = resolveMacOSNotarizationEnvironment(env)
|
||||
await submit({ appPath: artifact.file, ...credentials })
|
||||
verify(artifact.file, expected)
|
||||
process.stdout.write(`desktop macOS notarization: verified disk image ${artifact.file}\n`)
|
||||
}
|
||||
@@ -14,7 +14,19 @@ import {
|
||||
readDesktopCorePackageSet,
|
||||
verifyDesktopCoreLockfile,
|
||||
} from '../src/core-package-set.ts'
|
||||
import { archivePnpmStore, removePnpmProjectRegistrations } from '../src/seed-store.ts'
|
||||
import {
|
||||
archivePnpmStore,
|
||||
extractPnpmStoreArchives,
|
||||
removePnpmProjectRegistrations,
|
||||
} from '../src/seed-store.ts'
|
||||
import {
|
||||
resolveDesktopAppId,
|
||||
resolveMacOSSigningEnvironment,
|
||||
} from './desktop-release-environment.mjs'
|
||||
import {
|
||||
signMacOSSeedStore,
|
||||
verifyMacOSSeedStore,
|
||||
} from './macos-seed-store.ts'
|
||||
|
||||
const APP_ROOT = resolve(import.meta.dirname, '..')
|
||||
const BUILD_ROOT = join(APP_ROOT, '.desktop-build')
|
||||
@@ -109,6 +121,21 @@ function inventory(root: string): readonly { path: string; bytes: number; sha256
|
||||
})
|
||||
}
|
||||
|
||||
async function verifyOfflineInstallation(release: DesktopRelease): Promise<void> {
|
||||
const installedModules = join(SEED_ROOT, 'node_modules')
|
||||
try {
|
||||
await runPnpm(['install', '--offline', '--frozen-lockfile', '--trust-lockfile'])
|
||||
const desktopHost = join(installedModules, '@deepseek-ai', 'dsh', 'lib', 'desktop-host.js')
|
||||
if (!existsSync(desktopHost)) {
|
||||
throw new Error(
|
||||
`desktop seed: local @deepseek-ai/dsh@${release.version} does not contain lib/desktop-host.js`,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
rmSync(installedModules, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
rmSync(SEED_OUTPUT_ROOT, { recursive: true, force: true })
|
||||
rmSync(PNPM_BUILD_STATE, { recursive: true, force: true })
|
||||
@@ -124,20 +151,41 @@ async function main(): Promise<void> {
|
||||
readDesktopCorePackageSet(SEED_ROOT, release.version),
|
||||
)
|
||||
await runPnpm(['fetch', '--prod', '--frozen-lockfile'])
|
||||
const installedModules = join(SEED_ROOT, 'node_modules')
|
||||
try {
|
||||
await runPnpm(['install', '--offline', '--frozen-lockfile', '--trust-lockfile'])
|
||||
const desktopHost = join(installedModules, '@deepseek-ai', 'dsh', 'lib', 'desktop-host.js')
|
||||
if (!existsSync(desktopHost)) {
|
||||
throw new Error(
|
||||
`desktop seed: local @deepseek-ai/dsh@${release.version} does not contain lib/desktop-host.js`,
|
||||
)
|
||||
await verifyOfflineInstallation(release)
|
||||
const targetPlatform = process.env.DSH_DESKTOP_TARGET_PLATFORM ?? process.platform
|
||||
let signedMachOFiles: number | undefined
|
||||
let macOSSigning: ReturnType<typeof resolveMacOSSigningEnvironment> | undefined
|
||||
if (targetPlatform === 'darwin') {
|
||||
macOSSigning = resolveMacOSSigningEnvironment(process.env)
|
||||
const signing = signMacOSSeedStore(
|
||||
STORE_ROOT,
|
||||
resolveDesktopAppId(process.env),
|
||||
macOSSigning,
|
||||
)
|
||||
signedMachOFiles = signing.signedFiles
|
||||
process.stdout.write(
|
||||
`desktop seed: signed ${signing.signedFiles} Mach-O files, updated ${signing.updatedIndexRows} pnpm index records, and pruned ${signing.prunedOrphans} native orphans\n`,
|
||||
)
|
||||
await verifyOfflineInstallation(release)
|
||||
const verified = verifyMacOSSeedStore(STORE_ROOT, macOSSigning)
|
||||
if (verified !== signedMachOFiles) {
|
||||
throw new Error(`desktop seed: verified ${verified} Mach-O files after signing ${signedMachOFiles}`)
|
||||
}
|
||||
} finally {
|
||||
rmSync(installedModules, { recursive: true, force: true })
|
||||
}
|
||||
removePnpmProjectRegistrations(STORE_ROOT)
|
||||
archivePnpmStore(SEED_ROOT, STORE_ROOT)
|
||||
if (macOSSigning !== undefined && signedMachOFiles !== undefined) {
|
||||
const extractedStore = mkdtempSync(join(tmpdir(), 'dsh-desktop-seed-verification-'))
|
||||
try {
|
||||
extractPnpmStoreArchives(SEED_ROOT, extractedStore)
|
||||
const verified = verifyMacOSSeedStore(extractedStore, macOSSigning)
|
||||
if (verified !== signedMachOFiles) {
|
||||
throw new Error(`desktop seed: archived store contains ${verified} signed Mach-O files; expected ${signedMachOFiles}`)
|
||||
}
|
||||
} finally {
|
||||
rmSync(extractedStore, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
const records = inventory(SEED_ROOT).filter(entry => entry.path !== 'integrity.json')
|
||||
writeFileSync(join(SEED_ROOT, 'integrity.json'), `${JSON.stringify({ schemaVersion: 2, files: records }, undefined, 2)}\n`)
|
||||
cpSync(SEED_ROOT, SEED_OUTPUT_ROOT, { recursive: true })
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { MacOSSigningEnvironment } from './desktop-release-environment.mjs'
|
||||
|
||||
/**
|
||||
* Reject signature metadata that does not name the company release authority and team.
|
||||
* @param details - Output from `codesign --display --verbose=4`.
|
||||
* @param expected - Public release identity.
|
||||
*/
|
||||
export function assertMacOSSignatureDetails(details: string, expected: MacOSSigningEnvironment): void
|
||||
|
||||
/**
|
||||
* Require the signature properties Apple validates for executable seed content.
|
||||
* @param details - Output from `codesign --display --verbose=4`.
|
||||
* @param expected - Public release identity.
|
||||
*/
|
||||
export function assertMacOSSeedSignatureDetails(details: string, expected: MacOSSigningEnvironment): void
|
||||
|
||||
/**
|
||||
* Sign one Mach-O file embedded in the seed store and verify Apple's required properties.
|
||||
* @param path - Writable standalone Mach-O file.
|
||||
* @param identifier - Stable code-signing identifier derived from the release app ID and CAS digest.
|
||||
* @param expected - Public release identity.
|
||||
*/
|
||||
export function signMacOSSeedCode(
|
||||
path: string,
|
||||
identifier: string,
|
||||
expected: MacOSSigningEnvironment,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Verify one Mach-O file embedded in the seed store.
|
||||
* @param path - Mach-O file to inspect.
|
||||
* @param expected - Public release identity.
|
||||
*/
|
||||
export function verifyMacOSSeedCode(path: string, expected: MacOSSigningEnvironment): void
|
||||
|
||||
/**
|
||||
* Verify the full application signature and its release owner.
|
||||
* @param appPath - Path to the packaged `.app` directory.
|
||||
* @param expected - Public release identity.
|
||||
*/
|
||||
export function verifyMacOSSignature(appPath: string, expected: MacOSSigningEnvironment): void
|
||||
|
||||
/**
|
||||
* Verify the release identity, stapled ticket, and Gatekeeper acceptance of one disk image.
|
||||
* @param diskImagePath - Path to the packaged `.dmg` file.
|
||||
* @param expected - Public release identity.
|
||||
*/
|
||||
export function verifyMacOSDiskImage(
|
||||
diskImagePath: string,
|
||||
expected: MacOSSigningEnvironment,
|
||||
): void
|
||||
|
||||
/** Electron-builder fields required to locate a signed macOS application. */
|
||||
export interface MacOSAfterSignContext {
|
||||
readonly electronPlatformName: string
|
||||
readonly appOutDir: string
|
||||
readonly packager: {
|
||||
readonly appInfo: {
|
||||
readonly productFilename: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the macOS application produced by electron-builder's signing phase.
|
||||
* @param context - electron-builder hook context.
|
||||
* @param expected - Public release identity.
|
||||
*/
|
||||
export function verifyMacOSSignatureAfterSign(
|
||||
context: MacOSAfterSignContext,
|
||||
expected: MacOSSigningEnvironment,
|
||||
): void
|
||||
@@ -0,0 +1,150 @@
|
||||
/** Verify that a packaged macOS application carries the company release identity. */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { resolve } from 'node:path'
|
||||
import { resolveMacOSSigningEnvironment } from './desktop-release-environment.mjs'
|
||||
|
||||
/**
|
||||
* Reject signature metadata that does not name the company release authority and team.
|
||||
* @param {string} details - Output from `codesign --display --verbose=4`.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function assertMacOSSignatureDetails(details, expected) {
|
||||
const fields = new Set(details.split(/\r?\n/u).map(line => line.trim()))
|
||||
const expectedAuthority = `Authority=Developer ID Application: ${expected.signingIdentity}`
|
||||
const expectedTeam = `TeamIdentifier=${expected.teamId}`
|
||||
const missing = [expectedAuthority, expectedTeam].filter(field => !fields.has(field))
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`desktop macOS signing: signature does not match the release identity; missing ${missing.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require the signature properties Apple validates for executable seed content.
|
||||
* @param {string} details - Output from `codesign --display --verbose=4`.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function assertMacOSSeedSignatureDetails(details, expected) {
|
||||
assertMacOSSignatureDetails(details, expected)
|
||||
const fields = details.split(/\r?\n/u).map(line => line.trim())
|
||||
if (!fields.some(line => /^Timestamp=.+/u.test(line))) {
|
||||
throw new Error('desktop macOS signing: seed signature has no secure timestamp')
|
||||
}
|
||||
if (!fields.some(line => /\bflags=0x[0-9a-f]+\(runtime\)(?:\s|$)/iu.test(line))) {
|
||||
throw new Error('desktop macOS signing: seed signature does not enable hardened runtime')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute one Apple release tool and return its diagnostic streams.
|
||||
* @param {string} command - Absolute executable path.
|
||||
* @param {readonly string[]} args - Tool arguments.
|
||||
* @param {string} label - Stable diagnostic name.
|
||||
* @returns {string} Combined stdout and stderr.
|
||||
*/
|
||||
function runAppleCommand(command, args, label) {
|
||||
const result = spawnSync(command, args, { encoding: 'utf8' })
|
||||
if (result.error !== undefined) {
|
||||
throw new Error(`desktop macOS signing: could not execute ${label}: ${result.error.message}`)
|
||||
}
|
||||
if (result.signal !== null) {
|
||||
throw new Error(`desktop macOS signing: ${label} was terminated by ${result.signal}`)
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
const diagnostic = `${result.stdout}${result.stderr}`.trim()
|
||||
throw new Error(`desktop macOS signing: ${label} exited with ${String(result.status)}${diagnostic === '' ? '' : `: ${diagnostic}`}`)
|
||||
}
|
||||
return `${result.stdout}${result.stderr}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Apple's code-signing tool and return its diagnostic streams.
|
||||
* @param {readonly string[]} args - Arguments passed to `/usr/bin/codesign`.
|
||||
* @returns {string} Combined stdout and stderr.
|
||||
*/
|
||||
function runCodeSign(args) {
|
||||
return runAppleCommand('/usr/bin/codesign', args, 'codesign')
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign one Mach-O file embedded in the seed store and verify Apple's required properties.
|
||||
* @param {string} path - Writable standalone Mach-O file.
|
||||
* @param {string} identifier - Stable code-signing identifier derived from the release app ID and CAS digest.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function signMacOSSeedCode(path, identifier, expected) {
|
||||
runCodeSign([
|
||||
'--force',
|
||||
'--sign', expected.signingIdentity,
|
||||
'--identifier', identifier,
|
||||
'--timestamp',
|
||||
'--options', 'runtime',
|
||||
path,
|
||||
])
|
||||
verifyMacOSSeedCode(path, expected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify one Mach-O file embedded in the seed store.
|
||||
* @param {string} path - Mach-O file to inspect.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function verifyMacOSSeedCode(path, expected) {
|
||||
runCodeSign(['--verify', '--strict', '--verbose=2', path])
|
||||
const details = runCodeSign(['--display', '--verbose=4', path])
|
||||
assertMacOSSeedSignatureDetails(details, expected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the full application signature and its release owner.
|
||||
* @param {string} appPath - Path to the packaged `.app` directory.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function verifyMacOSSignature(appPath, expected) {
|
||||
runCodeSign(['--verify', '--deep', '--strict', '--verbose=2', appPath])
|
||||
const details = runCodeSign(['--display', '--verbose=4', appPath])
|
||||
assertMacOSSignatureDetails(details, expected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the release identity, stapled ticket, and Gatekeeper acceptance of one disk image.
|
||||
* @param {string} diskImagePath - Path to the packaged `.dmg` file.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function verifyMacOSDiskImage(diskImagePath, expected) {
|
||||
runCodeSign(['--verify', '--strict', '--verbose=2', diskImagePath])
|
||||
const details = runCodeSign(['--display', '--verbose=4', diskImagePath])
|
||||
assertMacOSSignatureDetails(details, expected)
|
||||
runAppleCommand('/usr/bin/xcrun', ['stapler', 'validate', diskImagePath], 'stapler validate')
|
||||
runAppleCommand('/usr/sbin/spctl', ['--assess', '--type', 'install', '--verbose=4', diskImagePath], 'spctl')
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the macOS application produced by electron-builder's signing phase.
|
||||
* @param {{ electronPlatformName: string, appOutDir: string, packager: { appInfo: { productFilename: string } } }} context - electron-builder hook context.
|
||||
* @param {{ signingIdentity: string, teamId: string }} expected - Public release identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function verifyMacOSSignatureAfterSign(context, expected) {
|
||||
if (context.electronPlatformName !== 'darwin') return
|
||||
const appPath = resolve(context.appOutDir, `${context.packager.appInfo.productFilename}.app`)
|
||||
verifyMacOSSignature(appPath, expected)
|
||||
process.stdout.write(`desktop macOS signing: verified Developer ID Application: ${expected.signingIdentity} (${expected.teamId})\n`)
|
||||
}
|
||||
|
||||
if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) {
|
||||
const cliArgs = process.argv[2] === '--' ? process.argv.slice(3) : process.argv.slice(2)
|
||||
const appPath = cliArgs[0]
|
||||
if (appPath === undefined || cliArgs.length !== 1) {
|
||||
throw new Error('usage: node scripts/verify-macos-signature.mjs <path-to-app>')
|
||||
}
|
||||
const expected = resolveMacOSSigningEnvironment(process.env)
|
||||
verifyMacOSSignature(resolve(appPath), expected)
|
||||
process.stdout.write(`desktop macOS signing: verified Developer ID Application: ${expected.signingIdentity} (${expected.teamId})\n`)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import {
|
||||
appendFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { Packr } from 'msgpackr'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
signMacOSSeedStore,
|
||||
verifyMacOSSeedStore,
|
||||
} from '../scripts/macos-seed-store.ts'
|
||||
|
||||
const temporaryRoots: string[] = []
|
||||
const packr = new Packr({ moreTypes: true, useRecords: true })
|
||||
const SIGNING_ENVIRONMENT = {
|
||||
signingIdentity: 'Example Company (TEAMID1234)',
|
||||
teamId: 'TEAMID1234',
|
||||
}
|
||||
|
||||
function temporaryRoot(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-desktop-seed-signing-test-'))
|
||||
temporaryRoots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
function casPath(store: string, body: Buffer, executable = false): { digest: string; path: string } {
|
||||
const digest = createHash('sha512').update(body).digest('hex')
|
||||
return {
|
||||
digest,
|
||||
path: join(store, 'v11', 'files', digest.slice(0, 2), `${digest.slice(2)}${executable ? '-exec' : ''}`),
|
||||
}
|
||||
}
|
||||
|
||||
function createStoreFile(path: string, body: Buffer): void {
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, body)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('desktop macOS seed store signing', () => {
|
||||
it('rehashes signed Mach-O content, rewrites every package reference, and prunes native orphans', () => {
|
||||
const store = temporaryRoot()
|
||||
const native = Buffer.concat([Buffer.from('cffaedfe', 'hex'), Buffer.from('native-code')])
|
||||
const nativeCas = casPath(store, native)
|
||||
createStoreFile(nativeCas.path, native)
|
||||
const orphan = Buffer.concat([Buffer.from('cafebabe', 'hex'), Buffer.from('orphan')])
|
||||
const orphanCas = casPath(store, orphan)
|
||||
createStoreFile(orphanCas.path, orphan)
|
||||
const plain = Buffer.from('plain package content')
|
||||
const plainCas = casPath(store, plain)
|
||||
createStoreFile(plainCas.path, plain)
|
||||
|
||||
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 (?, ?)')
|
||||
for (const key of ['package-a', 'package-b']) {
|
||||
insert.run(key, packr.pack({
|
||||
algo: 'sha512',
|
||||
files: new Map([
|
||||
['native.node', { checkedAt: 1, digest: nativeCas.digest, mode: 0o644, size: native.length }],
|
||||
['index.js', { checkedAt: 1, digest: plainCas.digest, mode: 0o644, size: plain.length }],
|
||||
]),
|
||||
sideEffects: key === 'package-b'
|
||||
? new Map([['build', { added: new Map([
|
||||
['built/native.node', { checkedAt: 1, digest: nativeCas.digest, mode: 0o644, size: native.length }],
|
||||
]) }]])
|
||||
: undefined,
|
||||
}))
|
||||
}
|
||||
database.close()
|
||||
|
||||
const result = signMacOSSeedStore(
|
||||
store,
|
||||
'com.example.desktop',
|
||||
SIGNING_ENVIRONMENT,
|
||||
(path, identifier) => {
|
||||
expect(identifier).toBe(`com.example.desktop.seed.${nativeCas.digest.slice(0, 32)}`)
|
||||
appendFileSync(path, 'signed')
|
||||
},
|
||||
)
|
||||
|
||||
expect(result).toEqual({ signedFiles: 1, prunedOrphans: 1, updatedIndexRows: 2 })
|
||||
expect(existsSync(nativeCas.path)).toBe(false)
|
||||
expect(existsSync(orphanCas.path)).toBe(false)
|
||||
expect(readFileSync(plainCas.path)).toEqual(plain)
|
||||
|
||||
const updated = new DatabaseSync(join(store, 'v11', 'index.db'), { readOnly: true })
|
||||
const digests = [...updated.prepare('SELECT data FROM package_index').iterate() as Iterable<{ data: Uint8Array }>]
|
||||
.flatMap((row) => {
|
||||
const record = packr.unpack(row.data) as {
|
||||
files: Map<string, { digest: string }>
|
||||
sideEffects?: Map<string, { added: Map<string, { digest: string }> }>
|
||||
}
|
||||
return [
|
||||
record.files.get('native.node')?.digest,
|
||||
...[...(record.sideEffects?.values() ?? [])].map(effect => effect.added.get('built/native.node')?.digest),
|
||||
].filter((digest): digest is string => digest !== undefined)
|
||||
})
|
||||
updated.close()
|
||||
expect(new Set(digests).size).toBe(1)
|
||||
expect(digests).toHaveLength(3)
|
||||
expect(digests[0]).not.toBe(nativeCas.digest)
|
||||
|
||||
const verified: string[] = []
|
||||
expect(verifyMacOSSeedStore(store, SIGNING_ENVIRONMENT, (path) => { verified.push(path) })).toBe(1)
|
||||
expect(verified).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('propagates a signature-verification failure', () => {
|
||||
const store = temporaryRoot()
|
||||
const native = Buffer.concat([Buffer.from('feedfacf', 'hex'), Buffer.from('native-code')])
|
||||
const nativeCas = casPath(store, native)
|
||||
createStoreFile(nativeCas.path, native)
|
||||
|
||||
expect(() => {
|
||||
verifyMacOSSeedStore(store, SIGNING_ENVIRONMENT, () => {
|
||||
throw new Error('invalid signature')
|
||||
})
|
||||
}).toThrow(/invalid signature/u)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,157 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import type { NotarizeOptions } from '@electron/notarize'
|
||||
import {
|
||||
resolveDesktopAppId,
|
||||
resolveMacOSNotarizationEnvironment,
|
||||
resolveMacOSSigningEnvironment,
|
||||
} from '../scripts/desktop-release-environment.mjs'
|
||||
import { notarizeMacOSDiskImageArtifact } from '../scripts/notarize-macos-disk-images.mjs'
|
||||
import {
|
||||
assertMacOSSeedSignatureDetails,
|
||||
assertMacOSSignatureDetails,
|
||||
} from '../scripts/verify-macos-signature.mjs'
|
||||
|
||||
const RELEASE_ENVIRONMENT = {
|
||||
DSH_DESKTOP_APP_ID: 'com.example.desktop',
|
||||
DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Example Company (TEAMID1234)',
|
||||
DSH_DESKTOP_MACOS_TEAM_ID: 'TEAMID1234',
|
||||
APPLE_API_KEY: '/private/credentials/AuthKey_TEST123456.p8',
|
||||
APPLE_API_KEY_ID: 'TEST123456',
|
||||
APPLE_API_ISSUER: '11111111-2222-3333-4444-555555555555',
|
||||
}
|
||||
|
||||
describe('desktop macOS release signature', () => {
|
||||
beforeAll(() => {
|
||||
for (const [name, value] of Object.entries(RELEASE_ENVIRONMENT)) vi.stubEnv(name, value)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('loads release identifiers from the environment and requires code signing', async () => {
|
||||
const { createElectronBuilderConfig } = await import('../electron-builder.config.mjs')
|
||||
const config = createElectronBuilderConfig(RELEASE_ENVIRONMENT, 'darwin')
|
||||
expect(config).toMatchObject({
|
||||
appId: RELEASE_ENVIRONMENT.DSH_DESKTOP_APP_ID,
|
||||
mac: {
|
||||
identity: RELEASE_ENVIRONMENT.DSH_DESKTOP_MACOS_SIGNING_IDENTITY,
|
||||
forceCodeSigning: true,
|
||||
notarize: true,
|
||||
},
|
||||
dmg: {
|
||||
sign: true,
|
||||
writeUpdateInfo: false,
|
||||
},
|
||||
})
|
||||
expect(typeof config.artifactBuildCompleted).toBe('function')
|
||||
})
|
||||
|
||||
it('does not require macOS identifiers for a Windows target', async () => {
|
||||
const { createElectronBuilderConfig } = await import('../electron-builder.config.mjs')
|
||||
expect(createElectronBuilderConfig({
|
||||
DSH_DESKTOP_APP_ID: RELEASE_ENVIRONMENT.DSH_DESKTOP_APP_ID,
|
||||
DSH_DESKTOP_TARGET_PLATFORM: 'win32',
|
||||
}, 'win32').mac).toMatchObject({
|
||||
identity: undefined,
|
||||
forceCodeSigning: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts the configured authority and team', () => {
|
||||
const expected = resolveMacOSSigningEnvironment(RELEASE_ENVIRONMENT)
|
||||
expect(() => {
|
||||
assertMacOSSignatureDetails([
|
||||
`Authority=Developer ID Application: ${expected.signingIdentity}`,
|
||||
`TeamIdentifier=${expected.teamId}`,
|
||||
].join('\n'), expected)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('requires a secure timestamp and hardened runtime for seed code', () => {
|
||||
const expected = resolveMacOSSigningEnvironment(RELEASE_ENVIRONMENT)
|
||||
const details = [
|
||||
`Authority=Developer ID Application: ${expected.signingIdentity}`,
|
||||
`TeamIdentifier=${expected.teamId}`,
|
||||
'Timestamp=31 Aug 2026 at 20:00:00',
|
||||
'CodeDirectory v=20500 size=773 flags=0x10000(runtime) hashes=13+7 location=embedded',
|
||||
].join('\n')
|
||||
expect(() => { assertMacOSSeedSignatureDetails(details, expected) }).not.toThrow()
|
||||
expect(() => {
|
||||
assertMacOSSeedSignatureDetails(details.replace(/^Timestamp=.*\n/um, ''), expected)
|
||||
}).toThrow(/secure timestamp/u)
|
||||
expect(() => {
|
||||
assertMacOSSeedSignatureDetails(details.replace('flags=0x10000(runtime)', 'flags=0x0(none)'), expected)
|
||||
}).toThrow(/hardened runtime/u)
|
||||
})
|
||||
|
||||
it('rejects another developer identity', () => {
|
||||
const expected = resolveMacOSSigningEnvironment(RELEASE_ENVIRONMENT)
|
||||
expect(() => {
|
||||
assertMacOSSignatureDetails([
|
||||
'Authority=Developer ID Application: Other Company (OTHERID123)',
|
||||
'TeamIdentifier=OTHERID123',
|
||||
].join('\n'), expected)
|
||||
}).toThrow(/release identity/u)
|
||||
})
|
||||
|
||||
it('rejects an unexpected team even when the authority is present', () => {
|
||||
const expected = resolveMacOSSigningEnvironment(RELEASE_ENVIRONMENT)
|
||||
expect(() => {
|
||||
assertMacOSSignatureDetails([
|
||||
`Authority=Developer ID Application: ${expected.signingIdentity}`,
|
||||
'TeamIdentifier=OTHERID123',
|
||||
].join('\n'), expected)
|
||||
}).toThrow(`TeamIdentifier=${expected.teamId}`)
|
||||
})
|
||||
|
||||
it('rejects missing and malformed release identifiers', () => {
|
||||
expect(() => resolveDesktopAppId({})).toThrow(/DSH_DESKTOP_APP_ID/u)
|
||||
expect(() => resolveDesktopAppId({ DSH_DESKTOP_APP_ID: 'not-a-bundle-id' })).toThrow(/reverse-DNS/u)
|
||||
expect(() => resolveMacOSSigningEnvironment({})).toThrow(/DSH_DESKTOP_MACOS_SIGNING_IDENTITY/u)
|
||||
expect(() => resolveMacOSSigningEnvironment({
|
||||
DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Developer ID Application: Example Company (TEAMID1234)',
|
||||
DSH_DESKTOP_MACOS_TEAM_ID: 'TEAMID1234',
|
||||
})).toThrow(/must omit/u)
|
||||
expect(() => resolveMacOSSigningEnvironment({
|
||||
DSH_DESKTOP_MACOS_SIGNING_IDENTITY: 'Example Company (TEAMID1234)',
|
||||
DSH_DESKTOP_MACOS_TEAM_ID: 'short',
|
||||
})).toThrow(/10 uppercase/u)
|
||||
})
|
||||
|
||||
it('requires one complete notarization credential strategy', () => {
|
||||
expect(resolveMacOSNotarizationEnvironment(RELEASE_ENVIRONMENT)).toEqual({
|
||||
appleApiKey: RELEASE_ENVIRONMENT.APPLE_API_KEY,
|
||||
appleApiKeyId: RELEASE_ENVIRONMENT.APPLE_API_KEY_ID,
|
||||
appleApiIssuer: RELEASE_ENVIRONMENT.APPLE_API_ISSUER,
|
||||
})
|
||||
expect(resolveMacOSNotarizationEnvironment({
|
||||
APPLE_KEYCHAIN_PROFILE: 'dsh-notary',
|
||||
})).toEqual({ keychainProfile: 'dsh-notary' })
|
||||
expect(() => resolveMacOSNotarizationEnvironment({})).toThrow(/macOS packaging requires/u)
|
||||
expect(() => resolveMacOSNotarizationEnvironment({ APPLE_API_KEY: '/tmp/key.p8' })).toThrow(/APPLE_API_KEY_ID/u)
|
||||
})
|
||||
|
||||
it('notarizes and qualifies a DMG before electron-builder publishes it', async () => {
|
||||
const submitted: string[] = []
|
||||
const submit = vi.fn(async (options: NotarizeOptions) => { submitted.push(options.appPath) })
|
||||
const verified: string[] = []
|
||||
const verify = vi.fn((path: string) => { verified.push(path) })
|
||||
await notarizeMacOSDiskImageArtifact(
|
||||
{ file: '/tmp/release.dmg' },
|
||||
RELEASE_ENVIRONMENT,
|
||||
resolveMacOSSigningEnvironment(RELEASE_ENVIRONMENT),
|
||||
submit,
|
||||
verify,
|
||||
)
|
||||
await notarizeMacOSDiskImageArtifact(
|
||||
{ file: '/tmp/release.zip' },
|
||||
RELEASE_ENVIRONMENT,
|
||||
resolveMacOSSigningEnvironment(RELEASE_ENVIRONMENT),
|
||||
submit,
|
||||
verify,
|
||||
)
|
||||
expect(submitted).toEqual(['/tmp/release.dmg'])
|
||||
expect(verified).toEqual(['/tmp/release.dmg'])
|
||||
})
|
||||
})
|
||||
Generated
+86
@@ -494,6 +494,9 @@ importers:
|
||||
'@deepseek-ai/dsh-home-paths':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/util/home-paths
|
||||
'@electron/notarize':
|
||||
specifier: 2.5.0
|
||||
version: 2.5.0
|
||||
'@types/node':
|
||||
specifier: ^22.20.0
|
||||
version: 22.20.0
|
||||
@@ -509,6 +512,9 @@ importers:
|
||||
extract-zip:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
msgpackr:
|
||||
specifier: 2.0.4
|
||||
version: 2.0.4
|
||||
pnpm:
|
||||
specifier: 11.7.0
|
||||
version: 11.7.0
|
||||
@@ -12468,6 +12474,36 @@ packages:
|
||||
resolution: {integrity: sha512-Mmjg4anFBD5OzbPnGJOA0jPPN8645ERhQk38HQLpSenx1ox9bfdPkmAzUnNjeQtqQGFLtKe13J20RtLBmUKMZA==}
|
||||
hasBin: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
|
||||
resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4':
|
||||
resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4':
|
||||
resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4':
|
||||
resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4':
|
||||
resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4':
|
||||
resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.5':
|
||||
resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
|
||||
peerDependencies:
|
||||
@@ -15953,6 +15989,13 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
msgpackr-extract@3.0.4:
|
||||
resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==}
|
||||
hasBin: true
|
||||
|
||||
msgpackr@2.0.4:
|
||||
resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==}
|
||||
|
||||
nanoid@3.3.12:
|
||||
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -16040,6 +16083,10 @@ packages:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
||||
hasBin: true
|
||||
|
||||
node-gyp@12.4.0:
|
||||
resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
@@ -18878,6 +18925,24 @@ snapshots:
|
||||
- supports-color
|
||||
- zod
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
@@ -22600,6 +22665,22 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
msgpackr-extract@3.0.4:
|
||||
dependencies:
|
||||
node-gyp-build-optional-packages: 5.2.2
|
||||
optionalDependencies:
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4
|
||||
'@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4
|
||||
'@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4
|
||||
optional: true
|
||||
|
||||
msgpackr@2.0.4:
|
||||
optionalDependencies:
|
||||
msgpackr-extract: 3.0.4
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
@@ -22675,6 +22756,11 @@ snapshots:
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
optional: true
|
||||
|
||||
node-gyp@12.4.0:
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
|
||||
@@ -49,6 +49,8 @@ allowBuilds:
|
||||
# install script only selects its bundled 7-Zip executable. Desktop ships
|
||||
# Windows through NSIS, so that mutation is not part of our build.
|
||||
electron-winstaller: false
|
||||
# Store-index rewriting only needs msgpackr's portable JavaScript codec.
|
||||
msgpackr-extract: false
|
||||
|
||||
minimumReleaseAgeExclude:
|
||||
# Fresh pi-ai releases carry the model catalog updates that are the whole
|
||||
|
||||
Reference in New Issue
Block a user