Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ macOS 虚拟显示依赖未公开的 `CGVirtualDisplay` CoreGraphics API。它
- Windows 子端通过 UDP 广播,Mac 主端监听并合并本机记忆列表。
- WebSocket 信令、协议版本/消息大小/字段校验、单主端占用保护。
- 主端主动连接、断开;子端可全屏并在会话期间阻止显示器休眠。
- 子端画面信息条默认隐藏,鼠标移动、触摸或键盘聚焦时短暂显示,避免遮挡扩展桌面。
- JSON 设置持久化,最多记忆 32 台设备;离线设备仍可显示和删除。
- 启动时及侧边栏手动检查 GitHub Releases;发现新版本后打开官方发布页,由用户下载并安装。
- Node 单元测试、源码语法检查,以及 macOS/Windows GitHub Actions 构建产物。

## 快速开始
Expand Down Expand Up @@ -74,7 +76,7 @@ npm run dev:host
5. 如果私有虚拟显示不可用,GUI 会切到“已有显示器”兼容模式;此时才需要手动选择捕获源。选择已有屏会发送该屏全部内容,请先清除敏感信息。
6. 使用主端“断开扩展屏”结束投放;由本会话自动创建的虚拟显示器会随断开清理。

完整操作和未签名产物说明见[用户指南](docs/user-guide.md)。
完整操作和未签名产物说明见[用户指南](docs/user-guide.md)。公开安装包可从 [GitHub Releases](https://github.com/Modole/LanExtend/releases) 获取。

## 架构概览

Expand Down Expand Up @@ -109,7 +111,7 @@ flowchart LR
- [故障排除](docs/troubleshooting.md):发现、端口、权限、黑屏、连接和性能问题。
- [验收清单](docs/acceptance.md):自动化与双机手工验收,包含尚未验证项的记录方式。
- [开源方案调研](docs/open-source-research.md):OpenDisplay、VoidDisplay、DeskPad、Deskreen、Weylus、Sunshine 等方案与许可证矩阵。
- [发布说明](docs/release.md):CI artifacts、签名/公证边界和正式发布门槛。
- [发布说明](docs/release.md):GitHub Releases、CI artifacts、更新检查、签名/公证边界和正式发布门槛。
- [第三方声明](THIRD_PARTY_NOTICES.md)。

## 开源与许可证边界
Expand Down
2 changes: 1 addition & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Third-Party Notices

This file describes direct runtime/build dependencies and implementation references for LanExtend 0.1.0. The authoritative resolved dependency graph is `package-lock.json`. A release maintainer must re-run license review whenever the lockfile or packaging inputs change.
This file describes direct runtime/build dependencies and implementation references for LanExtend 0.2.0. The authoritative resolved dependency graph is `package-lock.json`. A release maintainer must re-run license review whenever the lockfile or packaging inputs change.

LanExtend source code is licensed under the repository's MIT License. Third-party components remain under their respective licenses.

Expand Down
6 changes: 4 additions & 2 deletions docs/acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Windows 型号 / CPU / GPU / Windows build:
| A08 | CI | `build.yml` 两个 job | 同一提交 Mac/Windows job 均绿且 artifacts 可下载 | 执行时填写 |
| A09 | 许可证 | 人工检查 | LICENSE、第三方声明、锁文件与打包依赖一致 | 执行时填写 |
| A10 | 文档范围 | 人工检查 | 明确 14+、Win10/11、MVP 限制、无认证和无签名 | 执行时填写 |
| A11 | 更新检查 | `tests/updates.test.js` + GUI | 语义版本比较正确,只接受本仓库 HTTPS Release 链接,失败可重试 | 执行时填写 |

## 4. 安装与首次启动(P0)

Expand Down Expand Up @@ -90,6 +91,7 @@ Windows 型号 / CPU / GPU / Windows build:
- [ ] R10 已连接时第二台 Mac 被子端以单会话策略拒绝,不影响现有会话。
- [ ] R11 两端使用不同协议版本时明确失败,不进入假连接状态。
- [ ] R12 让 Mac/Windows 分别睡眠/唤醒,记录实际行为;若不能恢复,UI 至少允许干净断开重连。
- [ ] R13 子端视频出现后底部信息条默认隐藏;画面内移动鼠标/触摸/键盘聚焦后显示,无操作约 2.2 秒后隐藏;悬停按钮时不会在操作中消失。

## 8. 画质、性能与稳定性(P0/P1)

Expand Down Expand Up @@ -122,15 +124,15 @@ Windows 型号 / CPU / GPU / Windows build:

## 10. 当前明确不验收为“支持”的能力

以下项目若意外“看似可用”也不应纳入 v0.1.0 支持声明:
以下项目若意外“看似可用”也不应纳入 v0.2.0 支持声明:

- 音频播放/转发;
- Windows 到 Mac 的键盘、鼠标、触控、剪贴板或文件回传;
- HDR、广色域、色彩校准保证;
- 一台 Mac 同时连接多个 Windows 子端或创建多块受管扩展屏;
- IPv6、DNS 名称、跨公网、NAT、TURN、云中继;
- 身份认证、配对、授权、受信设备安全列表;
- Mac App Store、自动更新、正式签名/公证;
- Mac App Store、后台下载/静默自动安装、正式签名/公证;
- 无人值守服务和企业集中管理。

## 11. 发布判定
Expand Down
4 changes: 2 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ npm test
npm run dist:win
```

生成 portable EXE 和 ZIP。当前没有 MSI、系统服务、驱动、自动更新或 Authenticode 签名。Windows SmartScreen 可能提示未知发布者,这是正式发布前的阻塞项,不应通过文案声称“安全”来规避。
生成 portable EXE 和 ZIP。当前没有 MSI、系统服务、驱动、静默自动安装或 Authenticode 签名。应用只检查 GitHub 最新 Release 并打开固定下载页;Windows SmartScreen 可能提示未知发布者,这是正式发布前的阻塞项,不应通过文案声称“安全”来规避。

## 8. GitHub Actions

Expand All @@ -184,7 +184,7 @@ npm run dist:win
1. `macos-build`:`npm ci` → `npm test` → `npm run dist:mac` → 上传 DMG/ZIP;
2. `windows-build`:`npm ci` → `npm test` → `npm run dist:win` → 上传 portable EXE/ZIP。

workflow 使用最小 `contents: read` 权限,并上传 14 天 artifacts。它不创建 Release、不签名、不公证、不上传商店。
workflow 使用最小 `contents: read` 权限,并上传 14 天 artifacts。它不自动创建 Release、不签名、不公证、不上传商店;维护者可在两个 job 均通过后,另外创建 GitHub Release 并上传同一版本的候选包与哈希

## 9. 发布前版本与合规检查

Expand Down
18 changes: 13 additions & 5 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

## 1. 当前交付级别

`.github/workflows/build.yml` 生成的是**开发/验收 artifacts**,不是面向最终用户的正式 Release:
`.github/workflows/build.yml` 生成的是**开发/验收 artifacts**;维护者在同一提交的两个平台 job 通过后,可另行创建面向用户的 GitHub Release:

- macOS job:测试、构建含 `arm64`/`x86_64` 的 universal2 私有 API helper,并以 `--universal` 打包 DMG/ZIP;
- Windows job:测试,打包 portable EXE/ZIP;
- 两端均未配置正式代码签名;Mac 未公证;
- workflow 不创建 GitHub Release、不上传商店、不自动更新
- workflow 不自动创建 GitHub Release、不上传商店,也不执行静默更新

Artifacts 保留 14 天,名称包含平台;实际文件名由 electron-builder 的 `LanExtend-${version}-${os}-${arch}.${ext}` 规则生成。
Artifacts 保留 14 天,名称包含平台;实际文件名由 electron-builder 的 `LanExtend-${version}-${os}-${arch}.${ext}` 规则生成。发布到 Releases 时还应上传 SHA-256 校验文件,并在正文中写明签名状态与已知限制。

## 2. 触发与权限

Expand Down Expand Up @@ -61,7 +61,7 @@ npm test
npm run dist:win
```

本地构建只能用于调试/验收。正式包应来自可审计、受保护且保存 provenance 的发布工作流
本地构建只能用于调试/验收。当前开发预览 Release 可由维护者上传经测试且附哈希的候选包;正式产品包仍应来自可审计、受保护且保存 provenance 的签名发布工作流

## 5. 正式发布前阻塞项

Expand Down Expand Up @@ -110,7 +110,15 @@ npm run dist:win

`PROTOCOL_VERSION` 与应用 SemVer 独立。只要线协议有不兼容变化,就必须提升协议版本并在发布说明中列出主/子端兼容矩阵。

由于无自动更新,主端和子端应从同一 Release 安装。协议不匹配时应失败关闭,不尝试静默降级。
应用会在启动时或用户点击侧边栏更新卡片后查询 GitHub 的最新稳定 Release;发现新版只打开本仓库的固定 HTTPS 发布页,不会下载或静默安装。主端和子端应从同一 Release 安装。协议不匹配时应失败关闭,不尝试静默降级。

版本检查的发布契约:

- tag 必须是稳定三段 SemVer,例如 `v0.2.0`;draft 和 prerelease 不会作为最新稳定版本;
- 公开元数据端点固定为 `api.github.com/repos/Modole/LanExtend/releases/latest`;遇到 GitHub API 速率限制时,只解析本仓库 `/releases/latest` 的 HTTPS 重定向作为降级;
- 下载页固定限制在 `https://github.com/Modole/LanExtend/releases/` 路径下;
- 更新失败不影响局域网扩展屏核心功能,用户可在侧边栏重试;
- 不把 Release 元数据视为安装包真实性证明,仍需核对哈希,并在正式产品阶段补签名、公证和安全升级机制。

## 8. 发布页必须包含的限制

Expand Down
8 changes: 5 additions & 3 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 1. 安全结论

LanExtend v0.1.0 是**无认证的可信内网 MVP**。只有在你控制网络、信任同网设备并能限制防火墙范围时才应运行。它不适合公网、端口转发、共享 VPN、访客 Wi‑Fi、酒店/机场网络、校园网或无法识别其他终端的办公网。
LanExtend v0.2.0 是**无认证的可信内网 MVP**。只有在你控制网络、信任同网设备并能限制防火墙范围时才应运行。它不适合公网、端口转发、共享 VPN、访客 Wi‑Fi、酒店/机场网络、校园网或无法识别其他终端的办公网。

“Mac 主端权限最高”目前表示:

Expand Down Expand Up @@ -107,13 +107,15 @@ macOS 主端通过运行时类名使用未公开的 `CGVirtualDisplay`、`CGVirt

## 7. 未签名构建与供应链

当前 GitHub Actions artifacts 只用于开发验收:
当前 GitHub Actions artifacts 只用于开发验收;GitHub Releases 可分发维护者选定的候选包

- 没有 Apple Developer ID 签名/公证;
- 没有 Windows Authenticode 签名;
- 没有发布证书固定、自动更新、SBOM 或独立可复现构建声明;
- 没有发布证书固定、静默自动安装、SBOM 或独立可复现构建声明;
- workflow 中的 `npm ci` 会从 npm registry 获取锁定包,仍需供应链治理。

应用的版本检查只向 `api.github.com/repos/Modole/LanExtend/releases/latest` 请求公开发布元数据;遇到 API 速率限制时,只解析本仓库 `/releases/latest` 的 HTTPS 重定向。应用只允许打开本仓库固定的 HTTPS Releases 页面。请求不会上传设备 UUID、局域网地址、设置或画面,但 GitHub 仍会看到普通 HTTPS 请求所包含的公网 IP 和网络元数据。安装包不会自动下载或执行,用户必须自行核对发布说明与 SHA-256 后安装。

不要指导普通用户长期关闭 Gatekeeper/SmartScreen。内部测试可以在确认提交、构建日志和哈希后使用系统提供的单次“仍要打开”流程。正式发布必须配置签名、证书保护、公证、哈希、依赖扫描和回滚。

直接依赖和研究参考见[第三方声明](../THIRD_PARTY_NOTICES.md)与[开源调研](open-source-research.md)。GPL/AGPL 项目只可作为概念参考;复制代码可能改变本项目分发义务。
Expand Down
14 changes: 11 additions & 3 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,24 @@

## 2. 获取和安装

### CI 构建产物
### GitHub Releases 与 CI 构建产物

面向用户发布的安装包位于项目的 [GitHub Releases](https://github.com/Modole/LanExtend/releases):

- Mac:Universal DMG 和 ZIP;目前未配置 Developer ID 正式签名与 Apple 公证;
- Windows:portable EXE 和 ZIP;目前未配置 Authenticode 签名;
- 发布页同时提供 SHA-256 校验文件;下载后应先核对哈希和版本说明。

仓库的 `Build desktop artifacts` GitHub Actions 会分别生成 Mac 和 Windows artifacts。它们是开发/验收产物:

- Mac:DMG 和 ZIP;未配置 Developer ID 正式签名与 Apple 公证;
- Windows:portable EXE 和 ZIP;未配置 Authenticode 签名;
- artifacts 不是 GitHub Release,也没有自动更新能力
- artifacts 只保留有限时间,不等同于维护者发布的 Release。

只从你信任的仓库运行记录下载,并核对工作流对应的提交。正式分发要求见[发布说明](release.md)。

应用启动后会向 GitHub 的公开 Release API 检查一次稳定版本,也可点击侧边栏底部的更新卡片手动复查。发现新版本时,卡片会打开本项目固定的 GitHub Releases 页面;LanExtend **不会后台下载、不会静默安装,也不会绕过系统安全提示**。升级前先断开会话,并让主端与子端安装相同版本。

### 从源码运行

两端都需要 Node.js 22+ 和 npm 10+。Mac 还需要 Xcode Command Line Tools 和 macOS SDK。
Expand Down Expand Up @@ -115,7 +123,7 @@ HiDPI 使同一逻辑尺寸的物理像素数变为四倍。发送端会尝试
## 7. 投放与结束

1. 默认模式连接成功后,把要显示的 Mac 窗口拖过桌面边缘,移入自动创建的虚拟显示器。
2. Windows 子端可用顶部按钮或画面悬浮按钮进入/退出全屏,按 Esc 也会退出全屏。
2. Windows 子端可用顶部按钮或画面悬浮按钮进入/退出全屏,按 Esc 也会退出全屏。画面底部的主端名称、分辨率、FPS、码率与操作按钮默认隐藏;在画面内移动鼠标、触摸或用键盘聚焦后会出现,无操作约 2.2 秒后再次隐藏。
3. 需要切换分辨率时,先点击“断开扩展屏”,修改参数后重新点击“扩展到 …”;不需要单独创建/销毁按钮。
4. 由 Mac 主端点击断开结束会话。
5. 断开会清理本会话自动创建的虚拟显示器;直接退出主端也会尝试清理 helper 和显示器。兼容模式下不会删除已有物理/系统显示器。
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lanextend",
"version": "0.1.0",
"version": "0.2.0",
"description": "LAN virtual extended display: macOS host to Windows receiver",
"main": "src/main.js",
"author": "LanExtend contributors",
Expand All @@ -18,8 +18,8 @@
"pretest": "node scripts/check-source.mjs",
"test": "node --test --test-reporter=spec",
"verify": "npm run build:native && npm test",
"dist:mac": "npm run build:native && electron-builder --mac dmg zip --universal",
"dist:win": "electron-builder --win portable zip --x64",
"dist:mac": "npm run build:native && electron-builder --mac dmg zip --universal --publish never",
"dist:win": "electron-builder --win portable zip --x64 --publish never",
"dist": "npm run dist:mac"
},
"dependencies": {
Expand Down
86 changes: 86 additions & 0 deletions src/core/updates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

const GITHUB_REPOSITORY = 'Modole/LanExtend';
const RELEASE_API_URL = `https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest`;
const RELEASE_PAGE_URL = `https://github.com/${GITHUB_REPOSITORY}/releases/latest`;
const MAX_RELEASE_RESPONSE_BYTES = 256 * 1024;

function parseVersion(value) {
const match = String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)$/);
if (!match) throw new Error('版本号格式无效');
return match.slice(1).map(Number);
}

