@j0hanz/fetch-url-mcp 1.6.0 → 1.7.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 (108) hide show
  1. package/README.md +507 -403
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/http/auth.d.ts +12 -0
  5. package/dist/http/auth.d.ts.map +1 -0
  6. package/dist/http/auth.js +85 -6
  7. package/dist/http/health.d.ts +1 -0
  8. package/dist/http/health.d.ts.map +1 -0
  9. package/dist/http/helpers.d.ts +1 -0
  10. package/dist/http/helpers.d.ts.map +1 -0
  11. package/dist/http/native.d.ts +1 -0
  12. package/dist/http/native.d.ts.map +1 -0
  13. package/dist/http/native.js +80 -63
  14. package/dist/http/rate-limit.d.ts +1 -0
  15. package/dist/http/rate-limit.d.ts.map +1 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/lib/content.d.ts +3 -0
  19. package/dist/lib/content.d.ts.map +1 -0
  20. package/dist/lib/content.js +16 -11
  21. package/dist/lib/core.d.ts +6 -8
  22. package/dist/lib/core.d.ts.map +1 -0
  23. package/dist/lib/core.js +111 -97
  24. package/dist/lib/fetch-pipeline.d.ts +1 -1
  25. package/dist/lib/fetch-pipeline.d.ts.map +1 -0
  26. package/dist/lib/fetch-pipeline.js +54 -44
  27. package/dist/lib/http.d.ts +1 -0
  28. package/dist/lib/http.d.ts.map +1 -0
  29. package/dist/lib/mcp-tools.d.ts +45 -7
  30. package/dist/lib/mcp-tools.d.ts.map +1 -0
  31. package/dist/lib/mcp-tools.js +37 -6
  32. package/dist/lib/net-utils.d.ts +1 -0
  33. package/dist/lib/net-utils.d.ts.map +1 -0
  34. package/dist/lib/progress.d.ts +1 -0
  35. package/dist/lib/progress.d.ts.map +1 -0
  36. package/dist/lib/task-handlers.d.ts +9 -1
  37. package/dist/lib/task-handlers.d.ts.map +1 -0
  38. package/dist/lib/task-handlers.js +30 -38
  39. package/dist/lib/types.d.ts +4 -0
  40. package/dist/lib/types.d.ts.map +1 -0
  41. package/dist/lib/types.js +12 -1
  42. package/dist/lib/url.d.ts +3 -0
  43. package/dist/lib/url.d.ts.map +1 -0
  44. package/dist/lib/url.js +78 -151
  45. package/dist/lib/utils.d.ts +2 -2
  46. package/dist/lib/utils.d.ts.map +1 -0
  47. package/dist/lib/utils.js +60 -94
  48. package/dist/lib/zod.d.ts +3 -0
  49. package/dist/lib/zod.d.ts.map +1 -0
  50. package/dist/lib/zod.js +33 -0
  51. package/dist/prompts/index.d.ts +2 -1
  52. package/dist/prompts/index.d.ts.map +1 -0
  53. package/dist/prompts/index.js +2 -13
  54. package/dist/resources/index.d.ts +2 -1
  55. package/dist/resources/index.d.ts.map +1 -0
  56. package/dist/resources/index.js +5 -19
  57. package/dist/resources/instructions.d.ts +1 -0
  58. package/dist/resources/instructions.d.ts.map +1 -0
  59. package/dist/resources/instructions.js +2 -0
  60. package/dist/schemas/cache.d.ts +18 -0
  61. package/dist/schemas/cache.d.ts.map +1 -0
  62. package/dist/schemas/cache.js +19 -0
  63. package/dist/schemas/inputs.d.ts +1 -0
  64. package/dist/schemas/inputs.d.ts.map +1 -0
  65. package/dist/schemas/outputs.d.ts +6 -5
  66. package/dist/schemas/outputs.d.ts.map +1 -0
  67. package/dist/schemas/outputs.js +5 -9
  68. package/dist/server.d.ts +1 -0
  69. package/dist/server.d.ts.map +1 -0
  70. package/dist/server.js +9 -7
  71. package/dist/tasks/execution.d.ts +1 -0
  72. package/dist/tasks/execution.d.ts.map +1 -0
  73. package/dist/tasks/execution.js +3 -21
  74. package/dist/tasks/manager.d.ts +2 -6
  75. package/dist/tasks/manager.d.ts.map +1 -0
  76. package/dist/tasks/manager.js +2 -4
  77. package/dist/tasks/owner.d.ts +1 -0
  78. package/dist/tasks/owner.d.ts.map +1 -0
  79. package/dist/tasks/tool-registry.d.ts +2 -0
  80. package/dist/tasks/tool-registry.d.ts.map +1 -0
  81. package/dist/tasks/tool-registry.js +3 -0
  82. package/dist/tools/fetch-url.d.ts +4 -6
  83. package/dist/tools/fetch-url.d.ts.map +1 -0
  84. package/dist/tools/fetch-url.js +61 -59
  85. package/dist/tools/index.d.ts +1 -0
  86. package/dist/tools/index.d.ts.map +1 -0
  87. package/dist/transform/html-translators.d.ts +1 -0
  88. package/dist/transform/html-translators.d.ts.map +1 -0
  89. package/dist/transform/html-translators.js +5 -2
  90. package/dist/transform/metadata.d.ts +1 -0
  91. package/dist/transform/metadata.d.ts.map +1 -0
  92. package/dist/transform/metadata.js +1 -0
  93. package/dist/transform/{workers/shared.d.ts → shared.d.ts} +2 -1
  94. package/dist/transform/shared.d.ts.map +1 -0
  95. package/dist/transform/{workers/shared.js → shared.js} +1 -1
  96. package/dist/transform/transform.d.ts +1 -0
  97. package/dist/transform/transform.d.ts.map +1 -0
  98. package/dist/transform/transform.js +21 -14
  99. package/dist/transform/types.d.ts +1 -4
  100. package/dist/transform/types.d.ts.map +1 -0
  101. package/dist/transform/worker-pool.d.ts +3 -18
  102. package/dist/transform/worker-pool.d.ts.map +1 -0
  103. package/dist/transform/worker-pool.js +51 -167
  104. package/package.json +9 -6
  105. package/dist/transform/workers/transform-child.d.ts +0 -1
  106. package/dist/transform/workers/transform-child.js +0 -15
  107. package/dist/transform/workers/transform-worker.d.ts +0 -1
  108. package/dist/transform/workers/transform-worker.js +0 -13
