@j0hanz/superfetch 2.0.0 → 2.1.0

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 (150) hide show
  1. package/README.md +139 -46
  2. package/dist/cache.d.ts +42 -0
  3. package/dist/cache.js +565 -0
  4. package/dist/config/env-parsers.d.ts +1 -0
  5. package/dist/config/env-parsers.js +12 -0
  6. package/dist/config/index.d.ts +7 -0
  7. package/dist/config/index.js +20 -8
  8. package/dist/config/types/content.d.ts +1 -0
  9. package/dist/config.d.ts +77 -0
  10. package/dist/config.js +261 -0
  11. package/dist/crypto.d.ts +2 -0
  12. package/dist/crypto.js +32 -0
  13. package/dist/errors.d.ts +10 -0
  14. package/dist/errors.js +28 -0
  15. package/dist/fetch.d.ts +40 -0
  16. package/dist/fetch.js +910 -0
  17. package/dist/http/auth.js +161 -2
  18. package/dist/http/base-middleware.d.ts +7 -0
  19. package/dist/http/base-middleware.js +143 -0
  20. package/dist/http/cors.d.ts +0 -5
  21. package/dist/http/cors.js +0 -6
  22. package/dist/http/download-routes.js +6 -2
  23. package/dist/http/error-handler.d.ts +2 -0
  24. package/dist/http/error-handler.js +55 -0
  25. package/dist/http/host-allowlist.d.ts +3 -0
  26. package/dist/http/host-allowlist.js +117 -0
  27. package/dist/http/mcp-routes.d.ts +8 -2
  28. package/dist/http/mcp-routes.js +101 -8
  29. package/dist/http/mcp-session-eviction.d.ts +3 -0
  30. package/dist/http/mcp-session-eviction.js +24 -0
  31. package/dist/http/mcp-session-init.d.ts +7 -0
  32. package/dist/http/mcp-session-init.js +94 -0
  33. package/dist/http/mcp-session-slots.d.ts +17 -0
  34. package/dist/http/mcp-session-slots.js +55 -0
  35. package/dist/http/mcp-session-transport-init.d.ts +7 -0
  36. package/dist/http/mcp-session-transport-init.js +41 -0
  37. package/dist/http/mcp-session-types.d.ts +5 -0
  38. package/dist/http/mcp-session-types.js +1 -0
  39. package/dist/http/mcp-session.d.ts +9 -9
  40. package/dist/http/mcp-session.js +5 -114
  41. package/dist/http/mcp-sessions.d.ts +41 -0
  42. package/dist/http/mcp-sessions.js +392 -0
  43. package/dist/http/rate-limit.js +2 -2
  44. package/dist/http/server-middleware.d.ts +6 -1
  45. package/dist/http/server-middleware.js +3 -117
  46. package/dist/http/server-shutdown.js +1 -1
  47. package/dist/http/server-tuning.d.ts +9 -0
  48. package/dist/http/server-tuning.js +45 -0
  49. package/dist/http/server.js +206 -9
  50. package/dist/http/session-cleanup.js +8 -5
  51. package/dist/http.d.ts +78 -0
  52. package/dist/http.js +1437 -0
  53. package/dist/index.js +3 -3
  54. package/dist/mcp.d.ts +3 -0
  55. package/dist/mcp.js +94 -0
  56. package/dist/middleware/error-handler.d.ts +1 -1
  57. package/dist/middleware/error-handler.js +31 -30
  58. package/dist/observability.d.ts +16 -0
  59. package/dist/observability.js +78 -0
  60. package/dist/resources/cached-content-params.d.ts +5 -0
  61. package/dist/resources/cached-content-params.js +36 -0
  62. package/dist/resources/cached-content.js +33 -33
  63. package/dist/server.js +21 -6
  64. package/dist/services/cache-events.d.ts +8 -0
  65. package/dist/services/cache-events.js +19 -0
  66. package/dist/services/cache.d.ts +5 -4
  67. package/dist/services/cache.js +49 -45
  68. package/dist/services/context.d.ts +2 -0
  69. package/dist/services/context.js +3 -0
  70. package/dist/services/extractor.d.ts +1 -0
  71. package/dist/services/extractor.js +77 -40
  72. package/dist/services/fetcher/agents.js +1 -1
  73. package/dist/services/fetcher/dns-selection.js +1 -1
  74. package/dist/services/fetcher/interceptors.js +29 -60
  75. package/dist/services/fetcher/redirects.js +12 -4
  76. package/dist/services/fetcher/response.js +18 -8
  77. package/dist/services/fetcher.d.ts +23 -0
  78. package/dist/services/fetcher.js +553 -13
  79. package/dist/services/logger.js +4 -1
  80. package/dist/services/telemetry.d.ts +19 -0
  81. package/dist/services/telemetry.js +43 -0
  82. package/dist/services/transform-worker-pool.d.ts +10 -3
  83. package/dist/services/transform-worker-pool.js +213 -184
  84. package/dist/tools/handlers/fetch-single.shared.d.ts +11 -3
  85. package/dist/tools/handlers/fetch-single.shared.js +131 -2
  86. package/dist/tools/handlers/fetch-url.tool.d.ts +6 -0
  87. package/dist/tools/handlers/fetch-url.tool.js +56 -12
  88. package/dist/tools/index.d.ts +1 -0
  89. package/dist/tools/index.js +13 -1
  90. package/dist/tools/schemas.d.ts +2 -0
  91. package/dist/tools/schemas.js +8 -0
  92. package/dist/tools/utils/content-shaping.js +19 -4
  93. package/dist/tools/utils/content-transform-core.d.ts +5 -0
  94. package/dist/tools/utils/content-transform-core.js +180 -0
  95. package/dist/tools/utils/content-transform-workers.d.ts +1 -0
  96. package/dist/tools/utils/content-transform-workers.js +1 -0
  97. package/dist/tools/utils/content-transform.d.ts +2 -1
  98. package/dist/tools/utils/content-transform.js +37 -136
  99. package/dist/tools/utils/fetch-pipeline.js +47 -56
  100. package/dist/tools/utils/frontmatter.d.ts +3 -0
  101. package/dist/tools/utils/frontmatter.js +73 -0
  102. package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
  103. package/dist/tools/utils/markdown-heuristics.js +19 -0
  104. package/dist/tools/utils/markdown-signals.d.ts +1 -0
  105. package/dist/tools/utils/markdown-signals.js +19 -0
  106. package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
  107. package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
  108. package/dist/tools/utils/raw-markdown.d.ts +6 -0
  109. package/dist/tools/utils/raw-markdown.js +149 -0
  110. package/dist/tools.d.ts +104 -0
  111. package/dist/tools.js +421 -0
  112. package/dist/transform.d.ts +69 -0
  113. package/dist/transform.js +1509 -0
  114. package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
  115. package/dist/transformers/markdown/fenced-code-rule.js +38 -0
  116. package/dist/transformers/markdown/frontmatter.d.ts +2 -0
  117. package/dist/transformers/markdown/frontmatter.js +45 -0
  118. package/dist/transformers/markdown/noise-rule.d.ts +2 -0
  119. package/dist/transformers/markdown/noise-rule.js +80 -0
  120. package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
  121. package/dist/transformers/markdown/turndown-instance.js +19 -0
  122. package/dist/transformers/markdown.d.ts +5 -0
  123. package/dist/transformers/markdown.js +314 -0
  124. package/dist/transformers/markdown.transformer.js +2 -189
  125. package/dist/utils/cancellation.d.ts +1 -0
  126. package/dist/utils/cancellation.js +18 -0
  127. package/dist/utils/code-language-bash.d.ts +1 -0
  128. package/dist/utils/code-language-bash.js +48 -0
  129. package/dist/utils/code-language-core.d.ts +2 -0
  130. package/dist/utils/code-language-core.js +13 -0
  131. package/dist/utils/code-language-detectors.d.ts +5 -0
  132. package/dist/utils/code-language-detectors.js +142 -0
  133. package/dist/utils/code-language-helpers.d.ts +5 -0
  134. package/dist/utils/code-language-helpers.js +62 -0
  135. package/dist/utils/code-language-parsing.d.ts +5 -0
  136. package/dist/utils/code-language-parsing.js +62 -0
  137. package/dist/utils/code-language.js +250 -46
  138. package/dist/utils/error-details.d.ts +3 -0
  139. package/dist/utils/error-details.js +12 -0
  140. package/dist/utils/filename-generator.js +14 -3
  141. package/dist/utils/host-normalizer.d.ts +1 -0
  142. package/dist/utils/host-normalizer.js +37 -0
  143. package/dist/utils/ip-address.d.ts +4 -0
  144. package/dist/utils/ip-address.js +6 -0
  145. package/dist/utils/tool-error-handler.js +12 -17
  146. package/dist/utils/url-redactor.d.ts +1 -0
  147. package/dist/utils/url-redactor.js +13 -0
  148. package/dist/utils/url-validator.js +35 -20
  149. package/dist/workers/transform-worker.js +82 -38
  150. package/package.json +13 -10
