dsh-permission-rules 0.6.15 → 0.6.17

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/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## v0.6.17 - 2026-09-10
2
+
3
+ ### Changed
4
+
5
+ - Pin the `@deepseek-ai/dsh-*` dev/test dependencies to the published `0.1.5-rc.1` line and record `0.1.5-rc.1` in `dshWorkshop.compatibility.dshVersions`; the monthly Compat workflow now runs against `0.1.5-rc.1`. The peer range `>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0` is unchanged, so no supported host line is dropped.
6
+ - Vendor integration tarball updated to `dsh-auto-review` 0.12.2 (`vendor/dsh-auto-review-0.12.2.tgz`, packed from the sibling repo at tag `v0.12.2`; the packed `files` are byte-identical to that tag's checkout), with the devDependency `file:` specifier and `pnpm-lock.yaml` refreshed. The sibling's runtime `@deepseek-ai/dsh-*` pins now follow the `0.1.5-rc.1` host line, matching this repo's dev peers. The six historical vendor tarballs (`0.9.0`–`0.12.1`) stay in the tree.
7
+
8
+ ### Docs
9
+
10
+ - Refresh the five-language README compatibility baseline to `dsh-v0.1.5-rc.1` (verified 2026-09-10).
11
+
12
+ ## v0.6.16 - 2026-09-09
13
+
14
+ ### Security
15
+
16
+ - **Fixed: an IPv4-mapped IPv6 literal bypassed `ips` rules.** `isIpLiteral()` classifies any host containing `:` as an IPv6 literal, while `ipMatches()` compared literal patterns by exact string equality and CIDR patterns with an IPv4 dotted-quad regex. A target spelled `::ffff:169.254.169.254` (or the hex form `::ffff:a9fe:a9fe`, or the full form `0:0:0:0:0:ffff:a9fe:a9fe`) therefore matched neither an IPv4 literal nor an IPv4 CIDR rule, even though Node routes the connection to that IPv4 destination — a sandboxed caller could reach an address the rules intended to block (for example a cloud metadata endpoint). Both sides now normalize through `unmapIpv4`: candidates and patterns are mapped back to their dotted IPv4 form before matching, a mapped pattern also covers the plain IPv4 spelling, and mapped globs keep their IPv4 remainder (`::ffff:10.0.*.*` → `10.0.*.*`). Reported by the maintainer during a network-rule audit; the shipped `docs/rules-format.md` / `.en.md` `ips` row documents the normalization.
17
+
18
+ ### Tests
19
+
20
+ - `test/network.spec.ts` gains a regression case: mapped dotted / mapped hex / full-form mapped literals against both literal and CIDR `ips` rules, the normalized `parseUrlTarget` candidate, the mapped-pattern direction, and an unrelated-IPv6 negative.
21
+
1
22
  ## v0.6.15 - 2026-09-09
2
23
 
3
24
  ### Changed
package/README.es.md CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  | Surface | Status |
28
28
  |---|---|
29
- | Harness | DeepSeek Harness `dsh-v0.1.5-alpha.1` (adaptado el 2026-09-09, cadena completa de gates + smoke de instalación de profile): su `Session.append` todavía no puede estampar el marcador `ignorable` —verificado en el paquete publicado `0.1.5-alpha.1`, donde el tercer argumento se descarta silenciosamente y el campo del sobre solo sobrevive para la lectura de logs almacenados—, así que toda la línea `0.1.5-alpha` se preverifica como no marcada y la auditoría del registro de sesión queda desactivada por defecto. La línea `0.1.3-alpha` mantiene la misma firma de append surface-only. Las migraciones de log de ambas líneas rechazan eventos de plugin sin clasificar incluso marcados: aplica `strip` a las filas de auditoría v1 antes de que un host `0.1.3-alpha` abra el log y a las v2 antes de que un host `0.1.5-alpha` lo migre (los logs v3 nativos solo necesitan `repair`). |
29
+ | Harness | DeepSeek Harness `dsh-v0.1.5-rc.1` (adaptado el 2026-09-09, cadena completa de gates + smoke de instalación de profile): su `Session.append` todavía no puede estampar el marcador `ignorable` —verificado en el paquete publicado `0.1.5-rc.1`, donde el tercer argumento se descarta silenciosamente y el campo del sobre solo sobrevive para la lectura de logs almacenados—, así que toda la línea `0.1.5-alpha` se preverifica como no marcada y la auditoría del registro de sesión queda desactivada por defecto. La línea `0.1.3-alpha` mantiene la misma firma de append surface-only. Las migraciones de log de ambas líneas rechazan eventos de plugin sin clasificar incluso marcados: aplica `strip` a las filas de auditoría v1 antes de que un host `0.1.3-alpha` abra el log y a las v2 antes de que un host `0.1.5-alpha` lo migre (los logs v3 nativos solo necesitan `repair`). |
30
30
  | Node | `^22.19.0 || >=24.0.0` |
31
31
  | Platforms | Todas (host + cliente web de settings) |
32
32
  | Model | Cualquiera (las razones deny/ask se muestran a través de los resultados de herramienta) |
package/README.hi.md CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  | Surface | Status |
28
28
  |---|---|
29
- | Harness | DeepSeek Harness `dsh-v0.1.5-alpha.1` (2026-09-09 को अनुकूलित, पूर्ण gates शृंखला + profile इंस्टॉल smoke): इसका `Session.append` अभी भी `ignorable` मार्कर स्टैम्प नहीं कर सकता — प्रकाशित `0.1.5-alpha.1` पैकेज पर सत्यापित, जहाँ तीसरा आर्ग्युमेंट चुपचाप छोड़ दिया जाता है और लिफ़ाफ़ा फ़ील्ड केवल संग्रहीत-लॉग पठन के लिए बचता है — इसलिए पूरी `0.1.5-alpha` पंक्ति पहले append से पहले ही अनमार्क्ड के रूप में पूर्व-जाँची जाती है और सत्र-लॉग ऑडिट डिफ़ॉल्ट रूप से बंद रहता है। `0.1.3-alpha` पंक्ति वही surface-only append हस्ताक्षर रखती है। दोनों पंक्तियों का लॉग प्रवासन चिह्नित अन-क्लासिफ़ाइड प्लगइन घटनाओं को भी अस्वीकारता है: `0.1.3-alpha` host के लॉग खोलने से पहले v1 ऑडिट पंक्तियाँ और `0.1.5-alpha` host के प्रवासन से पहले v2 पंक्तियाँ `strip` करें (v3 मूल लॉग के लिए केवल `repair` चाहिए)। |
29
+ | Harness | DeepSeek Harness `dsh-v0.1.5-rc.1` (2026-09-09 को अनुकूलित, पूर्ण gates शृंखला + profile इंस्टॉल smoke): इसका `Session.append` अभी भी `ignorable` मार्कर स्टैम्प नहीं कर सकता — प्रकाशित `0.1.5-rc.1` पैकेज पर सत्यापित, जहाँ तीसरा आर्ग्युमेंट चुपचाप छोड़ दिया जाता है और लिफ़ाफ़ा फ़ील्ड केवल संग्रहीत-लॉग पठन के लिए बचता है — इसलिए पूरी `0.1.5-alpha` पंक्ति पहले append से पहले ही अनमार्क्ड के रूप में पूर्व-जाँची जाती है और सत्र-लॉग ऑडिट डिफ़ॉल्ट रूप से बंद रहता है। `0.1.3-alpha` पंक्ति वही surface-only append हस्ताक्षर रखती है। दोनों पंक्तियों का लॉग प्रवासन चिह्नित अन-क्लासिफ़ाइड प्लगइन घटनाओं को भी अस्वीकारता है: `0.1.3-alpha` host के लॉग खोलने से पहले v1 ऑडिट पंक्तियाँ और `0.1.5-alpha` host के प्रवासन से पहले v2 पंक्तियाँ `strip` करें (v3 मूल लॉग के लिए केवल `repair` चाहिए)। |
30
30
  | Node | `^22.19.0 || >=24.0.0` |
31
31
  | Platforms | सभी (host + वेब settings क्लाइंट) |
32
32
  | Model | कोई भी (deny/ask कारण टूल परिणामों के माध्यम से दिखते हैं) |
package/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  | Surface | Status |
29
29
  |---|---|
30
- | Harness | DeepSeek Harness `dsh-v0.1.5-alpha.1` (adapted 2026-09-09, full gate chain + profile install smoke): its `Session.append` still cannot stamp the `ignorable` marker — verified on the published `0.1.5-alpha.1` package, where the third argument is silently dropped and the envelope field survives for stored-log reads only — so the whole `0.1.5-alpha` line is pre-checked as unmarked and session-log audit stays disabled by default. The `0.1.3-alpha` line keeps the same surface-only append signature. Both lines' log migrations refuse unclassified plugin events even when marked, so `strip` v1 audit rows before a `0.1.3-alpha` host opens the log and v2 audit rows before a `0.1.5-alpha` host migrates it (native v3 logs only need `repair`). |
30
+ | Harness | DeepSeek Harness `dsh-v0.1.5-rc.1` (adapted 2026-09-09, full gate chain + profile install smoke): its `Session.append` still cannot stamp the `ignorable` marker — verified on the published `0.1.5-rc.1` package, where the third argument is silently dropped and the envelope field survives for stored-log reads only — so the whole `0.1.5-alpha` line is pre-checked as unmarked and session-log audit stays disabled by default. The `0.1.3-alpha` line keeps the same surface-only append signature. Both lines' log migrations refuse unclassified plugin events even when marked, so `strip` v1 audit rows before a `0.1.3-alpha` host opens the log and v2 audit rows before a `0.1.5-alpha` host migrates it (native v3 logs only need `repair`). |
31
31
  | Node | `^22.19.0 || >=24.0.0` |
32
32
  | Platforms | All (host + web settings client) |
33
33
  | Model | Any (deny/ask reasons surface through tool results) |
package/README.pt.md CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  | Surface | Status |
28
28
  |---|---|
29
- | Harness | DeepSeek Harness `dsh-v0.1.5-alpha.1` (adaptado em 2026-09-09, cadeia completa de gates + smoke de instalação de profile): o `Session.append` ainda não consegue estampar o marcador `ignorable` — verificado no pacote publicado `0.1.5-alpha.1`, onde o terceiro argumento é descartado silenciosamente e o campo do envelope só sobrevive para a leitura de logs armazenados —, então toda a linha `0.1.5-alpha` é pré-verificada como não marcada e a auditoria do log de sessão fica desativada por padrão. A linha `0.1.3-alpha` mantém a mesma assinatura de append surface-only. As migrações de log de ambas as linhas recusam eventos de plugin não classificados mesmo marcados: aplique `strip` nas linhas de auditoria v1 antes de um host `0.1.3-alpha` abrir o log e nas v2 antes de um host `0.1.5-alpha` migrá-lo (logs v3 nativos só precisam de `repair`). |
29
+ | Harness | DeepSeek Harness `dsh-v0.1.5-rc.1` (adaptado em 2026-09-09, cadeia completa de gates + smoke de instalação de profile): o `Session.append` ainda não consegue estampar o marcador `ignorable` — verificado no pacote publicado `0.1.5-rc.1`, onde o terceiro argumento é descartado silenciosamente e o campo do envelope só sobrevive para a leitura de logs armazenados —, então toda a linha `0.1.5-alpha` é pré-verificada como não marcada e a auditoria do log de sessão fica desativada por padrão. A linha `0.1.3-alpha` mantém a mesma assinatura de append surface-only. As migrações de log de ambas as linhas recusam eventos de plugin não classificados mesmo marcados: aplique `strip` nas linhas de auditoria v1 antes de um host `0.1.3-alpha` abrir o log e nas v2 antes de um host `0.1.5-alpha` migrá-lo (logs v3 nativos só precisam de `repair`). |
30
30
  | Node | `^22.19.0 || >=24.0.0` |
31
31
  | Platforms | Todas (host + cliente web de settings) |
32
32
  | Model | Qualquer (razões deny/ask aparecem pelos resultados de ferramenta) |
package/README.zh.md CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  | Surface | Status |
28
28
  |---|---|
29
- | Harness | DeepSeek Harness `dsh-v0.1.5-alpha.1`(2026-09-09 已适配,完整门禁链 + profile 安装冒烟):其 `Session.append` 仍无法盖章 `ignorable` 标记——已在已发布的 `0.1.5-alpha.1` 包上实测,第三参数被静默丢弃,信封字段仅用于存量日志读取——故整条 `0.1.5-alpha` 线在首次追加前即被预检为未盖章,会话日志审计默认停用。`0.1.3-alpha` 线同样沿用 surface-only 追加签名。两条线的日志迁移连已标记的未分类插件事件也拒绝:在 `0.1.3-alpha` 宿主打开日志前 `strip` 掉 v1 审计行,在 `0.1.5-alpha` 宿主迁移前 `strip` 掉 v2 审计行(v3 原生日志只需 `repair`)。 |
29
+ | Harness | DeepSeek Harness `dsh-v0.1.5-rc.1`(2026-09-09 已适配,完整门禁链 + profile 安装冒烟):其 `Session.append` 仍无法盖章 `ignorable` 标记——已在已发布的 `0.1.5-rc.1` 包上实测,第三参数被静默丢弃,信封字段仅用于存量日志读取——故整条 `0.1.5-alpha` 线在首次追加前即被预检为未盖章,会话日志审计默认停用。`0.1.3-alpha` 线同样沿用 surface-only 追加签名。两条线的日志迁移连已标记的未分类插件事件也拒绝:在 `0.1.3-alpha` 宿主打开日志前 `strip` 掉 v1 审计行,在 `0.1.5-alpha` 宿主迁移前 `strip` 掉 v2 审计行(v3 原生日志只需 `repair`)。 |
30
30
  | Node | `^22.19.0 || >=24.0.0` |
31
31
  | Platforms | 全部(host + Web 设置客户端) |
32
32
  | Model | 任意(deny/ask 原因经工具结果呈现) |
@@ -41,7 +41,7 @@ A rule whose `match` carries a `network` block is a **network rule**. It matches
41
41
  | Field | Semantics |
42
42
  |---|---|
43
43
  | `domains` | Domain patterns, lowercased with a trailing dot stripped. A pattern without wildcards is subdomain-inclusive — `example.com` also matches `api.example.com`; `*.example.com` matches subdomains only; `*`/`**` compile as ordinary globs. ANY pattern matching the target host satisfies the dimension. |
44
- | `ips` | Exact IPv4/IPv6 literal, a glob (`10.0.*.*`), or an IPv4 CIDR (`10.0.0.0/8`). A literal IP in the URL is always a candidate; the proxy additionally resolves hostnames and tests their addresses, while `tools/pre-execute` matches literal IPs only (no DNS in the hot path). ANY match satisfies the dimension. |
44
+ | `ips` | Exact IPv4/IPv6 literal, a glob (`10.0.*.*`), or an IPv4 CIDR (`10.0.0.0/8`). A literal IP in the URL is always a candidate; the proxy additionally resolves hostnames and tests their addresses, then **connects on the adjudicated address** (no second resolution, so DNS rebinding cannot move the connection to an address the rules never approved; a multi-address host falls back in adjudicated order), while `tools/pre-execute` matches literal IPs only (no DNS in the hot path). An IPv4-mapped IPv6 literal (`::ffff:169.254.169.254`, `::ffff:a9fe:a9fe`, `0:0:0:0:0:ffff:a9fe:a9fe`) normalizes to its dotted IPv4 form for matching, and a rule written in the mapped form also covers the plain IPv4 spelling. ANY match satisfies the dimension. |
45
45
  | `ports` | `*`, one port (`443`), or an inclusive range (`8000-9000`); numeric YAML ports are accepted. Evaluated against the effective port (URL port, else 80/443 by scheme). ANY match satisfies the dimension. |
46
46
  | `schemes` | `http` and/or `https`; the target scheme must be one of them. |
47
47
 
@@ -41,7 +41,7 @@ rules: # 规则列表,按书写顺序求值;可以省略或为空
41
41
  | 字段 | 语义 |
42
42
  |---|---|
43
43
  | `domains` | 域名模式,小写化并去掉末尾点。无通配符的模式包含子域——`example.com` 同时命中 `api.example.com`;`*.example.com` 只匹配子域;`*`/`**` 按普通 glob 编译。任一模式命中目标主机即满足该维度。 |
44
- | `ips` | 精确 IPv4/IPv6 字面量、glob(`10.0.*.*`)或 IPv4 CIDR(`10.0.0.0/8`)。URL 中的字面量 IP 始终是候选;代理额外解析主机名并测试其地址,而 `tools/pre-execute` 只匹配字面量 IP(热路径不做 DNS)。任一命中即满足。 |
44
+ | `ips` | 精确 IPv4/IPv6 字面量、glob(`10.0.*.*`)或 IPv4 CIDR(`10.0.0.0/8`)。URL 中的字面量 IP 始终是候选;代理额外解析主机名并测试其地址,随后**按裁决到的地址建连**(不做二次解析,DNS rebinding 无法把连接改到未获批的地址;多地址主机按裁决顺序回退),而 `tools/pre-execute` 只匹配字面量 IP(热路径不做 DNS)。IPv4 映射的 IPv6 字面量(`::ffff:169.254.169.254`、`::ffff:a9fe:a9fe`、`0:0:0:0:0:ffff:a9fe:a9fe`)归一化为其 IPv4 点分形式参与匹配,规则写成映射形式时同样覆盖普通 IPv4 写法。任一命中即满足。 |
45
45
  | `ports` | `*`、单端口(`443`)或闭区间(`8000-9000`);接受数值型 YAML 端口。按有效端口求值(URL 端口,否则按 scheme 取 80/443)。任一命中即满足。 |
46
46
  | `schemes` | `http` 和/或 `https`;目标 scheme 必须是其中之一。 |
47
47
 
@@ -149,7 +149,7 @@
149
149
  "items": { "type": "string", "minLength": 1 }
150
150
  },
