diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..cb5b3a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,62 @@ +name: 缺陷报告 +description: 报告标注、笔记保存、恢复或界面中的问题 +title: '[Bug] ' +body: + - type: markdown + attributes: + value: | + 安全漏洞请使用[私密报告](https://github.com/aredddd/GlassNote/security/advisories/new),不要公开利用细节。 + 备份包含 URL、摘录、前后文和笔记。请使用公开网页或合成示例,不要上传真实完整备份、令牌、账号信息或私人截图。 + - type: checkboxes + id: preparation + attributes: + label: 提交前确认 + options: + - label: 我已搜索已有 Issue,未找到能覆盖此问题的报告。 + required: true + - label: 以下内容及附件已经脱敏,不含私人笔记、完整真实备份或凭据。 + required: true + - type: textarea + id: environment + attributes: + label: 使用环境 + description: 可在浏览器扩展管理页查看 GlassNote 版本。 + placeholder: | + GlassNote 版本或提交: + 浏览器名称与完整版本: + 操作系统: + 安装方式(源码加载、下载包、商店等): + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: 复现步骤 + description: 写明是首次创建、刷新、关闭重开、页面切换、升级还是导入后出现。 + placeholder: | + 1. 打开…… + 2. 选择……并保存…… + 3. 刷新或重新打开后…… + validations: + required: true + - type: textarea + id: expected_actual + attributes: + label: 预期与实际结果 + description: 如果标注消失或漂移,请说明笔记库里是否仍能看到摘录和想法,以及是否提示未定位或保存失败。 + placeholder: | + 预期: + 实际: + 出现频率: + validations: + required: true + - type: textarea + id: sample + attributes: + label: 最小示例与截图 + description: 可提供公开页面地址、最小 HTML、合成备份或脱敏截图。私人网页不必提供原地址;请用示例文字描述结构。 + - type: textarea + id: logs + attributes: + label: 错误信息或其他线索 + description: 如有错误提示,请粘贴相关片段。不要附上整个浏览器配置或未经检查的日志。 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..9b8c793 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: 私密安全报告 + url: https://github.com/aredddd/GlassNote/security/advisories/new + about: 漏洞利用细节和敏感复现材料请勿发布到公开 Issue。 + - name: 使用说明与支持范围 + url: https://github.com/aredddd/GlassNote/blob/main/README.md + about: 查看安装、升级、备份与网页兼容范围。 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..5d741cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: 功能建议 +description: 说明阅读或整理笔记时遇到的问题与希望改进的行为 +title: '[建议] ' +body: + - type: markdown + attributes: + value: | + 请从具体使用场景出发。涉及私人文章或笔记时,请使用虚构示例;安全问题请走[私密报告](https://github.com/aredddd/GlassNote/security/advisories/new)。 + - type: checkboxes + id: preparation + attributes: + label: 提交前确认 + options: + - label: 我已查看现有功能说明并搜索已有 Issue。 + required: true + - type: textarea + id: problem + attributes: + label: 你遇到了什么问题? + description: 描述阅读、标注、恢复或整理笔记时的具体场景,以及现有操作为什么不方便。 + validations: + required: true + - type: textarea + id: proposal + attributes: + label: 期望怎样改进? + description: 用一次实际操作说明希望看到的行为或结果。 + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: 现有做法与替代方案 + description: 如果已有临时解决办法,也请说明它的限制。 + - type: textarea + id: scope + attributes: + label: 其他说明 + description: 可附脱敏示意图。若涉及同步、网络服务或新增权限,请说明这些能力为什么是该场景所必需的。 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..40097a4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +## 问题与改动 + + + +## 验证 + + + +## 影响与限制 + + + +## 提交前检查 + +- [ ] 我已阅读 [贡献指南](https://github.com/aredddd/GlassNote/blob/main/CONTRIBUTING.md),确认有权提交这些内容。 +- [ ] 已检查附件和修改内容,不含私人笔记、真实完整备份、浏览器配置、令牌或无关产物。 +- [ ] 已完成与改动有关的验证,并写明尚未验证的情况。 +- [ ] 如涉及数据格式、导入迁移、权限或数据处理,已补相应回归并更新相关说明;不涉及则勾选。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2b5a41d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: ['**'] + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: 格式、单元测试与真实扩展回归 + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: 检出代码 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: 设置 Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: '24' + cache: npm + + - name: 按锁文件安装开发依赖 + run: npm ci + + - name: 检查语法、扩展入口和格式 + run: | + npm run check + npm run format:check + + - name: 安装测试浏览器 + run: npx playwright install --with-deps chromium + + - name: 运行单元测试 + run: npm test + + - name: 运行真实扩展回归 + run: npm run test:e2e + + # 测试使用隔离浏览器与本地网页,不自动访问用户指定的真实网站。 + - name: 构建并验证发布包 + run: | + npm run package + npm run package:verify + + - name: 保留失败时的测试截图 + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: failed-test-screenshots + path: test-results/ + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..588a0f5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,104 @@ +name: 发布扩展 + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: 校验标签、测试并构建 + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: 检出标签源码 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: 设置 Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: '24' + cache: npm + + - name: 校验标签和项目版本 + run: node scripts/verify-package.cjs --version-only --tag "$GITHUB_REF_NAME" + + - name: 按锁文件安装开发依赖 + run: npm ci + + - name: 检查语法、扩展入口和格式 + run: | + npm run check + npm run format:check + + - name: 安装测试浏览器 + run: npx playwright install --with-deps chromium + + - name: 运行单元测试和真实扩展回归 + run: | + npm test + npm run test:e2e + + - name: 构建并验证 ZIP 与 SHA256SUMS + run: | + npm run package -- --tag "$GITHUB_REF_NAME" + npm run package:verify -- --tag "$GITHUB_REF_NAME" + + - name: 保存经过校验的发布资产 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: glassnote-release + path: | + dist/GlassNote-${{ github.ref_name }}.zip + dist/SHA256SUMS + if-no-files-found: error + compression-level: 0 + retention-days: 7 + + publish: + name: 发布 GitHub Release + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: write + steps: + - name: 检出同一标签的校验脚本和发布说明 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: 设置 Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: '24' + + - name: 下载本次构建的发布资产 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: glassnote-release + path: dist + + - name: 发布前复验版本、文件清单和 SHA-256 + run: node scripts/verify-package.cjs --tag "$GITHUB_REF_NAME" + + - name: 创建 GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + archive="dist/GlassNote-${GITHUB_REF_NAME}.zip" + title="GlassNote ${GITHUB_REF_NAME#v}" + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + echo '::error::该标签已经存在 Release,拒绝覆盖已发布资产;请使用新版本。' + exit 1 + fi + gh release create "$GITHUB_REF_NAME" "$archive" dist/SHA256SUMS --verify-tag --target "$GITHUB_SHA" --title "$title" --notes-file CHANGELOG.md --draft=false --prerelease=false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..321add5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# 更新日志 + +## [3.0.0] - 2026-09-29 + +### 新增 + +- 集中管理所有网页的笔记库,支持搜索、编辑及 JSON 备份导入导出。 +- 通过原文、上下文和位置共同定位标注;无法可靠恢复时保留笔记并显示待定位。 +- MIT 许可、贡献与安全指南、反馈模板、自动回归及版本打包流程。 + +### 改进 + +- 重做扩展弹窗、浮动工具栏和页面笔记面板,采用浅色玻璃质感界面。 +- 使用 CSS Custom Highlight API 绘制标注,减少对原网页 DOM 的影响。 +- 后台串行写入,兼容旧版数据迁移,保留旧版原始存储。 + +### 修复 + +- 修复重开网页后笔记无法显示、重复文本误定位与页面变化后位置漂移的问题。 +- 同一处文字再次点击高亮、下划线或加粗即可取消格式;重复点击同色取消高亮,换色更新原记录。 +- 取消格式时保留附带笔记;编辑笔记不会重新创建已删除标注。 + +### 已知限制 + +- 面向桌面 Edge / Chrome 的普通网页主文档;不支持浏览器内部页、内置 PDF、Canvas、iframe 内文与封闭 Shadow DOM。 +- 大幅改写或删除原文后无法保证自动定位;笔记仍保留在笔记库。 +- 本地保存,无跨设备同步;Edge 商店发布进度与 GitHub 版本发布分别管理。 + +更早版本尚未整理为独立发行记录,历史改动见 [Git 提交记录](https://github.com/aredddd/GlassNote/commits/main/)。 + +[3.0.0]: https://github.com/aredddd/GlassNote/releases/tag/v3.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6675c2c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# 贡献指南 + +GlassNote 希望让阅读笔记可靠保存、准确找回,同时保持界面轻盈。欢迎修复缺陷、改进可访问性、完善测试和文档。 + +## 开始之前 + +- 先搜索已有 Issue 和 PR;较大的功能或数据格式调整,先说明使用场景和方案。 +- 普通问题使用 [Issue 模板](https://github.com/aredddd/GlassNote/issues/new/choose),安全问题按 [SECURITY.md](SECURITY.md) 私下报告。 +- 从 main 创建分支,一个 PR 尽量解决一个问题。不要混入个人笔记、浏览器配置、安装包或无关格式修改。 +- 只提交你有权分享的代码、图片和测试素材。引入第三方材料时,在 PR 中注明来源和许可证。 + +## 本地开发 + +准备 Node.js 22 或更新版本及 npm。安装开发依赖和测试浏览器: + +```sh +npm ci +npx playwright install chromium +``` + +扩展没有构建步骤,也没有运行时 npm 依赖。打开 Chrome 的 chrome://extensions/ 或 Edge 的 edge://extensions/,启用开发者模式,加载包含 manifest.json 的仓库目录。修改后重新加载扩展,并刷新用于测试的网页。 + +手动测试请使用单独的浏览器配置,不要拿个人笔记做迁移、删除或存储故障实验。升级已有安装前先导出备份;更换扩展目录可能改变扩展 ID,不能假定旧数据会自动跟随。 + +常用检查: + +```sh +npm run check +npm test +npm run test:e2e +npm run format:check +``` + +npm test 包含需要 Playwright Chromium 的浏览器测试;test:e2e 使用临时浏览器配置和本地示例文章加载真实扩展。两者不需要个人浏览器账号。 + +仅当改动涉及真实站点兼容性且需要联网验证时运行: + +```sh +npm run test:live +``` + +该命令会访问测试脚本中指定的公开站点,不属于默认离线回归。测试报告和截图保存在 test-results,提交前确认没有私人信息。 + +需要检查分发包时运行: + +```sh +npm run package +npm run package:verify +``` + +打包使用 Node.js 内建能力,无需安装系统 zip 命令;产物和校验和在 dist。验包会检查版本、文件清单、内容与校验和。不要把 node_modules、dist、test-results、work 或真实浏览器配置提交到仓库。 + +## 改动需要覆盖什么 + +按实际影响选择验证,不用为文案修改重复执行整套浏览器测试。 + +| 改动范围 | 需要保留的行为和验证重点 | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| 原文定位、网页标注 | 不包裹或替换宿主正文;跨节点选区、重复原文、DOM 改写、异步加载与 SPA 切换;无法可靠定位时保留笔记并显示状态,不猜位置。 | +| 保存、删除、迁移和导入 | 旧版数据仍可读取;相同页面相同 ID 的导入不覆盖本地编辑;并发写入、配额失败、格式取消保留想法、已删除记录不被旧编辑器复活。使用合成数据回归。 | +| 弹窗、侧栏和笔记库 | 选文工具、颜色与格式切换、编辑保存失败后的草稿、删除确认、键盘操作,以及窄窗口布局。UI 改动附无隐私的前后截图。 | +| 权限、数据处理和外联 | 说明功能为什么需要变化;同步核对 [隐私说明](PRIVACY.md) 和商店材料,不能只修改 manifest。 | + +锚点测试在 tests/anchor.test.cjs;存储、迁移与格式切换测试在 tests/storage*.test.cjs 和 tests/toggle-storage.test.cjs;界面与真实扩展回归在 tests/content*.test.cjs 和 tests/extension.e2e.cjs。优先为可复现的缺陷增加对应回归用例。 + +## 报告和提交 + +复现材料使用公开网页或最小本地 HTML。导出的 JSON 可能包含完整 URL、摘录、前后文和私人笔记,请用合成数据替代真实备份;截图和日志也应移除账号、令牌及私人路径。详细数据范围见 [PRIVACY.md](PRIVACY.md)。 + +PR 写清问题、修改后的行为、实际执行的验证及尚未覆盖的情况。仅打包成功不代表浏览器交互已经通过;如跳过某项验证,请写明原因。新增或更新依赖时同步提交锁文件。 + +提交贡献即表示你有权提交这些内容,并同意将你拥有版权的原创贡献按根目录 [LICENSE](LICENSE) 的 MIT 条款提供;第三方材料继续适用其原许可。 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..0e2bdc4 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,7 @@ +# 贡献者 + +- [aredddd](https://github.com/aredddd) — 项目创建、产品需求、验收与维护。 + +其他贡献以 [Git 提交记录](https://github.com/aredddd/GlassNote/commits/main/) 和 [GitHub 贡献者记录](https://github.com/aredddd/GlassNote/graphs/contributors) 为准。欢迎通过缺陷复现、代码、测试、设计和文档参与改进,具体方式见 [贡献指南](CONTRIBUTING.md)。 + +本次重构使用 Codex 辅助代码实现、测试和文档整理。AI 工具使用说明不代替实际提交归属;版权与许可见 [LICENSE](LICENSE)。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..401726d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License +=========== + +Copyright (c) 2025-2026 aredddd and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 92f5521..0bc31cf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # GlassNote -留住阅读时的一点灵感。GlassNote 是一个 Chrome 网页高亮与笔记扩展,笔记保存在自己的浏览器里。 +[![CI](https://github.com/aredddd/GlassNote/actions/workflows/ci.yml/badge.svg)](https://github.com/aredddd/GlassNote/actions/workflows/ci.yml) +[![许可证:MIT](https://img.shields.io/badge/license-MIT-3b6b56)](LICENSE) + +GlassNote 是一个适用于 Edge 和 Chrome 的网页高亮与笔记扩展。选中文字即可标注、记笔记,并在下次打开网页时恢复。数据保存在自己的浏览器里。 + +![GlassNote 网页标注和页面笔记面板](docs/screenshots/web-annotations.png) ## v3 改了什么 @@ -12,10 +17,12 @@ ## 安装与升级 -适用于支持 CSS Custom Highlight API 的桌面 Chromium 浏览器(Chrome 105 或更新版本)。 +适用于支持 CSS Custom Highlight API 的桌面 Chromium 浏览器,最低内核版本为 Chromium 105。 -1. 下载源码或解压安装包。 -2. 打开 chrome://extensions/,开启「开发者模式」。 +Edge 商店上架准备中,目前可使用源码或 GitHub 安装包。 + +1. 从 [GitHub Releases](https://github.com/aredddd/GlassNote/releases) 下载 GlassNote-v版本号.zip 和 SHA256SUMS 并解压;也可下载仓库源码。 +2. Edge 打开 edge://extensions/;Chrome 打开 chrome://extensions/,开启「开发者模式」。 3. 点击「加载已解压的扩展程序」,选择包含 manifest.json 的目录。 4. 刷新已打开的网页,选中文字开始标注。 @@ -35,6 +42,10 @@ 同一文章的普通章节锚点和常见追踪参数不会产生多份笔记;业务查询参数和以 #/、#! 开头的页面路由分别保存。普通 #name 被视为文章章节,如果网站用这种形式承载不同页面,需要注意它们会归入同一页面。 +![GlassNote 笔记库](docs/screenshots/library.png) + +截图使用项目自编文章和演示笔记,不含真实用户数据。 + ## 适用范围 面向普通网页的主文档文本。浏览器内部页面、应用商店、浏览器内置 PDF 阅读器、Canvas 内容、iframe 内文和封闭 Shadow DOM 不在本版支持范围内。可编辑输入区不参与标注。访问本地 HTML 需要在扩展详情中开启「允许访问文件网址」。 @@ -53,15 +64,17 @@ ## 开发与验证 -扩展本身无构建步骤、无运行时依赖。测试使用 Node.js 22 和 Playwright。 +扩展本身无构建步骤、无运行时依赖。开发和测试需要 Node.js 22 或更新版本及 npm,打包无需额外构建工具。CI 使用 Node.js 24 和 Playwright。使用扩展无需安装这些开发工具。 ```sh npm ci npx playwright install chromium npm run check +npm run format:check npm test npm run test:e2e npm run package +npm run package:verify ``` 可选运行指定真实站点的验收(需要联网): @@ -72,7 +85,7 @@ npm run test:live 该测试在临时浏览器配置中访问小林笔记的 Agent 专栏,结果和截图默认保存在 test-results。可用 GN_SCREENSHOT_DIR 指定输出目录。它不读取或修改个人 Chrome 配置。 -安装包生成在 dist/GlassNote-v3.0.0.zip。测试使用独立的临时浏览器配置,不读取个人浏览器数据。 +Linux 首次安装测试浏览器时使用 npx playwright install --with-deps chromium。安装包生成在 dist/GlassNote-v版本号.zip,同目录生成 SHA256SUMS。打包只包含扩展文件及许可证、隐私与第三方说明,不包含 node_modules、测试和个人数据。测试使用独立的临时浏览器配置,不读取个人浏览器数据。 ```text src/ @@ -85,4 +98,14 @@ styles/ 网页高亮样式 tests/ 存储、锚点与真实扩展回归 ``` -目前未指定开源许可证。 +## 参与和发布 + +- [贡献指南](CONTRIBUTING.md):开发流程、回归要求和 PR 约定。 +- [问题反馈](https://github.com/aredddd/GlassNote/issues/new/choose):Bug 报告和功能建议。 +- [安全政策](SECURITY.md):通过私密渠道报告漏洞。 +- [更新日志](CHANGELOG.md)与[版本发布流程](docs/RELEASING.md)。 +- [贡献者](CONTRIBUTORS.md)。 + +## 许可 + +本项目采用 [MIT 许可证](LICENSE),版权归 aredddd 及项目贡献者所有。开发工具、浏览器和测试网站的许可边界见[第三方声明](THIRD_PARTY_NOTICES.md)。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4e53102 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +# 安全报告 + +GlassNote 在浏览器本地处理网页摘录、定位上下文、笔记和备份。安全报告覆盖网页、导入文件、扩展消息及本地数据读写。 + +## 私下报告漏洞 + +本仓库已启用 GitHub 私密漏洞报告。请通过 [Report a vulnerability](https://github.com/aredddd/GlassNote/security/advisories/new) 提交,不要在公开 Issue 或 PR 中发布未修复漏洞的利用步骤、私人笔记、真实备份、账号信息或令牌。 + +报告尽量包含: + +- 受影响的扩展版本或提交,以及浏览器和操作系统版本; +- 最小复现步骤、预期行为、实际影响; +- 使用合成笔记、最小 HTML 或脱敏 JSON 制作的复现材料; +- 你确认受影响的范围,以及是否存在修复建议。 + +如私密入口不可用,可创建一个只请求私下联系、不含漏洞细节的普通 Issue,待建立私下渠道后再提供材料。请在问题得到处理和协调前避免公开利用细节;这里不承诺固定响应或修复时限。 + +## 报告范围 + +重点关注网页或备份中的内容被当作代码执行、未经用户预期的数据外传、扩展消息导致的越权读写,以及会破坏本地笔记的安全缺陷。普通定位偏移、界面问题或功能建议可使用 [缺陷与建议入口](https://github.com/aredddd/GlassNote/issues/new/choose),同样不要附上敏感数据。 + +请注明问题是否能在 main 的最新提交或最新发布包复现;旧版本的问题也请说明具体版本,不要为验证漏洞直接替换日常使用的扩展或个人浏览器配置。属于浏览器或开发依赖本身的问题,应同时参考相应上游的报告渠道。 + +笔记和导出备份可能包含个人信息,且没有 GlassNote 提供的额外密码保护或端到端加密。删除记录、空页面元数据和旧版迁移源的保留边界见 [隐私说明](PRIVACY.md)。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..f82f528 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,32 @@ +# 第三方声明 + +GlassNote 的扩展源码和项目图标按根目录 [MIT 许可证](LICENSE) 提供。当前发行 ZIP 不打包第三方 JavaScript 库、字体文件或浏览器二进制,也不从 CDN 加载运行时代码。 + +## 开发与测试工具 + +以下工具仅用于开发、格式检查和测试,通过 npm 安装;不随扩展 ZIP 分发。确切版本和依赖完整性记录在 package-lock.json 中。 + +| 项目 | 当前版本 | 许可 | 用途 | +| ----------------------------------------------------------------------- | -------- | ---------- | ------------------------------ | +| [Playwright](https://github.com/microsoft/playwright) / playwright-core | 1.63.0 | Apache-2.0 | 在独立浏览器配置中运行扩展测试 | +| [Prettier](https://github.com/prettier/prettier) | 3.9.9 | MIT | 格式化源码与文档 | + +安装后的许可文本位于各 npm 包的 LICENSE 文件;Playwright 同时提供 NOTICE。Prettier 包中部分组件另有声明,应保留其完整许可文件。上述工具及其间接组件继续适用各自的许可,项目 MIT 许可不替代它们。 + +Playwright 下载的 Chromium 测试浏览器由其上游许可约束,既不提交到本仓库,也不随扩展发行。Node.js 和 GitHub Actions 是开发、打包或 CI 环境中的工具,同样不属于扩展发行包。 + +## 字体、图标和截图 + +- 界面使用系统字体回退列表;仓库不包含或下载字体文件。 +- 项目图标源文件位于 assets/icon.svg,PNG 是其不同尺寸的导出。 +- README 与商店截图使用项目自编演示文章和笔记;它们不包含用户的实际笔记、账号或私有网页。 + +## 测试网站与用户内容 + +可选联网测试访问[小林笔记](https://xiaolinnote.com/agent/)的公开页面,仅用于检查扩展在真实网页上的行为。该站文章、商标和页面设计归各自权利人所有,不属于本项目 MIT 授权范围,也不随发行 ZIP 打包。 + +用户摘录的网页文字与自己的笔记不因使用 GlassNote 而改变归属。公开分享备份或截图前,请自行确认其中内容的使用权限。 + +## 更新依赖或发布时 + +新增第三方代码、图片、字体或其他资产时,应在本文件记录来源、版本、许可和实际分发范围;需要随包提供的版权、LICENSE 与 NOTICE 必须进入发行包。不要仅因某项内容可以下载就将其纳入发行包。 diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..33e256a --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,62 @@ +# 版本发布 + +GitHub 发行包和 Edge 商店送审是两个独立步骤。GitHub Release 成功不代表商店已上架;Microsoft 后台仍需完成注册、资料验证和审核。 + +## 发布准备 + +1. 更新 manifest.json、package.json 和 package-lock.json 中的版本,三者保持一致。标签使用 v 加版本号,例如 v3.0.0。 +2. 更新 CHANGELOG.md,核对 README、PRIVACY.md、THIRD_PARTY_NOTICES.md 与实际行为一致。 +3. 如涉及权限、存储或页面定位,补充对应回归;确认旧版备份可以导入,格式开关不会丢失附带笔记。 +4. 执行本地检查,合并 PR,并确认主干 CI 通过。 + +```sh +npm ci +npx playwright install chromium +npm run check +npm run format:check +npm test +npm run test:e2e +npm run package +npm run package:verify +``` + +Linux 安装浏览器时可使用 npx playwright install --with-deps chromium。真实网站验收是可选的人工发布检查,不在 CI 中依赖第三方站点的可用性。 + +## GitHub Release + +在要发布的主干提交上创建版本标签并推送: + +```sh +git switch main +git pull --ff-only +git tag -a v3.0.0 -m "GlassNote v3.0.0" +git push origin v3.0.0 +``` + +示例版本号仅用于说明,后续发布应替换成实际的新版本。不要覆盖已经发布的标签或在同一版本名下替换不同内容的安装包。 + +Release 工作流检查标签和项目版本、运行回归、构建并复验 ZIP,再上传安装包和 SHA256SUMS。发布失败时应先检查工作流日志,修正后使用新的版本;不要把打包成功当作发布成功。 + +用户应下载 GlassNote-v版本号.zip;GitHub 自动生成的 Source code 压缩包是源码归档,与可安装的扩展包不同。扩展包根目录直接包含 manifest.json,不含开发依赖和测试数据。 + +下载后可在两个文件所在目录核对 SHA-256: + +```sh +# macOS +shasum -a 256 -c SHA256SUMS + +# Linux +sha256sum -c SHA256SUMS +``` + +Windows PowerShell 可计算 ZIP 的散列,并与 SHA256SUMS 中对应行比较: + +```powershell +Get-FileHash .\GlassNote-v3.0.0.zip -Algorithm SHA256 +``` + +## Edge 商店 + +使用经校验的同一份 ZIP。商店文案、隐私链接、权限理由及审核步骤见 [Edge 发布材料](EDGE_STORE.md)。更新商店截图时使用自编演示数据,不上传私有页面或个人笔记。 + +在商店审核通过并取得公开安装链接后,再更新 README 的商店状态和安装入口。开发者注册联系方式只填写到 Microsoft 后台,不提交到仓库或发行包。 diff --git a/docs/screenshots/library.png b/docs/screenshots/library.png new file mode 100644 index 0000000..8e2c08b Binary files /dev/null and b/docs/screenshots/library.png differ diff --git a/docs/screenshots/web-annotations.png b/docs/screenshots/web-annotations.png new file mode 100644 index 0000000..01c41ab Binary files /dev/null and b/docs/screenshots/web-annotations.png differ diff --git a/package-lock.json b/package-lock.json index 8e65c3a..a1dc9f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,10 @@ "devDependencies": { "playwright": "1.63.0", "prettier": "3.9.9" + }, + "license": "MIT", + "engines": { + "node": ">=22" } }, "node_modules/playwright": { diff --git a/package.json b/package.json index 3990285..683bd73 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,26 @@ "test:e2e": "node --test tests/extension.e2e.cjs", "test:live": "node tests/live-site.cjs", "check": "node scripts/check.cjs", - "format": "prettier --write src styles scripts tests manifest.json package.json README.md", - "format:check": "prettier --check src styles scripts tests manifest.json package.json README.md", - "package": "node scripts/package.cjs" + "format": "prettier --write src styles scripts tests .github docs manifest.json package.json package-lock.json \"*.md\"", + "format:check": "prettier --check src styles scripts tests .github docs manifest.json package.json package-lock.json \"*.md\"", + "package": "node scripts/package.cjs", + "package:verify": "node scripts/verify-package.cjs" }, "devDependencies": { "playwright": "1.63.0", "prettier": "3.9.9" + }, + "license": "MIT", + "author": "aredddd", + "repository": { + "type": "git", + "url": "git+https://github.com/aredddd/GlassNote.git" + }, + "homepage": "https://github.com/aredddd/GlassNote#readme", + "bugs": { + "url": "https://github.com/aredddd/GlassNote/issues" + }, + "engines": { + "node": ">=22" } } diff --git a/scripts/package-lib.cjs b/scripts/package-lib.cjs new file mode 100644 index 0000000..6b9bbf7 --- /dev/null +++ b/scripts/package-lib.cjs @@ -0,0 +1,216 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const { createHash } = require('node:crypto'); +const { deflateRawSync, inflateRawSync } = require('node:zlib'); + +const REQUIRED_FILES = [ + 'manifest.json', + 'README.md', + 'LICENSE', + 'PRIVACY.md', + 'THIRD_PARTY_NOTICES.md', + 'CONTRIBUTING.md', + 'SECURITY.md', +]; +const TREES = { + src: /\.(js|html|css)$/, + styles: /\.css$/, + assets: /\.(png|svg|ico|webp)$/, + docs: /\.(md|png|svg|webp)$/, +}; +const sha256 = (data) => createHash('sha256').update(data).digest('hex'); + +function releaseVersion(root, tag) { + const read = (name) => JSON.parse(fs.readFileSync(path.join(root, name), 'utf8')); + const manifest = read('manifest.json'); + const pkg = read('package.json'); + const lock = read('package-lock.json'); + const version = manifest.version; + if ( + !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version) || + version.split('.').some((part) => Number(part) > 65535) || + version === '0.0.0' + ) + throw new Error('扩展版本必须是 Chrome 支持的三段数字版本,如 3.0.0'); + if (manifest.manifest_version !== 3) throw new Error('发布包必须是 Manifest V3 扩展'); + if ( + pkg.version !== version || + lock.version !== version || + lock.packages?.['']?.version !== version + ) + throw new Error('manifest.json、package.json 和 package-lock.json 的版本必须一致'); + if (tag !== undefined && tag !== `v${version}`) + throw new Error(`标签 ${tag} 与项目版本不一致,预期 v${version}`); + return version; +} + +function tagArgument(args) { + if (!args.length) return undefined; + if (args.length !== 2 || args[0] !== '--tag' || !args[1]) + throw new Error('参数格式:--tag v3.0.0'); + return args[1]; +} + +function packageFiles(root) { + const files = []; + function add(name) { + const full = path.join(root, name); + if (!fs.lstatSync(full).isFile()) throw new Error(`发布文件不能是目录或符号链接:${name}`); + files.push({ name, data: fs.readFileSync(full) }); + } + for (const name of REQUIRED_FILES) add(name); + for (const name of ['CHANGELOG.md', 'CONTRIBUTORS.md']) { + if (fs.existsSync(path.join(root, name))) add(name); + } + function walk(name, extension) { + const full = path.join(root, name); + const stat = fs.lstatSync(full); + if (stat.isSymbolicLink()) throw new Error(`发布目录不能包含符号链接:${name}`); + if (stat.isDirectory()) { + for (const child of fs.readdirSync(full).sort()) { + if (!child.startsWith('.')) walk(`${name}/${child}`, extension); + } + } else if (stat.isFile() && extension.test(name)) add(name); + } + for (const [name, extension] of Object.entries(TREES)) walk(name, extension); + return files.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); +} + +function crc32(data) { + let crc = 0xffffffff; + for (const value of data) { + crc ^= value; + for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); + } + return (crc ^ 0xffffffff) >>> 0; +} + +// 小型标准 ZIP(deflate、UTF-8、无 ZIP64),无需系统 zip 命令或运行时依赖。 +// 固定时间与文件排序,让相同源文件在同一 Node 环境中重复打包结果一致。 +function createArchive(files) { + const locals = []; + const central = []; + let offset = 0; + if (files.length > 65535) throw new Error('发布文件数量超过 ZIP 限制'); + for (const { name, data } of files) { + const filename = Buffer.from(name, 'utf8'); + const compressed = deflateRawSync(data, { level: 9 }); + const crc = crc32(data); + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50); + local.writeUInt16LE(20, 4); + local.writeUInt16LE(0x800, 6); + local.writeUInt16LE(8, 8); + local.writeUInt16LE(33, 12); // 1980-01-01 + local.writeUInt32LE(crc, 14); + local.writeUInt32LE(compressed.length, 18); + local.writeUInt32LE(data.length, 22); + local.writeUInt16LE(filename.length, 26); + const record = Buffer.alloc(46); + record.writeUInt32LE(0x02014b50); + record.writeUInt16LE(20, 4); + local.copy(record, 6, 4, 30); + record.writeUInt32LE(offset, 42); + locals.push(local, filename, compressed); + central.push(record, filename); + offset += local.length + filename.length + compressed.length; + } + const directory = Buffer.concat(central); + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50); + end.writeUInt16LE(files.length, 8); + end.writeUInt16LE(files.length, 10); + end.writeUInt32LE(directory.length, 12); + end.writeUInt32LE(offset, 16); + return Buffer.concat([...locals, directory, end]); +} + +// 只接受本项目生成的标准 ZIP 布局,检查中央目录、局部头、大小和每个文件的 CRC。 +function readArchive(archive) { + const fail = () => { + throw new Error('ZIP 结构或文件校验失败'); + }; + const end = archive.length - 22; + if (end < 0 || archive.readUInt32LE(end) !== 0x06054b50) fail(); + if (archive.readUInt32LE(end + 4) !== 0 || archive.readUInt16LE(end + 20) !== 0) fail(); + const count = archive.readUInt16LE(end + 10); + if (count !== archive.readUInt16LE(end + 8)) fail(); + let offset = archive.readUInt32LE(end + 16); + if (offset + archive.readUInt32LE(end + 12) !== end) fail(); + const centralStart = offset; + const files = new Map(); + let localEnd = 0; + for (let index = 0; index < count; index++) { + if (offset + 46 > end || archive.readUInt32LE(offset) !== 0x02014b50) fail(); + const nameLength = archive.readUInt16LE(offset + 28); + const extraLength = archive.readUInt16LE(offset + 30); + const commentLength = archive.readUInt16LE(offset + 32); + const localOffset = archive.readUInt32LE(offset + 42); + const compressedSize = archive.readUInt32LE(offset + 20); + const size = archive.readUInt32LE(offset + 24); + if ( + extraLength || + commentLength || + offset + 46 + nameLength > end || + localOffset !== localEnd || + localOffset + 30 + nameLength + compressedSize > centralStart || + archive.readUInt16LE(offset + 8) !== 0x800 || + archive.readUInt16LE(offset + 10) !== 8 || + archive.readUInt16LE(offset + 34) !== 0 || + archive.readUInt32LE(localOffset) !== 0x04034b50 + ) + fail(); + const nameBytes = archive.subarray(offset + 46, offset + 46 + nameLength); + const name = nameBytes.toString('utf8'); + if ( + !name || + name.includes('\\') || + name.includes('\0') || + name.split('/').some((part) => !part || part === '.' || part === '..') || + files.has(name) || + !archive + .subarray(localOffset + 4, localOffset + 30) + .equals(archive.subarray(offset + 6, offset + 32)) || + !nameBytes.equals(archive.subarray(localOffset + 30, localOffset + 30 + nameLength)) + ) + fail(); + localEnd = localOffset + 30 + nameLength + compressedSize; + const data = inflateRawSync(archive.subarray(localOffset + 30 + nameLength, localEnd), { + maxOutputLength: 16 * 1024 * 1024, + }); + if (data.length !== size || crc32(data) !== archive.readUInt32LE(offset + 16)) fail(); + files.set(name, data); + offset += 46 + nameLength; + } + if (offset !== end || localEnd !== centralStart) fail(); + return files; +} + +function verifyPackage(root, output, tag) { + const version = releaseVersion(root, tag); + const filename = `GlassNote-v${version}.zip`; + const archivePath = path.join(output, filename); + const archive = fs.readFileSync(archivePath); + const sumsPath = path.join(output, 'SHA256SUMS'); + if (fs.readFileSync(sumsPath, 'utf8') !== `${sha256(archive)} ${filename}\n`) + throw new Error('SHA256SUMS 与发布包不匹配'); + const entries = readArchive(archive); + const expected = packageFiles(root); + if (entries.size !== expected.length) throw new Error('ZIP 文件清单与发布白名单不一致'); + for (const { name, data } of expected) { + if (!entries.get(name)?.equals(data)) throw new Error(`ZIP 文件缺失或内容不一致:${name}`); + } + return { version, archive: archivePath, checksums: sumsPath, files: entries.size }; +} + +function packageExtension(root, output, tag) { + const version = releaseVersion(root, tag); + const filename = `GlassNote-v${version}.zip`; + const archive = createArchive(packageFiles(root)); + fs.mkdirSync(output, { recursive: true }); + fs.writeFileSync(path.join(output, filename), archive); + fs.writeFileSync(path.join(output, 'SHA256SUMS'), `${sha256(archive)} ${filename}\n`); + return verifyPackage(root, output, tag); +} + +module.exports = { packageExtension, releaseVersion, verifyPackage, readArchive, tagArgument }; diff --git a/scripts/package.cjs b/scripts/package.cjs index e8c6dca..9a5e967 100644 --- a/scripts/package.cjs +++ b/scripts/package.cjs @@ -1,14 +1,10 @@ -const fs = require('node:fs'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); +const { packageExtension, releaseVersion, tagArgument } = require('./package-lib.cjs'); + const root = path.resolve(__dirname, '..'); +const tag = tagArgument(process.argv.slice(2)); +releaseVersion(root, tag); execFileSync(process.execPath, [path.join(__dirname, 'check.cjs')], { stdio: 'inherit' }); -const output = path.join(root, 'dist'); -fs.mkdirSync(output, { recursive: true }); -const filename = 'GlassNote-v3.0.0.zip'; -const target = path.join(output, filename); -fs.rmSync(target, { force: true }); -execFileSync('zip', ['-qr', target, 'manifest.json', 'src', 'styles', 'assets', 'README.md'], { - cwd: root, -}); -console.log('安装包:' + target); +const result = packageExtension(root, path.join(root, 'dist'), tag); +console.log(`安装包:${result.archive}\n校验和:${result.checksums}\n包内文件:${result.files}`); diff --git a/scripts/verify-package.cjs b/scripts/verify-package.cjs new file mode 100644 index 0000000..04027c0 --- /dev/null +++ b/scripts/verify-package.cjs @@ -0,0 +1,12 @@ +const path = require('node:path'); +const { releaseVersion, verifyPackage, tagArgument } = require('./package-lib.cjs'); +const root = path.resolve(__dirname, '..'); +const args = process.argv.slice(2); +const versionOnly = args[0] === '--version-only'; +const tag = tagArgument(versionOnly ? args.slice(1) : args); +if (versionOnly) { + console.log(`版本校验通过:v${releaseVersion(root, tag)}`); +} else { + const result = verifyPackage(root, path.join(root, 'dist'), tag); + console.log(`发布包校验通过:v${result.version},${result.files} 个文件,SHA-256 一致。`); +} diff --git a/tests/package.test.cjs b/tests/package.test.cjs new file mode 100644 index 0000000..921859a --- /dev/null +++ b/tests/package.test.cjs @@ -0,0 +1,127 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); +const { createHash } = require('node:crypto'); +const { + packageExtension, + releaseVersion, + verifyPackage, + readArchive, +} = require('../scripts/package-lib.cjs'); + +function fixture(t) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'glassnote-package-')); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const write = (name, value) => { + fs.mkdirSync(path.dirname(path.join(root, name)), { recursive: true }); + fs.writeFileSync(path.join(root, name), value); + }; + write('manifest.json', JSON.stringify({ manifest_version: 3, version: '3.2.1' })); + write('package.json', JSON.stringify({ version: '3.2.1' })); + write( + 'package-lock.json', + JSON.stringify({ version: '3.2.1', packages: { '': { version: '3.2.1' } } }), + ); + for (const name of [ + 'README.md', + 'LICENSE', + 'PRIVACY.md', + 'THIRD_PARTY_NOTICES.md', + 'CONTRIBUTING.md', + 'SECURITY.md', + ]) + write(name, `公开发布文件:${name}\n`); + write('src/background/background.js', "console.log('GlassNote');\n"); + write('styles/content.css', ':host { color: green; }'); + write('assets/icon.svg', ''); + write('docs/安装说明.md', '中文文档应当正确保存在 ZIP 中。\n'); + const output = path.join(root, 'dist'); + return { root, output, write }; +} + +test('发布包按版本命名,附许可证和隐私声明,排除依赖、工作文件和隐藏文件', (t) => { + const { root, output, write } = fixture(t); + for (const name of [ + '.env', + 'src/.env', + 'src/debug.log', + 'work/draft.md', + 'tests/fixture.html', + 'node_modules/dev/index.js', + ]) + write(name, '不应发布'); + const result = packageExtension(root, output, 'v3.2.1'); + assert.equal(path.basename(result.archive), 'GlassNote-v3.2.1.zip'); + const archive = fs.readFileSync(result.archive); + const entries = readArchive(archive); + assert.equal(entries.size, 11); + for (const name of [ + 'LICENSE', + 'PRIVACY.md', + 'THIRD_PARTY_NOTICES.md', + 'CONTRIBUTING.md', + 'SECURITY.md', + 'docs/安装说明.md', + ]) + assert.ok(entries.has(name), name); + assert.equal(entries.get('docs/安装说明.md').toString(), '中文文档应当正确保存在 ZIP 中。\n'); + assert.ok( + [...entries.keys()].every((name) => !/node_modules|work\/|tests\/|\.env|debug/.test(name)), + ); + const hash = createHash('sha256').update(archive).digest('hex'); + assert.equal(fs.readFileSync(result.checksums, 'utf8'), `${hash} GlassNote-v3.2.1.zip\n`); + packageExtension(root, output, 'v3.2.1'); + assert.deepEqual(fs.readFileSync(result.archive), archive, '重复打包结果应稳定'); +}); + +test('发布前拒绝标签、manifest、package 和锁文件之间的版本不一致', (t) => { + const { root, write } = fixture(t); + assert.equal(releaseVersion(root, 'v3.2.1'), '3.2.1'); + assert.throws(() => releaseVersion(root, 'v3.2.2'), /标签/); + write('package.json', JSON.stringify({ version: '3.2.2' })); + assert.throws(() => releaseVersion(root), /版本必须一致/); + write('package.json', JSON.stringify({ version: '3.2.1' })); + write( + 'package-lock.json', + JSON.stringify({ version: '3.2.1', packages: { '': { version: '3.2.2' } } }), + ); + assert.throws(() => releaseVersion(root), /版本必须一致/); +}); + +test('拒绝无法安装的版本、非 MV3 和缺失许可证的发布包', (t) => { + const { root, output, write } = fixture(t); + for (const version of ['3.2.1-beta', '03.2.1', '65536.2.1', '0.0.0']) { + write('manifest.json', JSON.stringify({ manifest_version: 3, version })); + assert.throws(() => releaseVersion(root), /三段数字/); + } + write('manifest.json', JSON.stringify({ manifest_version: 2, version: '3.2.1' })); + assert.throws(() => releaseVersion(root), /Manifest V3/); + write('manifest.json', JSON.stringify({ manifest_version: 3, version: '3.2.1' })); + fs.rmSync(path.join(root, 'LICENSE')); + assert.throws(() => packageExtension(root, output), /LICENSE/); + assert.equal(fs.existsSync(output), false, '缺文件时不能留下看似可用的产物'); +}); + +test('发布包复验能够发现 ZIP 损坏、校验和篡改与源文件变化', (t) => { + const { root, output, write } = fixture(t); + const result = packageExtension(root, output); + const archive = fs.readFileSync(result.archive); + const damaged = Buffer.from(archive); + damaged[0] ^= 1; + fs.writeFileSync(result.archive, damaged); + assert.throws(() => verifyPackage(root, output), /SHA256SUMS/); + const hash = createHash('sha256').update(damaged).digest('hex'); + fs.writeFileSync(result.checksums, `${hash} GlassNote-v3.2.1.zip\n`); + assert.throws(() => verifyPackage(root, output), /ZIP 结构/); + packageExtension(root, output); + write('src/background/background.js', 'changed after packaging'); + assert.throws(() => verifyPackage(root, output), /内容不一致/); +}); + +test('发布白名单内的符号链接不能将外部文件带入安装包', (t) => { + const { root, output } = fixture(t); + fs.symlinkSync(path.join(root, 'LICENSE'), path.join(root, 'src/linked.js')); + assert.throws(() => packageExtension(root, output), /符号链接/); +});