@@ -1,56 +1,260 @@
1
- const CODE_PATTERNS = [
2
- [
3
- /^\s*import\s+.*\s+from\s+['"]react['"]|<[A-Z][a-zA-Z]*[\s/>]|jsx\s*:|className=/m,
4
- 'jsx',
5
- ],
6
- [
7
- /:\s*(string|number|boolean|void|any|unknown|never)\b|interface\s+\w+|type\s+\w+\s*=/m,
8
- 'typescript',
9
- ],
10
- [/^\s*(fn|let\s+mut|impl|struct|enum|use\s+\w+::)/m, 'rust'],
11
- [
12
- /^\s*(export|const|let|var|function|class|async|await)\b|^\s*import\s+.*['"]]/m,
13
- 'javascript',
14
- ],
15
- [/^\s*(def|class|import|from|if __name__|print\()/m, 'python'],
16
- [
17
- /^\s*(npm|yarn|pnpm|npx|brew|apt|pip|cargo|go )\s+(install|add|run|build|start)/m,
18
- 'bash',
19
- ],
20
- [/^\s*[$#]\s+\w+|^\s*#!|^\s*(sudo|chmod|mkdir|cd|ls|cat|echo)\s+/m, 'bash'],
21
- [/^\s*[.#@]?[\w-]+\s*\{[^}]*\}|@media|@import|@keyframes/m, 'css'],
22
- [/^\s*<(!DOCTYPE|html|head|body|div|span|p|a|script|style)\b/im, 'html'],
23
- [/^\s*\{\s*"|^\s*\[\s*("|\d|true|false|null)/m, 'json'],
24
- [/^\s*[\w-]+:\s*.+$/m, 'yaml'],
25
- [/^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\s+/im, 'sql'],
26
- [/^\s*(func|package|import\s+")/m, 'go'],
1
+ function containsJsxTag(code) {
2
+ for (let index = 0; index < code.length - 1; index += 1) {
3
+ if (code[index] !== '<')
4
+ continue;
5
+ const next = code[index + 1];
6
+ if (!next)
7
+ continue;
8
+ if (next >= 'A' && next <= 'Z')
9
+ return true;
10
+ }
11
+ return false;
12
+ }
13
+ function containsWord(source, word) {
14
+ let startIndex = source.indexOf(word);
15
+ while (startIndex !== -1) {
16
+ const before = startIndex === 0 ? '' : source[startIndex - 1];
17
+ const afterIndex = startIndex + word.length;
18
+ const after = afterIndex >= source.length ? '' : source[afterIndex];
19
+ if (!isWordChar(before) && !isWordChar(after))
20
+ return true;
21
+ startIndex = source.indexOf(word, startIndex + word.length);
22
+ }
23
+ return false;
24
+ }
25
+ function splitLines(content) {
26
+ return content.split('\n');
27
+ }
28
+ function extractLanguageFromClassName(className) {
29
+ const tokens = className.match(/\S+/g);
30
+ if (!tokens)
31
+ return undefined;
32
+ for (const token of tokens) {
33
+ const lower = token.toLowerCase();
34
+ if (lower.startsWith('language-'))
35
+ return token.slice('language-'.length);
36
+ if (lower.startsWith('lang-'))
37
+ return token.slice('lang-'.length);
38
+ if (lower.startsWith('highlight-')) {
39
+ return token.slice('highlight-'.length);
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+ function resolveLanguageFromDataAttribute(dataLang) {
45
+ const trimmed = dataLang.trim();
46
+ if (!trimmed)
47
+ return undefined;
48
+ for (const char of trimmed) {
49
+ if (!isWordChar(char))
50
+ return undefined;
51
+ }
52
+ return trimmed;
53
+ }
54
+ function isWordChar(char) {
55
+ if (!char)
56
+ return false;
57
+ const code = char.charCodeAt(0);
58
+ return ((code >= 48 && code <= 57) ||
59
+ (code >= 65 && code <= 90) ||
60
+ (code >= 97 && code <= 122) ||
61
+ char === '_');
62
+ }
63
+ const BASH_PACKAGE_MANAGERS = [
64
+ 'npm',
65
+ 'yarn',
66
+ 'pnpm',
67
+ 'npx',
68
+ 'brew',
69
+ 'apt',
70
+ 'pip',
71
+ 'cargo',
72
+ 'go',
73
+ ];
74
+ const BASH_VERBS = ['install', 'add', 'run', 'build', 'start'];
75
+ const BASH_COMMANDS = ['sudo', 'chmod', 'mkdir', 'cd', 'ls', 'cat', 'echo'];
76
+ function detectBash(code) {
77
+ const lines = splitLines(code);
78
+ for (const line of lines) {
79
+ const trimmed = line.trimStart();
80
+ if (!trimmed)
81
+ continue;
82
+ if (isBashIndicator(trimmed))
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ function startsWithCommand(line, commands) {
88
+ return commands.some((command) => line === command || line.startsWith(`${command} `));
89
+ }
90
+ function isBashIndicator(line) {
91
+ return (isShebang(line) ||
92
+ isPromptLine(line) ||
93
+ startsWithCommand(line, BASH_COMMANDS) ||
94
+ startsWithPackageManagerCommand(line));
95
+ }
96
+ function isShebang(line) {
97
+ return line.startsWith('#!');
98
+ }
99
+ function isPromptLine(line) {
100
+ return line.startsWith('$ ') || line.startsWith('# ');
101
+ }
102
+ function startsWithPackageManagerCommand(line) {
103
+ return BASH_PACKAGE_MANAGERS.some((manager) => {
104
+ if (!line.startsWith(`${manager} `))
105
+ return false;
106
+ const rest = line.slice(manager.length + 1);
107
+ return BASH_VERBS.some((verb) => rest === verb || rest.startsWith(`${verb} `));
108
+ });
109
+ }
110
+ const TYPE_HINTS = [
111
+ 'string',
112
+ 'number',
113
+ 'boolean',
114
+ 'void',
115
+ 'any',
116
+ 'unknown',
117
+ 'never',
118
+ ];
119
+ const HTML_TAGS = [
120
+ '<!doctype',
121
+ '<html',
122
+ '<head',
123
+ '<body',
124
+ '<div',
125
+ '<span',
126
+ '<p',
127
+ '<a',
128
+ '<script',
129
+ '<style',
130
+ ];
131
+ const SQL_KEYWORDS = [
132
+ 'select',
133
+ 'insert',
134
+ 'update',
135
+ 'delete',
136
+ 'create',
137
+ 'alter',
138
+ 'drop',
27
139
  ];
28
- const CLASS_PATTERNS = [
29
- /language-(\w+)/,
30
- /lang-(\w+)/,
31
- /highlight-(\w+)/,
140
+ const JS_WORD_REGEX = /\b(?:const|let|var|function|class|async|await|export|import)\b/;
141
+ const PYTHON_WORD_REGEX = /\b(?:def|class|import|from)\b/;
142
+ const RUST_WORD_REGEX = /\b(?:fn|impl|struct|enum)\b/;
143
+ const CSS_DIRECTIVE_REGEX = /@media|@import|@keyframes/;
144
+ const CODE_DETECTORS = [
145
+ { language: 'jsx', detect: detectJsx },
146
+ { language: 'typescript', detect: detectTypescript },
147
+ { language: 'rust', detect: detectRust },
148
+ { language: 'javascript', detect: detectJavascript },
149
+ { language: 'python', detect: detectPython },
150
+ { language: 'bash', detect: detectBash },
151
+ { language: 'css', detect: detectCss },
152
+ { language: 'html', detect: detectHtml },
153
+ { language: 'json', detect: detectJson },
154
+ { language: 'yaml', detect: detectYaml },
155
+ { language: 'sql', detect: detectSql },
156
+ { language: 'go', detect: detectGo },
32
157
  ];
158
+ function detectJsx(code) {
159
+ const lower = code.toLowerCase();
160
+ if (lower.includes('classname='))
161
+ return true;
162
+ if (lower.includes('jsx:'))
163
+ return true;
164
+ if (lower.includes("from 'react'") || lower.includes('from "react"')) {
165
+ return true;
166
+ }
167
+ return containsJsxTag(code);
168
+ }
169
+ function detectTypescript(code) {
170
+ const lower = code.toLowerCase();
171
+ if (containsWord(lower, 'interface'))
172
+ return true;
173
+ if (containsWord(lower, 'type'))
174
+ return true;
175
+ return TYPE_HINTS.some((hint) => lower.includes(`: ${hint}`) || lower.includes(`:${hint}`));
176
+ }
177
+ function detectRust(code) {
178
+ const lower = code.toLowerCase();
179
+ return (RUST_WORD_REGEX.test(lower) ||
180
+ lower.includes('let mut') ||
181
+ (lower.includes('use ') && lower.includes('::')));
182
+ }
183
+ function detectJavascript(code) {
184
+ const lower = code.toLowerCase();
185
+ return JS_WORD_REGEX.test(lower);
186
+ }
187
+ function detectPython(code) {
188
+ const lower = code.toLowerCase();
189
+ return (PYTHON_WORD_REGEX.test(lower) ||
190
+ lower.includes('print(') ||
191
+ lower.includes('__name__'));
192
+ }
193
+ function detectCss(code) {
194
+ const lower = code.toLowerCase();
195
+ if (CSS_DIRECTIVE_REGEX.test(lower))
196
+ return true;
197
+ const lines = splitLines(code);
198
+ for (const line of lines) {
199
+ const trimmed = line.trimStart();
200
+ if (!trimmed)
201
+ continue;
202
+ if (isCssSelectorLine(trimmed) || isCssPropertyLine(trimmed))
203
+ return true;
204
+ }
205
+ return false;
206
+ }
207
+ function detectHtml(code) {
208
+ const lower = code.toLowerCase();
209
+ return HTML_TAGS.some((tag) => lower.includes(tag));
210
+ }
211
+ function detectJson(code) {
212
+ const trimmed = code.trimStart();
213
+ if (!trimmed)
214
+ return false;
215
+ return trimmed.startsWith('{') || trimmed.startsWith('[');
216
+ }
217
+ function detectYaml(code) {
218
+ const lines = splitLines(code);
219
+ for (const line of lines) {
220
+ const trimmed = line.trim();
221
+ if (!trimmed)
222
+ continue;
223
+ const colonIndex = trimmed.indexOf(':');
224
+ if (colonIndex <= 0)
225
+ continue;
226
+ const after = trimmed[colonIndex + 1];
227
+ if (after === ' ' || after === '\t')
228
+ return true;
229
+ }
230
+ return false;
231
+ }
232
+ function detectSql(code) {
233
+ const lower = code.toLowerCase();
234
+ return SQL_KEYWORDS.some((keyword) => containsWord(lower, keyword));
235
+ }
236
+ function detectGo(code) {
237
+ const lower = code.toLowerCase();
238
+ return (containsWord(lower, 'package') ||
239
+ containsWord(lower, 'func') ||
240
+ lower.includes('import "'));
241
+ }
242
+ function isCssSelectorLine(line) {
243
+ if (!line.startsWith('.') && !line.startsWith('#'))
244
+ return false;
245
+ return line.includes('{');
246
+ }
247
+ function isCssPropertyLine(line) {
248
+ return line.includes(':') && line.includes(';');
249
+ }
33
250
  export function detectLanguageFromCode(code) {
34
- for (const [pattern, language] of CODE_PATTERNS) {
35
- if (pattern.test(code)) {
251
+ for (const { language, detect } of CODE_DETECTORS) {
252
+ if (detect(code))
36
253
  return language;
37
- }
38
254
  }
39
255
  return undefined;
40
256
  }
41
257
  export function resolveLanguageFromAttributes(className, dataLang) {
42
- const classMatch = matchFirstCapture(className, CLASS_PATTERNS);
258
+ const classMatch = extractLanguageFromClassName(className);
43
259
  return classMatch ?? resolveLanguageFromDataAttribute(dataLang);
44
260
  }
45
- function matchFirstCapture(value, patterns) {
46
- for (const pattern of patterns) {
47
- const match = pattern.exec(value);
48
- if (match?.[1])
49
- return match[1];
50
- }
51
- return undefined;
52
- }
53
- function resolveLanguageFromDataAttribute(dataLang) {
54
- const match = /^(\w+)$/.exec(dataLang);
55
- return match?.[1];
56
- }
@@ -0,0 +1,3 @@
1
+ export declare function getErrorMessage(error: unknown): string;
2
+ export declare function createErrorWithCode(message: string, code: string): NodeJS.ErrnoException;
3
+ export declare function isSystemError(error: unknown): error is NodeJS.ErrnoException;
@@ -0,0 +1,12 @@
1
+ export function getErrorMessage(error) {
2
+ return error instanceof Error ? error.message : 'Unknown error';
3
+ }
4
+ export function createErrorWithCode(message, code) {
5
+ const error = new Error(message);
6
+ return Object.assign(error, { code });
7
+ }
8
+ export function isSystemError(error) {
9
+ return (error instanceof Error &&
10
+ 'code' in error &&
11
+ typeof Reflect.get(error, 'code') === 'string');
12
+ }
@@ -2,6 +2,17 @@ const MAX_FILENAME_LENGTH = 200;
2
2
  const UNSAFE_CHARS_REGEX = /[<>:"/\\|?*]|\p{C}/gu;
3
3
  const WHITESPACE_REGEX = /\s+/g;
4
4
  const DEFAULT_EXTENSION = '.md';
5
+ function trimHyphens(value) {
6
+ let start = 0;
7
+ let end = value.length;
8
+ while (start < end && value[start] === '-') {
9
+ start += 1;
10
+ }
11
+ while (end > start && value[end - 1] === '-') {
12
+ end -= 1;
13
+ }
14
+ return value.slice(start, end);
15
+ }
5
16
  export function generateSafeFilename(url, title, hashFallback, extension = DEFAULT_EXTENSION) {
6
17
  const fromUrl = extractFilenameFromUrl(url);
7
18
  if (fromUrl)
@@ -52,9 +63,9 @@ function slugifyTitle(title) {
52
63
  .trim()
53
64
  .replace(UNSAFE_CHARS_REGEX, '')
54
65
  .replace(WHITESPACE_REGEX, '-')
55
- .replace(/-+/g, '-')
56
- .replace(/^-|-$/g, '');
57
- return slug || null;
66
+ .replace(/-+/g, '-');
67
+ const trimmed = trimHyphens(slug);
68
+ return trimmed || null;
58
69
  }
59
70
  function sanitizeFilename(name, extension) {
60
71
  let sanitized = name
@@ -0,0 +1 @@
1
+ export declare function normalizeHost(value: string): string | null;
@@ -0,0 +1,37 @@
1
+ import { isIP } from 'node:net';
2
+ function takeFirstHostValue(value) {
3
+ const first = value.split(',')[0];
4
+ if (!first)
5
+ return null;
6
+ const trimmed = first.trim();
7
+ return trimmed ? trimmed : null;
8
+ }
9
+ function stripIpv6Brackets(value) {
10
+ if (!value.startsWith('['))
11
+ return null;
12
+ const end = value.indexOf(']');
13
+ if (end === -1)
14
+ return null;
15
+ return value.slice(1, end);
16
+ }
17
+ function stripPortIfPresent(value) {
18
+ const colonIndex = value.indexOf(':');
19
+ if (colonIndex === -1)
20
+ return value;
21
+ return value.slice(0, colonIndex);
22
+ }
23
+ export function normalizeHost(value) {
24
+ const trimmed = value.trim().toLowerCase();
25
+ if (!trimmed)
26
+ return null;
27
+ const first = takeFirstHostValue(trimmed);
28
+ if (!first)
29
+ return null;
30
+ const ipv6 = stripIpv6Brackets(first);
31
+ if (ipv6)
32
+ return ipv6;
33
+ if (isIP(first) === 6) {
34
+ return first;
35
+ }
36
+ return stripPortIfPresent(first);
37
+ }
@@ -0,0 +1,4 @@
1
+ type IpSegment = number | string;
2
+ export declare function buildIpv4(parts: readonly [number, number, number, number]): string;
3
+ export declare function buildIpv6(parts: readonly IpSegment[]): string;
4
+ export {};
@@ -0,0 +1,6 @@
1
+ export function buildIpv4(parts) {
2
+ return parts.join('.');
3
+ }
4
+ export function buildIpv6(parts) {
5
+ return parts.map(String).join(':');
6
+ }
@@ -1,11 +1,5 @@
1
1
  import { FetchError } from '../errors/app-error.js';
2
- import { isSystemError } from './error-utils.js';
3
- function createFallbackErrorResponse(fallbackMessage, url, error) {
4
- return createToolErrorResponse(`${fallbackMessage}: ${error.message}`, url);
5
- }
6
- function createUnknownErrorResponse(fallbackMessage, url) {
7
- return createToolErrorResponse(`${fallbackMessage}: Unknown error`, url);
8
- }
2
+ import { isSystemError } from './error-details.js';
9
3
  export function createToolErrorResponse(message, url) {
10
4
  const structuredContent = {
11
5
  error: message,
@@ -18,19 +12,20 @@ export function createToolErrorResponse(message, url) {
18
12
  };
19
13
  }
20
14
  export function handleToolError(error, url, fallbackMessage = 'Operation failed') {
21
- if (isValidationError(error)) {
22
- return createToolErrorResponse(error.message, url);
23
- }
24
- if (error instanceof FetchError) {
25
- return createToolErrorResponse(error.message, url);
26
- }
27
- if (error instanceof Error) {
28
- return createFallbackErrorResponse(fallbackMessage, url, error);
29
- }
30
- return createUnknownErrorResponse(fallbackMessage, url);
15
+ const message = resolveToolErrorMessage(error, fallbackMessage);
16
+ return createToolErrorResponse(message, url);
31
17
  }
32
18
  function isValidationError(error) {
33
19
  return (error instanceof Error &&
34
20
  isSystemError(error) &&
35
21
  error.code === 'VALIDATION_ERROR');
36
22
  }
23
+ function resolveToolErrorMessage(error, fallbackMessage) {
24
+ if (isValidationError(error) || error instanceof FetchError) {
25
+ return error.message;
26
+ }
27
+ if (error instanceof Error) {
28
+ return `${fallbackMessage}: ${error.message}`;
29
+ }
30
+ return `${fallbackMessage}: Unknown error`;
31
+ }
@@ -0,0 +1 @@
1
+ export declare function redactUrl(rawUrl: string): string;
@@ -0,0 +1,13 @@
1
+ export function redactUrl(rawUrl) {
2
+ try {
3
+ const url = new URL(rawUrl);
4
+ url.username = '';
5
+ url.password = '';
6
+ url.hash = '';
7
+ url.search = '';
8
+ return url.toString();
9
+ }
10
+ catch {
11
+ return rawUrl;
12
+ }
13
+ }
@@ -1,28 +1,38 @@
1
1
  import { BlockList, isIP } from 'node:net';
2
2
  import { config } from '../config/index.js';
3
- import { createErrorWithCode } from './error-utils.js';
3
+ import { createErrorWithCode } from './error-details.js';
4
+ import { buildIpv4, buildIpv6 } from './ip-address.js';
4
5
  const BLOCK_LIST = new BlockList();
6
+ const IPV6_ZERO = buildIpv6([0, 0, 0, 0, 0, 0, 0, 0]);
7
+ const IPV6_LOOPBACK = buildIpv6([0, 0, 0, 0, 0, 0, 0, 1]);
8
+ const IPV6_64_FF9B = buildIpv6(['64', 'ff9b', 0, 0, 0, 0, 0, 0]);
9
+ const IPV6_64_FF9B_1 = buildIpv6(['64', 'ff9b', 1, 0, 0, 0, 0, 0]);
10
+ const IPV6_2001 = buildIpv6(['2001', 0, 0, 0, 0, 0, 0, 0]);
11
+ const IPV6_2002 = buildIpv6(['2002', 0, 0, 0, 0, 0, 0, 0]);
12
+ const IPV6_FC00 = buildIpv6(['fc00', 0, 0, 0, 0, 0, 0, 0]);
13
+ const IPV6_FE80 = buildIpv6(['fe80', 0, 0, 0, 0, 0, 0, 0]);
14
+ const IPV6_FF00 = buildIpv6(['ff00', 0, 0, 0, 0, 0, 0, 0]);
5
15
  const BLOCKED_IPV4_SUBNETS = [
6
- { subnet: '0.0.0.0', prefix: 8 },
7
- { subnet: '10.0.0.0', prefix: 8 },
8
- { subnet: '100.64.0.0', prefix: 10 },
9
- { subnet: '127.0.0.0', prefix: 8 },
10
- { subnet: '169.254.0.0', prefix: 16 },
11
- { subnet: '172.16.0.0', prefix: 12 },
12
- { subnet: '192.168.0.0', prefix: 16 },
13
- { subnet: '224.0.0.0', prefix: 4 },
14
- { subnet: '240.0.0.0', prefix: 4 },
16
+ { subnet: buildIpv4([0, 0, 0, 0]), prefix: 8 },
17
+ { subnet: buildIpv4([10, 0, 0, 0]), prefix: 8 },
18
+ { subnet: buildIpv4([100, 64, 0, 0]), prefix: 10 },
19
+ { subnet: buildIpv4([127, 0, 0, 0]), prefix: 8 },
20
+ { subnet: buildIpv4([169, 254, 0, 0]), prefix: 16 },
21
+ { subnet: buildIpv4([172, 16, 0, 0]), prefix: 12 },
22
+ { subnet: buildIpv4([192, 168, 0, 0]), prefix: 16 },
23
+ { subnet: buildIpv4([224, 0, 0, 0]), prefix: 4 },
24
+ { subnet: buildIpv4([240, 0, 0, 0]), prefix: 4 },
15
25
  ];
16
26
  const BLOCKED_IPV6_SUBNETS = [
17
- { subnet: '::', prefix: 128 },
18
- { subnet: '::1', prefix: 128 },
19
- { subnet: '64:ff9b::', prefix: 96 },
20
- { subnet: '64:ff9b:1::', prefix: 48 },
21
- { subnet: '2001::', prefix: 32 },
22
- { subnet: '2002::', prefix: 16 },
23
- { subnet: 'fc00::', prefix: 7 },
24
- { subnet: 'fe80::', prefix: 10 },
25
- { subnet: 'ff00::', prefix: 8 },
27
+ { subnet: IPV6_ZERO, prefix: 128 },
28
+ { subnet: IPV6_LOOPBACK, prefix: 128 },
29
+ { subnet: IPV6_64_FF9B, prefix: 96 },
30
+ { subnet: IPV6_64_FF9B_1, prefix: 48 },
31
+ { subnet: IPV6_2001, prefix: 32 },
32
+ { subnet: IPV6_2002, prefix: 16 },
33
+ { subnet: IPV6_FC00, prefix: 7 },
34
+ { subnet: IPV6_FE80, prefix: 10 },
35
+ { subnet: IPV6_FF00, prefix: 8 },
26
36
  ];
27
37
  for (const entry of BLOCKED_IPV4_SUBNETS) {
28
38
  BLOCK_LIST.addSubnet(entry.subnet, entry.prefix, 'ipv4');
@@ -68,6 +78,8 @@ export function normalizeUrl(urlString) {
68
78
  assertNoCredentials(url);
69
79
  const hostname = normalizeHostname(url);
70
80
  assertHostnameAllowed(hostname);
81
+ // Canonicalize hostname to avoid trailing-dot variants and keep url.href consistent.
82
+ url.hostname = hostname;
71
83
  return { normalizedUrl: url.href, hostname };
72
84
  }
73
85
  export function validateAndNormalizeUrl(urlString) {
@@ -109,7 +121,10 @@ function assertNoCredentials(url) {
109
121
  throw createValidationError('URLs with embedded credentials are not allowed');
110
122
  }
111
123
  function normalizeHostname(url) {
112
- const hostname = url.hostname.toLowerCase();
124
+ let hostname = url.hostname.toLowerCase();
125
+ while (hostname.endsWith('.')) {
126
+ hostname = hostname.slice(0, -1);
127
+ }
113
128
  if (!hostname) {
114
129
  throw createValidationError('URL must have a valid hostname');
115
130
  }