dsh-web-fetch-enhanced 0.0.5 → 0.0.7

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.en.md CHANGED
@@ -35,7 +35,7 @@ With an empty CIDR allowlist, the security boundary remains equivalent to the na
35
35
 
36
36
  ## Compatibility & Support Policy
37
37
 
38
- - **Minimum Supported DSH Version**: `0.1.5-rc.2`
38
+ - **Minimum Supported DSH Version**: `0.1.7-rc.1`
39
39
  - **Support Policy**: This plugin **only supports DeepSeek Harness RC (Release Candidate) releases and future stable releases**. Compatibility is not maintained for rapid-moving Alpha or development snapshot versions.
40
40
 
41
41
  ## Quick start
@@ -60,9 +60,9 @@ dsh plugin --profile web add link:/absolute/path/to/dsh-web-fetch-enhanced
60
60
 
61
61
  Open DSH Web and go to:
62
62
 
63
- **Settings → Plugins → Configurable plugins → Web Fetch Enhanced**
63
+ **Plugins → dsh-web-fetch-enhanced**
64
64
 
65
- Expand the card and enter one network per line under **Allowed CIDRs**. For a typical Clash / Mihomo fake-IP setup:
65
+ The details page shows the allowlist form directly. Enter one network per line under **Allowed CIDRs**. For a typical Clash / Mihomo fake-IP setup:
66
66
 
67
67
  ```text
68
68
  198.18.0.0/15
@@ -75,7 +75,9 @@ api.example.com
75
75
  *.docs.example.com
76
76
  ```
77
77
 
78
- Choose **Save**. The next `web_fetch` uses the new policy; no Profile restart is required.
78
+ Choose **Save**. The page shows **Saved** and keeps the fields visible. The next `web_fetch` uses the new policy; no Profile restart is required.
79
+
80
+ DSH displays component status below the form. You do not need to open a component to configure the allowlist.
79
81
 
80
82
  ### 3. Keep using `web_fetch` normally
81
83
 
@@ -150,12 +152,21 @@ Allow only the smallest range you actually need. Do not add all RFC 1918 space f
150
152
 
151
153
  ## Settings actions
152
154
 
153
- - **Save** writes the current draft to the user settings layer.
154
- - **Discard** drops unsaved edits and restores the effective values.
155
- - **Reset to Profile** stages removal of the user overrides for `allowCidrs` and `allowHostnames`, restoring Profile inheritance after you choose **Save**.
156
- - A **read-only** card means the current connection cannot persist Host Profile settings. Normally, open DSH Web from a loopback address on the Host.
155
+ - **Save** writes both allowlists in one mutation to the selected Profile plugin row. Host rejection or a revision conflict is not reported as success.
156
+ - **Discard** drops unsaved edits and restores effective values.
157
+ - **Restore inherited values** stages removal of both allowlist overrides; choose **Save** to apply. Inherited values may contain a non-empty allowlist, so review the resulting policy.
158
+ - **Read-only** means the form is not ready, the row is unavailable, or the connection cannot write. The card never falls back to another Profile or global configuration.
159
+
160
+ ## Profile configuration and migration
161
+
162
+ Starting with DSH `0.1.7-rc.1`, this plugin uses Profile-owned Cordis `Config`, `.volatile()`, and plugin-manager `configForms`. It no longer reads global `$DSH_HOME/settings.yaml`.
157
163
 
158
- Saving explicit empty lists is different from resetting: empty lists override the Profile with nothing, while reset restores inheritance.
164
+ - Effective configuration combines schema defaults with the current Profile composition/patch. Web edits target only the selected plugin row.
165
+ - **Clearing a field and saving writes an explicit `[]`** to revoke inherited allowlists. Empty arrays are meaningful security overrides and are no longer pruned.
166
+ - **Restore is not clear**: only an explicit restore uses `unset`, which may reactivate an inherited allowlist.
167
+ - Back up legacy settings, review the old `web-fetch-enhanced` section, and move intended rules into the target Profile plugin configuration. Global authorizations are never automatically copied to every Profile.
168
+
169
+ See the [0.1.7-rc.1 migration notes (Chinese)](docs/migration-0.1.7-rc.1.zh-CN.md).
159
170
 
160
171
  ## Security guidance
161
172
 
@@ -211,7 +222,9 @@ No. After a successful Web settings save, the next `web_fetch` uses the new poli
211
222
  <details>
212
223
  <summary><strong>Why is the settings card read-only?</strong></summary>
213
224
 
214
- The current browser connection cannot persist Host settings. Open DSH Web from `127.0.0.1` or `localhost` on the Host and verify that the Profile settings service is writable.
225
+ Verify that the target Profile plugin row is enabled, its form is available, and the current connection has configuration write permission. An unavailable form never falls back to a different configuration.
226
+
227
+ If both inputs remain disabled on DSH `0.1.7-rc.1` while the card renders normally, update to a build containing the configuration schema serialization fix, restart the Host, and refresh the page. Older builds expose custom validators whose callbacks are stripped during transport, preventing the browser form from becoming ready. The fix retains full Host validation.
215
228
  </details>
216
229
 
217
230
  <details>
package/README.md CHANGED
@@ -35,7 +35,7 @@ DeepSeek Harness 原生 HTTP provider 默认拒绝所有非公网地址,这是
35
35
 
36
36
  ## 版本兼容性与支持策略
37
37
 
38
- - **最低支持的 DSH 版本**:`0.1.5-rc.2`
38
+ - **最低支持的 DSH 版本**:`0.1.7-rc.1`
39
39
  - **版本支持策略**:本插件**仅对 DeepSeek Harness 的 RC(Release Candidate)候选发布版本及后续稳定正式版提供支持**。由于 Alpha 或开发快照版本更迭频繁且缺乏稳定的 API 保证,本插件不再对 Alpha 等非 RC 阶段版本进行维护与适配。
40
40
 
41
41
  ## 快速开始
@@ -60,9 +60,9 @@ dsh plugin --profile web add link:/absolute/path/to/dsh-web-fetch-enhanced
60
60
 
61
61
  打开 DSH Web,进入:
62
62
 
63
- **设置 → 插件 → 可配置插件 → Web Fetch Enhanced**
63
+ **插件 → dsh-web-fetch-enhanced**
64
64
 
65
- 展开卡片,在“允许的 CIDR”中每行填写一个网段。例如 Clash / Mihomo 的常见 Fake-IP 配置:
65
+ 详情页直接显示白名单表单,在“允许的 CIDR”中每行填写一个网段。例如 Clash / Mihomo 的常见 Fake-IP 配置:
66
66
 
67
67
  ```text
68
68
  198.18.0.0/15
@@ -75,7 +75,9 @@ api.example.com
75
75
  *.docs.example.com
76
76
  ```
77
77
 
78
- 点击“保存”。无需重启 Profile,下一次 `web_fetch` 就会使用新规则。
78
+ 点击“保存”,页面显示“已保存”并保留输入框。无需重启 Profile,下一次 `web_fetch` 就会使用新规则。
79
+
80
+ 下方“包含的组件”由 DSH 展示插件运行状态,配置白名单不需要进入组件。
79
81
 
80
82
  ### 3. 正常使用 `web_fetch`
81
83
 
@@ -150,12 +152,21 @@ wiki.corp.example
150
152
 
151
153
  ## 设置页按钮说明
152
154
 
153
- - **保存**:把当前草稿写入用户设置层;
154
- - **放弃修改**:丢弃尚未保存的编辑,恢复当前生效值;
155
- - **重置为 Profile 配置**:把“删除 `allowCidrs` 和 `allowHostnames` 用户覆盖、重新继承 Profile composition”的操作加入草稿;仍需点击“保存”才会生效;
156
- - **只读状态**:当前连接没有持久化 Host Profile 设置的权限。通常应从 Host 本机的 loopback 地址打开 Web GUI。
155
+ - **保存**:将两项白名单作为一次配置变更写入当前 Profile 的插件行;Host 校验失败或版本冲突不会显示为成功。
156
+ - **放弃修改**:丢弃尚未保存的编辑,恢复当前生效值。
157
+ - **恢复继承值**:将移除两项白名单覆盖的操作加入草稿;仍需点击“保存”才生效。继承值可能包含非空白名单,请检查保存后的生效值。
158
+ - **只读状态**:当前表单尚未就绪、配置行不可用或连接没有写权限;不会回退到其他 Profile 或全局配置。
159
+
160
+ ## Profile 配置与旧版本迁移
161
+
162
+ 从 DSH `0.1.7-rc.1` 起,本插件使用 Profile 拥有的 Cordis `Config`、`.volatile()` 和插件管理页的 `configForms` 表单,不再读取全局 `$DSH_HOME/settings.yaml`。
157
163
 
158
- “保存空列表”和“重置为 Profile 配置”含义不同:前者显式覆盖为空,后者恢复继承。
164
+ - 配置由 Schema 默认值与当前 Profile composition/patch 决定;Web 保存只修改选中的插件行。
165
+ - **清空输入并保存会显式写入 `[]`**,用于撤销继承的白名单。空数组具有安全意义,不再自动清理。
166
+ - **恢复继承与清空不同**:只有显式恢复操作才执行 `unset`,可能重新启用底层配置中的白名单。
167
+ - 升级前备份旧设置,将旧 `web-fetch-enhanced` 节中的授权规则逐项审查后迁入目标 Profile 的插件配置。插件不会自动将全局授权复制到所有 Profile。
168
+
169
+ 详见 [0.1.7-rc.1 升级说明](docs/migration-0.1.7-rc.1.zh-CN.md)。
159
170
 
160
171
  ## 安全提示
161
172
 
@@ -211,7 +222,9 @@ Fake-IP 模式会把域名解析到 `198.18.0.0/15` 等保留网段,再由代
211
222
  <details>
