dsh-web-fetch-enhanced 0.0.1

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.
@@ -0,0 +1,82 @@
1
+ # 安全设计与上线检查表
2
+
3
+ ## 1. 信任边界
4
+
5
+ <code>web_fetch</code> 的 URL 来自模型,必须视为不可信输入;DNS、HTTP 响应、redirect 与正文也都不可信。operator 的 Host composition 和本插件白名单属于受信配置。
6
+
7
+ 本插件的核心安全假设是:只有 operator 能修改 <code>allowCidrs</code> 与 <code>allowHostnames</code>。白名单不会作为模型工具参数暴露。
8
+
9
+ ## 2. 主要威胁与缓解
10
+
11
+ ### SSRF 到本机、内网和元数据服务
12
+
13
+ 默认策略只允许公网单播。loopback、RFC1918、link-local、CGNAT、ULA、benchmark/fake-IP、组播、保留网段均拒绝。只有显式 CIDR 例外可以扩大可达面。
14
+
15
+ 云元数据地址(例如 <code>169.254.169.254</code>)不要加入白名单。也不要使用 <code>0.0.0.0/0</code>、<code>::/0</code> 或宽泛 RFC1918 网段来“解决”单一代理路由问题。
16
+
17
+ ### DNS rebinding 与检查后再解析
18
+
19
+ 解析使用完整答案集,一项失败则全部失败。通过策略的答案直接交给 request-local Undici lookup;transport 不会重新查询 URL hostname。URL hostname 仍保留用于 Host header 和 TLS SNI。
20
+
21
+ ### 混合 DNS 答案绕过
22
+
23
+ 攻击者可能返回一个公网地址和一个私网地址。实现不会挑选“第一个安全答案”,而是拒绝整个集合,避免连接层稍后选中私网答案。
24
+
25
+ ### IPv4-mapped IPv6 与 NAT64
26
+
27
+ IPv4-mapped IPv6 按嵌入 IPv4 分类。对于活动 DNS64,自定义 RFC 6052 前缀通过 <code>ipv4only.arpa</code> 发现;映射后的 IPv4 也必须为公网或命中白名单。这样不会因 translator 的公网 IPv6 外观而隐藏内网 IPv4。
28
+
29
+ ### Redirect 逃逸
30
+
31
+ 只跟随 301、302、303、307、308;只允许 scheme、hostname 和 port 都相同的 redirect。每一跳重新执行 URL、DNS、白名单和连接固定。跨源地址必须由新的工具调用显式请求。
32
+
33
+ ### 凭据与横向权限
34
+
35
+ 请求固定为 GET,不发送 cookie、Authorization 或环境中的 ambient credentials,也不接受 URL username/password。自定义 User-Agent 不应包含秘密。
36
+
37
+ ### 资源耗尽和恶意内容
38
+
39
+ URL、响应字节、解码字符、redirect 跳数和 provider 时间均有上限。只接受 HTML、XHTML、text/*、JSON/XML 与 +json/+xml;二进制内容拒绝。响应正文仍是外部不可信数据,模型必须把它作为数据而非指令。
40
+
41
+ ## 3. fake-IP 特有风险
42
+
43
+ 在 Clash/Mihomo fake-IP 模式下,<code>198.18.0.0/15</code> 往往是“任意域名”的代理句柄,而不是一个单一后端。放行整个网段意味着所有解析到该 fake-IP 池的域名都满足地址条件。
44
+
45
+ 建议:
46
+
47
+ 1. 确认该网段只由受控透明代理接管;
48
+ 2. 只放行代理实际使用的最小 CIDR;
49
+ 3. 若业务站点集合有限,同时配置 <code>allowHostnames</code>;
50
+ 4. 确认代理对本机、内网和元数据目的地仍有独立拒绝规则;
51
+ 5. 不要把普通 HTTP forward proxy 的地址误当作目标白名单;本插件没有实现 CONNECT 或代理认证。
52
+
53
+ <code>allowHostnames</code> 只能限制 URL hostname,不能验证代理最终选择的真实上游。最终路由仍依赖透明代理的可信配置。
54
+
55
+ ## 4. 配置审查建议
56
+
57
+ 对白名单变更做代码审查,并记录:
58
+
59
+ - CIDR 所有者和用途;
60
+ - 为什么原生 public-only 策略不适用;
61
+ - 是否属于 fake-IP、VPN、服务网格或真实内网服务;
62
+ - 是否可以增加 hostname 第二因子;
63
+ - 过期时间和撤销负责人;
64
+ - 代理/路由层仍保留哪些目的地拒绝规则。
65
+
66
+ ## 5. 上线检查表
67
+
68
+ - [ ] 本插件挂在 Host composition,而不是 agent preset;
69
+ - [ ] <code>web.fetchProvider</code> 显式指向预期 ID;
70
+ - [ ] 若使用 <code>providerId: http</code>,原生 http provider 已禁用;
71
+ - [ ] <code>allowCidrs</code> 是最小范围且没有默认路由;
72
+ - [ ] 宽 fake-IP 网段已评估并尽可能增加 <code>allowHostnames</code>;
73
+ - [ ] loopback、RFC1918、link-local、云元数据仍有负向测试;
74
+ - [ ] 混合公网/私网 DNS 答案会失败;
75
+ - [ ] 同源 redirect 正常,跨源 redirect 失败;
76
+ - [ ] timeout、字节和字符上限符合部署预算;
77
+ - [ ] 代理不会把白名单地址绕到未授权的内网目的地;
78
+ - [ ] 运行 <code>pnpm run check</code> 并在实际 Profile 中执行一次允许和一次拒绝用例。
79
+
80
+ ## 6. 安全报告
81
+
82
+ 报告问题时请提供:DSH 版本、本插件版本、最小 composition、URL 的解析答案、预期/实际错误 code,以及是否启用 DNS64、fake-IP 或透明代理。不要在公开 issue 中提交凭据、内网域名或敏感响应正文。
@@ -0,0 +1,10 @@
1
+ # Profile user patch after installing the dsh-web-fetch-enhanced bundle.
2
+ # The bundle already inserts this row and selects fetchProvider: http-enhanced.
3
+ # Merge this entry into the existing profile cordis.patch.yml; do not overwrite it.
4
+ - id: web-fetch-enhanced
5
+ config:
6
+ allowCidrs:
7
+ - 198.18.0.0/15
8
+ # Optional hostname second factor for non-public exceptions.
9
+ allowHostnames:
10
+ - '*.example.com'
@@ -0,0 +1,16 @@
1
+ # Profile user patch after installing the bundle, retaining fetchProvider: http.
2
+ # A patch replaces a row's whole config, so both WebRuntime fields are restated.
3
+ - id: web
4
+ config:
5
+ searchProvider: deepseek-official
6
+ fetchProvider: http
7
+
8
+ # The native provider MUST be disabled to avoid duplicate provider id "http".
9
+ - id: web-fetch-http
10
+ disabled: true
11
+
12
+ - id: web-fetch-enhanced
13
+ config:
14
+ providerId: http
15
+ allowCidrs:
16
+ - 198.18.0.0/15
@@ -0,0 +1,13 @@
1
+ # Manual Host overlay when the package is resolvable but its dsh.bundle layer
2
+ # is not active. Bundle users should use coexist.cordis.yml instead.
3
+ - id: web
4
+ config:
5
+ searchProvider: deepseek-official
6
+ fetchProvider: http-enhanced
7
+
8
+ - insert:
9
+ - id: web-fetch-enhanced
10
+ name: dsh-web-fetch-enhanced
11
+ config:
12
+ allowCidrs:
13
+ - 198.18.0.0/15
package/lib/client.js ADDED
@@ -0,0 +1,341 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-web-fetch-enhanced",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
+ //#region lib/types/client/AllowlistCard.js
10
+ function formatLines(value) {
11
+ return (value ?? []).join("\n");
12
+ }
13
+ function layerValues(layer, field) {
14
+ if (typeof layer !== "object" || layer === null) return void 0;
15
+ const value = Reflect.get(layer, field);
16
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
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
+ function parseLines(text) {
25
+ const values = text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
26
+ return {
27
+ values: [...new Set(values)],
28
+ duplicate: new Set(values).size !== values.length
29
+ };
30
+ }
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);
36
+ const [cidrs, setCidrs] = (0, react.useState)(resolvedCidrs);
37
+ const [hostnames, setHostnames] = (0, react.useState)(resolvedHostnames);
38
+ const [open, setOpen] = (0, react.useState)(false);
39
+ const [saving, setSaving] = (0, react.useState)(false);
40
+ const [failed, setFailed] = (0, react.useState)(false);
41
+ const [hasDraft, setHasDraft] = (0, react.useState)(false);
42
+ const [resetToProfile, setResetToProfile] = (0, react.useState)(false);
43
+ const bodyId = (0, react.useId)();
44
+ const cidrsHintId = (0, react.useId)();
45
+ const hostnamesHintId = (0, react.useId)();
46
+ (0, react.useEffect)(() => {
47
+ if (!saving && !hasDraft) {
48
+ setCidrs(resolvedCidrs);
49
+ setHostnames(resolvedHostnames);
50
+ }
51
+ }, [
52
+ hasDraft,
53
+ resolvedCidrs,
54
+ resolvedHostnames,
55
+ saving
56
+ ]);
57
+ const parsedCidrs = (0, react.useMemo)(() => parseLines(cidrs), [cidrs]);
58
+ const parsedHostnames = (0, react.useMemo)(() => parseLines(hostnames), [hostnames]);
59
+ const dirty = resetToProfile || cidrs !== resolvedCidrs || hostnames !== resolvedHostnames;
60
+ const invalid = parsedCidrs.duplicate || parsedHostnames.duplicate;
61
+ const disabled = snapshot.status !== "ready" || !snapshot.writable || saving;
62
+ const save = async () => {
63
+ if (disabled || !dirty || invalid) return;
64
+ setSaving(true);
65
+ 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
+ }
84
+ if (accepted) {
85
+ setHasDraft(false);
86
+ setResetToProfile(false);
87
+ setOpen(false);
88
+ } else setFailed(true);
89
+ };
90
+ const stageReset = () => {
91
+ if (disabled) return;
92
+ setCidrs(formatLines(layerValues(snapshot.base, "allowCidrs")));
93
+ setHostnames(formatLines(layerValues(snapshot.base, "allowHostnames")));
94
+ setResetToProfile(true);
95
+ setHasDraft(true);
96
+ setFailed(false);
97
+ };
98
+ const discard = () => {
99
+ setCidrs(resolvedCidrs);
100
+ setHostnames(resolvedHostnames);
101
+ setResetToProfile(false);
102
+ setHasDraft(false);
103
+ setFailed(false);
104
+ };
105
+ 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,
110
+ children: [
111
+ (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
+ },
121
+ 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
+ })]
131
+ }),
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"
139
+ })
140
+ ]
141
+ }),
142
+ open ? (0, react_jsx_runtime.jsxs)("div", {
143
+ className: "web-fetch-enhanced-body",
144
+ id: bodyId,
145
+ children: [
146
+ !snapshot.writable && snapshot.status === "ready" ? (0, react_jsx_runtime.jsx)("p", {
147
+ className: "web-fetch-enhanced-readonly",
148
+ role: "status",
149
+ children: t("readOnly")
150
+ }) : null,
151
+ (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",
185
+ children: [
186
+ (0, react_jsx_runtime.jsx)("label", {
187
+ className: "web-fetch-enhanced-label",
188
+ htmlFor: "web-fetch-enhanced-hostnames",
189
+ children: t("hostnames")
190
+ }),
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
+ }
208
+ }),
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")
213
+ })
214
+ ]
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
+ })
251
+ ]
252
+ }) : null
253
+ ]
254
+ });
255
+ }
256
+ //#endregion
257
+ //#region lib/types/client/locales.js
258
+ const en = {
259
+ title: "Enhanced web fetch allowlist",
260
+ description: "Allow selected non-public destinations for web_fetch. Public addresses remain available by default.",
261
+ expand: "Expand",
262
+ collapse: "Collapse",
263
+ unsaved: "Unsaved",
264
+ cidrs: "Allowed CIDRs",
265
+ cidrsHint: "One IPv4 or IPv6 CIDR per line. Keep the range as narrow as possible.",
266
+ cidrsPlaceholder: "198.18.0.0/15",
267
+ hostnames: "Allowed hostnames",
268
+ hostnamesHint: "Optional second factor. Use an exact hostname or a left-most *. wildcard; omit protocols, paths, and ports.",
269
+ hostnamesPlaceholder: "api.example.com\n*.docs.example.com",
270
+ save: "Save",
271
+ reset: "Reset to Profile",
272
+ discard: "Discard",
273
+ saving: "Saving…",
274
+ 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.",
277
+ readOnly: "This connection cannot persist Host Profile settings.",
278
+ duplicate: "Duplicate entries are not allowed.",
279
+ blank: "Blank entries are ignored."
280
+ };
281
+ const zh = {
282
+ title: "WebFetch白名单",
283
+ description: "允许 web_fetch 访问指定的非公网目标;公网地址默认仍可正常访问。",
284
+ expand: "展开",
285
+ collapse: "折叠",
286
+ unsaved: "未保存",
287
+ cidrs: "允许的 CIDR",
288
+ cidrsHint: "每行一个 IPv4 或 IPv6 CIDR;请尽量缩小放行范围。",
289
+ cidrsPlaceholder: "198.18.0.0/15",
290
+ hostnames: "允许的域名",
291
+ hostnamesHint: "可选的第二因子。填写精确域名或最左侧 *. 通配规则,不要包含协议、路径或端口。",
292
+ hostnamesPlaceholder: "api.example.com\n*.docs.example.com",
293
+ save: "保存",
294
+ reset: "重置为 Profile 配置",
295
+ discard: "放弃修改",
296
+ saving: "正在保存…",
297
+ saved: "已保存",
298
+ failed: "Host 拒绝了修改,请检查条目后重试。",
299
+ unavailable: "当前 Profile 未提供此设置命名空间。",
300
+ readOnly: "当前连接不能持久化 Host Profile 设置。",
301
+ duplicate: "不允许重复条目。",
302
+ blank: "空白行会被忽略。"
303
+ };
304
+ //#endregion
305
+ //#region lib/types/client/index.js
306
+ const SETTINGS_NAMESPACE = "web-fetch-enhanced";
307
+ const LOCALE_NAMESPACE = "settings.webFetchEnhanced";
308
+ const name = "web-fetch-enhanced-client";
309
+ const inject = [
310
+ "slots",
311
+ "locale",
312
+ "settingsScope"
313
+ ];
314
+ function apply(ctx) {
315
+ const t = ctx.locale.bind(LOCALE_NAMESPACE);
316
+ ctx.effect(() => ctx.locale.register(LOCALE_NAMESPACE, {
317
+ en,
318
+ zh
319
+ }), "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,
324
+ locale: LOCALE_NAMESPACE,
325
+ inject: () => ({
326
+ scope,
327
+ t
328
+ })
329
+ }, AllowlistCard));
330
+ }
331
+ //#endregion
332
+ exports.AllowlistCard = AllowlistCard;
333
+ exports.apply = apply;
334
+ exports.inject = inject;
335
+ exports.name = name;
336
+ exports.parseLines = parseLines;
337
+ return module.exports;
338
+ }
339
+ });
340
+
341
+ //# sourceMappingURL=client.js.map