Files
陈银军 1b0c3c012c feat: 按域名授权、iframe 多帧填充、自动提交限流修正,并补齐工程规范与文档
安全与权限:
- 站点访问权限改为 optional_host_permissions,保存配置时逐域名授权;移除 host_permissions 与 tabs,安装提示不再出现全站数据访问与浏览记录
- 主口令不再写入 storage.session,改用 IndexedDB 中不可导出的 CryptoKey 句柄恢复解锁,锁定即丢弃
- content script 注入范围只注册已授权域名,并随权限变化即时收敛

自动登录:
- 支持 iframe 内的登录表单:脚本注入所有帧,逐帧按自身地址匹配配置
- 手动填充改为逐帧探测,定向发送到真正含密码框的帧
- 限流修正:只在真正触发提交后计数;判定登录成功后立即清零;达到上限时页面给出可见提示;重置判断只看启用中的配置
- SPA 重试改为 DOM 变更门控 + 退避,并单独监听已发现的 Shadow Root

工程化与文档:
- 引入 ESLint(扁平配置)与 Prettier,CI 增加 lint 与 format:check
- 信息类日志改为 debugLog(默认静默,chrome.storage.local.debugLog 开关)
- 测试 123 → 128 用例(新增权限、iframe、限流相关用例)
- README / PRIVACY / CHANGELOG 同步
2026-09-18 00:05:20 +08:00

8.1 KiB
Raw Permalink Blame History

auto-login 项目长期记忆

自动提交限流语义(2026-09-18 修正)

  • 计数的是自动提交次数(不是"登录失败次数",扩展无法知道凭据对不对),key 为 envId::domain,存 chrome.storage.local.loginAttempts。上限 3 次,冷却 5 分钟。
  • 计数时机:只在真正触发提交后(fillForm 的 onSubmitted 回调)累加,fillSingleConfig 不再提前计数。填写阶段(未提交)不计数。
  • 成功检测(两条路径,命中任一即清零):
    1. 提交后 submitResultCheckMs(默认 4s)观察:登录框消失或已离开匹配地址 → resetDomainAttempts
    2. 整页跳转导致脚本销毁时,由新页面兜底:命中配置但 fillTimeoutMs(默认 30s)内始终没有登录框 → resetDomainAttempts
  • 达到上限时页面必须给可见提示(showAttemptsPausedToast,i18n attemptToastTitle/Body),仅写日志用户看不懂。
  • 测试钩子:window.__autoLoginTiming = { fillTimeoutMs, submitResultCheckMs },避免测试真等数十秒。
  • 重置路径还有:URL 变化后不再匹配任何启用中配置、popup 手动填充。

iframe / 多帧约定(2026-09-17)

  • 注入一律带 allFrames: true(registerContentScripts 与所有 executeScript),否则 iframe 内的登录表单填不了。
  • content.ts 每个帧都用自己的 window.location.href 匹配配置;isTopFrame()(window.top === window)决定是否渲染浮窗:浮窗只在顶层帧,子帧改用 pickConfigForFrame(matches, activeEnvId)(优先激活环境)确定性选择。
  • background.loadAllConfigs 的响应包含 activeEnvId,供子帧选择凭据。
  • popup 手动填充:先 executeScript({allFrames:true, func}) 逐帧探测已注入标记(只对未注入的帧用 frameIds 补注入,避免重复注入报错),再探测哪帧有 input[type=password](utils.pickFrameWithForm),最后带 {frameId} 定向 tabs.sendMessage。
  • 坑:chrome.runtime.onMessage 监听器必须保持同步返回 true,子帧里的异步填充要包在内部 IIFE 里,不能把监听器设成 async。

权限模型(2026-09-17 定为"方案 C")

  • manifest.json:不声明 host_permissions,改为 optional_host_permissions: ["*://*/*"];同时移除了 tabs(安装提示不再出现"所有网站数据"与"浏览记录")。当前 permissions = storage / activeTab / scripting / sidePanel / bookmarks。
  • 授权流程:保存配置(新增或编辑)时 Perms.requestForDomain(domain) 逐域名询问;必须紧跟用户手势,因此放在其它 await 之前。
  • src/permissions.ts(UMD 全局 Perms,依赖 utils.js 的 toMatchPatterns):patternsForDomain / isPatternGranted / partitionPatterns / partitionDomains / requestForDomain。
  • background 只注册已授权的 match pattern(未授权会让整批注册失败);监听 chrome.permissions.onAdded/onRemoved 重算注入范围。
  • 手动填充与"填入当前域名"走 activeTab(点扩展图标即授权,零弹框);因此切标签页后可能需要重新点图标。
  • 未授权域名在配置列表显示"未授权"标签,点击即可重新申请(i18n: tagUnauthorized 等 7 个新键)。
  • 兜底:若 registerContentScripts 失败(文档未明确"运行时授权的主机权限"能否用于动态注册),自动降级为 chrome.tabs.onUpdated + executeScript 按需注入(fallbackInjection 开关)。待真机验证。
  • 未声明 tabs 的副作用:chrome.tabs.query({url}) 的 url 过滤被忽略而非报错 → 任何按 url 过滤的结果必须再自行校验 isDomainMatch(见 ui-utils.fetchTitleByDomain)。