function compareVersions(left, right) {
const a = parseVersion(left);
const b = parseVersion(right);
for (let index = 0; index < 3; index += 1) {
if (a[index] !== b[index]) return a[index] > b[index] ? 1 : -1;
}
return 0;
}

function isAllowedReleaseUrl(value) {
try {
const url = new URL(value);
return url.protocol === 'https:'
&& url.hostname === 'github.com'
&& !url.port
&& !url.username
&& !url.password
&& url.pathname.startsWith(`/${GITHUB_REPOSITORY}/releases/`);
} catch {
return false;
}
}

function parseReleasePayload(payload, currentVersion) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('GitHub 返回的版本信息无效');
}
if (payload.draft === true || payload.prerelease === true) {
throw new Error('最新版本不是稳定公开版本');
}
const latestVersion = String(payload.tag_name || '').replace(/^v/, '');
parseVersion(currentVersion);
parseVersion(latestVersion);
const releaseUrl = isAllowedReleaseUrl(payload.html_url)
? payload.html_url
: RELEASE_PAGE_URL;
return {
currentVersion,
latestVersion,
updateAvailable: compareVersions(latestVersion, currentVersion) > 0,
releaseUrl,
publishedAt: typeof payload.published_at === 'string' ? payload.published_at : null
};
}

function parseReleasePageUrl(value, currentVersion) {
if (!isAllowedReleaseUrl(value)) throw new Error('GitHub 发布页地址无效');
const url = new URL(value);
const match = url.pathname.match(/^\/Modole\/LanExtend\/releases\/tag\/v?(\d+\.\d+\.\d+)\/?$/);
if (!match) throw new Error('GitHub 发布页不包含稳定版本号');
const latestVersion = match[1];
parseVersion(currentVersion);
parseVersion(latestVersion);
return {
currentVersion,
latestVersion,
updateAvailable: compareVersions(latestVersion, currentVersion) > 0,
releaseUrl: url.toString(),
publishedAt: null
};
}

module.exports = {
GITHUB_REPOSITORY,
MAX_RELEASE_RESPONSE_BYTES,
RELEASE_API_URL,
RELEASE_PAGE_URL,
compareVersions,
isAllowedReleaseUrl,
parseReleasePageUrl,
parseReleasePayload,
parseVersion
};
Loading
Loading