151
151
  "ips": {
152
- "description": "IP literals, globs, or IPv4 CIDRs; ANY matching the target address satisfies the dimension.",
152
+ "description": "IP literals, globs, or IPv4 CIDRs; an IPv4-mapped IPv6 literal (::ffff:169.254.169.254, ::ffff:a9fe:a9fe) is unmapped to its dotted IPv4 form before matching, and a mapped pattern also covers the plain IPv4 spelling; ANY matching the target address satisfies the dimension.",
153
153
  "type": "array",
154
154
  "items": { "type": "string", "minLength": 1 }
155
155
  },
package/lib/index.js CHANGED
@@ -1210,9 +1210,30 @@ function normalizeHost(host) {
1210
1210
  while (out.endsWith(".")) out = out.slice(0, -1);
1211
1211
  return out;
1212
1212
  }
1213
- /** The literal IP of a host, or `[]` when the host is a name. */
1213
+ /**
1214
+ * Map an IPv4-mapped IPv6 literal (dotted or hex form) back to its dotted IPv4
1215
+ * quad. Node routes `::ffff:169.254.169.254` and `::ffff:a9fe:a9fe` to the same
1216
+ * IPv4 destination as `169.254.169.254`, so both spellings must compare as that
1217
+ * address: without this normalization an `ips` rule written in IPv4 terms never
1218
+ * matches the mapped spelling and the deny is bypassed.
1219
+ * @param host - lowercased, unbracketed host or pattern.
1220
+ * @returns the IPv4 dotted quad when the input is IPv4-mapped, else the input.
1221
+ */
1222
+ function unmapIpv4(host) {
1223
+ const lower = host.toLowerCase();
1224
+ const dotted = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower);
1225
+ if (dotted !== null && dotted[1] !== void 0) return dotted[1];
1226
+ const hex = /^(?:(?:0{0,4}:){5}|::)ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(lower);
1227
+ if (hex !== null && hex[1] !== void 0 && hex[2] !== void 0) {
1228
+ const high = Number.parseInt(hex[1], 16);
1229
+ const low = Number.parseInt(hex[2], 16);
1230
+ return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
1231
+ }
1232
+ return lower;
1233
+ }
1234
+ /** The literal IP of a host, or `[]` when the host is a name. Mapped literals normalize to IPv4. */
1214
1235
  function literalIpOf(host) {
1215
- return isIpLiteral(host) ? [host.toLowerCase()] : [];
1236
+ return isIpLiteral(host) ? [unmapIpv4(host.toLowerCase())] : [];
1216
1237
  }