已知安全债(2026-09-17 已修复前两条)

  1. 已修复:原实现把主口令明文存 chrome.storage.session.__autoPwd。现改为把 non-extractable CryptoKey 句柄存入 IndexedDB(src/key-store.ts),SW 重启后用 CryptoStore.unlockWithKey(key) 恢复解锁,口令不落任何存储;doLock() 会删除句柄。旧版 __autoPwd 保留一次性迁移分支(读到即转句柄并删除明文)。
    • 已核实:storage.session 默认不向 content script 开放(仅受信任上下文)——早前"任何 content script 都能读到"的说法是错的。
    • 已核实:CryptoKey 支持 structured clone 存入 IndexedDB,取回后 extractable 仍为 false(exportKey 抛错)但可正常 decrypt。
  2. 已修复:原 manifest 静态 content_scripts + <all_urls> 无条件注入。现移除静态声明,background 通过 chrome.scripting.registerContentScripts 只注册用户配置过的域名(syncContentScripts,由 utils.toMatchPatterns 生成 match pattern);popup 手动填充改为"探测 window.__autoLoginInjected → chrome.scripting.executeScript 按需注入"。
    • 仍保留 host_permissions: ["<all_urls>"](按需注入需要),若要进一步收窄安装提示可改 optional_host_permissions 并按域名申请。
    • 锁定时 session 无明文域名 → 注入范围自动清空;解锁后才重新注册。

项目性质

Chrome MV3「Auto Login Manager」扩展(域名配置自动填充 + 加密存储 + 多环境)。自建 Gitea 仓库:http://218.61.196.156:41695/Chrome-extension/auto-login.git。

环境注意事项

  • npm 全局缓存 ~/.npm 有权限问题,npm install 必须加 --cache /tmp/npm-cache-autologin。
  • 全局安装不可用(权限),依赖一律装为 devDependency。

代码规范与日志约定(2026-09-17 建立)

  • ESLint 扁平配置 eslint.config.mjs:js.recommended + typescript-eslint.recommended,但关掉与 tsc 重叠/噪音大的规则(no-undef、no-explicit-any、no-non-null-assertion),重点保留 no-unused-vars(^_ 前缀豁免、caughtErrors: none)、prefer-const、no-var、no-empty(allowEmptyCatch)。测试文件按 CJS 编写,已单独关闭 no-require-imports。
  • Prettier:.prettierrc.json(printWidth 110 / 双引号 / 分号 / trailingComma all)。格式化范围仅 src、scripts、tests,不含 popup.html / popup.css / _locales(避免上千行空白 diff)。
  • 命令:npm run lint / lint:fix / format / format:check;CI 在测试前执行 lint + format:check。改代码后请跑 npm run format && npm run lint。
  • 日志:信息类日志一律用全局 debugLog(...)(utils.ts 提供,默认静默),不要直接 console.log。异常告警用 console.warn。开关:chrome.storage.local.set({ debugLog: true })。

构建约定(2026-09-17 建立)

  • 类型检查与产物生成分离:tsc --noEmit 只做类型检查(tsconfig 已设 noEmit: true),esbuild 负责产出 dist/。
  • 产物分三类,不可混为一谈:
    1. UMD 全局脚本 utils/env-store/crypto-store:必须不打包、不指定 format。一旦指定 iife,esbuild 会把 module.exports 换成合成模块对象,Node 测试 require(dist/xxx.js) 就取不到导出。
    2. 独立入口 background/content:不打包(运行期靠 importScripts 与全局变量协作)。
    3. popup/main.ts:唯一真正打包的入口(ESM,静态依赖全部内联,由 popup.html 以 type="module" 加载)。
  • scripts/build.mjs 会校验 manifest.json / popup.html 引用的文件是否都存在(11 处),并检查 manifest 与 package.json 版本一致性。
  • dist/ 已从 git 移除(.gitignore),克隆后必须先 npm run build 才能加载扩展。
  • 测试依赖 dist:npm test = build + node --test tests/*.test.js;只想跑测试用 npm run test:only(需已有 dist)。
  • 发布:npm run release -- patch|minor|major|x.y.z [--codebase <url>] → 同步 manifest.json / package.json / updates.xml(只替换 version 行,保留原格式)→ 发布构建 → release/auto-login-v<ver>.zip。不自动 commit/tag。

部署产物

  • updates.xml:自托管自更新清单,appid=dpcmpkmjkcenjpfcmkoohkcnhbomgmej,codebase 指向 Windows 本地路径的 .crx。
  • install-policy.reg:Windows 强制安装策略。两者都不打进扩展 zip。