@poolzin/pool-bot 2026.3.4 → 2026.3.6

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/assets/pool-bot-icon-dark.png +0 -0
  3. package/assets/pool-bot-logo-1.png +0 -0
  4. package/assets/pool-bot-mascot.png +0 -0
  5. package/dist/agents/pi-embedded-runner/tool-result-truncation.js +62 -7
  6. package/dist/agents/poolbot-tools.js +12 -0
  7. package/dist/agents/session-write-lock.js +93 -8
  8. package/dist/agents/tools/pdf-native-providers.js +102 -0
  9. package/dist/agents/tools/pdf-tool.helpers.js +86 -0
  10. package/dist/agents/tools/pdf-tool.js +508 -0
  11. package/dist/build-info.json +3 -3
  12. package/dist/cron/normalize.js +3 -0
  13. package/dist/cron/service/jobs.js +48 -0
  14. package/dist/gateway/protocol/schema/cron.js +3 -0
  15. package/dist/gateway/server-channels.js +99 -14
  16. package/dist/gateway/server-cron.js +89 -0
  17. package/dist/gateway/server-health-probes.js +55 -0
  18. package/dist/gateway/server-http.js +5 -0
  19. package/dist/hooks/bundled/session-memory/handler.js +8 -2
  20. package/dist/infra/abort-signal.js +12 -0
  21. package/dist/infra/boundary-file-read.js +118 -0
  22. package/dist/infra/boundary-path.js +594 -0
  23. package/dist/infra/file-identity.js +12 -0
  24. package/dist/infra/fs-safe.js +377 -12
  25. package/dist/infra/hardlink-guards.js +30 -0
  26. package/dist/infra/json-utf8-bytes.js +8 -0
  27. package/dist/infra/net/fetch-guard.js +63 -13
  28. package/dist/infra/net/proxy-env.js +17 -0
  29. package/dist/infra/net/ssrf.js +74 -272
  30. package/dist/infra/path-alias-guards.js +21 -0
  31. package/dist/infra/path-guards.js +13 -1
  32. package/dist/infra/ports-probe.js +19 -0
  33. package/dist/infra/prototype-keys.js +4 -0
  34. package/dist/infra/restart-stale-pids.js +254 -0
  35. package/dist/infra/safe-open-sync.js +71 -0
  36. package/dist/infra/secure-random.js +7 -0
  37. package/dist/media/ffmpeg-limits.js +4 -0
  38. package/dist/media/input-files.js +6 -2
  39. package/dist/media/temp-files.js +12 -0
  40. package/dist/memory/embedding-chunk-limits.js +5 -2
  41. package/dist/memory/embeddings-ollama.js +91 -138
  42. package/dist/memory/embeddings-remote-fetch.js +11 -10
  43. package/dist/memory/embeddings.js +25 -9
  44. package/dist/memory/manager-embedding-ops.js +1 -1
  45. package/dist/memory/post-json.js +23 -0
  46. package/dist/memory/qmd-manager.js +272 -77
  47. package/dist/memory/remote-http.js +33 -0
  48. package/dist/plugin-sdk/windows-spawn.js +214 -0
  49. package/dist/shared/net/ip-test-fixtures.js +1 -0
  50. package/dist/shared/net/ip.js +303 -0
  51. package/dist/shared/net/ipv4.js +8 -11
  52. package/dist/shared/pid-alive.js +59 -2
  53. package/dist/test-helpers/ssrf.js +13 -0
  54. package/dist/tui/tui.js +9 -4
  55. package/dist/utils/fetch-timeout.js +12 -1
  56. package/docs/adr/003-feature-gap-analysis.md +112 -0
  57. package/package.json +10 -4
@@ -1,6 +1,7 @@
1
1
  import { lookup as dnsLookupCb } from "node:dns";
2
2
  import { lookup as dnsLookup } from "node:dns/promises";
3
3
  import { Agent } from "undici";