212
223
  <summary><strong>为什么设置卡片是只读的?</strong></summary>
213
224
 
214
- 当前浏览器连接不能持久化 Host 设置。请确认你从 Host 本机通过 `127.0.0.1` 或 `localhost` 访问,并检查 Profile 的设置服务是否允许写入。
225
+ 请确认目标 Profile 的插件行已启用且配置表单可用,并检查当前连接的配置写权限。表单未就绪或已失效时不会尝试修改其他配置。
226
+
227
+ 若在 DSH `0.1.7-rc.1` 中卡片正常显示,但两个输入框持续禁用,请更新到包含配置 schema 序列化修复的构建,重启 Host 后刷新页面。旧构建的自定义校验回调在传输中被移除,导致浏览器表单无法就绪;修复保留了 Host 端的完整校验。
215
228
  </details>
216
229
 
217
230
  <details>
@@ -40,7 +40,7 @@ flowchart LR
40
40
 
41
41
  本插件是 Host 侧 provider:它消费已有的 <code>web</code> 服务并注册一个 fetch provider,但不发布新的 Cordis 服务。因此它属于 Host composition,不属于 agent preset。模型工具仍由 preset 中原有的 <code>tool-web</code> 行提供。
42
42
 
43
- 入口采用 namespace plugin 约定,只命名导出 <code>name</code>、<code>inject</code>、<code>Config</code>、安全的 <code>createProvider</code> 和 <code>apply</code>,没有 default export,也不公开低级 pinned transport/custom resolver helper。与 DeepSeek Harness 内部插件一致,源码使用显式 <code>.ts</code> 相对导入,<code>cordis.source.patch.yml</code> 在开发时直接加载源码;发布构建由 TypeScript 生成 <code>lib/types</code>,再由 tsdown 分别输出 Host ESM <code>lib/index.js</code> 和浏览器 Client ESM <code>lib/client.js</code>。Client face 以 <code>web-fetch-enhanced</code> 命名空间键向 <code>settings.plugin.item</code> 注册白名单卡片;Host face 通过 <code>installSettingsSection</code> 将 Profile composition 作为 base layer,并让 provider 在每次请求时读取最新 resolved section。随包发布的 <code>cordis.patch.yml</code> 供操作者显式合并到 Profile composition。
43
+ 入口采用 namespace plugin 约定,无 default export;源码使用显式 `.ts` 相对导入。构建生成 Host ESM `lib/index.js`、类型声明及通过 DSH ModuleLoader 注册的浏览器 `lib/client.js`。Client face 在 `plugins.bundle.config` 中提供插件详情页白名单表单,通过 `configForms.get('web-fetch-enhanced')` 绑定当前 Profile 的固定插件行,并用 `useSyncExternalStore` 订阅状态。页面不重复绘制插件标题或嵌套折叠卡片,保存后保持字段可见并显示结果。Host face 使用带 `.volatile()` 的 Config 字段,通过 `.get()` 读取每次请求的配置快照。Loader 在完整 schema 校验后原子提交 live refs,`loader/volatile-update` 通知更新系统提示词;`providerId` 保持非 volatile,修改时由 Loader 重建实例。随包发布的 `cordis.patch.yml` 为 Profile 提供默认 composition。
44
44
 
45
45
  ## 3. Provider 选择
46
46
 
@@ -100,6 +100,15 @@ flowchart LR
100
100
 
101
101
  ## 6. 配置与生命周期
102
102
 
103
+ ### 6.1 Profile 配置层级
104
+
105
+ 配置由 Schema 默认值、Profile composition 和 Profile patch 覆盖共同决定,不再使用全局 `settings.yaml` 或 `settings.installSection`。客户端只绑定随包 patch 声明的 `web-fetch-enhanced` 行;配置不可用或只读时不向其他行写入。
106
+
107
+ ### 6.2 保存、清空与恢复继承
108
+
109
+ 表单将两项白名单通过单次 `form.mutate(ops, revision)` 原子提交,使用草稿开始时的 revision 校验并发修改。清空字段保存为显式空数组,只有“恢复继承”执行 `unset`。保存失败时保留草稿,放弃修改重新读取当前 Profile 的值。离开页面会丢弃未保存的草稿;外部变更会更新未编辑的表单,不覆盖正在编辑的草稿。
110
+
111
+ ### 6.3 启动阶段校验
103
112
  Cordis Config schema 提供所有默认值;<code>createProvider()</code> 也在直接调用时应用同一默认值。启动阶段校验:
104
113
 
105
114
  - provider ID 字符集和长度;
@@ -129,7 +138,7 @@ Cordis Config schema 提供所有默认值;<code>createProvider()</code> 也
129
138
 
130
139
  ## 8. 兼容策略
131
140
 
