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 同步
This commit is contained in:
@@ -23,7 +23,22 @@
|
||||
- 事实核对:vault PBKDF2 20 万次、导出文件 10 万次、SPA 重试 30s、失败 3 次/冷却 5 分钟、自动锁定 10 分钟、首次提交 2026-09-14、共 30 次提交。
|
||||
- 扩展 zip 仍只含运行时文件(22 个文件),文档与源码不入包。
|
||||
|
||||
## 段位复评(改造后)
|
||||
## 采用方案 C:站点权限改为按域名可选授权
|
||||
- 新增 `src/permissions.ts`(UMD 全局 `Perms`)+ `tests/permissions.test.js`(13 用例)→ 测试 **112 全绿**。
|
||||
- manifest:去掉 `host_permissions: <all_urls>` 与 `tabs`,改 `optional_host_permissions: ["*://*/*"]`;popup.html 增加 `dist/permissions.js` 引用;build 的 GLOBAL_SCRIPTS 加 `permissions`(现 8 入口、12 处引用校验)。
|
||||
- background:只注册已授权 pattern;新增 `chrome.permissions.onAdded/onRemoved` 监听;抽出 `loadEnvironmentsForMatching` / `collectConfigDomains`;新增**兜底注入**(注册失败 → onUpdated 时 executeScript)。
|
||||
- popup:新增/编辑保存时先 `Perms.requestForDomain`(紧跟手势);失败提示 toastUnauthorized;配置列表加"未授权"标签可点击授权;`fill-current` 与两个"填入当前域名"按钮改用 activeTab 并区分提示(toastNoTabUrl / toastNeedActiveTab);ui-utils 新增 `resolveAlias` 并修正 `tabs.query({url})` 过滤失效的隐患。
|
||||
- i18n:中英各新增 7 个键;popup.css 新增 `.tag-unauthorized`。
|
||||
- 文档同步:README 安全设计与权限表、PRIVACY 双语权限段、CHANGELOG(Changed/Security)。
|
||||
- 待真机验证(我无法在此环境跑 Chrome):① 安装提示是否已无全站/浏览记录警告 ② 保存配置时是否弹授权框 ③ 授权后自动填充是否生效、`registerContentScripts` 是否接受运行时授权(若失败看 SW 控制台是否出现"降级为按需注入")④ 升级后旧配置需逐个点"未授权"标签授权。
|
||||
|
||||
## 收尾四项打磨(用户要求,其中第 4 项已过期)
|
||||
- **第 4 项「activeTab 冗余」已失效**:C 方案移除了 host_permissions 与 tabs 之后,activeTab 变成手动填充/读 tab.url 的**必要**权限,不能删。已向用户说明。
|
||||
- lint/formatter:新增 `eslint.config.mjs` + `.prettierrc.json` + `.prettierignore`,devDeps 加 eslint/@eslint/js/typescript-eslint/prettier/globals(安装需 `--cache /tmp/npm-cache-autologin`)。修复了真实问题:main.ts 未使用的 `t` 导入、prefer-const(改用 api 容器 + 非空断言重构初始化顺序)、content.ts 三处"表达式语句"(requestSubmit 三元、两处 `overlay.parentNode && ...` → 抽出 `closeEnvPicker()`)、package.mjs 缺 `cause`、测试未使用参数。CI 增加 lint + format:check。
|
||||
- 调试日志:utils.ts 新增 `initDebugLog/isDebugLogEnabled/debugLog`,开关 `chrome.storage.local.debugLog`;content.ts/background.ts 的信息类日志全部改为 debugLog,异常类保留 warn;background 与 content 启动时先 init 再执行主流程。
|
||||
- 性能:SPA 重试改为"DOM 变更门控 + 退避"(`domVersion` + `SPA_RETRY_DELAYS_MS` 300→3000ms),30 秒窗口内尝试次数 60 → 14 且多数轮次跳过扫描;**关键修正**:body 上的 MutationObserver 看不到 Shadow DOM 内部变化,若只做版本门控会漏填组件内部后渲染的登录框 → 新增 `trackShadowRoot()`,对扫描中发现的 shadow root 单独挂 observer 递增 domVersion(WeakSet 去重)。
|
||||
- 测试 112 → **114 全绿**(新增 Shadow DOM 内部渲染用例、debugLog 开关用例)。lint 0 问题、format:check 通过、打包正常。
|
||||
|
||||
- 结论:**铂金(稳固)**,构建发布与安全设计摸到钻石边,被文档合规与测试深度拖住。
|
||||
- 量化现状:源码 3531 行(content.ts 612 / background.ts 341 / 最大 popup 模块 config-list 238);测试 920 行 75 用例全绿;压缩后产物合计 59KB(dev 97KB);双语 830 行文案。
|
||||
- 距钻石的差距(按阻塞程度):① 无 README/CHANGELOG/LICENSE/隐私政策(CWS 处理凭据需隐私政策,唯一硬阻塞)② content.ts 612 行最复杂逻辑无测试 ③ host_permissions <all_urls> 未收敛 ④ 无 lint/formatter 配置 ⑤ deepQuerySelectorAll 每 500ms 全量遍历+扫描 shadowRoot,最长 30s ⑥ 调试日志无开关 ⑦ activeTab 权限已冗余。
|
||||
@@ -34,6 +49,14 @@
|
||||
- 已核实 CryptoKey 可 structured clone 入 IndexedDB 且保持 extractable:false,这是替代 __autoPwd 的推荐实现路径。
|
||||
- 用户尚未开始动手改这两条,等待其决定。
|
||||
|
||||
## iframe 支持(用户反馈"iframe 里的表单填不了")
|
||||
- 根因:三处注入都没开 `allFrames`(动态注册 / 兜底注入 / popup 探测与注入),默认只作用于顶层帧。
|
||||
- 第一步:注册与注入全部加 `allFrames: true`;顺带修掉"兜底注入早于 body 就绪"时 `observer.observe(document.body)` 会抛错的问题(改为等 DOMContentLoaded)。
|
||||
- 第二步:`isTopFrame()` 门控浮窗(子帧不弹,避免被 iframe 裁切)→ 子帧用 `pickConfigForFrame(matches, activeEnvId)` 按激活环境选择;`loadAllConfigs` 响应新增 `activeEnvId`;popup 改为逐帧探测 + `frameIds` 精确补注入 + `{frameId}` 定向发消息(修掉"iframe 填成功却收到别帧失败诊断"的错报)。
|
||||
- 坑:onMessage 监听器不能改成 async(`return true` 变成微任务会让 Chrome 提前关闭通道),子帧异步填充包在内部 IIFE。
|
||||
- 测试:123 全绿(新增 4 个 iframe 用例 + 5 个 `pickFrameWithForm` 用例)。**测试 harness 注意**:jsdom 每个 realm 有自己的 HTMLElement.prototype,iframe 必须单独 `patchLayout(iwin)`,否则 `offsetParent` 为 null 导致用户名框被判为不可见(真实浏览器无此问题)。
|
||||
- 文档已同步 README/PRIVACY(披露"已授权域名下注入所有帧,各帧按自身地址匹配")与 CHANGELOG。
|
||||
|
||||
## popup.ts 1479 行巨石拆分完成
|
||||
- 拆为 src/popup/ 下 15 个模块:dom/toast/toggle-pwd/context(基础)、pwd-modal/env-modal/config-edit-modal(弹窗)、env-section/config-form/config-list(业务)、transfer/bookmark-import/fill-current/lock(功能)、main(入口)。
|
||||
- 关键设计:模块间不互相 import,统一通过 context.ts 的 PopupCtx 接口互调(showToast/resetForm/renderEnvSelect/renderConfigList/refreshEnvManage/openEditModal 等),由 main.ts 组装,避免循环依赖。pwd-modal/env-modal 用 Promise resolve 模式供 transfer 等模块 await。
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# 2026-09-18
|
||||
|
||||
## 自动提交限流逻辑的四项修正(用户要求"四条一起做")
|
||||
背景:用户核对"连续 3 次失败后不再自动登录"这条逻辑,核查后发现实际语义是"3 次自动提交",且有误伤场景。按认可的四条一并修复:
|
||||
|
||||
1. **计数时机前移错误 → 改为提交后计数**:原实现 `fillSingleConfig` 在探测表单之前就 increment,导致"命中域名但没有登录框"的页面也消耗配额(纯域名配置下,站内浏览 3 个页面就会禁用自动提交)。现改为 `fillForm(config, canSubmit, onSubmitted)`,只在真正 `click()` / `requestSubmit()` 之后累加。
|
||||
2. **新增登录成功检测并清零**:提交后 4s(`submitResultCheckMs`)观察,登录框消失或离开匹配地址 → 判定成功 → `resetDomainAttempts`;整页跳转导致脚本销毁的情况由新页面兜底——命中配置但 30s(`fillTimeoutMs`)内始终无登录框 → 清零。修正了"纯域名配置下连续 3 次成功登录后停止自动提交"。
|
||||
3. **达到上限时页面可见提示**:新增 `showPageToast(title, body)`(把原 `showCaptchaToast` 泛化,并对文案做了 escapeHtml),`showAttemptsPausedToast()` 显示"已连续自动提交 N 次、M 分钟后恢复"。同时修正文案措辞(原文写"失败次数",实际是提交次数),中英双语文案同步改。
|
||||
4. **重置判断过滤 enabled**:URL 变化时的 `stillMatched`/`prevMatched` 只看启用中的配置(此前一条被禁用的配置会阻止重置)。
|
||||
|
||||
其他细节:
|
||||
- 新增测试钩子 `window.__autoLoginTiming`(覆盖 `fillTimeoutMs` / `submitResultCheckMs`),否则测超时路径要真等 30 秒。
|
||||
- 测试:content-fill 34 用例 + 其余 94 用例 = **128 全绿**;lint 0 问题、format:check 通过。
|
||||
- 文档同步:README 功能特性改为"连续自动提交 3 次"并说明成功即清零;CHANGELOG 的 Fixed 段补 5 条。
|
||||
- 语义已记入 MEMORY.md("自动提交限流语义"一节)。
|
||||
@@ -1,5 +1,32 @@
|
||||
# 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 都能读到"的说法是错的。
|
||||
@@ -15,6 +42,12 @@ Chrome MV3「Auto Login Manager」扩展(域名配置自动填充 + 加密存
|
||||
- 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/**。
|
||||
- 产物分三类,不可混为一谈:
|
||||
|
||||
Reference in New Issue
Block a user