@xinvxueyuan/cordis-plugin-secret 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/lib/client/entry.js +50 -7
- package/package.json +1 -1
- package/src/client/entry.ts +73 -22
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
4. **会话级密钥不落盘**:`scope: "session"` 的值只存在于进程内存(Host 的暂存表与会话授权表)。落盘只走凭据服务,且只发生在 `persistent`。
|
|
24
24
|
5. **持久化只经凭据服务**:`persistent` 经 `ctx.credentials.set(<变量名>, value)` 写入凭据引用空间(provider 管理的可写源);同时向记录空间提交一条**不含密钥材料**的标记记录(索取方向 `kind: "grant"`,附加方向 `kind: "attachment"`,payload 只有 `envVar/name/scope/authorizedAt`)。绝不写自建文件,绝不在仓库里存明文。
|
|
25
25
|
6. **会话边界失败关闭**(见「边界处理」):锚点离开会话表面即撤销并不再注入。
|
|
26
|
-
7. **人类输入密钥的控件既不可自动填充,也不可被密码管理器捕获**(用户实机报出的缺陷的修复):**现象**:真实浏览器会在这四个密钥输入面上给出自动填充建议,已装的密码管理器扩展也会捕获它们。**根因**:修复前这四个字段只有 `autoComplete:'off'`(现代浏览器对登录类凭据字段基本忽略这个值)、**没有任何厂商忽略标记、也没有别的退出开关**,而掩码用的又是原生 `type="password"`,浏览器正是据此把它当作密码域。**修复**:本插件渲染的**文本/密码**输入都带一套抑制属性——`autocomplete="new-password"`、`autocorrect="off"`、`autocapitalize="off"`、`spellcheck=false`,以及厂商忽略标记 `data-1p-ignore`(1Password 官方文档逐字给出)、`data-lpignore`(LastPass:**只有其支持页的标题级引用**——本环境抓取失败、其公开仓库 0 命中,如实标注为证据最弱的一条)、`data-bwignore`(Bitwarden:**opt-in**,只有用户在该扩展的设置里打开对应选项后才生效)、`data-form-type="other"`(Dashlane)、`data-protonpass-ignore`(Proton Pass:依据是**厂商源码**——`WebClients` 里 `PASSWORD_MANAGER_IGNORE_PROPS` 收录的正是这同一套属性,见 `packages/pass/…/Field.tsx` 与 `applications/wallet/…/attribute.ts`)。按面分层(**不是"每一个 input 都一样"**):**4 个能持密钥明文的字段**(索要卡片的值输入、附加胶囊的密钥内容、管理面「改值」、管理确认卡片的「新密钥内容」)带齐 9 键袋**且**另加**只读直到聚焦**的守卫——字段先以 `readOnly` 渲染(自动填充只在加载时预填,而只读字段不在候选里),聚焦时由 `onFocus` 释放;**2
|
|
26
|
+
7. **人类输入密钥的控件既不可自动填充,也不可被密码管理器捕获**(用户实机报出的缺陷的修复):**现象**:真实浏览器会在这四个密钥输入面上给出自动填充建议,已装的密码管理器扩展也会捕获它们。**根因**:修复前这四个字段只有 `autoComplete:'off'`(现代浏览器对登录类凭据字段基本忽略这个值)、**没有任何厂商忽略标记、也没有别的退出开关**,而掩码用的又是原生 `type="password"`,浏览器正是据此把它当作密码域。**修复**:本插件渲染的**文本/密码**输入都带一套抑制属性——`autocomplete="new-password"`、`autocorrect="off"`、`autocapitalize="off"`、`spellcheck=false`,以及厂商忽略标记 `data-1p-ignore`(1Password 官方文档逐字给出)、`data-lpignore`(LastPass:**只有其支持页的标题级引用**——本环境抓取失败、其公开仓库 0 命中,如实标注为证据最弱的一条)、`data-bwignore`(Bitwarden:**opt-in**,只有用户在该扩展的设置里打开对应选项后才生效)、`data-form-type="other"`(Dashlane)、`data-protonpass-ignore`(Proton Pass:依据是**厂商源码**——`WebClients` 里 `PASSWORD_MANAGER_IGNORE_PROPS` 收录的正是这同一套属性,见 `packages/pass/…/Field.tsx` 与 `applications/wallet/…/attribute.ts`)。按面分层(**不是"每一个 input 都一样"**):**4 个能持密钥明文的字段**(索要卡片的值输入、附加胶囊的密钥内容、管理面「改值」、管理确认卡片的「新密钥内容」)带齐 9 键袋**且**另加**只读直到聚焦**的守卫——字段先以 `readOnly` 渲染(自动填充只在加载时预填,而只读字段不在候选里),聚焦时由 `onFocus` 释放;**2 个标识符输入**(凭据键、标题)带 `autocomplete="off"` 的同一套袋**且**同样带**只读直到聚焦**的守卫,并且**自 0.4.3 起不再带 `id`/`name`**、改用**隐式 label 关联**(`<label>` 包住可见标题与控件,见下)。**依据要分清归属(t21 纠偏,勿混读)**:真正有文档支撑的是 **(a) 浏览器侧的键控行为**——Chrome 团队 web.dev《Learn Forms: Autofill》逐字写明浏览器**按 `name`(某些浏览器还看 `id`)记录与回填字段**,并对"值每次都不同、不可能复用"的字段(该页给的例子是一次性验证码,凭据键与标题同属此类)**推荐 `autocomplete="off"`**;同页还写明 `off` 对**密码字段无效**("即使使用 `autocomplete="off"`,浏览器仍会提供密码自动填充选项"),这正是那四个密钥字段必须留着 `new-password` 而不能改用 `off` 的原因,而 `new-password` 用在纯文本字段上只会招来"生成强密码"的建议;以及 **(b) 五个厂商忽略标记**(1Password 官方文档逐字给出 `data-1p-ignore`;Bitwarden / Dashlane / Proton Pass 是**厂商自家源码**;LastPass 只到支持页标题级)。**而"去掉 `id`/`name`"是本次的设计选择,不是厂商文档的推荐做法**:1Password 官方文档的原意恰好相反——它建议给字段加**唯一的 `id`/`name`**,好让 1Password 能识别该字段;我们只在这两个**不持密钥**的标识符字段上反向操作,唯一目的是让浏览器不再把历史值当作可复用项、从而不再弹下拉,代价是放弃显式关联(见下面的可访问性口径)。不要把这一条读成"厂商推荐去 id";**1 个不持值的掩码展示**(`disabled`)也带袋、不带守卫;**自由文本 `textarea`** 只带 4 键卫生子集(`autocomplete`/`autocorrect`/`autocapitalize`/`spellcheck`);**2 个 scope radio** 不持值、不带袋、**且带 `name`**。所有文本/密码输入都没有 `name`,整个 UI **没有 `<form>`、没有 submit 按钮**(每个按钮都是 `type="button"`)。**可访问性口径(0.4.3,如实记录取舍)**:两个标识符字段改成隐式关联后,MDN 把嵌套与 `for`/`id` 记为"等价",但同时提醒"并非所有辅助技术都实现隐式关联",并**一般推荐**用显式 `for`;因此这两个控件同时带**与可见标题逐字相同的 `aria-label`**,把可访问名固定成显式提供的那一份(可见文本与可访问名一致,满足 Label-in-Name),点击标题仍会聚焦控件(嵌套 label 的原生行为)。**代价如实写明**:这两个字段失去了"显式关联"这一 MDN 推荐形态,换来的是去掉浏览器与扩展据以识别字段的稳定 `id`;`tabIndex` 未改、字段仍可聚焦、可用性未牺牲其它属性(正对照与机械断言见测试)。四个密钥字段**保留**显式 `for`/`id`:它们靠 `new-password` + 厂商忽略标记 + 守卫关闭浏览器侧凭据路径,而 MDN 推荐的显式关联在这里更值得留。掩码**仍用 `type="password"`**:替代方案 `-webkit-text-security` 是非标准属性(MDN 明写"不建议在生产使用、浏览器支持有限"),一旦被忽略就会把密钥显示成明文,比自动填充更糟。机械断言:两个 Client 测试逐一渲染这些面并断言上述属性与守卫,修复前的形态下它们**全部失败**(正对照见测试注释)。**需真人**:具体浏览器是否还弹自动填充建议/是否还提示保存密码、各管理器扩展是否仍捕获、以及只读守卫的代价(见下一条与「已知限制」),只能在真机浏览器里确认。
|
|
27
27
|
8. **明文允许存在的全部位置(穷举,仅此六处)**:
|
|
28
28
|
- P1 填值胶囊 / 管理面「改值」面的本地输入 state(掩码输入框);
|
|
29
29
|
- P2 一次 `POST /api/secret.attach`(附加方向)或一次 `action:"value"` 的 `POST /api/secret.manage`(改值方向)的请求体;
|
|
@@ -418,7 +418,7 @@ if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)"
|
|
|
418
418
|
```
|
|
419
419
|
共 8 个测试文件 / 128 个用例。管理面在 `test/register.test.ts`:两个工具的参数集(逐字断言没有 `value`)、`list`/`unbind`/`delete`/`scope`/`value` 五个动作、五条人类侧路径、降级两个按钮(文案与线上动作都不同)、真删两档(缺 `confirm` 零副作用)、列表两个方向的 `origin` 与委派子代理的可达性注意(含根代理的正对照);**0.4.1 起还有实施真实注册表规则的严格注册表**——它断言 9 条精确路径各注册一次、`/api/secret.manage` 一行两方法,并在修复前的形态上以**同一条** `already registered` 错误失败(这就是 0.4.0 漏网的那个门);**0.4.2 起新增两条密钥输入抑制用例**(`test/client-card.test.ts` / `test/client-attach.test.ts`)——它们逐面枚举渲染出的字段并断言那一套抑制属性与只读守卫,在修复前的形态上逐条列出缺口(正对照)。四类新历史事件在 `test/history.test.ts`。`npm test` 若挂住,按仓库红线处理:**先查泄漏**(本插件自己的 teardown/定时器),临时排障才用 `node --test --test-force-exit`,**不得按进程名清场**。
|
|
420
420
|
2. **活体(需真人;管理面每次都向 Host 现问,改完刷新即可再核对)**:按上面「人类侧信息框(五条路径)」逐条点一遍——列举(两个分区读得清)、改值(掩码框)、改作用域(两个降级按钮各点一次,核对其后果不同)、解绑(凭据库不动)、真删(两次点击)。断言点是上面「需真人确认」列出的那些:不可用的动作不出现;改值后活体 shell 里真的是新值;`scope → persistent` 真的写库;真删后该行从库侧分区消失、会话侧作用域如实变「仅本次会话」、历史多一条 `deleted`、其它三条外来记录一字未动;子代理调 `list` 不挂起、调四个写动作得到结构化 `DELEGATED_CALLER`(无对话框)。
|
|
421
|
-
3. **浏览器侧(0.4.2
|
|
421
|
+
3. **浏览器侧(0.4.2 起,只能真人做)**:在真实浏览器里打开四个密钥输入面**与两个标识符输入(凭据键、标题)**,逐条核「需真人确认」里那几项——是否还弹自动填充建议(标识符字段尤其要看历史值下拉是否消失)、Chromium 是否还给"生成强密码"建议、是否还弹"保存密码?"、已装的密码管理器扩展是否仍捕获、以及**聚焦之后能否正常键入**(只读守卫的代价)。
|
|
422
422
|
4. **旧版对照**:`git stash` 或 checkout `v0.3.0` 后 `git diff v0.3.0 -- src test` 可看到本轮的全部改动面;`.credentials.yaml` 的基线(大小与 SHA256)应在复测前后逐字节不变。
|
|
423
423
|
|
|
424
424
|
## 存储与传播
|
|
@@ -468,8 +468,9 @@ if ($env:DSH_SECRET_OPENAI) { "present length=$($env:DSH_SECRET_OPENAI.Length)"
|
|
|
468
468
|
- **R5(质量门 r2 报出,0.4.0 已知限制:重叠场景会低估可用性)**:列表的既定边界是**一个变量只占一行**;当同一变量**既有一条人工附加记录(`staged` / `bound`)又有一份经 `secret_request` 的活跃会话授权**时(复现两步:先把某变量附上但不发送 → 再对同名变量 `secret_request` 并批准),附加行先入行、`requestViews` 随即跳过该变量,于是列表只回 `{state:'staged', origin:'attach', source:'session'}` 这一行——**这枚变量其实已被授权、也可注入**,而问方向的授权只在本会话**历史**(`authorized` / 来源 `request`)里可见。影响范围**仅**是人类管理列表在重叠场景下**低估可用性、方向不完整**:它**不泄漏任何值**、**不误报可执行动作**(该行的 `can.*` 仍为真,`unbind` 仍能撤掉本会话的暴露),**安全不变量与权限边界未受影响**(口径由 `manageList` 的 `seen` 去重决定,发布后如需让一行同时承载两个方向,单独立项再改,0.4.0 不返工)。
|
|
469
469
|
- **记录键可能撞名**:记录键由 name 推导,`recordKeyId` 把 `_` 换成 `-`,所以 `a_b` 与 `a-b` 指向同一条记录键;真删按记录键寻址,撞键时删掉的是同一条。
|
|
470
470
|
- **历史里 `deleted` 行的 `scope` 字段记的是「被删掉那条的作用域」**(`persistent`),不是"现在还剩什么";会话里那份的当前作用域以管理列表为准(`session`)。
|
|
471
|
+
- **0.4.3 修的两个标识符字段:机械断言已就位,浏览器侧仍需真人确认**(如实标注):凭据键与标题自 0.4.3 起改用 `autocomplete="off"`(Chrome 团队文档给出的"值每次都不同、不可复用"字段的取值)、去掉稳定 `id`/`name`、改成隐式 label 关联 + 同文本 `aria-label`,并同样带只读直到聚焦的守卫;机械断言钉住这一整套(正对照证明修复前形态下必然失败)。**未经真人浏览器验证**的部分:① Chrome/Edge/Firefox/Safari 在这两个字段上是否**真的不再**给历史值/自动填充下拉(`autocomplete="off"` 与"去掉 `id`"都是**提示级**手段,MDN 明写"该属性是给浏览器的提示;有些浏览器可能不遵守");② 已装的 1Password / LastPass / Bitwarden(opt-in)/ Dashlane / Proton Pass 是否确实不再捕获这两个字段;③ 隐式 label 关联在现场的辅助技术(屏幕阅读器、语音控制)下是否与原先的显式 `for`/`id` 等效(MDN 提醒并非所有辅助技术都实现隐式关联;`aria-label` 是为此准备的显式可访问名)。以上 3 项在无浏览器控制与未安装这些扩展时均为**需真人确认**,本 README 不把它们写成已验证。**依据归属(勿混读)**:有文档支撑的是**浏览器键控行为**(web.dev 逐字:浏览器按 `name`,有些还看 `id`,对一次性/不可复用值推荐 `autocomplete="off"`)与**五个厂商忽略标记**(1Password 官方;Bitwarden / Dashlane / Proton 自家源码;LastPass 支持页级);而**"去掉稳定 `id`"是本插件自己的设计选择,不是厂商文档的推荐做法**——1Password 官方文档的原意相反,它建议给字段加唯一 `id`/`name` 以便识别。
|
|
471
472
|
- **密钥输入控件不会被自动填充/被密码管理器捕获,但"现场是否还弹提示"只能真人确认**(0.4.2 修复,如实标注):代码层面已加齐抑制属性与只读守卫(见「安全不变量」第 7 条)并由两个 Client 测试机械钉住,**未经真人浏览器验证**的部分是:① Chrome/Edge/Firefox/Safari 在四个密钥字段上是否**完全不再**给自动填充建议;② Chromium 在 `autocomplete="new-password"` 字段上可能出现的"生成强密码"建议是否出现、是否可忽略(它不会回填已保存的凭据,也不读取字段);③ 是否还会弹"保存密码?"气泡;④ 1Password / LastPass / Bitwarden / Dashlane / Proton Pass 扩展在现场是否确实**不再**捕获或提供保存(各厂商忽略标记的实际效力只能在其真实扩展上验;其中 Bitwarden 的忽略还是 **opt-in**,LastPass 那条只有支持页标题级引用);⑤ **只读直到聚焦的守卫在现场是否有代价**——辅助技术可能把 `readOnly` 字段读成"只读"、iOS 上 `readOnly` 可能压住软键盘(需要二次点按)、`disabled` 的字段永不释放、以及守卫目前只在 props 层被断言(真实 DOM 重渲染下是否稳定释放未验),因此"**聚焦之后能正常键入**"必须由真人确认。以上 5 项在无浏览器控制与未安装这些扩展时均为**需真人确认**,本 README 不把它们写成已验证。
|
|
472
|
-
- **只读直到聚焦的守卫自身有代价(0.4.2
|
|
473
|
+
- **只读直到聚焦的守卫自身有代价(0.4.2 新增;0.4.3 起也覆盖两个标识符字段)**:四个密钥字段与两个标识符字段默认以 `readOnly` 渲染,聚焦时才由 `onFocus` 释放。已知代价:① 辅助技术(屏幕阅读器)可能把它读成只读字段;② iOS Safari 等平台上 `readOnly` 可能阻止软键盘自动弹出,需要用户二次点按;③ 若字段处于 `disabled` 状态则永不释放(本插件在不应输入的场合才这么做);④ 该守卫目前只有 props 层断言(正对照证明修复前缺失),**真实 DOM 重渲染下是否每次都释放未验证**——属需真人确认项(见上一条 ⑤)。这是为防自动填充付出的可用性代价,如实记录,不粉饰。
|
|
473
474
|
- **0.3.0 新增的"需真人确认"清单(如实标注,未验证即写"未验证")**:① **旁挂胶囊确实渲染在用户气泡之后**(含四档"工作步骤展示"下的实际位置与视觉贴合);② **点旁挂胶囊**打开的是输入框上方的信息框;③ **点原胶囊**打开的是右侧栏详情页(且**不再是**"文件不存在"),以及页面顶栏右栏行为符合预期;④ **原胶囊接管的可争用性**在现场的表现(装/卸 `dsh-better-sidebar`,或临时注册一个更长的同档 pattern,观察是否被静默抢走);⑤ **移除即撤销**的真实时序(删掉 → 约 0.6s 后 `GET /api/secret.attached` 不再是 `staged`;删掉后 600ms 内插回 ⇒ 记录仍在;**发送不得触发撤销**);⑥ **历史区**在刷新后仍在、宿主重启/插件重载后清空、重放/分叉会话不重建;⑦ **`@` 菜单**两类来源的 `section`/`description` 文案在实际宽度下不被截断,且凭据库条目的确认→登记→插入链路真的能取到值。以上 7 项在无浏览器控制时均为**需真人确认**,本 README 不把它们写成已验证。
|
|
474
475
|
|
|
475
476
|
## 开发
|
|
@@ -575,7 +576,7 @@ npm stage approve <stage-id> # 需要 2FA
|
|
|
575
576
|
判断该版本是否已存在于目标 registry,已存在就跳过发布(并在 Step Summary 写明"该版本已存在,跳过发布"),
|
|
576
577
|
因此对已发布版本重推 tag 不会产生必然失败的公开红叉。其它检查错误(网络、鉴权、registry 故障)仍会让 job 失败。
|
|
577
578
|
|
|
578
|
-
**registry 现状(截至本文)**:npmjs 上已上线 `0.0.0-stage`、`0.1.0`、`0.2.0`、`0.2.1`、`0.3.0`(`dist-tags.latest = 0.
|
|
579
|
+
**registry 现状(截至本文)**:npmjs 上已上线 `0.0.0-stage`、`0.1.0`、`0.2.0`、`0.2.1`、`0.3.0`、`0.4.1`、`0.4.2`(`dist-tags.latest = 0.4.2`,2026-10-08T12:09:17Z 上线)。三个 0.4.x 的 stage 队列历史与**现状**逐个写清,避免批错版本:`0.4.0` 的 stage id `dd9ef8e8-866b-498d-a8f5-8ee33e32f62f`(shasum `22dfadfc613695018adfd5ee09eb8b71b1ba9edc`)**从未被批准、从未上线**(该制品有启动缺陷,见「0.4.0 的发布级缺陷与 0.4.1 修复」),建议显式丢弃:`npm stage reject dd9ef8e8-866b-498d-a8f5-8ee33e32f62f`;`0.4.1` 的 stage id `22abf7a3-606b-4ee4-a1f9-5e84b557d6d0`(shasum `9b64a832abad45123ec96b3c2eec398c4b7377dd`,provenance `logIndex=3142251804`)**已由用户 2FA 批准上线**(2026-10-08T09:30:00Z),现已被 0.4.2 取代;`0.4.2` 的 stage id `a4056fb7-0a1a-4740-91d3-856665498b5e`(shasum `60e61cc5395e2f279a8b9a0a769165ecf4b3c403`、integrity `sha512-S34f977fgdNiR…HDV7DobbX5K8A==`、provenance `logIndex=3146400254`;由 tag `v0.4.2` 推送触发:Release run `37764853236` / Publish run `37764853223`,tag 对象 `b5a20422edde9b3951ba433945c72c9a430c3e0f`(GitHub 显示 verified),Release <https://github.com/xinvxueyuan/cordis-plugin-secret/releases/tag/v0.4.2>)**已由用户 2FA 批准上线**(2026-10-08T12:09:17Z),即当前的 `dist-tags.latest`——**此刻队列里没有任何待批准的版本**(已上线的版本不能再 `npm stage reject`)。本机 npm CLI 12.2.0 的 `npm stage` 确实有 `reject` 子命令,帮助文本:「Reject a staged package, removing it from the registry」,可用 `--otp <otp>`——**批准与丢弃都需要登录 + 2FA,只能由人来做**;本发布流程不登录 npm、不读取或处理任何用户令牌、不代做任何需要 2FA 的动作。GitHub Packages 上 `0.1.0`/`0.2.0`/`0.2.1`/`0.3.0`/`0.4.1`/`0.4.2` 都在(`0.4.2` 的包版本 id `1355352850`;`0.4.1` 的是 `1354170442`;上一个可用版本 `0.3.0` 的是 `1346087280`),另有**不可用**的 `0.4.0`(包版本 id `1351737947`)——GitHub Packages 由 CI 直接发布、不经人工批准,所以撤不回,只能靠 Release notes 的警告与本文说明。
|
|
579
580
|
|
|
580
581
|
同一份包也会发布到 **GitHub Packages**(`npm.pkg.github.com`,`github-packages` job,用内置 `GITHUB_TOKEN`),
|
|
581
582
|
使包在仓库页面上可见、可被 `@xinvxueyuan:registry=https://npm.pkg.github.com` 的消费者安装。
|
package/lib/client/entry.js
CHANGED
|
@@ -694,6 +694,33 @@ function secretFieldGuards() {
|
|
|
694
694
|
},
|
|
695
695
|
};
|
|
696
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* The same bag for the two **identifier** fields (the credential key and the
|
|
699
|
+
* title), with one deliberate difference: `autocomplete="off"` instead of
|
|
700
|
+
* `new-password`.
|
|
701
|
+
*
|
|
702
|
+
* The first-hand reason is the Chrome team's own Autofill guide: for a field
|
|
703
|
+
* whose value is unique every time and can never be reused — its example is a
|
|
704
|
+
* one-time code, and the credential key and title are the same shape of value —
|
|
705
|
+
* it says to use `autocomplete="off"`, because "the value is different every
|
|
706
|
+
* time, and the browser should not save the value or offer autofill options".
|
|
707
|
+
* The same page records the limit that makes `off` the wrong token for the four
|
|
708
|
+
* *secret* fields: "even with `autocomplete="off"`, the browser will still offer
|
|
709
|
+
* password autofill options so that these password management tools keep
|
|
710
|
+
* working" — a `type="password"` field keeps its credential UI, which is why
|
|
711
|
+
* those four keep `new-password`.
|
|
712
|
+
*
|
|
713
|
+
* `new-password` on a plain text field is not a "do not autofill" hint at all: it
|
|
714
|
+
* is the sign-up hint that tells Chrome to offer *generating* a password, which
|
|
715
|
+
* is exactly the kind of noise the user reported on these two fields.
|
|
716
|
+
*
|
|
717
|
+
* @see https://web.dev/learn/forms/autofill (Learn Forms: Autofill)
|
|
718
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Turning_off_form_autocompletion
|
|
719
|
+
*/
|
|
720
|
+
const IDENTIFIER_FIELD_SUPPRESSION = {
|
|
721
|
+
...SECRET_FIELD_SUPPRESSION,
|
|
722
|
+
autoComplete: 'off',
|
|
723
|
+
};
|
|
697
724
|
/**
|
|
698
725
|
* The hygiene subset for a free-text box that can hold no secret (the card's
|
|
699
726
|
* "other instruction" field): no spelling service, no autocorrection, and no
|
|
@@ -1381,6 +1408,9 @@ const A = {
|
|
|
1381
1408
|
close: `${AP}_close`,
|
|
1382
1409
|
body: `${AP}_body`,
|
|
1383
1410
|
field: `${AP}_field`,
|
|
1411
|
+
// A `<label>` that wraps the visible caption and the control, so the control
|
|
1412
|
+
// needs no `id` and the caption no `for`: implicit association.
|
|
1413
|
+
fieldGroup: `${AP}_fieldGroup`,
|
|
1384
1414
|
label: `${AP}_label`,
|
|
1385
1415
|
input: `${AP}_input`,
|
|
1386
1416
|
inputRow: `${AP}_inputRow`,
|
|
@@ -1425,6 +1455,7 @@ const ATTACH_CSS = `
|
|
|
1425
1455
|
.${A.close}:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1426
1456
|
.${A.body}{flex-direction:column;gap:8px;display:flex;min-width:0}
|
|
1427
1457
|
.${A.field}{flex-direction:column;gap:6px;display:flex;min-width:0}
|
|
1458
|
+
.${A.fieldGroup}{flex-direction:column;gap:6px;display:flex;min-width:0}
|
|
1428
1459
|
.${A.label}{color:var(--dsw-alias-label-caption);font-size:12px;line-height:18px}
|
|
1429
1460
|
.${A.inputRow}{align-items:center;gap:8px;display:flex}
|
|
1430
1461
|
.${A.input}{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-primary);flex:auto;font:inherit;font-size:13px;height:32px;min-width:0;outline:none;padding:0 10px}
|
|
@@ -3062,25 +3093,37 @@ function SecretAttachCapsule(props) {
|
|
|
3062
3093
|
disabled: busy,
|
|
3063
3094
|
onChange: () => setScope(next),
|
|
3064
3095
|
}), h('span', { className: A.optionBody }, h('span', { className: A.optionTitle }, title), h('span', { className: A.optionHint }, hint)));
|
|
3065
|
-
return h('div', { className: A.box, 'data-secret-attach-capsule': 'fill', role: 'group', 'aria-label': t('fillTitle') }, head, h('div', { className: A.body }, h('div', { className: A.field },
|
|
3066
|
-
|
|
3067
|
-
|
|
3096
|
+
return h('div', { className: A.box, 'data-secret-attach-capsule': 'fill', role: 'group', 'aria-label': t('fillTitle') }, head, h('div', { className: A.body }, h('div', { className: A.field },
|
|
3097
|
+
// Implicit label association: the caption and the control live inside
|
|
3098
|
+
// one `<label>`, so the control carries no `id` and the caption no
|
|
3099
|
+
// `for`. Chrome's own Autofill guide says the browser stores and fills
|
|
3100
|
+
// a field by its `name` attribute and, in some browsers, its `id`, so
|
|
3101
|
+
// the stable identity is exactly what has to go; MDN documents the
|
|
3102
|
+
// nested form as the equivalent association ("the `for` and `id`
|
|
3103
|
+
// attributes are not needed because the association is implicit"), and
|
|
3104
|
+
// `aria-label` carries the same text as an explicit accessible name for
|
|
3105
|
+
// the assistive technologies that do not implement implicit labels.
|
|
3106
|
+
h('label', { className: A.fieldGroup }, h('span', { className: A.label }, t('keyLabel')), h('input', {
|
|
3107
|
+
...IDENTIFIER_FIELD_SUPPRESSION,
|
|
3108
|
+
...secretFieldGuards(),
|
|
3068
3109
|
className: A.input,
|
|
3069
3110
|
type: 'text',
|
|
3070
3111
|
value: key,
|
|
3071
3112
|
placeholder: t('keyPlaceholder'),
|
|
3072
3113
|
disabled: busy,
|
|
3114
|
+
'aria-label': t('keyLabel'),
|
|
3073
3115
|
onChange: (event) => setKey(event.target.value),
|
|
3074
|
-
}), h('span', { className: A.optionHint }, t('keyHint'))), h('div', { className: A.field }, h('label', { className: A.
|
|
3075
|
-
...
|
|
3076
|
-
|
|
3116
|
+
})), h('span', { className: A.optionHint }, t('keyHint'))), h('div', { className: A.field }, h('label', { className: A.fieldGroup }, h('span', { className: A.label }, t('labelLabel')), h('input', {
|
|
3117
|
+
...IDENTIFIER_FIELD_SUPPRESSION,
|
|
3118
|
+
...secretFieldGuards(),
|
|
3077
3119
|
className: A.input,
|
|
3078
3120
|
type: 'text',
|
|
3079
3121
|
value: label,
|
|
3080
3122
|
placeholder: t('labelPlaceholder'),
|
|
3081
3123
|
disabled: busy,
|
|
3124
|
+
'aria-label': t('labelLabel'),
|
|
3082
3125
|
onChange: (event) => setLabel(event.target.value),
|
|
3083
|
-
})), h('div', { className: A.field }, h('label', { className: A.label, htmlFor: 'dsh-secret-attach-value' }, t('valueLabel')), h('div', { className: A.inputRow }, h('input', {
|
|
3126
|
+
}))), h('div', { className: A.field }, h('label', { className: A.label, htmlFor: 'dsh-secret-attach-value' }, t('valueLabel')), h('div', { className: A.inputRow }, h('input', {
|
|
3084
3127
|
...SECRET_FIELD_SUPPRESSION,
|
|
3085
3128
|
...secretFieldGuards(),
|
|
3086
3129
|
id: 'dsh-secret-attach-value',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xinvxueyuan/cordis-plugin-secret",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Cordis (DeepSeek Harness) plugin: secrets travel between a human and the agent in both directions — the agent asks, or the human attaches one to their own message — and the agent only ever receives an opaque session-scoped variable name (DSH_SECRET_*), never the value",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/src/client/entry.ts
CHANGED
|
@@ -832,6 +832,34 @@ function secretFieldGuards(): { readOnly: true; onFocus: (event: unknown) => voi
|
|
|
832
832
|
}
|
|
833
833
|
}
|
|
834
834
|
|
|
835
|
+
/**
|
|
836
|
+
* The same bag for the two **identifier** fields (the credential key and the
|
|
837
|
+
* title), with one deliberate difference: `autocomplete="off"` instead of
|
|
838
|
+
* `new-password`.
|
|
839
|
+
*
|
|
840
|
+
* The first-hand reason is the Chrome team's own Autofill guide: for a field
|
|
841
|
+
* whose value is unique every time and can never be reused — its example is a
|
|
842
|
+
* one-time code, and the credential key and title are the same shape of value —
|
|
843
|
+
* it says to use `autocomplete="off"`, because "the value is different every
|
|
844
|
+
* time, and the browser should not save the value or offer autofill options".
|
|
845
|
+
* The same page records the limit that makes `off` the wrong token for the four
|
|
846
|
+
* *secret* fields: "even with `autocomplete="off"`, the browser will still offer
|
|
847
|
+
* password autofill options so that these password management tools keep
|
|
848
|
+
* working" — a `type="password"` field keeps its credential UI, which is why
|
|
849
|
+
* those four keep `new-password`.
|
|
850
|
+
*
|
|
851
|
+
* `new-password` on a plain text field is not a "do not autofill" hint at all: it
|
|
852
|
+
* is the sign-up hint that tells Chrome to offer *generating* a password, which
|
|
853
|
+
* is exactly the kind of noise the user reported on these two fields.
|
|
854
|
+
*
|
|
855
|
+
* @see https://web.dev/learn/forms/autofill (Learn Forms: Autofill)
|
|
856
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Turning_off_form_autocompletion
|
|
857
|
+
*/
|
|
858
|
+
const IDENTIFIER_FIELD_SUPPRESSION = {
|
|
859
|
+
...SECRET_FIELD_SUPPRESSION,
|
|
860
|
+
autoComplete: 'off',
|
|
861
|
+
} as const
|
|
862
|
+
|
|
835
863
|
/**
|
|
836
864
|
* The hygiene subset for a free-text box that can hold no secret (the card's
|
|
837
865
|
* "other instruction" field): no spelling service, no autocorrection, and no
|
|
@@ -1642,6 +1670,9 @@ const A = {
|
|
|
1642
1670
|
close: `${AP}_close`,
|
|
1643
1671
|
body: `${AP}_body`,
|
|
1644
1672
|
field: `${AP}_field`,
|
|
1673
|
+
// A `<label>` that wraps the visible caption and the control, so the control
|
|
1674
|
+
// needs no `id` and the caption no `for`: implicit association.
|
|
1675
|
+
fieldGroup: `${AP}_fieldGroup`,
|
|
1645
1676
|
label: `${AP}_label`,
|
|
1646
1677
|
input: `${AP}_input`,
|
|
1647
1678
|
inputRow: `${AP}_inputRow`,
|
|
@@ -1687,6 +1718,7 @@ const ATTACH_CSS = `
|
|
|
1687
1718
|
.${A.close}:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1688
1719
|
.${A.body}{flex-direction:column;gap:8px;display:flex;min-width:0}
|
|
1689
1720
|
.${A.field}{flex-direction:column;gap:6px;display:flex;min-width:0}
|
|
1721
|
+
.${A.fieldGroup}{flex-direction:column;gap:6px;display:flex;min-width:0}
|
|
1690
1722
|
.${A.label}{color:var(--dsw-alias-label-caption);font-size:12px;line-height:18px}
|
|
1691
1723
|
.${A.inputRow}{align-items:center;gap:8px;display:flex}
|
|
1692
1724
|
.${A.input}{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-primary);flex:auto;font:inherit;font-size:13px;height:32px;min-width:0;outline:none;padding:0 10px}
|
|
@@ -3599,33 +3631,52 @@ function SecretAttachCapsule(props: {
|
|
|
3599
3631
|
h(
|
|
3600
3632
|
'div',
|
|
3601
3633
|
{ className: A.field },
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3634
|
+
// Implicit label association: the caption and the control live inside
|
|
3635
|
+
// one `<label>`, so the control carries no `id` and the caption no
|
|
3636
|
+
// `for`. Chrome's own Autofill guide says the browser stores and fills
|
|
3637
|
+
// a field by its `name` attribute and, in some browsers, its `id`, so
|
|
3638
|
+
// the stable identity is exactly what has to go; MDN documents the
|
|
3639
|
+
// nested form as the equivalent association ("the `for` and `id`
|
|
3640
|
+
// attributes are not needed because the association is implicit"), and
|
|
3641
|
+
// `aria-label` carries the same text as an explicit accessible name for
|
|
3642
|
+
// the assistive technologies that do not implement implicit labels.
|
|
3643
|
+
h(
|
|
3644
|
+
'label',
|
|
3645
|
+
{ className: A.fieldGroup },
|
|
3646
|
+
h('span', { className: A.label }, t('keyLabel')),
|
|
3647
|
+
h('input', {
|
|
3648
|
+
...IDENTIFIER_FIELD_SUPPRESSION,
|
|
3649
|
+
...secretFieldGuards(),
|
|
3650
|
+
className: A.input,
|
|
3651
|
+
type: 'text',
|
|
3652
|
+
value: key,
|
|
3653
|
+
placeholder: t('keyPlaceholder'),
|
|
3654
|
+
disabled: busy,
|
|
3655
|
+
'aria-label': t('keyLabel'),
|
|
3656
|
+
onChange: (event: { target: { value: string } }) => setKey(event.target.value),
|
|
3657
|
+
}),
|
|
3658
|
+
),
|
|
3613
3659
|
h('span', { className: A.optionHint }, t('keyHint')),
|
|
3614
3660
|
),
|
|
3615
3661
|
h(
|
|
3616
3662
|
'div',
|
|
3617
3663
|
{ className: A.field },
|
|
3618
|
-
h(
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3664
|
+
h(
|
|
3665
|
+
'label',
|
|
3666
|
+
{ className: A.fieldGroup },
|
|
3667
|
+
h('span', { className: A.label }, t('labelLabel')),
|
|
3668
|
+
h('input', {
|
|
3669
|
+
...IDENTIFIER_FIELD_SUPPRESSION,
|
|
3670
|
+
...secretFieldGuards(),
|
|
3671
|
+
className: A.input,
|
|
3672
|
+
type: 'text',
|
|
3673
|
+
value: label,
|
|
3674
|
+
placeholder: t('labelPlaceholder'),
|
|
3675
|
+
disabled: busy,
|
|
3676
|
+
'aria-label': t('labelLabel'),
|
|
3677
|
+
onChange: (event: { target: { value: string } }) => setLabel(event.target.value),
|
|
3678
|
+
}),
|
|
3679
|
+
),
|
|
3629
3680
|
),
|
|
3630
3681
|
h(
|
|
3631
3682
|
'div',
|