1217
1238
  /** Whether a host string is an IP literal (IPv4 or IPv6). */
1218
1239
  function isIpLiteral(host) {
@@ -1260,9 +1281,25 @@ function compilePortPattern(pattern) {
1260
1281
  max: Number(range?.[2] ?? 0)
1261
1282
  };
1262
1283
  }
1284
+ /**
1285
+ * Normalize an IP pattern's spelling: a mapped literal becomes its IPv4 quad,
1286
+ * and a mapped glob keeps its IPv4 remainder (`::ffff:10.0.*.*` → `10.0.*.*`),
1287
+ * so IPv4 patterns cover mapped spellings too.
1288
+ * @param pattern - the raw pattern.
1289
+ * @returns the normalized, lowercased pattern.
1290
+ */
1291
+ function unmapIpv4Pattern(pattern) {
1292
+ const lower = pattern.toLowerCase();
1293
+ const mapped = unmapIpv4(lower);
1294
+ if (mapped !== lower) return mapped;
1295
+ const dottedPrefix = /^::ffff:(.+)$/.exec(lower);
1296
+ if (dottedPrefix !== null && dottedPrefix[1] !== void 0) return dottedPrefix[1];
1297
+ return lower;
1298
+ }
1263
1299
  /** Compile one IP pattern: CIDR first, then glob; exact literals glob-compile to themselves. */