132
- 插件只依赖 <code>@deepseek-ai/dsh-web</code> 和 <code>@deepseek-ai/dsh-timeout</code> 的公开包根 API,不依赖 DSH 包的 <code>src/*</code> 深路径。安全 transport 在本包内维护,相关行为通过契约测试固定。
141
+ 最低支持 DSH `0.1.7-rc.1`,DSH peers 声明 `>=0.1.7-rc.1 <0.2.0`,开发依赖固定为经过验证的 rc.1。使用 web、timeout、http-proxy、system-prompt、Cordis Loader 与客户端插件管理/配置表单的公开导出,不依赖 DSH 包的 `src/*` 深路径。安全 transport 在本包内维护,相关行为通过契约测试固定。
133
142
 
134
143
  版本升级时重点回归:
135
144
 
@@ -0,0 +1,63 @@
1
+ # DSH 0.1.7-rc.1 适配与迁移说明
2
+
3
+ ## 基线与版本门禁
4
+
5
+ 此次按上游 `dsh-v0.1.7-rc.1` 源码及 npm 发布包核对接口,不将历史会话评估当作 API 规范。最低支持版本由 `0.1.5-rc.2` 提升为 **`0.1.7-rc.1`**。
6
+
7
+ - `engines.dsh` 和所有 DSH peer 范围为 `>=0.1.7-rc.1 <0.2.0`。
8
+ - DSH 开发依赖固定 `0.1.7-rc.1`,锁文件同步更新;Cordis 使用 4.0.4、Schemastery 使用 3.18.4、Loader 使用 1.0.5。
9
+ - 上游 `packages/boot/app-boot/src/plugin-compatibility.ts` 实际使用 `semver.satisfies(..., { includePrerelease: true })`。显式 rc 下界避免错误接纳更早版本,也能在常规包管理器 semver 语义下接纳目标 rc。
10
+ - 本次验证针对 rc.1;范围内的未来版本不代表已实测,0.2 系列需重新评估。没有添加版本豁免,也没有修改用户 Profile。
11
+
12
+ ## 采用的上游新能力
13
+
14
+ | 接入面 | 原实现 | 本次迁移 |
15
+ | --- | --- | --- |
16
+ | Host 配置 | settings.installSection/register 与全局覆盖 | Profile-owned Config + `.volatile()` |
17
+ | 实时值 | 自定义 current/source/watch | Loader 稳定 live refs 的 `.get()` |
18
+ | 配置通知 | Settings watcher | `loader/volatile-update`;授权变更后通知系统提示词 |
19
+ | 客户端 | settingsScope + settings.plugin.item | 插件详情页 `plugins.bundle.config` 与订阅当前 Profile 的 configForms |
20
+ | 保存 | 比对 user layer 推测写入结果 | 单次批量 mutate、revision 乐观并发校验和 Promise<boolean> 结果 |
21
+ | 清空白名单 | unset 并自动清理空数组 | 显式保存 [];恢复继承才 unset |
22
+ | 类型契约 | 本地模拟 Settings/SystemPrompt seam | 公开 Loader、SystemPrompt、PluginManager 类型 |
23
+
24
+ 注意:rc.1 的 volatile 字段是引用对象,不是自动变更的普通数组;没有使用未经上游证实的 `ctx.accept` API。`providerId` 不是 volatile 字段,修改 composition 中的身份会重建 provider,而不是把旧 ID 静默指向新配置。
25
+
26
+ 白名单和资源限制在 schema 校验阶段完整验证,Loader 成功后原子更新;非法候选不会部分扩大授权。每个请求捕获一份配置快照,在途请求继续使用启动时规则,下一次请求使用新配置。
27
+
28
+ ## 操作者迁移步骤
29
+
30
+ 1. 备份旧版 `$DSH_HOME/settings.yaml` 与目标 Profile composition。
31
+ 2. 将 Host 升级至 DSH `0.1.7-rc.1` 或更高的兼容版本,并安装/链接本插件新构建。
32
+ 3. 审查旧 `web-fetch-enhanced` 节,将必要的 `allowCidrs`、`allowHostnames` 及资源限制迁入目标 Profile 的 `web-fetch-enhanced` 插件行 config;或在插件详情页直接编辑白名单。
33
+ 4. 不再依赖全局 settings.yaml;本插件不会自动把全局授权复制到各 Profile,以免扩大网络访问权限。
34
+ 5. 保存后确认生效值。清空 CIDR 并保存意味着明确撤销非公网例外;恢复继承可能重新启用底层白名单,两者不可混同。
35
+ 6. 升级插件文件后按部署方式重载/重启 Host 并刷新 Web 页面;配置字段的正常保存不需要重启。
36
+ 7. 在实际部署中验证一个明确授权目标及一个未授权私网目标;不要为了测试额外放宽生产白名单。
37
+
38
+ 默认 bundle 行 ID 为 `web-fetch-enhanced`,插件详情页注册键为包名 `dsh-web-fetch-enhanced`。bundle 插槽不提供 owner form,客户端显式订阅该 Profile 行的 ConfigForm。手写 composition 改名时,需要使用宿主通用配置表单或配套调整订阅的行 ID;组件不会猜测其他行的配置。
39
+
40
+ 白名单在包详情页直接显示,不再进入组件行或展开额外卡片。保存后保留字段并显示结果,宿主的组件状态列表位于表单下方。
41
+
42
+ ## 输入框持续禁用的修复
43
+
44
+ 在 rc.1 的真实页面中,插件行正常运行且 Host 可写,但两个白名单输入框持续禁用。原因是 `settings.describe` 的传输 schema 保留了 `transform` 节点,却移除了其 `callback`。浏览器的 `ConfigFormController.decode()` 调用 `SettingsSchemaService.validate()` 时因此失败,表单一直停留在 `loading`。
45
+
46
+ 插件现在仅在 Host 的运行时 schema 中保留自定义校验;序列化时输出对应的普通数组和数值 schema,并保留默认值、上下界及 volatile 元数据。Host 的 ConfigEditor/Loader 仍通过原始 Standard Schema 校验完整候选,再持久化和提交 live refs;浏览器表单不承担 CIDR 和域名的安全校验。序列化节点身份保持稳定,避免无配置变化时递增 revision。
47
+
48
+ 已运行的 Host 需要重载插件模块或重启后刷新浏览器,单独刷新页面不会替换内存中的旧 Host schema。
49
+
50
+ ## 保持不变及不适用项目
51
+
52
+ - `web_fetch` 工具名称、请求参数和返回结果不变;实际 rc.1 WebFetchProvider、WebFetchResult、代理路由接口与现有实现兼容。
53
+ - DNS 全答案检查、连接固定、NAT64 检查、同源重定向、取消/超时、匿名 GET 与非公网默认拒绝策略保持。
54
+ - 本插件没有旧图标导出、PTC、session-start、自定义会话附件、workspaceFiles 或工具视图接入,不为追逐新特性引入无关的浏览器/终端权限。
55
+ - 程序化调用 `createProvider()` 的原始配置类型现在为 `ProviderConfig`;`Config` 表示 Loader 解析后的 live-ref 配置。旧客户端 settingsScope 相关 helper 不再导出。
56
+
57
+ ## 验证入口
58
+
59
+ 运行 `pnpm run check`:类型检查、类型感知 lint、Host/Client 构建、全部测试及 publint。测试覆盖版本门禁、真实 Loader 热配置生命周期、原子拒绝非法候选、系统提示词刷新、请求快照、表单拒绝/版本冲突和既有 SSRF/代理回归。另在独立临时 Profile、DSH 0.1.7-rc.1 和 Chromium 中验收实际配置页面;该验收不代表运行中的用户 Profile 已重载插件。
60
+
61
+ 本次验证结果:类型检查通过;类型感知 lint 为 0 warnings / 0 errors;Host、Client 与声明构建通过;13 个测试文件、100 项测试通过;发布覆盖率检查通过(行 99.54%、分支 95.48%);`git diff --check` 通过。publint 退出码为 0,但保留一项提示:`./client` 的 `.js` 在 `type: module` 包中含 CJS 形式。该产物不是直接由 Node 导入的通用 ESM,而是 DSH `window.__ModuleLoader__.load` 的 factory 包装;已有构建产物测试验证实际 loader 注册与执行。没有为消除提示擅自改变宿主要求的客户端加载格式。
62
+
63
+ 真实浏览器验收通过:插件详情页直接显示可编辑表单、保存后字段保持可见并显示成功、刷新保留值、Host 拒绝非法 CIDR 且保留草稿、放弃修改、显式空数组持久化、恢复继承并从 Profile patch 中移除覆盖。桌面与 390px 窄屏布局均已检查;双标签页验证了未编辑表单同步、外部更新保留草稿及过期 revision 拒绝写入。测试使用隔离的临时 Profile,未改动用户生产 Profile。
@@ -0,0 +1,22 @@
1
+ # v0.0.7
2
+
3
+ ## 兼容性
4
+
5
+ - 最低支持 DeepSeek Harness **0.1.7-rc.1**,支持范围为 `>=0.1.7-rc.1 <0.2.0`。
6
+ - 配置改为当前 Profile 的 Cordis Config 与 volatile 字段,不再读取全局 `settings.yaml`。旧版用户请按[迁移说明](../migration-0.1.7-rc.1.zh-CN.md)迁移白名单。
7
+ - 程序化创建 provider 的原始配置类型为 `ProviderConfig`;`Config` 表示 Loader 解析后的 live-ref 配置。
8
+
9
+ ## 修复与改进
10
+
11
+ - 修复配置 schema 的校验回调在传输中被移除后,两个输入框持续禁用的问题;Host 仍完整校验所有保存请求。
12
+ - 白名单表单直接显示在插件详情页,无需进入组件;去掉重复标题和折叠卡片,保存后显示结果并保持字段可见。
13
+ - 同步外部配置变更,保留未保存草稿,通过 revision 检查拒绝过期写入。
14
+ - 清空白名单保存为显式空数组;只有“恢复继承”才移除覆盖,避免意外恢复底层授权。
15
+ - 白名单与资源限制更新后供下一次请求使用,在途请求保留原配置快照;授权变化同步更新系统提示词。
16
+ - 改善窄屏按钮布局,补充表单交互、schema 传输与 Loader 热更新回归测试。
17
+
18
+ 升级插件后重启 Host 并刷新 Web 页面。日常白名单保存无需重启。
19
+
20
+ ## English
21
+
22
+ Requires DSH 0.1.7-rc.1 or later within the 0.1 series. Configuration now belongs to the active Profile; review and migrate legacy global allowlists before upgrading. This release fixes disabled configuration inputs, moves the form onto the plugin details page, preserves drafts during external updates, rejects stale writes, and distinguishes explicit empty overrides from resetting to inherited values. Host validation and request-level configuration snapshots remain enforced. Restart the Host and refresh the browser after upgrading.
package/lib/client.js CHANGED
@@ -15,12 +15,6 @@ window.__ModuleLoader__.load({
15
15
  const value = Reflect.get(layer, field);
16
16
  return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
17
17
  }
18
- function hasLayerField(layer, field) {
19
- return typeof layer === "object" && layer !== null && Object.prototype.hasOwnProperty.call(layer, field);
20
- }
21
- function equalValues(actual, expected) {
22
- return actual !== void 0 && actual.length === expected.length && actual.every((value, index) => value === expected[index]);
23
- }
24
18
  function parseLines(text) {
25
19
  const values = text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
26
20
  return {
@@ -28,228 +22,287 @@ window.__ModuleLoader__.load({
28
22
  duplicate: new Set(values).size !== values.length
29
23
  };
30
24
  }
31
- const cardCss = ".web-fetch-enhanced-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);transition:border-color .16s,background .16s}.web-fetch-enhanced-card:hover{border-color:var(--dsw-alias-label-dimmed)}.web-fetch-enhanced-card[data-open='true']{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.web-fetch-enhanced-header{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px}.web-fetch-enhanced-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.web-fetch-enhanced-head-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.web-fetch-enhanced-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}.web-fetch-enhanced-description{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}.web-fetch-enhanced-pending{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}.web-fetch-enhanced-chevron{box-sizing:border-box;flex:none;width:8px;height:8px;margin-right:3px;border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);transform:rotate(45deg);transition:transform .16s}.web-fetch-enhanced-card[data-open='true'] .web-fetch-enhanced-chevron{transform:rotate(225deg)}.web-fetch-enhanced-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.web-fetch-enhanced-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}.web-fetch-enhanced-field+.web-fetch-enhanced-field{border-top:1px solid var(--dsw-alias-border-l2)}.web-fetch-enhanced-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}.web-fetch-enhanced-textarea{box-sizing:border-box;width:100%;min-height:96px;resize:vertical;padding:9px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-primary)}.web-fetch-enhanced-textarea:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)}.web-fetch-enhanced-textarea[aria-invalid='true']{border-color:var(--dsw-alias-label-error)}.web-fetch-enhanced-textarea:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.web-fetch-enhanced-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}.web-fetch-enhanced-invalid,.web-fetch-enhanced-failed{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}.web-fetch-enhanced-readonly{margin:12px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}.web-fetch-enhanced-footer{display:flex;align-items:center;gap:8px;padding:12px 0 4px;border-top:1px solid var(--dsw-alias-border-l2)}.web-fetch-enhanced-failed{flex:1;min-width:0}.web-fetch-enhanced-actions{margin-left:auto;display:flex;align-items:center;justify-content:flex-end;gap:8px}.web-fetch-enhanced-button{appearance:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 14px;background:none;font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer}.web-fetch-enhanced-button:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.web-fetch-enhanced-save{border-color:transparent;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.web-fetch-enhanced-save:hover:not(:disabled){border-color:transparent;color:var(--dsw-alias-bg-layer-3)}.web-fetch-enhanced-button:disabled{opacity:.4;cursor:default}.web-fetch-enhanced-button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}@media(max-width:620px){.web-fetch-enhanced-footer{align-items:stretch;flex-direction:column}.web-fetch-enhanced-actions{width:100%;margin-left:0;flex-wrap:wrap}.web-fetch-enhanced-button{flex:1}.web-fetch-enhanced-pending{display:none}}";
32
- function AllowlistCard({ scope, t }) {
33
- const snapshot = (0, react.useSyncExternalStore)(scope.subscribe.bind(scope), scope.getSnapshot.bind(scope));
34
- const resolvedCidrs = formatLines(snapshot.value?.allowCidrs);
35
- const resolvedHostnames = formatLines(snapshot.value?.allowHostnames);
25
+ /** Empty input is an explicit empty profile override, never an implicit reset. */
26
+ function buildSaveOps(input, resetToInherited = false) {
27
+ if (resetToInherited) return [{
28
+ op: "unset",
29
+ path: ["allowCidrs"]
30
+ }, {
31
+ op: "unset",
32
+ path: ["allowHostnames"]
33
+ }];
34
+ return [{
35
+ op: "set",
36
+ path: ["allowCidrs"],
37
+ value: [...input.cidrs]
38
+ }, {
39
+ op: "set",
40
+ path: ["allowHostnames"],
41
+ value: [...input.hostnames]
42
+ }];
43
+ }
44
+ /** The boolean Host result is authoritative, including when recovered values match. */
45
+ async function saveAllowlist(form, ops, revision) {
46
+ if (form.state.status !== "ready" || !form.state.writable || revision === void 0) return false;
47
+ try {
48
+ return await form.mutate(ops, revision);
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function isDirty(params) {
54
+ return Boolean(params.resetToInherited) || params.cidrs !== params.resolvedCidrs || params.hostnames !== params.resolvedHostnames;
55
+ }
56
+ /** Subscribe to the bundle's exact Profile entry, including writes and external edits. */
57
+ function AllowlistBundlePage({ configForm, t }) {
58
+ const store = (0, react.useMemo)(() => ({
59
+ subscribe: (listener) => configForm.subscribe(listener),
60
+ getSnapshot: () => configForm.getSnapshot(),
61
+ mutate: configForm.mutate.bind(configForm)
62
+ }), [configForm]);
63
+ return (0, react_jsx_runtime.jsx)(AllowlistPage, {
64
+ view: "page",
65
+ form: {
66
+ state: (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot),
67
+ mutate: store.mutate
68
+ },
69
+ t
70
+ });
71
+ }
72
+ /** Render only the supplied Profile form; never fall back to a global entry. */
73
+ function AllowlistPage({ view, form, t }) {
74
+ if (view === "summary") return (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: t("description") });
75
+ if (!form || form.state.status === "unavailable") return (0, react_jsx_runtime.jsx)("p", {
76
+ role: "status",
77
+ children: t("unavailable")
78
+ });
79
+ if (form.state.status === "loading") return (0, react_jsx_runtime.jsx)("p", {
80
+ role: "status",
81
+ children: t("loading")
82
+ });
83
+ return (0, react_jsx_runtime.jsx)(AllowlistCard, {
84
+ form,
85
+ t
86
+ });
87
+ }
88
+ const cardCss = `
89
+ .web-fetch-enhanced-form{color:var(--dsw-alias-label-primary)}
90
+ .web-fetch-enhanced-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}
91
+ .web-fetch-enhanced-field+.web-fetch-enhanced-field{border-top:.5px solid var(--dsw-alias-border-l2)}
92
+ .web-fetch-enhanced-label{font-size:13px;font-weight:500;line-height:1.5}
93
+ .web-fetch-enhanced-textarea{box-sizing:border-box;width:100%;min-height:96px;resize:vertical;padding:9px 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-primary)}
94
+ .web-fetch-enhanced-textarea:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)}
95
+ .web-fetch-enhanced-textarea[aria-invalid='true']{border-color:var(--dsw-alias-state-error-primary)}
96
+ .web-fetch-enhanced-textarea:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
97
+ .web-fetch-enhanced-hint,.web-fetch-enhanced-status{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
98
+ .web-fetch-enhanced-invalid,.web-fetch-enhanced-failed{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary)}
99
+ .web-fetch-enhanced-readonly{margin:0 0 12px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
100
+ .web-fetch-enhanced-footer{display:flex;align-items:center;gap:12px;padding:16px 0 4px}
101
+ .web-fetch-enhanced-failed,.web-fetch-enhanced-status{flex:1;min-width:0}
102
+ .web-fetch-enhanced-actions{margin-left:auto;display:flex;align-items:center;justify-content:flex-end;gap:8px}
103
+ .web-fetch-enhanced-button{appearance:none;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 14px;background:none;font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer}
104
+ .web-fetch-enhanced-button:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
105
+ .web-fetch-enhanced-save{border-color:transparent;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
106
+ .web-fetch-enhanced-save:hover:not(:disabled){border-color:transparent;color:var(--dsw-alias-bg-layer-3)}
107
+ .web-fetch-enhanced-button:disabled{opacity:.4;cursor:default}
108
+ .web-fetch-enhanced-button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
109
+ @media(max-width:620px){.web-fetch-enhanced-footer{align-items:stretch;flex-direction:column}.web-fetch-enhanced-actions{width:100%;margin-left:0;flex-wrap:wrap}.web-fetch-enhanced-button{flex:1}.web-fetch-enhanced-button:first-child{flex-basis:100%}}
110
+ `;
111
+ function AllowlistCard({ form, t }) {
112
+ const snapshot = form.state;
113
+ const resolvedCidrs = formatLines(layerValues(snapshot.value, "allowCidrs"));
114
+ const resolvedHostnames = formatLines(layerValues(snapshot.value, "allowHostnames"));
36
115
  const [cidrs, setCidrs] = (0, react.useState)(resolvedCidrs);
37
116
  const [hostnames, setHostnames] = (0, react.useState)(resolvedHostnames);
38
- const [open, setOpen] = (0, react.useState)(false);
39
117
  const [saving, setSaving] = (0, react.useState)(false);
118
+ const [saved, setSaved] = (0, react.useState)(false);
40
119
  const [failed, setFailed] = (0, react.useState)(false);
41
120
  const [hasDraft, setHasDraft] = (0, react.useState)(false);
42
- const [resetToProfile, setResetToProfile] = (0, react.useState)(false);
43
- const bodyId = (0, react.useId)();
121
+ const [resetToInherited, setResetToInherited] = (0, react.useState)(false);
122
+ const [draftRevision, setDraftRevision] = (0, react.useState)(snapshot.revision);
123
+ const cidrsId = (0, react.useId)();
124
+ const hostnamesId = (0, react.useId)();
44
125
  const cidrsHintId = (0, react.useId)();
45
126
  const hostnamesHintId = (0, react.useId)();
46
127
  (0, react.useEffect)(() => {
47
128
  if (!saving && !hasDraft) {
48
129
  setCidrs(resolvedCidrs);
49
130
  setHostnames(resolvedHostnames);
131
+ setDraftRevision(snapshot.revision);
50
132
  }
51
133
  }, [
52
134
  hasDraft,
53
135
  resolvedCidrs,
54
136
  resolvedHostnames,
55
- saving
137
+ saving,
138
+ snapshot.revision
56
139
  ]);
57
140
  const parsedCidrs = (0, react.useMemo)(() => parseLines(cidrs), [cidrs]);
58
141
  const parsedHostnames = (0, react.useMemo)(() => parseLines(hostnames), [hostnames]);
59
- const dirty = resetToProfile || cidrs !== resolvedCidrs || hostnames !== resolvedHostnames;
142
+ const dirty = failed || isDirty({
143
+ cidrs,
144
+ hostnames,
145
+ resolvedCidrs,
146
+ resolvedHostnames,
147
+ resetToInherited
148
+ });
60
149
  const invalid = parsedCidrs.duplicate || parsedHostnames.duplicate;
61
150
  const disabled = snapshot.status !== "ready" || !snapshot.writable || saving;
62
151
  const save = async () => {
63
152
  if (disabled || !dirty || invalid) return;
64
153
  setSaving(true);
154
+ setSaved(false);
65
155
  setFailed(false);
66
- let accepted = false;
67
- try {
68
- if (resetToProfile) {
69
- await scope.unset("allowCidrs");
70
- await scope.unset("allowHostnames");
71
- const user = scope.getSnapshot().user;
72
- accepted = !hasLayerField(user, "allowCidrs") && !hasLayerField(user, "allowHostnames");
73
- } else {
74
- await scope.set("allowCidrs", parsedCidrs.values);
75
- await scope.set("allowHostnames", parsedHostnames.values);
76
- const user = scope.getSnapshot().user;
77
- accepted = equalValues(layerValues(user, "allowCidrs"), parsedCidrs.values) && equalValues(layerValues(user, "allowHostnames"), parsedHostnames.values);
78
- }
79
- } catch {
80
- accepted = false;
81
- } finally {
82
- setSaving(false);
83
- }
156
+ const accepted = await saveAllowlist(form, buildSaveOps({
157
+ cidrs: parsedCidrs.values,
158
+ hostnames: parsedHostnames.values
159
+ }, resetToInherited), draftRevision);
160
+ setSaving(false);
84
161
  if (accepted) {
85
162
  setHasDraft(false);
86
- setResetToProfile(false);
87
- setOpen(false);
163
+ setResetToInherited(false);
164
+ setSaved(true);
88
165
  } else setFailed(true);
89
166
  };
90
167
  const stageReset = () => {
91
168
  if (disabled) return;
92
169
  setCidrs(formatLines(layerValues(snapshot.base, "allowCidrs")));
93
170
  setHostnames(formatLines(layerValues(snapshot.base, "allowHostnames")));
94
- setResetToProfile(true);
171
+ setResetToInherited(true);
95
172
  setHasDraft(true);
96
173
  setFailed(false);
174
+ setSaved(false);
97
175
  };
98
176
  const discard = () => {
99
177
  setCidrs(resolvedCidrs);
100
178
  setHostnames(resolvedHostnames);
101
- setResetToProfile(false);
179
+ setResetToInherited(false);
102
180
  setHasDraft(false);
103
181
  setFailed(false);
182
+ setSaved(false);
104
183
  };
105
184
  if (snapshot.status === "unavailable") return null;
106
- const title = t("title");
107
- return (0, react_jsx_runtime.jsxs)("li", {
108
- className: "web-fetch-enhanced-card",
109
- "data-open": open,
185
+ return (0, react_jsx_runtime.jsxs)("section", {
186
+ className: "web-fetch-enhanced-form",
187
+ "aria-label": t("title"),
188
+ "aria-busy": saving,
110
189
  children: [
111
190
  (0, react_jsx_runtime.jsx)("style", { children: cardCss }),
112
- (0, react_jsx_runtime.jsxs)("button", {
113
- type: "button",
114
- className: "web-fetch-enhanced-header",
115
- "aria-expanded": open,
116
- "aria-controls": bodyId,
117
- "aria-label": `${t(open ? "collapse" : "expand")}: ${title}`,
118
- onClick: () => {
119
- setOpen((value) => !value);
120
- },
191
+ !snapshot.writable && snapshot.status === "ready" ? (0, react_jsx_runtime.jsx)("p", {
192
+ className: "web-fetch-enhanced-readonly",
193
+ role: "status",
194
+ children: t("readOnly")
195
+ }) : null,
196
+ (0, react_jsx_runtime.jsxs)("div", {
197
+ className: "web-fetch-enhanced-field",
121
198
  children: [
122
- (0, react_jsx_runtime.jsxs)("span", {
123
- className: "web-fetch-enhanced-head-text",
124
- children: [(0, react_jsx_runtime.jsx)("span", {
125
- className: "web-fetch-enhanced-name",
126
- children: title
127
- }), (0, react_jsx_runtime.jsx)("span", {
128
- className: "web-fetch-enhanced-description",
129
- children: t("description")
130
- })]
199
+ (0, react_jsx_runtime.jsx)("label", {
200
+ className: "web-fetch-enhanced-label",
201
+ htmlFor: cidrsId,
202
+ children: t("cidrs")
131
203
  }),
132
- dirty ? (0, react_jsx_runtime.jsx)("span", {
133
- className: "web-fetch-enhanced-pending",
134
- children: t("unsaved")
135
- }) : null,
136
- (0, react_jsx_runtime.jsx)("span", {
137
- className: "web-fetch-enhanced-chevron",
138
- "aria-hidden": "true"
204
+ (0, react_jsx_runtime.jsx)("textarea", {
205
+ id: cidrsId,
206
+ className: "web-fetch-enhanced-textarea",
207
+ value: cidrs,
208
+ disabled,
209
+ placeholder: t("cidrsPlaceholder"),
210
+ "aria-describedby": cidrsHintId,
211
+ "aria-invalid": parsedCidrs.duplicate || void 0,
212
+ spellCheck: false,
213
+ onChange: (event) => {
214
+ const value = event.target.value;
215
+ setCidrs(value);
216
+ setResetToInherited(false);
217
+ setHasDraft(value !== resolvedCidrs || hostnames !== resolvedHostnames);
218
+ setFailed(false);
219
+ setSaved(false);
220
+ }
221
+ }),
222
+ (0, react_jsx_runtime.jsx)("p", {
223
+ id: cidrsHintId,
224
+ className: parsedCidrs.duplicate ? "web-fetch-enhanced-invalid" : "web-fetch-enhanced-hint",
225
+ children: parsedCidrs.duplicate ? t("duplicate") : t("cidrsHint")
139
226
  })
140
227
  ]
141
228
  }),
142
- open ? (0, react_jsx_runtime.jsxs)("div", {
143
- className: "web-fetch-enhanced-body",
144
- id: bodyId,
229
+ (0, react_jsx_runtime.jsxs)("div", {
230
+ className: "web-fetch-enhanced-field",
145
231
  children: [
146
- !snapshot.writable && snapshot.status === "ready" ? (0, react_jsx_runtime.jsx)("p", {
147
- className: "web-fetch-enhanced-readonly",
232
+ (0, react_jsx_runtime.jsx)("label", {
233
+ className: "web-fetch-enhanced-label",
234
+ htmlFor: hostnamesId,
235
+ children: t("hostnames")
236
+ }),
237
+ (0, react_jsx_runtime.jsx)("textarea", {
238
+ id: hostnamesId,
239
+ className: "web-fetch-enhanced-textarea",
240
+ value: hostnames,
241
+ disabled,
242
+ placeholder: t("hostnamesPlaceholder"),
243
+ "aria-describedby": hostnamesHintId,
244
+ "aria-invalid": parsedHostnames.duplicate || void 0,
245
+ autoCapitalize: "none",
246
+ spellCheck: false,
247
+ onChange: (event) => {
248
+ const value = event.target.value;
249
+ setHostnames(value);
250
+ setResetToInherited(false);
251
+ setHasDraft(cidrs !== resolvedCidrs || value !== resolvedHostnames);
252
+ setFailed(false);
253
+ setSaved(false);
254
+ }
255
+ }),
256
+ (0, react_jsx_runtime.jsx)("p", {
257
+ id: hostnamesHintId,
258
+ className: parsedHostnames.duplicate ? "web-fetch-enhanced-invalid" : "web-fetch-enhanced-hint",
259
+ children: parsedHostnames.duplicate ? t("duplicate") : t("hostnamesHint")
260
+ })
261
+ ]
262
+ }),
263
+ (0, react_jsx_runtime.jsxs)("div", {
264
+ className: "web-fetch-enhanced-footer",
265
+ children: [
266
+ failed ? (0, react_jsx_runtime.jsx)("p", {
267
+ className: "web-fetch-enhanced-failed",
268
+ role: "alert",
269
+ children: t("failed")
270
+ }) : null,
271
+ !failed ? (0, react_jsx_runtime.jsx)("p", {
272
+ className: "web-fetch-enhanced-status",
148
273
  role: "status",
149
- children: t("readOnly")
274
+ children: dirty ? t("unsaved") : saved ? t("saved") : ""
150
275
  }) : null,
151
276
  (0, react_jsx_runtime.jsxs)("div", {
152
- className: "web-fetch-enhanced-field",
153
- children: [
154
- (0, react_jsx_runtime.jsx)("label", {
155
- className: "web-fetch-enhanced-label",
156
- htmlFor: "web-fetch-enhanced-cidrs",
157
- children: t("cidrs")
158
- }),
159
- (0, react_jsx_runtime.jsx)("textarea", {
160
- id: "web-fetch-enhanced-cidrs",
161
- className: "web-fetch-enhanced-textarea",
162
- value: cidrs,
163
- disabled,
164
- placeholder: t("cidrsPlaceholder"),
165
- "aria-describedby": cidrsHintId,
166
- "aria-invalid": parsedCidrs.duplicate || void 0,
167
- spellCheck: false,
168
- onChange: (event) => {
169
- const value = event.target.value;
170
- setCidrs(value);
171
- setResetToProfile(false);
172
- setHasDraft(value !== resolvedCidrs || hostnames !== resolvedHostnames);
173
- setFailed(false);
174
- }
175
- }),
176
- (0, react_jsx_runtime.jsx)("p", {
177
- id: cidrsHintId,
178
- className: parsedCidrs.duplicate ? "web-fetch-enhanced-invalid" : "web-fetch-enhanced-hint",
179
- children: parsedCidrs.duplicate ? t("duplicate") : t("cidrsHint")
180
- })
181
- ]
182
- }),
183
- (0, react_jsx_runtime.jsxs)("div", {
184
- className: "web-fetch-enhanced-field",
277
+ className: "web-fetch-enhanced-actions",
185
278
  children: [
186
- (0, react_jsx_runtime.jsx)("label", {
187
- className: "web-fetch-enhanced-label",
188
- htmlFor: "web-fetch-enhanced-hostnames",
189
- children: t("hostnames")
279
+ (0, react_jsx_runtime.jsx)("button", {
280
+ type: "button",
281
+ className: "web-fetch-enhanced-button",
282
+ disabled: disabled || resetToInherited,
283
+ onClick: stageReset,
284
+ children: t("reset")
190
285
  }),
191
- (0, react_jsx_runtime.jsx)("textarea", {
192
- id: "web-fetch-enhanced-hostnames",
193
- className: "web-fetch-enhanced-textarea",
194
- value: hostnames,
195
- disabled,
196
- placeholder: t("hostnamesPlaceholder"),
197
- "aria-describedby": hostnamesHintId,
198
- "aria-invalid": parsedHostnames.duplicate || void 0,
199
- autoCapitalize: "none",
200
- spellCheck: false,
201
- onChange: (event) => {
202
- const value = event.target.value;
203
- setHostnames(value);
204
- setResetToProfile(false);
205
- setHasDraft(cidrs !== resolvedCidrs || value !== resolvedHostnames);
206
- setFailed(false);
207
- }
286
+ (0, react_jsx_runtime.jsx)("button", {
287
+ type: "button",
288
+ className: "web-fetch-enhanced-button",
289
+ disabled: disabled || !dirty,
290
+ onClick: discard,
291
+ children: t("discard")
208
292
  }),
209
- (0, react_jsx_runtime.jsx)("p", {
210
- id: hostnamesHintId,
211
- className: parsedHostnames.duplicate ? "web-fetch-enhanced-invalid" : "web-fetch-enhanced-hint",
212
- children: parsedHostnames.duplicate ? t("duplicate") : t("hostnamesHint")
293
+ (0, react_jsx_runtime.jsx)("button", {
294
+ type: "button",
295
+ className: "web-fetch-enhanced-button web-fetch-enhanced-save",
296
+ disabled: disabled || !dirty || invalid,
297
+ onClick: () => {
298
+ save();
299
+ },
300
+ children: saving ? t("saving") : t("save")
213
301
  })
214
302
  ]
215
- }),
216
- (0, react_jsx_runtime.jsxs)("div", {
217
- className: "web-fetch-enhanced-footer",
218
- children: [failed ? (0, react_jsx_runtime.jsx)("p", {
219
- className: "web-fetch-enhanced-failed",
220
- role: "alert",
221
- children: t("failed")
222
- }) : null, (0, react_jsx_runtime.jsxs)("div", {
223
- className: "web-fetch-enhanced-actions",
224
- children: [
225
- (0, react_jsx_runtime.jsx)("button", {
226
- type: "button",
227
- className: "web-fetch-enhanced-button",
228
- disabled: disabled || resetToProfile,
229
- onClick: stageReset,
230
- children: t("reset")
231
- }),
232
- (0, react_jsx_runtime.jsx)("button", {
233
- type: "button",
234
- className: "web-fetch-enhanced-button",
235
- disabled: disabled || !dirty,
236
- onClick: discard,
237
- children: t("discard")
238
- }),
239
- (0, react_jsx_runtime.jsx)("button", {
240
- type: "button",
241
- className: "web-fetch-enhanced-button web-fetch-enhanced-save",
242
- disabled: disabled || !dirty || invalid,
243
- onClick: () => {
244
- save();
245
- },
246
- children: saving ? t("saving") : t("save")
247
- })
248
- ]
249
- })]
250
303
  })
251
304
  ]
252
- }) : null
305
+ })
253
306
  ]
254
307
  });
255
308
  }
@@ -258,8 +311,7 @@ window.__ModuleLoader__.load({
258
311
  const en = {
259
312
  title: "Web Fetch Enhanced",
260
313
  description: "Allow selected non-public destinations for web_fetch. Public addresses remain available by default.",
261
- expand: "Expand",
262
- collapse: "Collapse",
314
+ loading: "Loading configuration…",
263
315
  unsaved: "Unsaved",
264
316
  cidrs: "Allowed CIDRs",
265
317
  cidrsHint: "One IPv4 or IPv6 CIDR per line. Keep the range as narrow as possible.",
@@ -268,12 +320,12 @@ window.__ModuleLoader__.load({
268
320
  hostnamesHint: "Optional second factor. Use an exact hostname or a left-most *. wildcard; omit protocols, paths, and ports.",
269
321
  hostnamesPlaceholder: "api.example.com\n*.docs.example.com",
270
322
  save: "Save",
271
- reset: "Reset to Profile",
323
+ reset: "Reset to inherited defaults",
272
324
  discard: "Discard",
273
325
  saving: "Saving…",
274
326
  saved: "Saved",
275
- failed: "The Host rejected the change. Review the entries and try again.",
276
- unavailable: "This settings namespace is not available in the current Profile.",
327
+ failed: "The change was not saved. Review the entries, or discard to reload the current Profile before retrying.",
328
+ unavailable: "This plugin configuration is not available in the current Profile.",
277
329
  readOnly: "This connection cannot persist Host Profile settings.",
278
330
  duplicate: "Duplicate entries are not allowed.",
279
331
  blank: "Blank entries are ignored."
@@ -281,8 +333,7 @@ window.__ModuleLoader__.load({
281
333
  const zh = {
282
334
  title: "Web Fetch Enhanced",
283
335
  description: "允许 web_fetch 访问指定的非公网目标;公网地址默认仍可正常访问。",
284
- expand: "展开",
285
- collapse: "折叠",
336
+ loading: "正在加载配置…",
286
337
  unsaved: "未保存",
287
338
  cidrs: "允许的 CIDR",
288
339
  cidrsHint: "每行一个 IPv4 或 IPv6 CIDR;请尽量缩小放行范围。",
@@ -291,25 +342,24 @@ window.__ModuleLoader__.load({
291
342
  hostnamesHint: "可选的第二因子。填写精确域名或最左侧 *. 通配规则,不要包含协议、路径或端口。",
292
343
  hostnamesPlaceholder: "api.example.com\n*.docs.example.com",
293
344
  save: "保存",
294
- reset: "重置为 Profile 配置",
345
+ reset: "重置为继承的默认配置",
295
346
  discard: "放弃修改",
296
347
  saving: "正在保存…",
297
348
  saved: "已保存",
298
- failed: "Host 拒绝了修改,请检查条目后重试。",
299
- unavailable: "当前 Profile 未提供此设置命名空间。",
349
+ failed: "修改未保存。请检查条目,或放弃修改以重新加载当前 Profile 后重试。",
350
+ unavailable: "当前 Profile 未提供此插件配置。",
300
351
  readOnly: "当前连接不能持久化 Host Profile 设置。",
301
352
  duplicate: "不允许重复条目。",
302
353
  blank: "空白行会被忽略。"
303
354
  };
304
355
  //#endregion
305
356
  //#region lib/types/client/index.js
306
- const SETTINGS_NAMESPACE = "web-fetch-enhanced";
307
357
  const LOCALE_NAMESPACE = "settings.webFetchEnhanced";
308
358
  const name = "web-fetch-enhanced-client";
309
359
  const inject = [
310
360
  "slots",
311
361
  "locale",
312
- "settingsScope"
362
+ "configForms"
313
363
  ];
314
364
  function apply(ctx) {
315
365
  const t = ctx.locale.bind(LOCALE_NAMESPACE);
@@ -317,23 +367,29 @@ window.__ModuleLoader__.load({
317
367
  en,
318
368
  zh
319
369
  }), "web-fetch-enhanced: settings dictionaries");
320
- const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
321
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
322
- name: "settings.plugin.item",
323
- key: SETTINGS_NAMESPACE,
370
+ const configForm = ctx.configForms.get("web-fetch-enhanced");
371
+ ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
372
+ name: "plugins.bundle.config",
373
+ key: "dsh-web-fetch-enhanced",
324
374
  locale: LOCALE_NAMESPACE,
325
375
  inject: () => ({
326
- scope,
327
- t
376
+ t,
377
+ configForm
328
378
  })
329
- }, AllowlistCard));
379
+ }, AllowlistBundlePage));
330
380
  }
331
381
  //#endregion
382
+ exports.AllowlistBundlePage = AllowlistBundlePage;
332
383
  exports.AllowlistCard = AllowlistCard;
384
+ exports.AllowlistPage = AllowlistPage;
333
385
  exports.apply = apply;
386
+ exports.buildSaveOps = buildSaveOps;
334
387
  exports.inject = inject;
388
+ exports.isDirty = isDirty;
389
+ exports.layerValues = layerValues;
335
390
  exports.name = name;
336
391
  exports.parseLines = parseLines;
392
+ exports.saveAllowlist = saveAllowlist;
337
393
  return module.exports;
338
394
  }
339
395
  });
package/lib/index.js CHANGED
@@ -601,37 +601,59 @@ function translateAbortOrNetwork(error, signal) {
601
601
  * @module dsh-web-fetch-enhanced
602
602
  */
603
603
  const MAX_NODE_TIMER_DELAY_MS = 2147483647;
604
- const FIBER_DISPOSED = 4;
605
- const FIBER_UNLOADING = 5;
606
604
  /** Explicit product User-Agent used by default. */
607
605
  const DEFAULT_USER_AGENT = "dsh-web-fetch-enhanced/0.1.0";
608
606
  /** Default provider id; select it in the dsh-web row with fetchProvider. */
609
607
  const DEFAULT_PROVIDER_ID = "http-enhanced";
610
- /** Settings namespace paired with the Web Profile configuration card. */
608
+ /** Default Loader entry id paired with the Web Profile configuration card. */
611
609
  const SETTINGS_NAMESPACE = "web-fetch-enhanced";
612
610
  /** Cordis plugin name used by loader diagnostics. */
613
611
  const name = "web-fetch-enhanced";
614
612
  /** The web seam this provider contributes to. */
615
613
  const inject = ["web"];
614
+ /** Keep Host-only checks out of the schema serialized for browser forms. */
615
+ function hostValidated(schema, validate) {
616
+ const checked = z.transform(schema, (value) => {
617
+ validate(value);
618
+ return value;
619
+ }, true);
620
+ const presentation = new z(schema.toJSON());
621
+ checked.toJSON = function() {
622
+ presentation.meta = {
623
+ ...schema.meta,
624
+ ...this.meta
625
+ };
626
+ return presentation.toJSON();
627
+ };
628
+ return checked;
629
+ }
630
+ /** Schemastery's number bounds alone do not reject NaN; validate finiteness before commit. */
631
+ function positiveLimit(field, max = Number.MAX_VALUE) {
632
+ return hostValidated(z.number().min(Number.MIN_VALUE).max(max), (value) => {
633
+ assertPositiveFinite(field, value);
634
+ });
635
+ }
636
+ /** Validate the whole candidate before Loader atomically commits any live references. */
616
637
  const Config = z.object({
617
- providerId: z.string().default(DEFAULT_PROVIDER_ID),
618
- allowCidrs: z.array(z.string()).default([]),
619
- allowHostnames: z.array(z.string()).default([]),
620
- maxResponseBytes: z.number().default(5e6),
621
- maxBodyChars: z.number().default(1e5),
622
- timeoutMs: z.number().default(3e4),
623
- maxRedirects: z.number().default(5),
624
- userAgent: z.string().default(DEFAULT_USER_AGENT)
638
+ providerId: z.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u).default(DEFAULT_PROVIDER_ID),
639
+ allowCidrs: hostValidated(z.array(z.string()), (value) => {
640
+ new AddressPolicy({ allowCidrs: value });
641
+ }).default([]).volatile(),
642
+ allowHostnames: hostValidated(z.array(z.string()), (value) => {
643
+ new AddressPolicy({ allowHostnames: value });
644
+ }).default([]).volatile(),
645
+ maxResponseBytes: positiveLimit("maxResponseBytes").default(5e6).volatile(),
646
+ maxBodyChars: positiveLimit("maxBodyChars").default(1e5).volatile(),
647
+ timeoutMs: positiveLimit("timeoutMs", MAX_NODE_TIMER_DELAY_MS).default(3e4).volatile(),
648
+ maxRedirects: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(5).volatile(),
649
+ userAgent: z.string().pattern(/^[\x20-\x7e\x80-\xff]*$/u).default(DEFAULT_USER_AGENT).volatile()
625
650
  });
626
651
  /** Build prompt guidance copy informing the model of authorized non-public destinations. */
627
652
  function formatAllowlistPrompt(config) {
628
653
  const cidrs = config.allowCidrs ?? [];
629
654
  const hostnames = config.allowHostnames ?? [];
630
- if (cidrs.length === 0 && hostnames.length === 0) return "";
631
- const items = [];
632
- if (cidrs.length > 0) items.push(`CIDRs: ${cidrs.join(", ")}`);
633
- if (hostnames.length > 0) items.push(`hostnames: ${hostnames.join(", ")}`);
634
- return `The operator has explicitly authorized web_fetch access to the following non-public destinations: [${items.join("; ")}]. You can safely fetch these endpoints.`;
655
+ if (cidrs.length === 0) return "";
656
+ return `${`The operator has explicitly authorized web_fetch access to non-public addresses within [CIDRs: ${cidrs.join(", ")}].`}${hostnames.length === 0 ? "" : ` This exception requires BOTH an address within those CIDRs AND a URL hostname matching one of [hostnames: ${hostnames.join(", ")}]. Hostname rules only restrict the CIDR exceptions; they do not independently authorize any non-public address.`} All other URL, DNS, redirect, and transport safety checks still apply.`;
635
657
  }
636
658
  /** Construct the provider without mounting it, useful for tests and custom compositions. */
637
659
  function createProvider(config = {}, proxyResolver) {
@@ -654,57 +676,38 @@ function createProvider(config = {}, proxyResolver) {
654
676
  };
655
677
  return new EnhancedHttpFetchProvider(resolved.providerId, limits, createAllowlistResolver(policy), proxyResolver);
656
678
  }
657
- function isUnloading(ctx) {
658
- const state = ctx.fiber?.state;
659
- return state === FIBER_UNLOADING || state === FIBER_DISPOSED;
679
+ /** Capture one coherent immutable configuration for a single request or prompt assembly. */
680
+ function snapshotConfig(config) {
681
+ return {
682
+ providerId: config.providerId,
683
+ allowCidrs: config.allowCidrs.get(),
684
+ allowHostnames: config.allowHostnames.get(),
685
+ maxResponseBytes: config.maxResponseBytes.get(),
686
+ maxBodyChars: config.maxBodyChars.get(),
687
+ timeoutMs: config.timeoutMs.get(),
688
+ maxRedirects: config.maxRedirects.get(),
689
+ userAgent: config.userAgent.get()
690
+ };
660
691
  }
661
- /** Register the enhanced fetch provider and its live Web Profile settings section. */
692
+ /** Register a provider backed exclusively by Loader-owned live Config references. */
662
693
  function apply(ctx, config) {
663
- const providerId = resolveConfig(config).providerId;
664
- let current = () => config;
665
- ctx.inject(["settings"], (settingsCtx) => {
666
- const settings = settingsCtx.settings;
667
- if (typeof settings.installSection === "function") {
668
- settings.installSection(ctx, SETTINGS_NAMESPACE, Config, config, {
669
- setSource: (source) => {
670
- current = source;
671
- },
672
- onChange: () => {
673
- ctx.emit("system-prompt/change");
674
- },
675
- validate: (value) => {
676
- if (resolveConfig(value).providerId !== providerId) throw new Error("web-fetch-enhanced: providerId cannot be changed through live settings");
677
- createProvider(value);
678
- }
679
- });
680
- return;
681
- }
682
- const scope = settings.register(SETTINGS_NAMESPACE, Config, {
683
- base: config,
684
- validate: (value) => {
685
- if (resolveConfig(value).providerId !== providerId) throw new Error("web-fetch-enhanced: providerId cannot be changed through live settings");
686
- createProvider(value);
687
- }
688
- });
689
- current = () => scope.get();
690
- settingsCtx.effect(() => () => {
691
- if (isUnloading(ctx)) return;
692
- current = () => config;
693
- ctx.emit("system-prompt/change");
694
- });
694
+ const providerId = createProvider(snapshotConfig(config)).id;
695
+ ctx.on("loader/volatile-update", (paths) => {
696
+ if (paths.some(([field]) => field === "allowCidrs" || field === "allowHostnames")) ctx.emit("system-prompt/change");
695
697
  });
696
698
  ctx.inject(["systemPrompt"], (promptCtx) => {
697
699
  promptCtx.systemPrompt.section({
698
700
  name: "web-fetch-enhanced:allowlist",
699
701
  order: 2105,
700
- text: () => formatAllowlistPrompt(current())
702
+ text: () => formatAllowlistPrompt(snapshotConfig(config)),
703
+ interpolate: false
701
704
  });
702
705
  });
703
706
  const dynamicProvider = {
704
707
  id: providerId,
705
708
  available: () => true,
706
709
  fetch: async (request, signal) => {
707
- return await createProvider(current()).fetch(request, signal);
710
+ return await createProvider(snapshotConfig(config)).fetch(request, signal);
708
711
  }
709
712
  };
710
713
  ctx.web.registerFetchProvider(dynamicProvider);
@@ -1,16 +1,51 @@
1
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
1
+ import type { ConfigPageForm, PluginConfigViewProps } from '@deepseek-ai/dsh-client-ui-plugin-manager/client';
2
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
2
3
  import type { LocaleKey } from './locales.ts';
3
4
  export interface AllowlistSettings {
4
5
  allowCidrs?: string[];
5
6
  allowHostnames?: string[];
6
7
  }
7
8
  export interface AllowlistCardProps {
8
- scope: SettingsScope<AllowlistSettings>;
9
+ form: ConfigPageForm;
9
10
  t: (key: LocaleKey) => string;
10
11
  }
12
+ export type SettingsPathOp = {
13
+ op: 'set';
14
+ path: string[];
15
+ value: string[];
16
+ } | {
17
+ op: 'unset';
18
+ path: string[];
19
+ };
20
+ export declare function formatLines(value: readonly string[] | undefined): string;
21
+ export declare function layerValues(layer: unknown, field: keyof AllowlistSettings): string[] | undefined;
11
22
  export declare function parseLines(text: string): {
12
23
  values: string[];
13
24
  duplicate: boolean;
14
25
  };
15
- export declare function AllowlistCard({ scope, t }: AllowlistCardProps): import("react").JSX.Element | null;
26
+ /** Empty input is an explicit empty profile override, never an implicit reset. */
27
+ export declare function buildSaveOps(input: {
28
+ cidrs: readonly string[];
29
+ hostnames: readonly string[];
30
+ }, resetToInherited?: boolean): SettingsPathOp[];
31
+ /** The boolean Host result is authoritative, including when recovered values match. */
32
+ export declare function saveAllowlist(form: ConfigPageForm, ops: readonly SettingsPathOp[], revision: number | undefined): Promise<boolean>;
33
+ export interface DirtyCheckParams {
34
+ cidrs: string;
35
+ hostnames: string;
36
+ resolvedCidrs: string;
37
+ resolvedHostnames: string;
38
+ resetToInherited?: boolean;
39
+ }
40
+ export declare function isDirty(params: DirtyCheckParams): boolean;
41
+ /** Subscribe to the bundle's exact Profile entry, including writes and external edits. */
42
+ export declare function AllowlistBundlePage({ configForm, t }: {
43
+ configForm: ConfigForm<Record<string, unknown>>;
44
+ t: (key: LocaleKey) => string;
45
+ }): import("react").JSX.Element;
46
+ /** Render only the supplied Profile form; never fall back to a global entry. */
47
+ export declare function AllowlistPage({ view, form, t }: PluginConfigViewProps & {
48
+ t: (key: LocaleKey) => string;
49
+ }): import("react").JSX.Element;
50
+ export declare function AllowlistCard({ form, t }: AllowlistCardProps): import("react").JSX.Element | null;
16
51
  //# sourceMappingURL=AllowlistCard.d.ts.map
@@ -8,6 +8,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
8
8
  export declare const name = "web-fetch-enhanced-client";
9
9
  export declare const inject: string[];
10
10
  export declare function apply(ctx: Context): void;
11
- export { AllowlistCard, parseLines } from './AllowlistCard.tsx';
12
- export type { AllowlistCardProps, AllowlistSettings } from './AllowlistCard.tsx';
11
+ export { AllowlistCard, AllowlistPage, AllowlistBundlePage, parseLines, buildSaveOps, saveAllowlist, isDirty, layerValues } from './AllowlistCard.tsx';
12
+ export type { AllowlistCardProps, AllowlistSettings, SettingsPathOp, DirtyCheckParams } from './AllowlistCard.tsx';
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -1,8 +1,7 @@
1
1
  export declare const en: {
2
2
  readonly title: "Web Fetch Enhanced";
3
3
  readonly description: "Allow selected non-public destinations for web_fetch. Public addresses remain available by default.";
4
- readonly expand: "Expand";
5
- readonly collapse: "Collapse";
4
+ readonly loading: "Loading configuration…";
6
5
  readonly unsaved: "Unsaved";
7
6
  readonly cidrs: "Allowed CIDRs";
8
7
  readonly cidrsHint: "One IPv4 or IPv6 CIDR per line. Keep the range as narrow as possible.";
@@ -11,12 +10,12 @@ export declare const en: {
11
10
  readonly hostnamesHint: "Optional second factor. Use an exact hostname or a left-most *. wildcard; omit protocols, paths, and ports.";
12
11
  readonly hostnamesPlaceholder: "api.example.com\n*.docs.example.com";
13
12
  readonly save: "Save";
14
- readonly reset: "Reset to Profile";
13
+ readonly reset: "Reset to inherited defaults";
15
14
  readonly discard: "Discard";
16
15
  readonly saving: "Saving…";
17
16
  readonly saved: "Saved";
18
- readonly failed: "The Host rejected the change. Review the entries and try again.";
19
- readonly unavailable: "This settings namespace is not available in the current Profile.";
17
+ readonly failed: "The change was not saved. Review the entries, or discard to reload the current Profile before retrying.";
18
+ readonly unavailable: "This plugin configuration is not available in the current Profile.";
20
19
  readonly readOnly: "This connection cannot persist Host Profile settings.";
21
20
  readonly duplicate: "Duplicate entries are not allowed.";
22
21
  readonly blank: "Blank entries are ignored.";
@@ -6,27 +6,26 @@
6
6
  */
7
7
  import type { Context } from '@deepseek-ai/cordis';
8
8
  import z from '@deepseek-ai/schemastery';
9
- import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
10
9
  import { EnhancedHttpFetchProvider } from './provider.ts';
11
10
  import type { ProxyRouteResolver } from './resolver.ts';
12
11
  /** Explicit product User-Agent used by default. */
13
12
  export declare const DEFAULT_USER_AGENT = "dsh-web-fetch-enhanced/0.1.0";
14
13
  /** Default provider id; select it in the dsh-web row with fetchProvider. */
15
14
  export declare const DEFAULT_PROVIDER_ID = "http-enhanced";
16
- /** Settings namespace paired with the Web Profile configuration card. */
17
- export declare const SETTINGS_NAMESPACE: SettingsNamespace;
15
+ /** Default Loader entry id paired with the Web Profile configuration card. */
16
+ export declare const SETTINGS_NAMESPACE = "web-fetch-enhanced";
18
17
  /** Cordis plugin name used by loader diagnostics. */
19
18
  export declare const name = "web-fetch-enhanced";
20
19
  /** The web seam this provider contributes to. */
21
20
  export declare const inject: string[];
22
21
  /** Plugin configuration. Every non-public exception is explicit and deny-by-default. */
23
- export interface Config {
22
+ export interface ProviderConfig {
24
23
  /** Provider id registered in ctx.web. Defaults to http-enhanced. */
25
24
  providerId?: string;
26
25
  /** Non-public IPv4/IPv6 CIDRs that may bypass the public-address filter. */
27
- allowCidrs?: string[];
26
+ allowCidrs?: readonly string[];
28
27
  /** Optional exact hosts or left-most wildcard rules required in addition to allowCidrs. */
29
- allowHostnames?: string[];
28
+ allowHostnames?: readonly string[];
30
29
  /** Maximum response body size in bytes. */
31
30
  maxResponseBytes?: number;
32
31
  /** Maximum decoded body length in characters. */
@@ -38,29 +37,35 @@ export interface Config {
38
37
  /** User-Agent header sent on every request. */
39
38
  userAgent?: string;
40
39
  }
41
- export declare const Config: z<Config>;
42
- interface SystemPromptSeam {
43
- section(section: {
44
- name: string;
45
- order: number;
46
- text: string | ((context?: unknown) => string);
47
- }): () => void;
48
- }
49
- declare module '@deepseek-ai/cordis' {
50
- interface Context {
51
- systemPrompt?: SystemPromptSeam;
52
- }
53
- interface Events {
54
- 'system-prompt/change'(): void;
55
- }
56
- }
40
+ /** Validate the whole candidate before Loader atomically commits any live references. */
41
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
42
+ providerId: z<string, string, "defined">;
43
+ allowCidrs: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
44
+ allowHostnames: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
45
+ maxResponseBytes: z<number, number, "volatile-defined">;
46
+ maxBodyChars: z<number, number, "volatile-defined">;
47
+ timeoutMs: z<number, number, "volatile-defined">;
48
+ maxRedirects: z<number, number, "volatile-defined">;
49
+ userAgent: z<string, string, "volatile-defined">;
50
+ }>>, Schemastery.ObjectT<NoInfer<{
51
+ providerId: z<string, string, "defined">;
52
+ allowCidrs: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
53
+ allowHostnames: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
54
+ maxResponseBytes: z<number, number, "volatile-defined">;
55
+ maxBodyChars: z<number, number, "volatile-defined">;
56
+ timeoutMs: z<number, number, "volatile-defined">;
57
+ maxRedirects: z<number, number, "volatile-defined">;
58
+ userAgent: z<string, string, "volatile-defined">;
59
+ }>>, "plain">;
60
+ /** Parsed plugin Config: ordinary identity plus stable live field references. */
61
+ export type Config = ReturnType<typeof Config>;
57
62
  /** Build prompt guidance copy informing the model of authorized non-public destinations. */
58
- export declare function formatAllowlistPrompt(config: Config): string;
63
+ export declare function formatAllowlistPrompt(config: ProviderConfig): string;
59
64
  /** Construct the provider without mounting it, useful for tests and custom compositions. */
60
- export declare function createProvider(config?: Config, proxyResolver?: ProxyRouteResolver): EnhancedHttpFetchProvider;
65
+ export declare function createProvider(config?: ProviderConfig, proxyResolver?: ProxyRouteResolver): EnhancedHttpFetchProvider;
61
66
  export { isNonPublicIpLiteral } from './address-policy.ts';
62
67
  export type { ProxyRouteResolver, ProxyRouteResult } from './resolver.ts';
63
68
  export { defaultProxyRoute } from './resolver.ts';
64
- /** Register the enhanced fetch provider and its live Web Profile settings section. */
69
+ /** Register a provider backed exclusively by Loader-owned live Config references. */
65
70
  export declare function apply(ctx: Context, config: Config): void;
66
71
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-web-fetch-enhanced",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Configurable non-public address allowlists for DeepSeek Harness web_fetch",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "client": {
38
38
  "inject": [
39
- "@deepseek-ai/dsh-client-ui-settings-plugins",
39
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
40
40
  "@deepseek-ai/dsh-client-ui-settings",
41
41
  "@deepseek-ai/dsh-client-locale",
42
42
  "@deepseek-ai/dsh-client-ui-renderer"
@@ -66,45 +66,51 @@
66
66
  },
67
67
  "engines": {
68
68
  "node": "^22.19.0 || >=24.0.0",
69
- "dsh": ">=0.1.5-rc.2"
69
+ "dsh": ">=0.1.7-rc.1 <0.2.0"
70
70
  },
71
71
  "peerDependencies": {
72
- "@deepseek-ai/cordis": "^4.0.1 || ^4.0.2",
73
- "@deepseek-ai/dsh-timeout": "^0.1.5-rc.2",
74
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
75
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
76
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
77
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
78
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.2",
79
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
80
- "@deepseek-ai/dsh-web": "^0.1.5-rc.2",
81
- "@deepseek-ai/dsh-http-proxy": "^0.1.5-rc.2"
72
+ "@deepseek-ai/cordis": "^4.0.4",
73
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.5",
74
+ "@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.2.0",
75
+ "@deepseek-ai/dsh-client-ui-plugin-manager": ">=0.1.7-rc.1 <0.2.0",
76
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.2.0",
77
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0",
78
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.2.0",
79
+ "@deepseek-ai/dsh-http-proxy": ">=0.1.7-rc.1 <0.2.0",
80
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.7-rc.1 <0.2.0",
81
+ "@deepseek-ai/dsh-timeout": ">=0.1.7-rc.1 <0.2.0",
82
+ "@deepseek-ai/dsh-web": ">=0.1.7-rc.1 <0.2.0"
82
83
  },
83
84
  "dependencies": {
84
- "@deepseek-ai/schemastery": "^3.18.1 || ^3.18.2",
85
+ "@deepseek-ai/schemastery": "^3.18.4",
85
86
  "ipaddr.js": "^2.5.0",
86
87
  "undici": "^8.10.0"
87
88
  },
88
89
  "devDependencies": {
89
- "@deepseek-ai/cordis": "^4.0.2",
90
- "@deepseek-ai/dsh-timeout": "^0.1.5-rc.2",
91
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
92
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
93
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
94
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
95
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.2",
96
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
97
- "@deepseek-ai/dsh-web": "^0.1.5-rc.2",
98
- "@deepseek-ai/dsh-http-proxy": "^0.1.5-rc.2",
90
+ "@deepseek-ai/cordis": "^4.0.4",
91
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.5",
92
+ "@deepseek-ai/dsh-client-locale": "0.1.7-rc.1",
93
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.1.7-rc.1",
94
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.1",
95
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.1",
96
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.1",
97
+ "@deepseek-ai/dsh-http-proxy": "0.1.7-rc.1",
98
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.1",
99
+ "@deepseek-ai/dsh-timeout": "0.1.7-rc.1",
100
+ "@deepseek-ai/dsh-web": "0.1.7-rc.1",
99
101
  "@types/js-yaml": "^4.0.9",
100
102
  "@types/node": "^22.19.13",
101
103
  "@types/react": "~18.3.1",
104
+ "@types/react-test-renderer": "18.3.1",
105
+ "@types/semver": "^7.7.1",
102
106
  "@vitest/coverage-v8": "^4.1.8",
103
107
  "js-yaml": "^4.1.0",
104
108
  "oxlint": "^1.51.0",
105
109
  "oxlint-tsgolint": "7.0.2001",
106
110
  "publint": "^0.3.21",
107
111
  "react": "^18.2.0",
112
+ "react-test-renderer": "18.3.1",
113
+ "semver": "^7.8.5",
108
114
  "tsdown": "^0.21.0",
109
115
  "typescript": "^6.0.3",
110
116
  "vitest": "^4.1.8"