4
+ import { extractEmbeddedIpv4FromIpv6, isBlockedSpecialUseIpv4Address, isBlockedSpecialUseIpv6Address, isCanonicalDottedDecimalIPv4, isIpv4Address, isLegacyIpv4Literal, parseCanonicalIpAddress, parseLooseIpAddress, } from "../../shared/net/ip.js";
4
5
  import { normalizeHostname } from "./hostname.js";
5
6
  export class SsrFBlockedError extends Error {
6
7
  constructor(message) {
@@ -27,6 +28,14 @@ function normalizeHostnameAllowlist(values) {
27
28
  .map((value) => normalizeHostname(value))
28
29
  .filter((value) => value !== "*" && value !== "*." && value.length > 0)));
29
30
  }
31
+ export function isPrivateNetworkAllowedByPolicy(policy) {
32
+ return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
33
+ }
34
+ function resolveIpv4SpecialUseBlockOptions(policy) {
35
+ return {
36
+ allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange === true,
37
+ };
38
+ }
30
39
  function isHostnameAllowedByPattern(hostname, pattern) {
31
40
  if (pattern.startsWith("*.")) {
32
41
  const suffix = pattern.slice(2);
@@ -43,45 +52,7 @@ function matchesHostnameAllowlist(hostname, allowlist) {
43
52
  }
44
53
  return allowlist.some((pattern) => isHostnameAllowedByPattern(hostname, pattern));
45
54
  }
46
- function parseStrictIpv4Octet(part) {
47
- if (!/^[0-9]+$/.test(part)) {
48
- return null;
49
- }
50
- const value = Number.parseInt(part, 10);
51
- if (Number.isNaN(value) || value < 0 || value > 255) {
52
- return null;
53
- }
54
- // Accept only canonical decimal octets (no leading zeros, no alternate radices).
55
- if (part !== String(value)) {
56
- return null;
57
- }
58
- return value;
59
- }
60
- function parseIpv4(address) {
61
- const parts = address.split(".");
62
- if (parts.length !== 4) {
63
- return null;
64
- }
65
- for (const part of parts) {
66
- if (parseStrictIpv4Octet(part) === null) {
67
- return null;
68
- }
69
- }
70
- return parts.map((part) => Number.parseInt(part, 10));
71
- }
72
- function classifyIpv4Part(part) {
73
- if (/^0x[0-9a-f]+$/i.test(part)) {
74
- return "hex";
75
- }
76
- if (/^0x/i.test(part)) {
77
- return "invalid-hex";
78
- }
79
- if (/^[0-9]+$/.test(part)) {
80
- return "decimal";
81
- }
82
- return "non-numeric";
83
- }
84
- function isUnsupportedLegacyIpv4Literal(address) {
55
+ function looksLikeUnsupportedIpv4Literal(address) {
85
56
  const parts = address.split(".");
86
57
  if (parts.length === 0 || parts.length > 4) {
87
58
  return false;
@@ -89,186 +60,12 @@ function isUnsupportedLegacyIpv4Literal(address) {
89
60
  if (parts.some((part) => part.length === 0)) {
90
61
  return true;
91
62
  }
92
- const partKinds = parts.map(classifyIpv4Part);
93
- if (partKinds.some((kind) => kind === "non-numeric")) {
94
- return false;
95
- }
96
- if (partKinds.some((kind) => kind === "invalid-hex")) {
97
- return true;
98
- }
99
- if (parts.length !== 4) {
100
- return true;
101
- }
102
- for (const part of parts) {
103
- if (/^0x/i.test(part)) {
104
- return true;
105
- }
106
- const value = Number.parseInt(part, 10);
107
- if (Number.isNaN(value) || value > 255 || part !== String(value)) {
108
- return true;
109
- }
110
- }
111
- return false;
112
- }
113
- function stripIpv6ZoneId(address) {
114
- const index = address.indexOf("%");
115
- return index >= 0 ? address.slice(0, index) : address;
116
- }
117
- function parseIpv6Hextets(address) {
118
- let input = stripIpv6ZoneId(address.trim().toLowerCase());
119
- if (!input) {
120
- return null;
121
- }
122
- // Handle IPv4-embedded IPv6 like ::ffff:127.0.0.1 by converting the tail to 2 hextets.
123
- if (input.includes(".")) {
124
- const lastColon = input.lastIndexOf(":");
125
- if (lastColon < 0) {
126
- return null;
127
- }
128
- const ipv4 = parseIpv4(input.slice(lastColon + 1));
129
- if (!ipv4) {
130
- return null;
131
- }
132
- const high = (ipv4[0] << 8) + ipv4[1];
133
- const low = (ipv4[2] << 8) + ipv4[3];
134
- input = `${input.slice(0, lastColon)}:${high.toString(16)}:${low.toString(16)}`;
135
- }
136
- const doubleColonParts = input.split("::");
137
- if (doubleColonParts.length > 2) {
138
- return null;
139
- }
140
- const headParts = doubleColonParts[0]?.length > 0 ? doubleColonParts[0].split(":").filter(Boolean) : [];
141
- const tailParts = doubleColonParts.length === 2 && doubleColonParts[1]?.length > 0
142
- ? doubleColonParts[1].split(":").filter(Boolean)
143
- : [];
144
- const missingParts = 8 - headParts.length - tailParts.length;
145
- if (missingParts < 0) {
146
- return null;
147
- }
148
- const fullParts = doubleColonParts.length === 1
149
- ? input.split(":")
150
- : [...headParts, ...Array.from({ length: missingParts }, () => "0"), ...tailParts];
151
- if (fullParts.length !== 8) {
152
- return null;
153
- }
154
- const hextets = [];
155
- for (const part of fullParts) {
156
- if (!part) {
157
- return null;
158
- }
159
- const value = Number.parseInt(part, 16);
160
- if (Number.isNaN(value) || value < 0 || value > 0xffff) {
161
- return null;
162
- }
163
- hextets.push(value);
164
- }
165
- return hextets;
166
- }
167
- function decodeIpv4FromHextets(high, low) {
168
- return [(high >>> 8) & 0xff, high & 0xff, (low >>> 8) & 0xff, low & 0xff];
169
- }
170
- const EMBEDDED_IPV4_RULES = [
171
- {
172
- // IPv4-mapped: ::ffff:a.b.c.d and IPv4-compatible ::a.b.c.d.
173
- matches: (hextets) => hextets[0] === 0 &&
174
- hextets[1] === 0 &&
175
- hextets[2] === 0 &&
176
- hextets[3] === 0 &&
177
- hextets[4] === 0 &&
178
- (hextets[5] === 0xffff || hextets[5] === 0),
179
- extract: (hextets) => [hextets[6], hextets[7]],
180
- },
181
- {
182
- // NAT64 well-known prefix: 64:ff9b::/96.
183
- matches: (hextets) => hextets[0] === 0x0064 &&
184
- hextets[1] === 0xff9b &&
185
- hextets[2] === 0 &&
186
- hextets[3] === 0 &&
187
- hextets[4] === 0 &&
188
- hextets[5] === 0,
189
- extract: (hextets) => [hextets[6], hextets[7]],
190
- },
191
- {
192
- // NAT64 local-use prefix: 64:ff9b:1::/48.
193
- matches: (hextets) => hextets[0] === 0x0064 &&
194
- hextets[1] === 0xff9b &&
195
- hextets[2] === 0x0001 &&
196
- hextets[3] === 0 &&
197
- hextets[4] === 0 &&
198
- hextets[5] === 0,
199
- extract: (hextets) => [hextets[6], hextets[7]],
200
- },
201
- {
202
- // 6to4 prefix: 2002::/16 where hextets[1..2] carry IPv4.
203
- matches: (hextets) => hextets[0] === 0x2002,
204
- extract: (hextets) => [hextets[1], hextets[2]],
205
- },
206
- {
207
- // Teredo prefix: 2001:0000::/32 with client IPv4 obfuscated via XOR 0xffff.
208
- matches: (hextets) => hextets[0] === 0x2001 && hextets[1] === 0x0000,
209
- extract: (hextets) => [hextets[6] ^ 0xffff, hextets[7] ^ 0xffff],
210
- },
211
- {
212
- // ISATAP IID format: 000000ug00000000:5efe:w.x.y.z (RFC 5214 section 6.1).
213
- // Match only the IID marker bits to avoid over-broad :5efe: detection.
214
- matches: (hextets) => (hextets[4] & 0xfcff) === 0 && hextets[5] === 0x5efe,
215
- extract: (hextets) => [hextets[6], hextets[7]],
216
- },
217
- ];
218
- function extractIpv4FromEmbeddedIpv6(hextets) {
219
- for (const rule of EMBEDDED_IPV4_RULES) {
220
- if (!rule.matches(hextets)) {
221
- continue;
222
- }
223
- const [high, low] = rule.extract(hextets);
224
- return decodeIpv4FromHextets(high, low);
225
- }
226
- return null;
227
- }
228
- function ipv4ToUint(parts) {
229
- const [a, b, c, d] = parts;
230
- return (((a << 24) >>> 0) | (b << 16) | (c << 8) | d) >>> 0;
231
- }
232
- function ipv4RangeFromCidr(cidr) {
233
- const base = ipv4ToUint(cidr.base);
234
- const hostBits = 32 - cidr.prefixLength;
235
- const mask = cidr.prefixLength === 0 ? 0 : (0xffffffff << hostBits) >>> 0;
236
- const start = (base & mask) >>> 0;
237
- const end = (start | (~mask >>> 0)) >>> 0;
238
- return [start, end];
239
- }
240
- const BLOCKED_IPV4_SPECIAL_USE_CIDRS = [
241
- { base: [0, 0, 0, 0], prefixLength: 8 },
242
- { base: [10, 0, 0, 0], prefixLength: 8 },
243
- { base: [100, 64, 0, 0], prefixLength: 10 },
244
- { base: [127, 0, 0, 0], prefixLength: 8 },
245
- { base: [169, 254, 0, 0], prefixLength: 16 },
246
- { base: [172, 16, 0, 0], prefixLength: 12 },
247
- { base: [192, 0, 0, 0], prefixLength: 24 },
248
- { base: [192, 0, 2, 0], prefixLength: 24 },
249
- { base: [192, 88, 99, 0], prefixLength: 24 },
250
- { base: [192, 168, 0, 0], prefixLength: 16 },
251
- { base: [198, 18, 0, 0], prefixLength: 15 },
252
- { base: [198, 51, 100, 0], prefixLength: 24 },
253
- { base: [203, 0, 113, 0], prefixLength: 24 },
254
- { base: [224, 0, 0, 0], prefixLength: 4 },
255
- { base: [240, 0, 0, 0], prefixLength: 4 },
256
- ];
257
- const BLOCKED_IPV4_SPECIAL_USE_RANGES = BLOCKED_IPV4_SPECIAL_USE_CIDRS.map(ipv4RangeFromCidr);
258
- function isBlockedIpv4SpecialUse(parts) {
259
- if (parts.length !== 4) {
260
- return false;
261
- }
262
- const value = ipv4ToUint(parts);
263
- for (const [start, end] of BLOCKED_IPV4_SPECIAL_USE_RANGES) {
264
- if (value >= start && value <= end) {
265
- return true;
266
- }
267
- }
268
- return false;
63
+ // Tighten only "ipv4-ish" literals (numbers + optional 0x prefix). Hostnames like
64
+ // "example.com" must stay in hostname policy handling and not be treated as malformed IPs.
65
+ return parts.every((part) => /^[0-9]+$/.test(part) || /^0x/i.test(part));
269
66
  }
270
67
  // Returns true for private/internal and special-use non-global addresses.
271
- export function isPrivateIpAddress(address) {
68
+ export function isPrivateIpAddress(address, policy) {
272
69
  let normalized = address.trim().toLowerCase();
273
70
  if (normalized.startsWith("[") && normalized.endsWith("]")) {
274
71
  normalized = normalized.slice(1, -1);
@@ -276,57 +73,29 @@ export function isPrivateIpAddress(address) {
276
73
  if (!normalized) {
277
74
  return false;
278
75
  }
279
- if (normalized.includes(":")) {
280
- const hextets = parseIpv6Hextets(normalized);
281
- if (!hextets) {
282
- // Security-critical parse failures should fail closed.
283
- return true;
76
+ const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
77
+ const strictIp = parseCanonicalIpAddress(normalized);
78
+ if (strictIp) {
79
+ if (isIpv4Address(strictIp)) {
80
+ return isBlockedSpecialUseIpv4Address(strictIp, blockOptions);
284
81
  }
285
- const isUnspecified = hextets[0] === 0 &&
286
- hextets[1] === 0 &&
287
- hextets[2] === 0 &&
288
- hextets[3] === 0 &&
289
- hextets[4] === 0 &&
290
- hextets[5] === 0 &&
291
- hextets[6] === 0 &&
292
- hextets[7] === 0;
293
- const isLoopback = hextets[0] === 0 &&
294
- hextets[1] === 0 &&
295
- hextets[2] === 0 &&
296
- hextets[3] === 0 &&
297
- hextets[4] === 0 &&
298
- hextets[5] === 0 &&
299
- hextets[6] === 0 &&
300
- hextets[7] === 1;
301
- if (isUnspecified || isLoopback) {
82
+ if (isBlockedSpecialUseIpv6Address(strictIp)) {
302
83
  return true;
303
84
  }
304
- const embeddedIpv4 = extractIpv4FromEmbeddedIpv6(hextets);
85
+ const embeddedIpv4 = extractEmbeddedIpv4FromIpv6(strictIp);
305
86
  if (embeddedIpv4) {
306
- return isBlockedIpv4SpecialUse(embeddedIpv4);
307
- }
308
- // IPv6 private/internal ranges
309
- // - link-local: fe80::/10
310
- // - site-local (deprecated, but internal): fec0::/10
311
- // - unique local: fc00::/7
312
- const first = hextets[0];
313
- if ((first & 0xffc0) === 0xfe80) {
314
- return true;
315
- }
316
- if ((first & 0xffc0) === 0xfec0) {
317
- return true;
318
- }
319
- if ((first & 0xfe00) === 0xfc00) {
320
- return true;
87
+ return isBlockedSpecialUseIpv4Address(embeddedIpv4, blockOptions);
321
88
  }
322
89
  return false;
323
90
  }
324
- const ipv4 = parseIpv4(normalized);
325
- if (ipv4) {
326
- return isBlockedIpv4SpecialUse(ipv4);
91
+ // Security-critical parse failures should fail closed for any malformed IPv6 literal.
92
+ if (normalized.includes(":") && !parseLooseIpAddress(normalized)) {
93
+ return true;
94
+ }
95
+ if (!isCanonicalDottedDecimalIPv4(normalized) && isLegacyIpv4Literal(normalized)) {
96
+ return true;
327
97
  }
328
- // Reject non-canonical IPv4 literal forms (octal/hex/short/packed) by default.
329
- if (isUnsupportedLegacyIpv4Literal(normalized)) {
98
+ if (looksLikeUnsupportedIpv4Literal(normalized)) {
330
99
  return true;
331
100
  }
332
101
  return false;
@@ -346,12 +115,27 @@ function isBlockedHostnameNormalized(normalized) {
346
115
  normalized.endsWith(".local") ||
347
116
  normalized.endsWith(".internal"));
348
117
  }
349
- export function isBlockedHostnameOrIp(hostname) {
118
+ export function isBlockedHostnameOrIp(hostname, policy) {
350
119
  const normalized = normalizeHostname(hostname);
351
120
  if (!normalized) {
352
121
  return false;
353
122
  }
354
- return isBlockedHostnameNormalized(normalized) || isPrivateIpAddress(normalized);
123
+ return isBlockedHostnameNormalized(normalized) || isPrivateIpAddress(normalized, policy);
124
+ }
125
+ const BLOCKED_HOST_OR_IP_MESSAGE = "Blocked hostname or private/internal/special-use IP address";
126
+ const BLOCKED_RESOLVED_IP_MESSAGE = "Blocked: resolves to private/internal/special-use IP address";
127
+ function assertAllowedHostOrIpOrThrow(hostnameOrIp, policy) {
128
+ if (isBlockedHostnameOrIp(hostnameOrIp, policy)) {
129
+ throw new SsrFBlockedError(BLOCKED_HOST_OR_IP_MESSAGE);
130
+ }
131
+ }
132
+ function assertAllowedResolvedAddressesOrThrow(results, policy) {
133
+ for (const entry of results) {
134
+ // Reuse the exact same host/IP classifier as the pre-DNS check to avoid drift.
135
+ if (isBlockedHostnameOrIp(entry.address, policy)) {
136
+ throw new SsrFBlockedError(BLOCKED_RESOLVED_IP_MESSAGE);
137
+ }
138
+ }
355
139
  }
356
140
  export function createPinnedLookup(params) {
357
141
  const normalizedHost = normalizeHostname(params.hostname);
@@ -392,34 +176,52 @@ export function createPinnedLookup(params) {
392
176
  cb(null, chosen.address, chosen.family);
393
177
  });
394
178
  }
179
+ function dedupeAndPreferIpv4(results) {
180
+ const seen = new Set();
181
+ const ipv4 = [];
182
+ const otherFamilies = [];
183
+ for (const entry of results) {
184
+ if (seen.has(entry.address)) {
185
+ continue;
186
+ }
187
+ seen.add(entry.address);
188
+ if (entry.family === 4) {
189
+ ipv4.push(entry.address);
190
+ continue;
191
+ }
192
+ otherFamilies.push(entry.address);
193
+ }
194
+ return [...ipv4, ...otherFamilies];
195
+ }
395
196
  export async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
396
197
  const normalized = normalizeHostname(hostname);
397
198
  if (!normalized) {
398
199
  throw new Error("Invalid hostname");
399
200
  }
400
- const allowPrivateNetwork = Boolean(params.policy?.allowPrivateNetwork);
201
+ const allowPrivateNetwork = isPrivateNetworkAllowedByPolicy(params.policy);
401
202
  const allowedHostnames = normalizeHostnameSet(params.policy?.allowedHostnames);
402
203
  const hostnameAllowlist = normalizeHostnameAllowlist(params.policy?.hostnameAllowlist);
403
204
  const isExplicitAllowed = allowedHostnames.has(normalized);
205
+ const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitAllowed;
404
206
  if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
405
207
  throw new SsrFBlockedError(`Blocked hostname (not in allowlist): ${hostname}`);
406
208
  }
407
- if (!allowPrivateNetwork && !isExplicitAllowed && isBlockedHostnameOrIp(normalized)) {
408
- throw new SsrFBlockedError("Blocked hostname or private/internal/special-use IP address");
209
+ if (!skipPrivateNetworkChecks) {
210
+ // Phase 1: fail fast for literal hosts/IPs before any DNS lookup side-effects.
211
+ assertAllowedHostOrIpOrThrow(normalized, params.policy);
409
212
  }
410
213
  const lookupFn = params.lookupFn ?? dnsLookup;
411
214
  const results = await lookupFn(normalized, { all: true });
412
215
  if (results.length === 0) {
413
216
  throw new Error(`Unable to resolve hostname: ${hostname}`);
414
217
  }
415
- if (!allowPrivateNetwork && !isExplicitAllowed) {
416
- for (const entry of results) {
417
- if (isPrivateIpAddress(entry.address)) {
418
- throw new SsrFBlockedError("Blocked: resolves to private/internal/special-use IP address");
419
- }
420
- }
218
+ if (!skipPrivateNetworkChecks) {
219
+ // Phase 2: re-check DNS answers so public hostnames cannot pivot to private targets.
220
+ assertAllowedResolvedAddressesOrThrow(results, params.policy);
421
221
  }
422
- const addresses = Array.from(new Set(results.map((entry) => entry.address)));
222
+ // Prefer addresses returned as IPv4 by DNS family metadata before other
223
+ // families so Happy Eyeballs and pinned round-robin both attempt IPv4 first.
224
+ const addresses = dedupeAndPreferIpv4(results);
423
225
  if (addresses.length === 0) {
424
226
  throw new Error(`Unable to resolve hostname: ${hostname}`);
425
227
  }
@@ -0,0 +1,21 @@
1
+ import { BOUNDARY_PATH_ALIAS_POLICIES, resolveBoundaryPath, } from "./boundary-path.js";
2
+ import { assertNoHardlinkedFinalPath } from "./hardlink-guards.js";
3
+ export const PATH_ALIAS_POLICIES = BOUNDARY_PATH_ALIAS_POLICIES;
4
+ export async function assertNoPathAliasEscape(params) {
5
+ const resolved = await resolveBoundaryPath({
6
+ absolutePath: params.absolutePath,
7
+ rootPath: params.rootPath,
8
+ boundaryLabel: params.boundaryLabel,
9
+ policy: params.policy,
10
+ });
11
+ const allowFinalSymlink = params.policy?.allowFinalSymlinkForUnlink === true;
12
+ if (allowFinalSymlink && resolved.kind === "symlink") {
13
+ return;
14
+ }
15
+ await assertNoHardlinkedFinalPath({
16
+ filePath: resolved.absolutePath,
17
+ root: resolved.rootPath,
18
+ boundaryLabel: params.boundaryLabel,
19
+ allowFinalHardlinkForUnlink: params.policy?.allowFinalHardlinkForUnlink,
20
+ });
21
+ }
@@ -1,6 +1,16 @@
1
1
  import path from "node:path";
2
2
  const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
3
3
  const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
4
+ export function normalizeWindowsPathForComparison(input) {
5
+ let normalized = path.win32.normalize(input);
6
+ if (normalized.startsWith("\\\\?\\")) {
7
+ normalized = normalized.slice(4);
8
+ if (normalized.toUpperCase().startsWith("UNC\\")) {
9
+ normalized = `\\\\${normalized.slice(4)}`;
10
+ }
11
+ }
12
+ return normalized.replaceAll("/", "\\").toLowerCase();
13
+ }
4
14
  export function isNodeError(value) {
5
15
  return Boolean(value && typeof value === "object" && "code" in value);
6
16
  }
@@ -17,7 +27,9 @@ export function isPathInside(root, target) {
17
27
  const resolvedRoot = path.resolve(root);
18
28
  const resolvedTarget = path.resolve(target);
19
29
  if (process.platform === "win32") {
20
- const relative = path.win32.relative(resolvedRoot.toLowerCase(), resolvedTarget.toLowerCase());
30
+ const rootForCompare = normalizeWindowsPathForComparison(resolvedRoot);
31
+ const targetForCompare = normalizeWindowsPathForComparison(resolvedTarget);
32
+ const relative = path.win32.relative(rootForCompare, targetForCompare);
21
33
  return relative === "" || (!relative.startsWith("..") && !path.win32.isAbsolute(relative));
22
34
  }
23
35
  const relative = path.relative(resolvedRoot, resolvedTarget);
@@ -0,0 +1,19 @@
1
+ import net from "node:net";
2
+ export async function tryListenOnPort(params) {
3
+ const listenOptions = { port: params.port };
4
+ if (params.host) {
5
+ listenOptions.host = params.host;
6
+ }
7
+ if (typeof params.exclusive === "boolean") {
8
+ listenOptions.exclusive = params.exclusive;
9
+ }
10
+ await new Promise((resolve, reject) => {
11
+ const tester = net
12
+ .createServer()
13
+ .once("error", (err) => reject(err))
14
+ .once("listening", () => {
15
+ tester.close(() => resolve());
16
+ })
17
+ .listen(listenOptions);
18
+ });
19
+ }
@@ -0,0 +1,4 @@
1
+ const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
2
+ export function isBlockedObjectKey(key) {
3
+ return BLOCKED_OBJECT_KEYS.has(key);
4
+ }