1264
1300
  function compileIpPattern(pattern, maxGlobStars) {
1265
- const cidr = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(pattern);
1301
+ const normalized = unmapIpv4Pattern(pattern);
1302
+ const cidr = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(normalized);
1266
1303
  if (cidr !== null) {
1267
1304
  const octets = cidr.slice(1, 5).map(Number);
1268
1305
  const prefix = Number(cidr[5]);
@@ -1271,8 +1308,8 @@ function compileIpPattern(pattern, maxGlobStars) {
1271
1308
  prefix
1272
1309
  } };
1273
1310
  }
1274
- if (!/[[*?]/.test(pattern) && !pattern.includes(":")) return { literal: pattern.toLowerCase() };
1275
- return { regex: compileGlob(pattern, {
1311
+ if (!/[[*?]/.test(normalized) && !normalized.includes(":")) return { literal: normalized };
1312
+ return { regex: compileGlob(normalized, {
1276
1313
  segments: false,
1277
1314
  maxStars: maxGlobStars,
1278
1315
  caseInsensitive: true
@@ -1345,9 +1382,9 @@ function targetMatchesNetwork(target, network) {
1345
1382
  }
1346
1383
  return true;
1347
1384
  }
1348
- /** Whether one compiled IP pattern matches one candidate address (lowercased, unbracketed). */
1385
+ /** Whether one compiled IP pattern matches one candidate address (lowercased, unbracketed, mapped IPv4 unmapped). */
1349
1386
  function ipMatches(pattern, candidate) {
1350
- const normalized = normalizeHost(candidate);
1387
+ const normalized = unmapIpv4(normalizeHost(candidate));
1351
1388
  if (pattern.cidr !== void 0) {
1352
1389
  const parts = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(normalized);
1353
1390
  if (parts === null) return false;
@@ -2118,23 +2155,59 @@ var NetworkProxy = class {
2118
2155
  res.end("permission-rules proxy: only absolute-form proxy requests are served here\n");
2119
2156
  return;
2120
2157
  }
2121
- await this.forwardOrBlock(res, target, () => {
2122
- const upstream = new URL(req.url);
2123
- const proxyReq = (upstream.protocol === "https:" ? request$1 : request)(upstream, {
2124
- method: req.method,
2125
- headers: req.headers
2126
- }, (proxyRes) => {
2127
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
2128
- proxyRes.pipe(res);
2129
- });
2130
- proxyReq.on("error", (error) => {
2131
- if (!res.headersSent) {
2132
- res.writeHead(502, { "content-type": "text/plain" });
2133
- res.end(`[network: upstream error] ${String(error)}\n`);
2134
- } else res.destroy();
2158
+ const { decision, target: adjudicated } = await this.decideWithResolution(target);
2159
+ if (decision.action !== "allow") {
2160
+ this.recordBlock(decision, target);
2161
+ const body = blockMessage(decision);
2162
+ res.writeHead(403, {
2163
+ "content-type": "text/plain; charset=utf-8",
2164
+ "content-length": Buffer.byteLength(body)
2135
2165
  });
2136
- req.pipe(proxyReq);
2166
+ res.end(body);
2167
+ return;
2168
+ }
2169
+ const upstream = new URL(req.url);
2170
+ const send = upstream.protocol === "https:" ? request$1 : request;
2171
+ const pinned = pinnedAddresses(adjudicated);
2172
+ const proxyReq = send(upstream, {
2173
+ method: req.method,
2174
+ headers: req.headers,
2175
+ ...pinned.length === 0 ? {} : { lookup: pinnedLookup(pinned) }
2176
+ }, (proxyRes) => {
2177
+ res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
2178
+ proxyRes.pipe(res);
2137
2179
  });
2180
+ proxyReq.on("error", (error) => {
2181
+ if (!res.headersSent) {
2182
+ res.writeHead(502, { "content-type": "text/plain" });
2183
+ res.end(`[network: upstream error] ${String(error)}\n`);
2184
+ } else res.destroy();
2185
+ });
2186
+ req.pipe(proxyReq);
2187
+ }
2188
+ /**
2189
+ * Connect to the first adjudicated address that accepts, so the tunnel lands
2190
+ * on an address the decision approved; a hostname target with no resolution
2191
+ * keeps the historical hostname connect. Each candidate is tried in order so
2192
+ * a multi-address host does not lose its fallback.
2193
+ */
2194
+ async connectUpstream(target, port) {
2195
+ const pinned = pinnedAddresses(target);
2196
+ if (pinned.length === 0) return connect(port, target.host);
2197
+ let lastError;
2198
+ for (const address of pinned) try {
2199
+ return await new Promise((resolve, reject) => {
2200
+ const socket = connect(port, address);
2201
+ socket.once("connect", () => resolve(socket));
2202
+ socket.once("error", (error) => {
2203
+ socket.destroy();
2204
+ reject(error instanceof Error ? error : new Error(String(error)));
2205
+ });
2206
+ });
2207
+ } catch (error) {
2208
+ lastError = error;
2209
+ }
2210
+ throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("no adjudicated address accepted the connection");
2138
2211
  }
2139
2212
  /** CONNECT tunneling (HTTPS and friends): adjudicate, then either 403 or an established TCP tunnel. */
2140
2213
  async handleConnect(req, socket, head) {
@@ -2149,14 +2222,21 @@ var NetworkProxy = class {
2149
2222
  socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
2150
2223
  return;
2151
2224
  }
2152
- const decision = await this.decideWithResolution(target);
2225
+ const { decision, target: adjudicated } = await this.decideWithResolution(target);
2153
2226
  if (decision.action !== "allow") {
2154
2227
  this.recordBlock(decision, target);
2155
2228
  const body = blockMessage(decision);
2156
2229
  socket.end(`HTTP/1.1 403 Forbidden\r\ncontent-type: text/plain\r\ncontent-length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
2157
2230
  return;
2158
2231
  }
2159
- const tunnel = connect(target.port ?? 443, target.host);
2232
+ let tunnel;
2233
+ try {
2234
+ tunnel = await this.connectUpstream(adjudicated, target.port ?? 443);
2235
+ } catch (error) {
2236
+ this.options.logger.warn(`permission-rules: upstream connect failed for ${target.host}: ${String(error)}`);
2237
+ socket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
2238
+ return;
2239
+ }
2160
2240
  tunnelHolder.tunnel = tunnel;
2161
2241
  this.sockets.add(socket);
2162
2242
  this.sockets.add(tunnel);
@@ -2167,27 +2247,10 @@ var NetworkProxy = class {
2167
2247
  socket.on("close", cleanup);
2168
2248
  tunnel.on("close", cleanup);
2169
2249
  tunnel.on("error", () => socket.destroy());
2170
- tunnel.once("connect", () => {
2171
- socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
2172
- if (head.length > 0) tunnel.write(head);
2173
- tunnel.pipe(socket);
2174
- socket.pipe(tunnel);
2175
- });
2176
- }
2177
- /** Adjudicate one plain-HTTP request; deny/ask blocks with a structured 403 before any forwarding. */
2178
- async forwardOrBlock(res, target, forward) {
2179
- const decision = await this.decideWithResolution(target);
2180
- if (decision.action !== "allow") {
2181
- this.recordBlock(decision, target);
2182
- const body = blockMessage(decision);
2183
- res.writeHead(403, {
2184
- "content-type": "text/plain; charset=utf-8",
2185
- "content-length": Buffer.byteLength(body)
2186
- });
2187
- res.end(body);
2188
- return;
2189
- }
2190
- forward();
2250
+ socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
2251
+ if (head.length > 0) tunnel.write(head);
2252
+ tunnel.pipe(socket);
2253
+ socket.pipe(tunnel);
2191
2254
  }
2192
2255
  /** Resolve a hostname once so `ips`-scoped rules see the real addresses, then decide. */
2193
2256
  async decideWithResolution(target) {
@@ -2196,12 +2259,21 @@ var NetworkProxy = class {
2196
2259
  all: true,
2197
2260
  verbatim: true
2198
2261
  })).map((entry) => entry.address);
2199
- if (resolved.length > 0) return this.options.decide({
2200
- ...target,
2201
- ips: [...target.ips, ...resolved]
2202
- });
2262
+ if (resolved.length > 0) {
2263
+ const effective = {
2264
+ ...target,
2265
+ ips: [...target.ips, ...resolved]
2266
+ };
2267
+ return {
2268
+ decision: await this.options.decide(effective),
2269
+ target: effective
2270
+ };
2271
+ }
2203
2272
  } catch {}
2204
- return this.options.decide(target);
2273
+ return {
2274
+ decision: await this.options.decide(target),
2275
+ target
2276
+ };
2205
2277
  }
2206
2278
  /** Record a block: counters, recent ring, and the runtime hook (logger + session audit). */
2207
2279
  recordBlock(decision, target) {
@@ -2248,6 +2320,33 @@ function connectTarget(authority) {
2248
2320
  };
2249
2321
  }
2250
2322
  /**
2323
+ * The literal addresses a connection may use — everything the decision was
2324
+ * made on. Returning them keeps the connection on an adjudicated address
2325
+ * instead of letting a second DNS lookup pick a different one.
2326
+ * @param target - the adjudicated target.
2327
+ * @returns the adjudicated literal addresses, in decision order.
2328
+ */
2329
+ function pinnedAddresses(target) {
2330
+ return target.ips.filter((candidate) => isIpLiteral(candidate));
2331
+ }
2332
+ /**
2333
+ * A `lookup` that answers from the adjudicated address list without a second
2334
+ * DNS query. Node's happy-eyeballs path asks with `all: true` and tries the
2335
+ * returned addresses in order, so a multi-address host keeps its fallbacks.
2336
+ * @param addresses - the adjudicated literal addresses.
2337
+ * @returns the lookup function handed to `http(s).request`.
2338
+ */
2339
+ function pinnedLookup(addresses) {
2340
+ const resolved = addresses.map((address) => ({
2341
+ address,
2342
+ family: address.includes(":") ? 6 : 4
2343
+ }));
2344
+ return (_hostname, options, callback) => {
2345
+ if (options.all === true) callback(null, resolved);
2346
+ else callback(null, resolved[0].address, resolved[0].family);
2347
+ };
2348
+ }
2349
+ /**
2251
2350
  * Inject the proxy environment variables for subprocesses and return a
2252
2351
  * disposer restoring every previous value exactly (so updates and
2253
2352
  * uninstalls leave the host environment untouched). `noProxy: 'clear'`
@@ -99,10 +99,15 @@ export declare class NetworkProxy {
99
99
  close(): Promise<void>;
100
100
  /** Plain-HTTP proxying: absolute-form requests are adjudicated and forwarded (or blocked with a structured 403). */
101
101
  private handleRequest;
102
+ /**
103
+ * Connect to the first adjudicated address that accepts, so the tunnel lands
104
+ * on an address the decision approved; a hostname target with no resolution
105
+ * keeps the historical hostname connect. Each candidate is tried in order so
106
+ * a multi-address host does not lose its fallback.
107
+ */
108
+ private connectUpstream;
102
109
  /** CONNECT tunneling (HTTPS and friends): adjudicate, then either 403 or an established TCP tunnel. */
103
110
  private handleConnect;
104
- /** Adjudicate one plain-HTTP request; deny/ask blocks with a structured 403 before any forwarding. */
105
- private forwardOrBlock;
106
111
  /** Resolve a hostname once so `ips`-scoped rules see the real addresses, then decide. */
107
112
  private decideWithResolution;
108
113
  /** Record a block: counters, recent ring, and the runtime hook (logger + session audit). */
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/rules.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/rules.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-permission-rules",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
4
4
  "description": "Declarative Claude Code-style permission rules plus a Codex-style process-level network policy for DeepSeek Harness: ordered allow/deny/ask rules with tool-name, argument (glob/regex), workspace-path, and network-target (domain/ip/port/scheme) matching on the tools/pre-execute waterfall, a built-in local HTTP/CONNECT proxy governing shell subprocess traffic, three network modes mapped onto the official sandbox presets, full session-log audit, HMR rule reload, and a settings page with a rule editor, block counters, and recent interception lists.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -76,7 +76,7 @@
76
76
  "network:outbound"
77
77
  ],
78
78
  "compatibility": {
79
- "dshVersions": ["0.1.2-rc.1","0.1.5-alpha.1"]
79
+ "dshVersions": ["0.1.2-rc.1","0.1.5-rc.1"]
80
80
  },
81
81
  "capability": {
82
82
  "id": "rules",
@@ -154,31 +154,31 @@
154
154
  "zod": "^4.4.3"
155
155
  },
156
156
  "devDependencies": {
157
- "@deepseek-ai/dsh-attachment": "0.1.5-alpha.1",
157
+ "@deepseek-ai/dsh-attachment": "0.1.5-rc.1",
158
158
  "@deepseek-ai/cordis": "^4.0.2",
159
159
  "@deepseek-ai/cordis-plugin-include": "^1.0.7",
160
160
  "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
161
- "@deepseek-ai/dsh-agent": "0.1.5-alpha.1",
162
- "@deepseek-ai/dsh-brand": "0.1.5-alpha.1",
163
- "@deepseek-ai/dsh-client-connection": "0.1.5-alpha.1",
164
- "@deepseek-ai/dsh-client-locale": "0.1.5-alpha.1",
165
- "@deepseek-ai/dsh-client-ui-settings": "0.1.5-alpha.1",
166
- "@deepseek-ai/dsh-client-ui-slots": "0.1.5-alpha.1",
167
- "@deepseek-ai/dsh-client-web": "0.1.5-alpha.1",
168
- "@deepseek-ai/dsh-commands": "0.1.5-alpha.1",
169
- "@deepseek-ai/dsh-invariants": "0.1.5-alpha.1",
170
- "@deepseek-ai/dsh-llm": "0.1.5-alpha.1",
171
- "@deepseek-ai/dsh-session": "0.1.5-alpha.1",
172
- "@deepseek-ai/dsh-session-projection": "0.1.5-alpha.1",
173
- "@deepseek-ai/dsh-subagent": "0.1.5-alpha.1",
174
- "@deepseek-ai/dsh-tools": "0.1.5-alpha.1",
175
- "@deepseek-ai/dsh-typert-protocol": "0.1.5-alpha.1",
176
- "@deepseek-ai/dsh-user-approval": "0.1.5-alpha.1",
161
+ "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
162
+ "@deepseek-ai/dsh-brand": "0.1.5-rc.1",
163
+ "@deepseek-ai/dsh-client-connection": "0.1.5-rc.1",
164
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.1",
165
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.1",
166
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
167
+ "@deepseek-ai/dsh-client-web": "0.1.5-rc.1",
168
+ "@deepseek-ai/dsh-commands": "0.1.5-rc.1",
169
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.1",
170
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
171
+ "@deepseek-ai/dsh-session": "0.1.5-rc.1",
172
+ "@deepseek-ai/dsh-session-projection": "0.1.5-rc.1",
173
+ "@deepseek-ai/dsh-subagent": "0.1.5-rc.1",
174
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
175
+ "@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.1",
176
+ "@deepseek-ai/dsh-user-approval": "0.1.5-rc.1",
177
177
  "@deepseek-ai/schemastery": "^3.18.2",
178
178
  "@eslint/js": "^10.0.1",
179
179
  "@types/node": "^22.19.0",
180
180
  "@vitest/coverage-v8": "^3.2.0",
181
- "dsh-auto-review": "file:vendor/dsh-auto-review-0.12.1.tgz",
181
+ "dsh-auto-review": "file:vendor/dsh-auto-review-0.12.2.tgz",
182
182
  "eslint": "^10.8.1",
183
183
  "globals": "^17.11.0",
184
184
  "react": "18.3.1",
@@ -82,13 +82,14 @@ const STORAGE_ROW_TYPES = new Set([
82
82
  /**
83
83
  * The harness build's generated event vocabulary
84
84
  * (`KNOWN_SESSION_EVENT_TYPES`, `packages/core/session/src/known-event-types.ts`,
85
- * synced with the `0.1.5-alpha.1` catalog), plus the v1/v2-only spellings the
85
+ * synced with the `0.1.5-rc.1` catalog), plus the v1/v2-only spellings the
86
86
  * older generations wrote (`assistant/chunk`, `tool/code-dispatch`,
87
87
  * `tool/code-dispatch-start`; V3 renamed the latter two to `tool/ptc-dispatch`
88
88
  * and `tool/ptc-dispatch-start`), so every generation scans clean. Rows whose
89
89
  * type is outside this set are what the read path refuses when the envelope
90
90
  * lacks `ignorable: true`; the scan reports them so nothing is missed.
91
- * Regenerate this list from the harness checkout when it changes.
91
+ * Regenerate this list from the harness checkout when it changes (0.1.5-rc.1
92
+ * added `deliverables/presented` and `subagent/catalog`).
92
93
  */
93
94
  const KNOWN_SESSION_EVENT_TYPES = new Set([
94
95
  'agent-preset/selected',
@@ -107,6 +108,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
107
108
  'compaction/prune',
108
109
  'compaction/start',
109
110
  'compaction/summary',
111
+ 'deliverables/presented',
110
112
  'feedback/message-delete',
111
113
  'feedback/message-put',
112
114
  'feedback/record',
@@ -128,6 +130,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
128
130
  'session/title-llm-request',
129
131
  'step/end',
130
132
  'step/start',
133
+ 'subagent/catalog',
131
134
  'subagent/descriptor',
132
135
  'subagent/model-selection-policy',
133
136
  'system/message',
package/src/proxy.ts CHANGED
@@ -21,7 +21,7 @@ import { connect } from 'node:net'
21
21
  import { lookup } from 'node:dns/promises'
22
22
  import type { AddressInfo } from 'node:net'
23
23
  import type { Duplex } from 'node:stream'
24
- import type { Server, IncomingMessage, ServerResponse } from 'node:http'
24
+ import type { Server, IncomingMessage, RequestOptions, ServerResponse } from 'node:http'
25
25
  import type { CallId } from './call-id.ts'
26
26
  import type { NetworkDecision, NetworkMode } from './network.ts'
27
27
  import { blockMessage } from './network.ts'
@@ -163,23 +163,65 @@ export class NetworkProxy {
163
163
  res.end('permission-rules proxy: only absolute-form proxy requests are served here\n')
164
164
  return
165
165
  }
166
- await this.forwardOrBlock(res, target, () => {
167
- const upstream = new URL(req.url as string)
168
- const send = upstream.protocol === 'https:' ? httpsRequest : httpRequest
169
- const proxyReq = send(upstream, { method: req.method, headers: req.headers }, proxyRes => {
170
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers)
171
- proxyRes.pipe(res)
172
- })
173
- proxyReq.on('error', (error: unknown) => {
174
- if (!res.headersSent) {
175
- res.writeHead(502, { 'content-type': 'text/plain' })
176
- res.end(`[network: upstream error] ${String(error)}\n`)
177
- } else {
178
- res.destroy()
179
- }
180
- })
181
- req.pipe(proxyReq)
166
+ const { decision, target: adjudicated } = await this.decideWithResolution(target)
167
+ if (decision.action !== 'allow') {
168
+ this.recordBlock(decision, target)
169
+ const body = blockMessage(decision)
170
+ res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8', 'content-length': Buffer.byteLength(body) })
171
+ res.end(body)
172
+ return
173
+ }
174
+ const upstream = new URL(req.url as string)
175
+ const send = upstream.protocol === 'https:' ? httpsRequest : httpRequest
176
+ // The forwarded connection is pinned to the addresses the decision was made
177
+ // on, so a hostname that re-resolves between adjudication and connect
178
+ // (DNS rebinding) cannot reach an address the rules never approved.
179
+ const pinned = pinnedAddresses(adjudicated)
180
+ const options: RequestOptions = {
181
+ method: req.method,
182
+ headers: req.headers,
183
+ ...(pinned.length === 0 ? {} : { lookup: pinnedLookup(pinned) }),
184
+ }
185
+ const proxyReq = send(upstream, options, proxyRes => {
186
+ res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers)
187
+ proxyRes.pipe(res)
188
+ })
189
+ proxyReq.on('error', (error: unknown) => {
190
+ if (!res.headersSent) {
191
+ res.writeHead(502, { 'content-type': 'text/plain' })
192
+ res.end(`[network: upstream error] ${String(error)}\n`)
193
+ } else {
194
+ res.destroy()
195
+ }
182
196
  })
197
+ req.pipe(proxyReq)
198
+ }
199
+
200
+ /**
201
+ * Connect to the first adjudicated address that accepts, so the tunnel lands
202
+ * on an address the decision approved; a hostname target with no resolution
203
+ * keeps the historical hostname connect. Each candidate is tried in order so
204
+ * a multi-address host does not lose its fallback.
205
+ */
206
+ private async connectUpstream(target: NetworkTarget, port: number): Promise<Duplex> {
207
+ const pinned = pinnedAddresses(target)
208
+ if (pinned.length === 0) return connect(port, target.host)
209
+ let lastError: unknown
210
+ for (const address of pinned) {
211
+ try {
212
+ return await new Promise<Duplex>((resolve, reject) => {
213
+ const socket = connect(port, address)
214
+ socket.once('connect', () => resolve(socket))
215
+ socket.once('error', (error: unknown) => {
216
+ socket.destroy()
217
+ reject(error instanceof Error ? error : new Error(String(error)))
218
+ })
219
+ })
220
+ } catch (error) {
221
+ lastError = error
222
+ }
223
+ }
224
+ throw lastError instanceof Error ? lastError : new Error('no adjudicated address accepted the connection')
183
225
  }
184
226
 
185
227
  /** CONNECT tunneling (HTTPS and friends): adjudicate, then either 403 or an established TCP tunnel. */
@@ -201,14 +243,21 @@ export class NetworkProxy {
201
243
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
202
244
  return
203
245
  }
204
- const decision = await this.decideWithResolution(target)
246
+ const { decision, target: adjudicated } = await this.decideWithResolution(target)
205
247
  if (decision.action !== 'allow') {
206
248
  this.recordBlock(decision, target)
207
249
  const body = blockMessage(decision)
208
250
  socket.end(`HTTP/1.1 403 Forbidden\r\ncontent-type: text/plain\r\ncontent-length: ${Buffer.byteLength(body)}\r\n\r\n${body}`)
209
251
  return
210
252
  }
211
- const tunnel = connect(target.port ?? 443, target.host)
253
+ let tunnel: Duplex
254
+ try {
255
+ tunnel = await this.connectUpstream(adjudicated, target.port ?? 443)
256
+ } catch (error) {
257
+ this.options.logger.warn(`permission-rules: upstream connect failed for ${target.host}: ${String(error)}`)
258
+ socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n')
259
+ return
260
+ }
212
261
  tunnelHolder.tunnel = tunnel
213
262
  this.sockets.add(socket)
214
263
  this.sockets.add(tunnel)
@@ -219,39 +268,27 @@ export class NetworkProxy {
219
268
  socket.on('close', cleanup)
220
269
  tunnel.on('close', cleanup)
221
270
  tunnel.on('error', () => socket.destroy())
222
- tunnel.once('connect', () => {
223
- socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
224
- if (head.length > 0) tunnel.write(head)
225
- tunnel.pipe(socket)
226
- socket.pipe(tunnel)
227
- })
228
- }
229
-
230
- /** Adjudicate one plain-HTTP request; deny/ask blocks with a structured 403 before any forwarding. */
231
- private async forwardOrBlock(res: ServerResponse, target: NetworkTarget, forward: () => void): Promise<void> {
232
- const decision = await this.decideWithResolution(target)
233
- if (decision.action !== 'allow') {
234
- this.recordBlock(decision, target)
235
- const body = blockMessage(decision)
236
- res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8', 'content-length': Buffer.byteLength(body) })
237
- res.end(body)
238
- return
239
- }
240
- forward()
271
+ socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
272
+ if (head.length > 0) tunnel.write(head)
273
+ tunnel.pipe(socket)
274
+ socket.pipe(tunnel)
241
275
  }
242
276
 
243
277
  /** Resolve a hostname once so `ips`-scoped rules see the real addresses, then decide. */
244
- private async decideWithResolution(target: NetworkTarget): Promise<NetworkDecision> {
278
+ private async decideWithResolution(target: NetworkTarget): Promise<{ decision: NetworkDecision; target: NetworkTarget }> {
245
279
  if (!isIpLiteral(target.host)) {
246
280
  try {
247
281
  const addresses = await lookup(target.host, { all: true, verbatim: true })
248
282
  const resolved = addresses.map(entry => entry.address)
249
- if (resolved.length > 0) return this.options.decide({ ...target, ips: [...target.ips, ...resolved] })
283
+ if (resolved.length > 0) {
284
+ const effective: NetworkTarget = { ...target, ips: [...target.ips, ...resolved] }
285
+ return { decision: await this.options.decide(effective), target: effective }
286
+ }
250
287
  } catch {
251
288
  // Unresolvable target: decide on the literal name (ip-scoped rules simply cannot fire).
252
289
  }
253
290
  }
254
- return this.options.decide(target)
291
+ return { decision: await this.options.decide(target), target }
255
292
  }
256
293
 
257
294
  /** Record a block: counters, recent ring, and the runtime hook (logger + session audit). */
@@ -295,6 +332,32 @@ function connectTarget(authority: string): NetworkTarget | undefined {
295
332
  return { scheme: 'https', host, port, ips: isIpLiteral(host) ? [host] : [] }
296
333
  }
297
334
 
335
+ /**
336
+ * The literal addresses a connection may use — everything the decision was
337
+ * made on. Returning them keeps the connection on an adjudicated address
338
+ * instead of letting a second DNS lookup pick a different one.
339
+ * @param target - the adjudicated target.
340
+ * @returns the adjudicated literal addresses, in decision order.
341
+ */
342
+ function pinnedAddresses(target: NetworkTarget): readonly string[] {
343
+ return target.ips.filter(candidate => isIpLiteral(candidate))
344
+ }
345
+
346
+ /**
347
+ * A `lookup` that answers from the adjudicated address list without a second
348
+ * DNS query. Node's happy-eyeballs path asks with `all: true` and tries the
349
+ * returned addresses in order, so a multi-address host keeps its fallbacks.
350
+ * @param addresses - the adjudicated literal addresses.
351
+ * @returns the lookup function handed to `http(s).request`.
352
+ */
353
+ function pinnedLookup(addresses: readonly string[]): NonNullable<RequestOptions['lookup']> {
354
+ const resolved = addresses.map(address => ({ address, family: address.includes(':') ? 6 : 4 }))
355
+ return (_hostname, options, callback) => {
356
+ if (options.all === true) callback(null, resolved)
357
+ else callback(null, resolved[0]!.address, resolved[0]!.family)
358
+ }
359
+ }
360
+
298
361
  /**
299
362
  * Inject the proxy environment variables for subprocesses and return a
300
363
  * disposer restoring every previous value exactly (so updates and
package/src/rules.ts CHANGED
@@ -1087,9 +1087,31 @@ function normalizeHost(host: string): string {
1087
1087
  return out
1088
1088
  }
1089
1089
 
1090
- /** The literal IP of a host, or `[]` when the host is a name. */
1090
+ /**
1091
+ * Map an IPv4-mapped IPv6 literal (dotted or hex form) back to its dotted IPv4
1092
+ * quad. Node routes `::ffff:169.254.169.254` and `::ffff:a9fe:a9fe` to the same
1093
+ * IPv4 destination as `169.254.169.254`, so both spellings must compare as that
1094
+ * address: without this normalization an `ips` rule written in IPv4 terms never
1095
+ * matches the mapped spelling and the deny is bypassed.
1096
+ * @param host - lowercased, unbracketed host or pattern.
1097
+ * @returns the IPv4 dotted quad when the input is IPv4-mapped, else the input.
1098
+ */
1099
+ function unmapIpv4(host: string): string {
1100
+ const lower = host.toLowerCase()
1101
+ const dotted = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower)
1102
+ if (dotted !== null && dotted[1] !== undefined) return dotted[1]
1103
+ const hex = /^(?:(?:0{0,4}:){5}|::)ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(lower)
1104
+ if (hex !== null && hex[1] !== undefined && hex[2] !== undefined) {
1105
+ const high = Number.parseInt(hex[1], 16)
1106
+ const low = Number.parseInt(hex[2], 16)
1107
+ return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`
1108
+ }
1109
+ return lower
1110
+ }
1111
+
1112
+ /** The literal IP of a host, or `[]` when the host is a name. Mapped literals normalize to IPv4. */
1091
1113
  function literalIpOf(host: string): readonly string[] {
1092
- return isIpLiteral(host) ? [host.toLowerCase()] : []
1114
+ return isIpLiteral(host) ? [unmapIpv4(host.toLowerCase())] : []
1093
1115
  }
1094
1116
 
1095
1117
  /** Whether a host string is an IP literal (IPv4 or IPv6). */
@@ -1139,19 +1161,36 @@ function compilePortPattern(pattern: string): CompiledPortRange {
1139
1161
  return { min: low, max: high }
1140
1162
  }
1141
1163
 
1164
+ /**
1165
+ * Normalize an IP pattern's spelling: a mapped literal becomes its IPv4 quad,
1166
+ * and a mapped glob keeps its IPv4 remainder (`::ffff:10.0.*.*` → `10.0.*.*`),
1167
+ * so IPv4 patterns cover mapped spellings too.
1168
+ * @param pattern - the raw pattern.
1169
+ * @returns the normalized, lowercased pattern.
1170
+ */
1171
+ function unmapIpv4Pattern(pattern: string): string {
1172
+ const lower = pattern.toLowerCase()
1173
+ const mapped = unmapIpv4(lower)
1174
+ if (mapped !== lower) return mapped
1175
+ const dottedPrefix = /^::ffff:(.+)$/.exec(lower)
1176
+ if (dottedPrefix !== null && dottedPrefix[1] !== undefined) return dottedPrefix[1]
1177
+ return lower
1178
+ }
1179
+
1142
1180
  /** Compile one IP pattern: CIDR first, then glob; exact literals glob-compile to themselves. */
1143
1181
  function compileIpPattern(pattern: string, maxGlobStars: number): CompiledIpPattern {
1144
- const cidr = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(pattern)
1182
+ const normalized = unmapIpv4Pattern(pattern)
1183
+ const cidr = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(normalized)
1145
1184
  if (cidr !== null) {
1146
1185
  const octets = cidr.slice(1, 5).map(Number) as [number, number, number, number]
1147
1186
  const prefix = Number(cidr[5])
1148
1187
  const network = ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0
1149
1188
  return { cidr: { network, prefix } }
1150
1189
  }
1151
- if (!/[[*?]/.test(pattern) && !pattern.includes(':')) {
1152
- return { literal: pattern.toLowerCase() }
1190
+ if (!/[[*?]/.test(normalized) && !normalized.includes(':')) {
1191
+ return { literal: normalized }
1153
1192
  }
1154
- return { regex: compileGlob(pattern, { segments: false, maxStars: maxGlobStars, caseInsensitive: true }) }
1193
+ return { regex: compileGlob(normalized, { segments: false, maxStars: maxGlobStars, caseInsensitive: true }) }
1155
1194
  }
1156
1195
 
1157
1196
  /**
@@ -1226,9 +1265,9 @@ export function targetMatchesNetwork(target: NetworkTarget, network: CompiledNet
1226
1265
  return true
1227
1266
  }
1228
1267
 
1229
- /** Whether one compiled IP pattern matches one candidate address (lowercased, unbracketed). */
1268
+ /** Whether one compiled IP pattern matches one candidate address (lowercased, unbracketed, mapped IPv4 unmapped). */
1230
1269
  function ipMatches(pattern: CompiledIpPattern, candidate: string): boolean {
1231
- const normalized = normalizeHost(candidate)
1270
+ const normalized = unmapIpv4(normalizeHost(candidate))
1232
1271
  if (pattern.cidr !== undefined) {
1233
1272
  const parts = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(normalized)
1234
1273
  if (parts === null) return false