@@ -2,17 +2,46 @@ import {} from '@modelcontextprotocol/sdk/types.js';
2
2
  import { z } from 'zod';
3
3
  import { FetchError, isAbortError, isSystemError } from './utils.js';
4
4
  const paramsSchema = z.looseObject({});
5
- const mcpRequestSchema = z.strictObject({
5
+ const jsonRpcRequestIdSchema = z.union([z.string(), z.number()]);
6
+ const jsonRpcRequestSchema = z.strictObject({
6
7
  jsonrpc: z.literal('2.0'),
7
8
  method: z.string().min(1),
8
- id: z.union([z.string(), z.number(), z.null()]).optional(),
9
+ id: jsonRpcRequestIdSchema.optional(),
9
10
  params: paramsSchema.optional(),
10
11
  });
12
+ const jsonRpcResultResponseSchema = z.strictObject({
13
+ jsonrpc: z.literal('2.0'),
14
+ id: z.union([z.string(), z.number()]),
15
+ result: z.record(z.string(), z.unknown()),
16
+ });
17
+ const jsonRpcErrorResponseSchema = z.strictObject({
18
+ jsonrpc: z.literal('2.0'),
19
+ id: z.union([z.string(), z.number(), z.null()]).optional(),
20
+ error: z.strictObject({
21
+ code: z.number().int(),
22
+ message: z.string(),
23
+ data: z.unknown().optional(),
24
+ }),
25
+ });
26
+ const jsonRpcResponseSchema = z.union([
27
+ jsonRpcResultResponseSchema,
28
+ jsonRpcErrorResponseSchema,
29
+ ]);
30
+ const jsonRpcMessageSchema = z.union([
31
+ jsonRpcRequestSchema,
32
+ jsonRpcResponseSchema,
33
+ ]);
11
34
  export function isJsonRpcBatchRequest(body) {
12
35
  return Array.isArray(body);
13
36
  }
14
37
  export function isMcpRequestBody(body) {
15
- return mcpRequestSchema.safeParse(body).success;
38
+ return jsonRpcRequestSchema.safeParse(body).success;
39
+ }
40
+ export function isJsonRpcResponseBody(body) {
41
+ return jsonRpcResponseSchema.safeParse(body).success;
42
+ }
43
+ export function isMcpMessageBody(body) {
44
+ return jsonRpcMessageSchema.safeParse(body).success;
16
45
  }
17
46
  function parseAcceptMediaTypes(header) {
18
47
  if (!header)
@@ -76,8 +105,10 @@ function resolveToolErrorMessage(error, fallbackMessage) {
76
105
  return `${fallbackMessage}: Unknown error`;
77
106
  }
78
107
  function resolveToolErrorCode(error) {
79
- if (error instanceof FetchError)
80
- return error.code;
108
+ if (error instanceof FetchError) {
109
+ const detailsCode = error.details['code'];
110
+ return typeof detailsCode === 'string' ? detailsCode : error.code;
111
+ }
81
112
  if (isValidationError(error))
82
113
  return 'VALIDATION_ERROR';
83
114
  if (isAbortError(error))
@@ -103,5 +134,5 @@ export function handleToolError(error, url, fallbackMessage = 'Operation failed'
103
134
  * without changes. Direct imports from the sub-modules are preferred for new code.
104
135
  * ------------------------------------------------------------------------------------------------- */
105
136
  export { registerServerLifecycleCleanup, registerTaskHandlers, cancelTasksForOwner, abortAllTaskExecutions, } from './task-handlers.js';
106
- export { readString, readNestedRecord, withSignal, TRUNCATION_MARKER, appendTruncationMarker, executeFetchPipeline, parseCachedMarkdownResult, markdownTransform, serializeMarkdownResult, performSharedFetch, } from './fetch-pipeline.js';
137
+ export { readNestedRecord, withSignal, TRUNCATION_MARKER, appendTruncationMarker, executeFetchPipeline, parseCachedMarkdownResult, markdownTransform, serializeMarkdownResult, performSharedFetch, } from './fetch-pipeline.js';
107
138
  export { createProgressReporter, } from './progress.js';
@@ -1,3 +1,4 @@
1
1
  export declare function buildIpv4(parts: readonly [number, number, number, number]): string;
2
2
  export declare function stripTrailingDots(value: string): string;
3
3
  export declare function normalizeHostname(value: string): string | null;
4
+ //# sourceMappingURL=net-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"net-utils.d.ts","sourceRoot":"","sources":["../../src/lib/net-utils.ts"],"names":[],"mappings":"AAGA,wBAAgB,SAAS,CACvB,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAC/C,MAAM,CAER;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS9D"}
@@ -28,3 +28,4 @@ export interface ProgressReporter {
28
28
  }
29
29
  export declare function createProgressReporter(extra?: ToolHandlerExtra): ProgressReporter;
30
30
  export {};
31
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../src/lib/progress.ts"],"names":[],"mappings":"AAOA,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AACrC,UAAU,WAAW;IACnB,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC1C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AACD,MAAM,WAAW,0BAA0B;IACzC,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AACD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,wBAAwB,CAAC;IACjC,MAAM,EAAE,0BAA0B,CAAC;CACpC;AACD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,gBAAgB,CAAC,EAAE,CAAC,YAAY,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1D;AACD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AA0ID,wBAAgB,sBAAsB,CACpC,KAAK,CAAC,EAAE,gBAAgB,GACvB,gBAAgB,CAElB"}
@@ -2,4 +2,12 @@ import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  type CleanupCallback = () => void;
3
3
  export declare function registerServerLifecycleCleanup(server: McpServer, callback: CleanupCallback): void;
4
4
  export { cancelTasksForOwner, abortAllTaskExecutions, } from '../tasks/execution.js';
5
- export declare function registerTaskHandlers(server: McpServer): void;
5
+ interface TaskHandlerRegistrationOptions {
6
+ requireInterception?: boolean;
7
+ }
8
+ interface TaskHandlerRegistrationResult {
9
+ interceptedToolsCall: boolean;
10
+ taskCapableToolsRegistered: boolean;
11
+ }
12
+ export declare function registerTaskHandlers(server: McpServer, options?: TaskHandlerRegistrationOptions): TaskHandlerRegistrationResult;
13
+ //# sourceMappingURL=task-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-handlers.d.ts","sourceRoot":"","sources":["../../src/lib/task-handlers.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA0CzE,KAAK,eAAe,GAAG,MAAM,IAAI,CAAC;AAyClC,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,eAAe,GACxB,IAAI,CAGN;AAED,OAAO,EACL,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AA+E/B,UAAU,8BAA8B;IACtC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,UAAU,6BAA6B;IACrC,oBAAoB,EAAE,OAAO,CAAC;IAC9B,0BAA0B,EAAE,OAAO,CAAC;CACrC;AAUD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,OAAO,CAAC,EAAE,8BAA8B,GACvC,6BAA6B,CAiK/B"}
@@ -5,8 +5,9 @@ import { z } from 'zod';
5
5
  import { abortTaskExecution, emitTaskStatusNotification, handleToolCallRequest, throwTaskNotFound, toTaskSummary, withRelatedTaskMeta, } from '../tasks/execution.js';
6
6
  import { taskManager } from '../tasks/manager.js';
7
7
  import { isServerResult, parseHandlerExtra, resolveTaskOwnerKey, resolveToolCallContext, } from '../tasks/owner.js';
8
- import { hasTaskCapableTool } from '../tasks/tool-registry.js';
8
+ import { hasRegisteredTaskCapableTools, hasTaskCapableTool, } from '../tasks/tool-registry.js';
9
9
  import { logWarn, runWithRequestContext } from './core.js';
10
+ import { formatZodError } from './zod.js';
10
11
  const patchedCleanupServers = new WeakSet();
11
12
  const serverCleanupCallbacks = new WeakMap();
12
13
  function getServerCleanupCallbackSet(server) {
@@ -55,42 +56,31 @@ export { cancelTasksForOwner, abortAllTaskExecutions, } from '../tasks/execution
55
56
  /* -------------------------------------------------------------------------------------------------
56
57
  * Task handler schemas and registration
57
58
  * ------------------------------------------------------------------------------------------------- */
58
- const TaskGetSchema = z
59
- .object({
59
+ const TaskGetSchema = z.looseObject({
60
60
  method: z.literal('tasks/get'),
61
- params: z.object({ taskId: z.string() }).loose(),
62
- })
63
- .loose();
64
- const TaskListSchema = z
65
- .object({
61
+ params: z.looseObject({ taskId: z.string() }),
62
+ });
63
+ const TaskListSchema = z.looseObject({
66
64
  method: z.literal('tasks/list'),
67
65
  params: z
68
- .object({
66
+ .looseObject({
69
67
  cursor: z.string().optional(),
70
68
  })
71
- .loose()
72
69
  .optional(),
73
- })
74
- .loose();
75
- const TaskCancelSchema = z
76
- .object({
70
+ });
71
+ const TaskCancelSchema = z.looseObject({
77
72
  method: z.literal('tasks/cancel'),
78
- params: z.object({ taskId: z.string() }).loose(),
79
- })
80
- .loose();
81
- const TaskResultSchema = z
82
- .object({
73
+ params: z.looseObject({ taskId: z.string() }),
74
+ });
75
+ const TaskResultSchema = z.looseObject({
83
76
  method: z.literal('tasks/result'),
84
- params: z.object({ taskId: z.string() }).loose(),
85
- })
86
- .loose();
77
+ params: z.looseObject({ taskId: z.string() }),
78
+ });
87
79
  const MIN_TASK_TTL_MS = 1_000;
88
80
  const MAX_TASK_TTL_MS = 86_400_000;
89
- const ExtendedCallToolRequestSchema = z
90
- .object({
81
+ const ExtendedCallToolRequestSchema = z.looseObject({
91
82
  method: z.literal('tools/call'),
92
- params: z
93
- .object({
83
+ params: z.looseObject({
94
84
  name: z.string().min(1),
95
85
  arguments: z.record(z.string(), z.unknown()).optional(),
96
86
  task: z
@@ -104,7 +94,7 @@ const ExtendedCallToolRequestSchema = z
104
94
  })
105
95
  .optional(),
106
96
  _meta: z
107
- .object({
97
+ .looseObject({
108
98
  progressToken: z.union([z.string(), z.number()]).optional(),
109
99
  'io.modelcontextprotocol/related-task': z
110
100
  .strictObject({
@@ -112,21 +102,14 @@ const ExtendedCallToolRequestSchema = z
112
102
  })
113
103
  .optional(),
114
104
  })
115
- .loose()
116
105
  .optional(),
117
- })
118
- .loose(),
119
- })
120
- .loose();
106
+ }),
107
+ });
121
108
  function parseExtendedCallToolRequest(request) {
122
109
  const parsed = ExtendedCallToolRequestSchema.safeParse(request);
123
110
  if (parsed.success)
124
111
  return parsed.data;
125
- const flat = z.flattenError(parsed.error);
126
- const details = Object.entries(flat.fieldErrors)
127
- .map(([k, v]) => `${k}: ${(v ?? []).join(', ')}`)
128
- .join('; ') || flat.formErrors.join('; ');
129
- throw new McpError(ErrorCode.InvalidParams, `Invalid tool request params: ${details}`);
112
+ throw new McpError(ErrorCode.InvalidParams, `Invalid tool request params: ${formatZodError(parsed.error)}`);
130
113
  }
131
114
  function resolveOwnerScopedExtra(extra) {
132
115
  const parsedExtra = parseHandlerExtra(extra);
@@ -143,9 +126,14 @@ function getSdkCallToolHandler(server) {
143
126
  const handler = maybeHandlers.get('tools/call');
144
127
  return typeof handler === 'function' ? handler : null;
145
128
  }
146
- export function registerTaskHandlers(server) {
129
+ export function registerTaskHandlers(server, options) {
147
130
  const sdkCallToolHandler = getSdkCallToolHandler(server);
131
+ const taskCapableToolsRegistered = hasRegisteredTaskCapableTools();
132
+ const requireInterception = options?.requireInterception ?? true;
148
133
  if (!sdkCallToolHandler) {
134
+ if (taskCapableToolsRegistered && requireInterception) {
135
+ throw new Error('Task-capable tools are registered but SDK tools/call interception is unavailable. Upgrade compatibility or disable strict interception with TASKS_REQUIRE_INTERCEPTION=false.');
136
+ }
149
137
  logWarn('Task call interception disabled: SDK tools/call handler unavailable; task-capable tools require MCP SDK compatibility update', { sdkVersion: 'unknown' });
150
138
  }
151
139
  if (sdkCallToolHandler) {
@@ -239,4 +227,8 @@ export function registerTaskHandlers(server) {
239
227
  emitTaskStatusNotification(server, task);
240
228
  return toTaskSummary(task);
241
229
  });
230
+ return {
231
+ interceptedToolsCall: sdkCallToolHandler !== null,
232
+ taskCapableToolsRegistered,
233
+ };
242
234
  }
@@ -2,3 +2,7 @@ export interface IconInfo {
2
2
  src: string;
3
3
  mimeType: string;
4
4
  }
5
+ export declare function buildOptionalIcons(iconInfo?: IconInfo): {
6
+ icons: IconInfo[];
7
+ } | Record<string, never>;
8
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,CAAC,EAAE,QAAQ,GAClB;IAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAU/C"}
package/dist/lib/types.js CHANGED
@@ -1 +1,12 @@
1
- export {};
1
+ export function buildOptionalIcons(iconInfo) {
2
+ if (!iconInfo)
3
+ return {};
4
+ return {
5
+ icons: [
6
+ {
7
+ src: iconInfo.src,
8
+ mimeType: iconInfo.mimeType,
9
+ },
10
+ ],
11
+ };
12
+ }
package/dist/lib/url.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare class SafeDnsResolver {
4
4
  private readonly ipBlocker;
5
5
  private readonly security;
6
6
  private readonly blockedHostSuffixes;
7
+ private readonly cnameResolver;
7
8
  constructor(ipBlocker: IpBlocker, security: SecurityConfig, blockedHostSuffixes: readonly string[]);
8
9
  resolveAndValidate(hostname: string, signal?: AbortSignal): Promise<string>;
9
10
  private isBlockedHostname;
@@ -50,6 +51,7 @@ export declare class IpBlocker {
50
51
  private readonly blockList;
51
52
  constructor(security: SecurityConfig);
52
53
  isBlockedIp(candidate: string): boolean;
54
+ isHostBlocked(hostname: string, blockedHostSuffixes: readonly string[]): boolean;
53
55
  }
54
56
  type ConstantsConfig = typeof config.constants;
55
57
  export declare class UrlNormalizer {
@@ -68,3 +70,4 @@ export declare class UrlNormalizer {
68
70
  private assertHostnameAllowed;
69
71
  }
70
72
  export {};
73
+ //# sourceMappingURL=url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/lib/url.ts"],"names":[],"mappings":"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"}
package/dist/lib/url.js CHANGED
@@ -9,29 +9,6 @@ function normalizeDnsName(value) {
9
9
  const normalized = value.trim().toLowerCase().replace(/\.+$/, '');
10
10
  return normalized;
11
11
  }
12
- function createSignalAbortRace(signal, isAbort, onTimeout, onAbort) {
13
- let abortListener = null;
14
- const abortPromise = new Promise((_, reject) => {
15
- abortListener = () => {
16
- reject(isAbort() ? onAbort() : onTimeout());
17
- };
18
- signal.addEventListener('abort', abortListener, { once: true });
19
- if (signal.aborted)
20
- abortListener();
21
- });
22
- const cleanup = () => {
23
- if (!abortListener)
24
- return;
25
- try {
26
- signal.removeEventListener('abort', abortListener);
27
- }
28
- catch {
29
- // Ignore listener cleanup failures; they are non-fatal by design.
30
- }
31
- abortListener = null;
32
- };
33
- return { abortPromise, cleanup };
34
- }
35
12
  async function withTimeout(promise, timeoutMs, onTimeout, signal, onAbort) {
36
13
  const timeoutSignal = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
37
14
  const raceSignal = signal && timeoutSignal
@@ -39,13 +16,25 @@ async function withTimeout(promise, timeoutMs, onTimeout, signal, onAbort) {
39
16
  : (signal ?? timeoutSignal);
40
17
  if (!raceSignal)
41
18
  return promise;
42
- const abortRace = createSignalAbortRace(raceSignal, () => signal?.aborted === true, onTimeout, onAbort ?? (() => new Error('Request was canceled')));
43
- try {
44
- return await Promise.race([promise, abortRace.abortPromise]);
45
- }
46
- finally {
47
- abortRace.cleanup();
48
- }
19
+ return new Promise((resolve, reject) => {
20
+ const onAborted = () => {
21
+ reject(signal?.aborted
22
+ ? (onAbort?.() ?? new Error('Request was canceled'))
23
+ : onTimeout());
24
+ };
25
+ if (raceSignal.aborted) {
26
+ onAborted();
27
+ return;
28
+ }
29
+ raceSignal.addEventListener('abort', onAborted, { once: true });
30
+ promise.then((value) => {
31
+ raceSignal.removeEventListener('abort', onAborted);
32
+ resolve(value);
33
+ }, (err) => {
34
+ raceSignal.removeEventListener('abort', onAborted);
35
+ reject(err instanceof Error ? err : new Error(String(err)));
36
+ });
37
+ });
49
38
  }
50
39
  function createAbortSignalError() {
51
40
  const err = new Error('Request was canceled');
@@ -56,6 +45,9 @@ export class SafeDnsResolver {
56
45
  ipBlocker;
57
46
  security;
58
47
  blockedHostSuffixes;
48
+ cnameResolver = new dns.promises.Resolver({
49
+ timeout: DNS_LOOKUP_TIMEOUT_MS,
50
+ });
59
51
  constructor(ipBlocker, security, blockedHostSuffixes) {
60
52
  this.ipBlocker = ipBlocker;
61
53
  this.security = security;
@@ -105,13 +97,7 @@ export class SafeDnsResolver {
105
97
  return addresses[0].address;
106
98
  }
107
99
  isBlockedHostname(hostname) {
108
- if (isCloudMetadataHost(hostname))
109
- return true;
110
- if (isLocalFetchAllowed())
111
- return false;
112
- if (this.security.blockedHosts.has(hostname))
113
- return true;
114
- return this.blockedHostSuffixes.some((suffix) => hostname.endsWith(suffix));
100
+ return this.ipBlocker.isHostBlocked(hostname, this.blockedHostSuffixes);
115
101
  }
116
102
  async assertNoBlockedCname(hostname, signal) {
117
103
  let current = hostname;
@@ -133,8 +119,9 @@ export class SafeDnsResolver {
133
119
  }
134
120
  async resolveCname(hostname, signal) {
135
121
  try {
136
- const resultPromise = dns.promises.resolveCname(hostname);
137
- const cnames = await withTimeout(resultPromise, DNS_LOOKUP_TIMEOUT_MS, () => createErrorWithCode(`DNS CNAME lookup timed out for ${hostname}`, 'ETIMEOUT'), signal, createAbortSignalError);
122
+ if (signal?.aborted)
123
+ throw createAbortSignalError();
124
+ const cnames = await this.cnameResolver.resolveCname(hostname);
138
125
  return cnames
139
126
  .map((value) => normalizeDnsName(value))
140
127
  .filter((value) => value.length > 0);
@@ -146,7 +133,8 @@ export class SafeDnsResolver {
146
133
  if (isSystemError(error) &&
147
134
  (error.code === 'ENODATA' ||
148
135
  error.code === 'ENOTFOUND' ||
149
- error.code === 'ENODOMAIN')) {
136
+ error.code === 'ENODOMAIN' ||
137
+ error.code === 'ETIMEOUT')) {
150
138
  return [];
151
139
  }
152
140
  logDebug('DNS CNAME lookup failed; continuing with address lookup', {
@@ -158,12 +146,10 @@ export class SafeDnsResolver {
158
146
  }
159
147
  }
160
148
  function extractHostname(url) {
161
- try {
162
- return new URL(url).hostname;
163
- }
164
- catch {
149
+ const parsed = URL.parse(url);
150
+ if (!parsed)
165
151
  throw createErrorWithCode('Invalid URL', 'EINVAL');
166
- }
152
+ return parsed.hostname;
167
153
  }
168
154
  export function createDnsPreflight(dnsResolver) {
169
155
  return async (url, signal) => {
@@ -178,19 +164,13 @@ export function normalizeHost(value) {
178
164
  const first = takeFirstHostValue(trimmedLower);
179
165
  if (!first)
180
166
  return null;
181
- for (const resolveCandidate of [
182
- () => normalizeSocketAddress(first),
183
- () => parseHostWithUrl(first),
184
- () => normalizeBracketedIpv6(first),
185
- ]) {
186
- const candidate = resolveCandidate();
187
- if (candidate !== null)
188
- return candidate;
189
- }
190
- if (isIpV6Literal(first)) {
191
- return normalizeHostname(first);
192
- }
193
- return normalizeHostname(stripPortIfPresent(first));
167
+ const socketAddr = SocketAddress.parse(first);
168
+ if (socketAddr)
169
+ return normalizeHostname(socketAddr.address);
170
+ const parsed = URL.parse(`http://${first}`);
171
+ if (parsed)
172
+ return normalizeHostname(parsed.hostname);
173
+ return normalizeHostname(first);
194
174
  }
195
175
  function takeFirstHostValue(value) {
196
176
  // Faster than split(',') for large forwarded headers; preserves behavior.
@@ -198,47 +178,6 @@ function takeFirstHostValue(value) {
198
178
  const first = commaIndex === -1 ? value : value.slice(0, commaIndex);
199
179
  return first ? trimToNull(first) : null;
200
180
  }
201
- function stripIpv6Brackets(value) {
202
- if (!value.startsWith('['))
203
- return null;
204
- const end = value.indexOf(']');
205
- if (end === -1)
206
- return null;
207
- return value.slice(1, end);
208
- }
209
- function stripPortIfPresent(value) {
210
- const colonIndex = value.indexOf(':');
211
- if (colonIndex === -1)
212
- return value;
213
- return value.slice(0, colonIndex);
214
- }
215
- function isIpV6Literal(value) {
216
- return isIP(value) === 6;
217
- }
218
- function normalizeSocketAddress(value) {
219
- const socketAddress = SocketAddress.parse(value);
220
- if (!socketAddress)
221
- return null;
222
- return normalizeHostname(socketAddress.address);
223
- }
224
- function normalizeBracketedIpv6(value) {
225
- const ipv6 = stripIpv6Brackets(value);
226
- if (!ipv6)
227
- return null;
228
- return normalizeHostname(ipv6);
229
- }
230
- function parseHostWithUrl(value) {
231
- const candidateUrl = `http://${value}`;
232
- if (!URL.canParse(candidateUrl))
233
- return null;
234
- try {
235
- const parsed = new URL(candidateUrl);
236
- return normalizeHostname(parsed.hostname);
237
- }
238
- catch {
239
- return null;
240
- }
241
- }
242
181
  function trimToNull(value) {
243
182
  const trimmed = value.trim();
244
183
  return trimmed ? trimmed : null;
@@ -284,23 +223,13 @@ export function createDefaultBlockList() {
284
223
  }
285
224
  return list;
286
225
  }
287
- function extractMappedIpv4(ip) {
288
- if (!ip.startsWith(IPV6_MAPPED_PREFIX))
289
- return null;
290
- const mapped = ip.slice(IPV6_MAPPED_PREFIX.length);
291
- return isIP(mapped) === 4 ? mapped : null;
292
- }
293
- function stripIpv6ZoneId(ip) {
294
- const zoneIndex = ip.indexOf('%');
295
- if (zoneIndex <= 0)
296
- return ip;
297
- return ip.slice(0, zoneIndex);
298
- }
299
226
  export function normalizeIpForBlockList(input) {
300
227
  const lowered = input.trim().toLowerCase();
301
228
  if (!lowered)
302
229
  return null;
303
- const normalizedInput = stripIpv6ZoneId(lowered);
230
+ // Strip IPv6 zone ID (e.g. %eth0)
231
+ const zoneIndex = lowered.indexOf('%');
232
+ const normalizedInput = zoneIndex > 0 ? lowered.slice(0, zoneIndex) : lowered;
304
233
  if (!normalizedInput)
305
234
  return null;
306
235
  const ipType = isIP(normalizedInput);
@@ -308,10 +237,13 @@ export function normalizeIpForBlockList(input) {
308
237
  case 4:
309
238
  return { ip: normalizedInput, family: 'ipv4' };
310
239
  case 6: {
311
- const mapped = extractMappedIpv4(normalizedInput);
312
- return mapped
313
- ? { ip: mapped, family: 'ipv4' }
314
- : { ip: normalizedInput, family: 'ipv6' };
240
+ // Extract mapped IPv4 from ::ffff: prefix
241
+ if (normalizedInput.startsWith(IPV6_MAPPED_PREFIX)) {
242
+ const mapped = normalizedInput.slice(IPV6_MAPPED_PREFIX.length);
243
+ if (isIP(mapped) === 4)
244
+ return { ip: mapped, family: 'ipv4' };
245
+ }
246
+ return { ip: normalizedInput, family: 'ipv6' };
315
247
  }
316
248
  default:
317
249
  return null;
@@ -385,12 +317,13 @@ export class RawUrlTransformer {
385
317
  let base;
386
318
  let hash;
387
319
  let parsed;
388
- try {
389
- parsed = new URL(url);
320
+ const maybeParsed = URL.parse(url);
321
+ if (maybeParsed) {
322
+ parsed = maybeParsed;
390
323
  base = parsed.origin + parsed.pathname;
391
324
  ({ hash } = parsed);
392
325
  }
393
- catch {
326
+ else {
394
327
  ({ base, hash } = this.splitParams(url));
395
328
  }
396
329
  const match = this.tryTransformWithUrl(base, hash, parsed);
@@ -408,22 +341,20 @@ export class RawUrlTransformer {
408
341
  return false;
409
342
  if (this.isRawUrl(urlString))
410
343
  return true;
411
- try {
412
- const url = new URL(urlString);
413
- const pathname = url.pathname.toLowerCase();
344
+ const parsed = URL.parse(urlString);
345
+ if (parsed) {
346
+ const pathname = parsed.pathname.toLowerCase();
414
347
  const lastDot = pathname.lastIndexOf('.');
415
348
  if (lastDot === -1)
416
349
  return false;
417
350
  return RAW_TEXT_EXTENSIONS.has(pathname.slice(lastDot));
418
351
  }
419
- catch {
420
- const { base } = this.splitParams(urlString);
421
- const lowerBase = base.toLowerCase();
422
- const lastDot = lowerBase.lastIndexOf('.');
423
- if (lastDot === -1)
424
- return false;
425
- return RAW_TEXT_EXTENSIONS.has(lowerBase.slice(lastDot));
426
- }
352
+ const { base } = this.splitParams(urlString);
353
+ const lowerBase = base.toLowerCase();
354
+ const lastDot = lowerBase.lastIndexOf('.');
355
+ if (lastDot === -1)
356
+ return false;
357
+ return RAW_TEXT_EXTENSIONS.has(lowerBase.slice(lastDot));
427
358
  }
428
359
  isRawUrl(url) {
429
360
  const lower = url.toLowerCase();
@@ -440,15 +371,7 @@ export class RawUrlTransformer {
440
371
  return { base: urlString.slice(0, endIndex), hash };
441
372
  }
442
373
  tryTransformWithUrl(base, hash, preParsed) {
443
- let parsed = preParsed ?? null;
444
- if (!parsed) {
445
- try {
446
- parsed = new URL(base);
447
- }
448
- catch {
449
- // Ignore invalid URLs
450
- }
451
- }
374
+ const parsed = preParsed ?? URL.parse(base);
452
375
  if (!parsed)
453
376
  return null;
454
377
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
@@ -594,6 +517,15 @@ export class IpBlocker {
594
517
  ? this.blockList.check(normalizedIp.ip, normalizedIp.family)
595
518
  : false;
596
519
  }
520
+ isHostBlocked(hostname, blockedHostSuffixes) {
521
+ if (isCloudMetadataHost(hostname))
522
+ return true;
523
+ if (isLocalFetchAllowed())
524
+ return false;
525
+ if (this.security.blockedHosts.has(hostname))
526
+ return true;
527
+ return blockedHostSuffixes.some((suffix) => hostname.endsWith(suffix));
528
+ }
597
529
  }
598
530
  export class UrlNormalizer {
599
531
  constants;
@@ -611,11 +543,8 @@ export class UrlNormalizer {
611
543
  if (trimmedUrl.length > this.constants.maxUrlLength) {
612
544
  throw createValidationError(`URL exceeds maximum length of ${this.constants.maxUrlLength} characters`);
613
545
  }
614
- let url;
615
- try {
616
- url = new URL(trimmedUrl);
617
- }
618
- catch {
546
+ const url = URL.parse(trimmedUrl);
547
+ if (!url) {
619
548
  throw createValidationError('Invalid URL format');
620
549
  }
621
550
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
@@ -652,13 +581,11 @@ export class UrlNormalizer {
652
581
  if (isCloudMetadataHost(hostname)) {
653
582
  throw createValidationError(`Blocked host: ${hostname}. Cloud metadata endpoints are not allowed`);
654
583
  }
655
- if (!isLocalFetchAllowed()) {
656
- if (this.security.blockedHosts.has(hostname)) {
657
- throw createValidationError(`Blocked host: ${hostname}. Internal hosts are not allowed`);
658
- }
659
- if (this.ipBlocker.isBlockedIp(hostname)) {
660
- throw createValidationError(`Blocked IP range: ${hostname}. Private IPs are not allowed`);
661
- }
584
+ if (!isLocalFetchAllowed() && this.security.blockedHosts.has(hostname)) {
585
+ throw createValidationError(`Blocked host: ${hostname}. Internal hosts are not allowed`);
586
+ }
587
+ if (!isLocalFetchAllowed() && this.ipBlocker.isBlockedIp(hostname)) {
588
+ throw createValidationError(`Blocked IP range: ${hostname}. Private IPs are not allowed`);
662
589
  }
663
590
  if (this.blockedHostSuffixes.some((suffix) => hostname.endsWith(suffix))) {
664
591
  throw createValidationError(`Blocked hostname pattern: ${hostname}. Internal domain suffixes are not allowed`);
@@ -15,8 +15,7 @@ export declare function toError(error: unknown): Error;
15
15
  export declare function isAbortError(error: unknown): boolean;
16
16
  export declare function createErrorWithCode(message: string, code: string, options?: ErrorOptions): NodeJS.ErrnoException;
17
17
  export declare function isSystemError(error: unknown): error is NodeJS.ErrnoException;
18
- export declare const RESOURCE_NOT_FOUND_ERROR_CODE = -32002;
19
- export declare function stableStringify(obj: unknown, depth?: number, seen?: WeakSet<WeakKey>): string;
18
+ export declare function stableStringify(obj: unknown): string;
20
19
  interface HttpServerTuningTarget {
21
20
  headersTimeout?: number;
22
21
  requestTimeout?: number;
@@ -56,3 +55,4 @@ interface LikeNode {
56
55
  }
57
56
  export declare function isLikeNode(value: unknown): value is LikeNode;
58
57
  export {};
58
+ //# sourceMappingURL=utils.d.ts.map