@llmindset/hf-mcp 0.3.2 → 0.3.3

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 (137) hide show
  1. package/dist/docs-search/doc-fetch.d.ts +1 -0
  2. package/dist/docs-search/doc-fetch.d.ts.map +1 -1
  3. package/dist/docs-search/doc-fetch.js +9 -12
  4. package/dist/docs-search/doc-fetch.js.map +1 -1
  5. package/dist/docs-search/doc-fetch.test.js +56 -11
  6. package/dist/docs-search/doc-fetch.test.js.map +1 -1
  7. package/dist/file-icons.d.ts +3 -0
  8. package/dist/file-icons.d.ts.map +1 -0
  9. package/dist/file-icons.js +38 -0
  10. package/dist/file-icons.js.map +1 -0
  11. package/dist/gradio-files.d.ts +0 -1
  12. package/dist/gradio-files.d.ts.map +1 -1
  13. package/dist/gradio-files.js +2 -35
  14. package/dist/gradio-files.js.map +1 -1
  15. package/dist/hf-api-call.d.ts.map +1 -1
  16. package/dist/hf-api-call.js +7 -7
  17. package/dist/hf-api-call.js.map +1 -1
  18. package/dist/index.browser.d.ts +48 -0
  19. package/dist/index.browser.d.ts.map +1 -0
  20. package/dist/index.browser.js +153 -0
  21. package/dist/index.browser.js.map +1 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/jobs/commands/uv-utils.d.ts +0 -3
  27. package/dist/jobs/commands/uv-utils.d.ts.map +1 -1
  28. package/dist/jobs/commands/uv-utils.js +2 -2
  29. package/dist/jobs/commands/uv-utils.js.map +1 -1
  30. package/dist/jobs/jobs-tool.d.ts.map +1 -1
  31. package/dist/jobs/jobs-tool.js +11 -12
  32. package/dist/jobs/jobs-tool.js.map +1 -1
  33. package/dist/jobs/schema-help.d.ts +2 -9
  34. package/dist/jobs/schema-help.d.ts.map +1 -1
  35. package/dist/jobs/schema-help.js +3 -3
  36. package/dist/jobs/schema-help.js.map +1 -1
  37. package/dist/jobs/sse-handler.d.ts +3 -2
  38. package/dist/jobs/sse-handler.d.ts.map +1 -1
  39. package/dist/jobs/sse-handler.js +8 -4
  40. package/dist/jobs/sse-handler.js.map +1 -1
  41. package/dist/jobs/types.d.ts +1 -1
  42. package/dist/logger.d.ts +2 -2
  43. package/dist/logger.d.ts.map +1 -1
  44. package/dist/network/fetch-profile.d.ts +24 -0
  45. package/dist/network/fetch-profile.d.ts.map +1 -0
  46. package/dist/network/fetch-profile.js +80 -0
  47. package/dist/network/fetch-profile.js.map +1 -0
  48. package/dist/network/index.d.ts +5 -0
  49. package/dist/network/index.d.ts.map +1 -0
  50. package/dist/network/index.js +5 -0
  51. package/dist/network/index.js.map +1 -0
  52. package/dist/network/ip-policy.d.ts +6 -0
  53. package/dist/network/ip-policy.d.ts.map +1 -0
  54. package/dist/network/ip-policy.js +166 -0
  55. package/dist/network/ip-policy.js.map +1 -0
  56. package/dist/network/ip-policy.test.d.ts +2 -0
  57. package/dist/network/ip-policy.test.d.ts.map +1 -0
  58. package/dist/network/ip-policy.test.js +26 -0
  59. package/dist/network/ip-policy.test.js.map +1 -0
  60. package/dist/network/safe-fetch.d.ts +16 -0
  61. package/dist/network/safe-fetch.d.ts.map +1 -0
  62. package/dist/network/safe-fetch.js +124 -0
  63. package/dist/network/safe-fetch.js.map +1 -0
  64. package/dist/network/safe-fetch.test.d.ts +2 -0
  65. package/dist/network/safe-fetch.test.d.ts.map +1 -0
  66. package/dist/network/safe-fetch.test.js +136 -0
  67. package/dist/network/safe-fetch.test.js.map +1 -0
  68. package/dist/network/url-policy.d.ts +32 -0
  69. package/dist/network/url-policy.d.ts.map +1 -0
  70. package/dist/network/url-policy.js +230 -0
  71. package/dist/network/url-policy.js.map +1 -0
  72. package/dist/network/url-policy.test.d.ts +2 -0
  73. package/dist/network/url-policy.test.d.ts.map +1 -0
  74. package/dist/network/url-policy.test.js +57 -0
  75. package/dist/network/url-policy.test.js.map +1 -0
  76. package/dist/readme-utils.d.ts.map +1 -1
  77. package/dist/readme-utils.js +3 -4
  78. package/dist/readme-utils.js.map +1 -1
  79. package/dist/space/commands/discover.d.ts +0 -5
  80. package/dist/space/commands/discover.d.ts.map +1 -1
  81. package/dist/space/commands/discover.js +9 -2
  82. package/dist/space/commands/discover.js.map +1 -1
  83. package/dist/space/commands/invoke.js +1 -59
  84. package/dist/space/commands/invoke.js.map +1 -1
  85. package/dist/space/commands/view-parameters.d.ts.map +1 -1
  86. package/dist/space/commands/view-parameters.js +3 -98
  87. package/dist/space/commands/view-parameters.js.map +1 -1
  88. package/dist/space/dynamic-space-tool.d.ts.map +1 -1
  89. package/dist/space/dynamic-space-tool.js +5 -2
  90. package/dist/space/dynamic-space-tool.js.map +1 -1
  91. package/dist/space/utils/gradio-caller.d.ts.map +1 -1
  92. package/dist/space/utils/gradio-caller.js +13 -6
  93. package/dist/space/utils/gradio-caller.js.map +1 -1
  94. package/dist/space/utils/space-http.d.ts +8 -0
  95. package/dist/space/utils/space-http.d.ts.map +1 -0
  96. package/dist/space/utils/space-http.js +49 -0
  97. package/dist/space/utils/space-http.js.map +1 -0
  98. package/dist/space-files.d.ts +0 -1
  99. package/dist/space-files.d.ts.map +1 -1
  100. package/dist/space-files.js +3 -36
  101. package/dist/space-files.js.map +1 -1
  102. package/package.json +6 -2
  103. package/src/docs-search/doc-fetch.test.ts +98 -28
  104. package/src/docs-search/doc-fetch.ts +9 -16
  105. package/src/file-icons.ts +39 -0
  106. package/src/gradio-files.ts +2 -40
  107. package/src/hf-api-call.ts +8 -10
  108. package/src/index.browser.ts +183 -0
  109. package/src/index.ts +1 -0
  110. package/src/jobs/commands/uv-utils.ts +2 -2
  111. package/src/jobs/jobs-tool.ts +13 -12
  112. package/src/jobs/schema-help.ts +4 -4
  113. package/src/jobs/sse-handler.ts +12 -7
  114. package/src/logger.ts +2 -2
  115. package/src/network/fetch-profile.ts +112 -0
  116. package/src/network/index.ts +4 -0
  117. package/src/network/ip-policy.test.ts +29 -0
  118. package/src/network/ip-policy.ts +206 -0
  119. package/src/network/safe-fetch.test.ts +181 -0
  120. package/src/network/safe-fetch.ts +174 -0
  121. package/src/network/url-policy.test.ts +100 -0
  122. package/src/network/url-policy.ts +304 -0
  123. package/src/readme-utils.ts +11 -10
  124. package/src/space/commands/discover.ts +10 -2
  125. package/src/space/commands/invoke.ts +1 -88
  126. package/src/space/commands/view-parameters.ts +3 -136
  127. package/src/space/dynamic-space-tool.ts +6 -2
  128. package/src/space/utils/gradio-caller.ts +25 -12
  129. package/src/space/utils/space-http.ts +75 -0
  130. package/src/space-files.ts +3 -41
  131. package/test/fetch-guard.spec.ts +70 -0
  132. package/test/jobs/sse-handler.spec.ts +60 -0
  133. package/dist/space/utils/result-formatter.d.ts +0 -4
  134. package/dist/space/utils/result-formatter.d.ts.map +0 -1
  135. package/dist/space/utils/result-formatter.js +0 -146
  136. package/dist/space/utils/result-formatter.js.map +0 -1
  137. package/src/space/utils/result-formatter.ts +0 -226
@@ -0,0 +1,80 @@
1
+ import { safeFetch } from './safe-fetch.js';
2
+ import { createExternalHttpsPolicy, createGradioMcpHostPolicy, createGradioSchemaHostPolicy, createHfDocsPolicy, createHttpOrHttpsPolicy, createHuggingFaceHubPolicy, createLocalhostHttpPolicy, isLocalhostHostname, } from './url-policy.js';
3
+ const DEFAULT_TIMEOUT_MS = 12_500;
4
+ const DEFAULT_MAX_REDIRECTS = 3;
5
+ export async function fetchWithProfile(url, profile, options = {}) {
6
+ return safeFetch(url, {
7
+ urlPolicy: profile.urlPolicy,
8
+ timeoutMs: options.timeoutMs ?? profile.timeoutMs,
9
+ maxRedirects: profile.maxRedirects,
10
+ externalOnly: profile.externalOnly,
11
+ requestInit: options.requestInit,
12
+ });
13
+ }
14
+ export const NETWORK_FETCH_PROFILES = {
15
+ externalHttps() {
16
+ return {
17
+ urlPolicy: createExternalHttpsPolicy(),
18
+ timeoutMs: DEFAULT_TIMEOUT_MS,
19
+ maxRedirects: DEFAULT_MAX_REDIRECTS,
20
+ externalOnly: true,
21
+ };
22
+ },
23
+ httpOrHttpsPermissive() {
24
+ return {
25
+ urlPolicy: createHttpOrHttpsPolicy(),
26
+ timeoutMs: DEFAULT_TIMEOUT_MS,
27
+ maxRedirects: DEFAULT_MAX_REDIRECTS,
28
+ externalOnly: false,
29
+ };
30
+ },
31
+ streamableProxy() {
32
+ return {
33
+ urlPolicy: createHttpOrHttpsPolicy(),
34
+ timeoutMs: 0,
35
+ maxRedirects: DEFAULT_MAX_REDIRECTS,
36
+ externalOnly: false,
37
+ };
38
+ },
39
+ hfHub() {
40
+ return {
41
+ urlPolicy: createHuggingFaceHubPolicy(),
42
+ timeoutMs: DEFAULT_TIMEOUT_MS,
43
+ maxRedirects: DEFAULT_MAX_REDIRECTS,
44
+ externalOnly: true,
45
+ };
46
+ },
47
+ hfDocs() {
48
+ return {
49
+ urlPolicy: createHfDocsPolicy(),
50
+ timeoutMs: DEFAULT_TIMEOUT_MS,
51
+ maxRedirects: 5,
52
+ externalOnly: true,
53
+ };
54
+ },
55
+ localhostHttp() {
56
+ return {
57
+ urlPolicy: createLocalhostHttpPolicy(),
58
+ timeoutMs: DEFAULT_TIMEOUT_MS,
59
+ maxRedirects: 2,
60
+ externalOnly: false,
61
+ };
62
+ },
63
+ gradioSchemaHost(hostname) {
64
+ return {
65
+ urlPolicy: createGradioSchemaHostPolicy(hostname),
66
+ timeoutMs: 10_000,
67
+ maxRedirects: 2,
68
+ externalOnly: !isLocalhostHostname(hostname),
69
+ };
70
+ },
71
+ gradioMcpHost(hostname, allowedProtocol) {
72
+ return {
73
+ urlPolicy: createGradioMcpHostPolicy(hostname, allowedProtocol),
74
+ timeoutMs: 0,
75
+ maxRedirects: 0,
76
+ externalOnly: !isLocalhostHostname(hostname) && process.env.NODE_ENV !== 'test',
77
+ };
78
+ },
79
+ };
80
+ //# sourceMappingURL=fetch-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-profile.js","sourceRoot":"","sources":["../../src/network/fetch-profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAwB,MAAM,iBAAiB,CAAC;AAClE,OAAO,EACN,yBAAyB,EACzB,yBAAyB,EACzB,4BAA4B,EAC5B,kBAAkB,EAClB,uBAAuB,EACvB,0BAA0B,EAC1B,yBAAyB,EACzB,mBAAmB,GAGnB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAClC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAchC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,GAAiB,EACjB,OAAyB,EACzB,UAAmC,EAAE;IAErC,OAAO,SAAS,CAAC,GAAG,EAAE;QACrB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS;QACjD,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,WAAW,EAAE,OAAO,CAAC,WAAW;KAChC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACrC,aAAa;QACZ,OAAO;YACN,SAAS,EAAE,yBAAyB,EAAE;YACtC,SAAS,EAAE,kBAAkB;YAC7B,YAAY,EAAE,qBAAqB;YACnC,YAAY,EAAE,IAAI;SAClB,CAAC;IACH,CAAC;IACD,qBAAqB;QACpB,OAAO;YACN,SAAS,EAAE,uBAAuB,EAAE;YACpC,SAAS,EAAE,kBAAkB;YAC7B,YAAY,EAAE,qBAAqB;YACnC,YAAY,EAAE,KAAK;SACnB,CAAC;IACH,CAAC;IACD,eAAe;QACd,OAAO;YACN,SAAS,EAAE,uBAAuB,EAAE;YACpC,SAAS,EAAE,CAAC;YACZ,YAAY,EAAE,qBAAqB;YACnC,YAAY,EAAE,KAAK;SACnB,CAAC;IACH,CAAC;IACD,KAAK;QACJ,OAAO;YACN,SAAS,EAAE,0BAA0B,EAAE;YACvC,SAAS,EAAE,kBAAkB;YAC7B,YAAY,EAAE,qBAAqB;YACnC,YAAY,EAAE,IAAI;SAClB,CAAC;IACH,CAAC;IACD,MAAM;QACL,OAAO;YACN,SAAS,EAAE,kBAAkB,EAAE;YAC/B,SAAS,EAAE,kBAAkB;YAC7B,YAAY,EAAE,CAAC;YACf,YAAY,EAAE,IAAI;SAClB,CAAC;IACH,CAAC;IACD,aAAa;QACZ,OAAO;YACN,SAAS,EAAE,yBAAyB,EAAE;YACtC,SAAS,EAAE,kBAAkB;YAC7B,YAAY,EAAE,CAAC;YACf,YAAY,EAAE,KAAK;SACnB,CAAC;IACH,CAAC;IACD,gBAAgB,CAAC,QAAgB;QAChC,OAAO;YACN,SAAS,EAAE,4BAA4B,CAAC,QAAQ,CAAC;YACjD,SAAS,EAAE,MAAM;YACjB,YAAY,EAAE,CAAC;YACf,YAAY,EAAE,CAAC,mBAAmB,CAAC,QAAQ,CAAC;SAC5C,CAAC;IACH,CAAC;IACD,aAAa,CACZ,QAAgB,EAChB,eAA4B;QAE5B,OAAO;YACN,SAAS,EAAE,yBAAyB,CAAC,QAAQ,EAAE,eAAe,CAAC;YAC/D,SAAS,EAAE,CAAC;YACZ,YAAY,EAAE,CAAC;YACf,YAAY,EAAE,CAAC,mBAAmB,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM;SAC/E,CAAC;IACH,CAAC;CACD,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './url-policy.js';
2
+ export * from './ip-policy.js';
3
+ export * from './safe-fetch.js';
4
+ export * from './fetch-profile.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/network/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './url-policy.js';
2
+ export * from './ip-policy.js';
3
+ export * from './safe-fetch.js';
4
+ export * from './fetch-profile.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/network/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,6 @@
1
+ export interface ExternalAddressOptions {
2
+ allowDnsRebindMitigation?: boolean;
3
+ }
4
+ export declare function isIpInternalOrReserved(ip: string): boolean;
5
+ export declare function assertExternalAddress(hostname: string, options?: ExternalAddressOptions): Promise<void>;
6
+ //# sourceMappingURL=ip-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ip-policy.d.ts","sourceRoot":"","sources":["../../src/network/ip-policy.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACtC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACnC;AAmID,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAY1D;AAwBD,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAoCjH"}
@@ -0,0 +1,166 @@
1
+ function normalizeIpLiteral(host) {
2
+ if (host.startsWith('[') && host.endsWith(']')) {
3
+ return host.slice(1, -1);
4
+ }
5
+ return host;
6
+ }
7
+ function parseIpv4ToInt(ip) {
8
+ const parts = ip.split('.').map((part) => Number.parseInt(part, 10));
9
+ if (parts.length !== 4 || parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)) {
10
+ throw new Error(`Invalid IPv4 address: ${ip}`);
11
+ }
12
+ return parts.reduce((acc, part) => acc * 256 + part, 0);
13
+ }
14
+ function ipv4InRange(ipValue, start, end) {
15
+ const startValue = parseIpv4ToInt(start);
16
+ const endValue = parseIpv4ToInt(end);
17
+ return ipValue >= startValue && ipValue <= endValue;
18
+ }
19
+ function isIpv4InternalOrReserved(ip) {
20
+ const value = parseIpv4ToInt(ip);
21
+ return (ipv4InRange(value, '0.0.0.0', '0.255.255.255') ||
22
+ ipv4InRange(value, '10.0.0.0', '10.255.255.255') ||
23
+ ipv4InRange(value, '100.64.0.0', '100.127.255.255') ||
24
+ ipv4InRange(value, '127.0.0.0', '127.255.255.255') ||
25
+ ipv4InRange(value, '169.254.0.0', '169.254.255.255') ||
26
+ ipv4InRange(value, '172.16.0.0', '172.31.255.255') ||
27
+ ipv4InRange(value, '192.0.0.0', '192.0.0.255') ||
28
+ ipv4InRange(value, '192.0.2.0', '192.0.2.255') ||
29
+ ipv4InRange(value, '192.88.99.0', '192.88.99.255') ||
30
+ ipv4InRange(value, '192.168.0.0', '192.168.255.255') ||
31
+ ipv4InRange(value, '198.18.0.0', '198.19.255.255') ||
32
+ ipv4InRange(value, '198.51.100.0', '198.51.100.255') ||
33
+ ipv4InRange(value, '203.0.113.0', '203.0.113.255') ||
34
+ ipv4InRange(value, '224.0.0.0', '239.255.255.255') ||
35
+ ipv4InRange(value, '240.0.0.0', '255.255.255.255'));
36
+ }
37
+ function parseIpv6ToBigInt(ip) {
38
+ const zoneIndex = ip.indexOf('%');
39
+ const zoneStripped = zoneIndex >= 0 ? ip.slice(0, zoneIndex) : ip;
40
+ let working = zoneStripped;
41
+ if (working.includes('.')) {
42
+ const lastColon = working.lastIndexOf(':');
43
+ if (lastColon < 0) {
44
+ throw new Error(`Invalid IPv6 address: ${ip}`);
45
+ }
46
+ const ipv4Part = working.slice(lastColon + 1);
47
+ const ipv4Value = parseIpv4ToInt(ipv4Part);
48
+ const high = ((ipv4Value >>> 16) & 0xffff).toString(16);
49
+ const low = (ipv4Value & 0xffff).toString(16);
50
+ working = `${working.slice(0, lastColon)}:${high}:${low}`;
51
+ }
52
+ const split = working.split('::');
53
+ if (split.length > 2) {
54
+ throw new Error(`Invalid IPv6 address: ${ip}`);
55
+ }
56
+ const left = split[0] ? split[0].split(':').filter(Boolean) : [];
57
+ const right = split[1] ? split[1].split(':').filter(Boolean) : [];
58
+ const missingCount = 8 - (left.length + right.length);
59
+ if (split.length === 1 && missingCount !== 0) {
60
+ throw new Error(`Invalid IPv6 address: ${ip}`);
61
+ }
62
+ if (missingCount < 0) {
63
+ throw new Error(`Invalid IPv6 address: ${ip}`);
64
+ }
65
+ const full = [...left, ...Array.from({ length: missingCount }, () => '0'), ...right];
66
+ if (full.length !== 8) {
67
+ throw new Error(`Invalid IPv6 address: ${ip}`);
68
+ }
69
+ let value = 0n;
70
+ for (const part of full) {
71
+ const segment = Number.parseInt(part, 16);
72
+ if (Number.isNaN(segment) || segment < 0 || segment > 0xffff) {
73
+ throw new Error(`Invalid IPv6 address: ${ip}`);
74
+ }
75
+ value = (value << 16n) + BigInt(segment);
76
+ }
77
+ return value;
78
+ }
79
+ function isIpv6InCidr(ipValue, prefixValue, prefixLength) {
80
+ const hostBits = 128n - BigInt(prefixLength);
81
+ const mask = ((1n << BigInt(prefixLength)) - 1n) << hostBits;
82
+ return (ipValue & mask) === (prefixValue & mask);
83
+ }
84
+ function isIpv6InternalOrReserved(ip) {
85
+ const value = parseIpv6ToBigInt(ip);
86
+ if (value === 0n || value === 1n) {
87
+ return true;
88
+ }
89
+ if (value >> 32n === 0xffffn) {
90
+ const ipv4Value = Number(value & 0xffffffffn);
91
+ const octet1 = (ipv4Value >>> 24) & 0xff;
92
+ const octet2 = (ipv4Value >>> 16) & 0xff;
93
+ const octet3 = (ipv4Value >>> 8) & 0xff;
94
+ const octet4 = ipv4Value & 0xff;
95
+ return isIpv4InternalOrReserved(`${octet1.toString()}.${octet2.toString()}.${octet3.toString()}.${octet4.toString()}`);
96
+ }
97
+ return (isIpv6InCidr(value, 0xfc00n << 112n, 7) ||
98
+ isIpv6InCidr(value, 0xfe80n << 112n, 10) ||
99
+ isIpv6InCidr(value, 0xff00n << 112n, 8) ||
100
+ isIpv6InCidr(value, 0x20010db8n << 96n, 32) ||
101
+ isIpv6InCidr(value, 0x20010010n << 96n, 28));
102
+ }
103
+ export function isIpInternalOrReserved(ip) {
104
+ const normalizedIp = normalizeIpLiteral(ip);
105
+ const ipVersion = detectIpVersion(normalizedIp);
106
+ if (ipVersion === 0) {
107
+ throw new Error(`Invalid IP address: ${ip}`);
108
+ }
109
+ if (ipVersion === 4) {
110
+ return isIpv4InternalOrReserved(normalizedIp);
111
+ }
112
+ return isIpv6InternalOrReserved(normalizedIp);
113
+ }
114
+ async function lookupAll(hostname) {
115
+ const { lookup } = await import('node:dns/promises');
116
+ const results = await lookup(hostname, { all: true, verbatim: true });
117
+ return results.map((entry) => entry.address);
118
+ }
119
+ function detectIpVersion(candidate) {
120
+ try {
121
+ parseIpv4ToInt(candidate);
122
+ return 4;
123
+ }
124
+ catch {
125
+ }
126
+ try {
127
+ parseIpv6ToBigInt(candidate);
128
+ return 6;
129
+ }
130
+ catch {
131
+ return 0;
132
+ }
133
+ }
134
+ export async function assertExternalAddress(hostname, options = {}) {
135
+ const { allowDnsRebindMitigation = true } = options;
136
+ const normalized = hostname.trim().replace(/\.+$/, '');
137
+ if (!normalized) {
138
+ throw new Error('Hostname is required for external address check');
139
+ }
140
+ const ipLiteral = normalizeIpLiteral(normalized);
141
+ const ipVersion = detectIpVersion(ipLiteral);
142
+ if (ipVersion !== 0) {
143
+ if (isIpInternalOrReserved(ipLiteral)) {
144
+ throw new Error(`Blocked internal or reserved address: ${ipLiteral}`);
145
+ }
146
+ return;
147
+ }
148
+ const firstLookup = await lookupAll(normalized);
149
+ if (firstLookup.length === 0) {
150
+ throw new Error(`No DNS records found for hostname: ${normalized}`);
151
+ }
152
+ for (const address of firstLookup) {
153
+ if (isIpInternalOrReserved(address)) {
154
+ throw new Error(`Blocked internal or reserved address for hostname ${normalized}: ${address}`);
155
+ }
156
+ }
157
+ if (allowDnsRebindMitigation) {
158
+ const secondLookup = await lookupAll(normalized);
159
+ for (const address of secondLookup) {
160
+ if (isIpInternalOrReserved(address)) {
161
+ throw new Error(`Blocked internal or reserved address for hostname ${normalized}: ${address}`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ //# sourceMappingURL=ip-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ip-policy.js","sourceRoot":"","sources":["../../src/network/ip-policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ip-policy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ip-policy.test.d.ts","sourceRoot":"","sources":["../../src/network/ip-policy.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { assertExternalAddress, isIpInternalOrReserved } from './ip-policy.js';
3
+ describe('ip-policy', () => {
4
+ it('classifies internal/reserved IPv4 ranges', () => {
5
+ expect(isIpInternalOrReserved('127.0.0.1')).toBe(true);
6
+ expect(isIpInternalOrReserved('10.1.2.3')).toBe(true);
7
+ expect(isIpInternalOrReserved('172.20.10.2')).toBe(true);
8
+ expect(isIpInternalOrReserved('192.168.1.1')).toBe(true);
9
+ expect(isIpInternalOrReserved('8.8.8.8')).toBe(false);
10
+ });
11
+ it('classifies internal/reserved IPv6 ranges', () => {
12
+ expect(isIpInternalOrReserved('::1')).toBe(true);
13
+ expect(isIpInternalOrReserved('fc00::1')).toBe(true);
14
+ expect(isIpInternalOrReserved('fe80::1')).toBe(true);
15
+ expect(isIpInternalOrReserved('2001:db8::1')).toBe(true);
16
+ expect(isIpInternalOrReserved('2607:f8b0:4005:80a::200e')).toBe(false);
17
+ });
18
+ it('blocks internal literal addresses in assertExternalAddress', async () => {
19
+ await expect(assertExternalAddress('127.0.0.1')).rejects.toThrow('Blocked internal or reserved address');
20
+ await expect(assertExternalAddress('::1')).rejects.toThrow('Blocked internal or reserved address');
21
+ });
22
+ it('allows external literal addresses in assertExternalAddress', async () => {
23
+ await expect(assertExternalAddress('8.8.8.8')).resolves.toBeUndefined();
24
+ });
25
+ });
26
+ //# sourceMappingURL=ip-policy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ip-policy.test.js","sourceRoot":"","sources":["../../src/network/ip-policy.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE/E,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,sBAAsB,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,CAAC,sBAAsB,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,CAAC,sBAAsB,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzD,MAAM,CAAC,sBAAsB,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzD,MAAM,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,CAAC,sBAAsB,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzD,MAAM,CAAC,sBAAsB,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,MAAM,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC;QACzG,MAAM,MAAM,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC;IACpG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,MAAM,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;IACzE,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type UrlPolicy } from './url-policy.js';
2
+ export interface SafeFetchOptions {
3
+ urlPolicy: UrlPolicy;
4
+ timeoutMs?: number;
5
+ maxRedirects?: number;
6
+ externalOnly?: boolean;
7
+ requestInit?: RequestInit;
8
+ stripSensitiveHeadersOnCrossHostRedirect?: boolean;
9
+ }
10
+ export interface SafeFetchResult {
11
+ response: Response;
12
+ finalUrl: URL;
13
+ redirectsFollowed: number;
14
+ }
15
+ export declare function safeFetch(url: string | URL, options: SafeFetchOptions): Promise<SafeFetchResult>;
16
+ //# sourceMappingURL=safe-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/network/safe-fetch.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,SAAS,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,wCAAwC,CAAC,EAAE,OAAO,CAAC;CACnD;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,GAAG,CAAC;IACd,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AA4ED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAiFtG"}
@@ -0,0 +1,124 @@
1
+ import { assertExternalAddress } from './ip-policy.js';
2
+ import { parseAndValidateUrl } from './url-policy.js';
3
+ const DEFAULT_TIMEOUT_MS = 12500;
4
+ const DEFAULT_MAX_REDIRECTS = 5;
5
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
6
+ const SENSITIVE_HEADERS = new Set(['authorization', 'proxy-authorization', 'cookie', 'x-hf-authorization']);
7
+ function isRedirectStatus(status) {
8
+ return REDIRECT_STATUSES.has(status);
9
+ }
10
+ function dropSensitiveHeaders(headersInit) {
11
+ const headers = new Headers(headersInit);
12
+ for (const key of SENSITIVE_HEADERS) {
13
+ headers.delete(key);
14
+ }
15
+ return headers;
16
+ }
17
+ function withMethodAndBody(requestInit, method, body) {
18
+ const nextInit = {
19
+ ...requestInit,
20
+ method,
21
+ redirect: 'manual',
22
+ };
23
+ if (body !== undefined && body !== null && method !== 'GET' && method !== 'HEAD') {
24
+ nextInit.body = body;
25
+ }
26
+ else {
27
+ delete nextInit.body;
28
+ }
29
+ return nextInit;
30
+ }
31
+ async function fetchWithTimeout(url, requestInit, timeoutMs) {
32
+ if (timeoutMs <= 0) {
33
+ return fetch(url.toString(), {
34
+ ...requestInit,
35
+ redirect: 'manual',
36
+ });
37
+ }
38
+ const outerSignal = requestInit.signal;
39
+ if (outerSignal) {
40
+ if (outerSignal.aborted) {
41
+ throw new Error('Request was aborted');
42
+ }
43
+ }
44
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
45
+ const signal = outerSignal ? AbortSignal.any([outerSignal, timeoutSignal]) : timeoutSignal;
46
+ try {
47
+ return await fetch(url.toString(), {
48
+ ...requestInit,
49
+ signal,
50
+ redirect: 'manual',
51
+ });
52
+ }
53
+ catch (error) {
54
+ if (error instanceof Error && error.name === 'AbortError') {
55
+ if (outerSignal?.aborted) {
56
+ throw new Error('Request was aborted');
57
+ }
58
+ if (timeoutSignal.aborted) {
59
+ throw new Error(`Request timed out after ${timeoutMs.toString()}ms`);
60
+ }
61
+ throw new Error(`Request timed out after ${timeoutMs.toString()}ms`);
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+ export async function safeFetch(url, options) {
67
+ const { urlPolicy, timeoutMs = DEFAULT_TIMEOUT_MS, maxRedirects = DEFAULT_MAX_REDIRECTS, externalOnly = false, requestInit = {}, stripSensitiveHeadersOnCrossHostRedirect = true, } = options;
68
+ if (maxRedirects < 0) {
69
+ throw new Error('maxRedirects must be >= 0');
70
+ }
71
+ let currentUrl = parseAndValidateUrl(url, urlPolicy);
72
+ if (externalOnly) {
73
+ await assertExternalAddress(currentUrl.hostname);
74
+ }
75
+ const baseHeaders = new Headers(requestInit.headers);
76
+ let currentMethod = (requestInit.method || 'GET').toUpperCase();
77
+ let currentBody = requestInit.body;
78
+ let redirectsFollowed = 0;
79
+ while (true) {
80
+ const currentInit = withMethodAndBody({
81
+ ...requestInit,
82
+ headers: baseHeaders,
83
+ }, currentMethod, currentBody);
84
+ const response = await fetchWithTimeout(currentUrl, currentInit, timeoutMs);
85
+ if (!isRedirectStatus(response.status)) {
86
+ return {
87
+ response,
88
+ finalUrl: currentUrl,
89
+ redirectsFollowed,
90
+ };
91
+ }
92
+ if (redirectsFollowed >= maxRedirects) {
93
+ throw new Error(`Redirect limit exceeded (${maxRedirects.toString()})`);
94
+ }
95
+ const location = response.headers.get('location');
96
+ if (!location) {
97
+ throw new Error(`Redirect response missing Location header (status ${response.status.toString()})`);
98
+ }
99
+ const nextCandidate = new URL(location, currentUrl);
100
+ const nextUrl = parseAndValidateUrl(nextCandidate, urlPolicy);
101
+ if (externalOnly) {
102
+ await assertExternalAddress(nextUrl.hostname);
103
+ }
104
+ if (stripSensitiveHeadersOnCrossHostRedirect && currentUrl.origin !== nextUrl.origin) {
105
+ const filtered = dropSensitiveHeaders(baseHeaders);
106
+ baseHeaders.forEach((_, key) => {
107
+ baseHeaders.delete(key);
108
+ });
109
+ filtered.forEach((value, key) => {
110
+ baseHeaders.set(key, value);
111
+ });
112
+ }
113
+ if (response.status === 303 || ((response.status === 301 || response.status === 302) && currentMethod === 'POST')) {
114
+ currentMethod = 'GET';
115
+ currentBody = undefined;
116
+ baseHeaders.delete('content-length');
117
+ baseHeaders.delete('content-type');
118
+ }
119
+ redirectsFollowed += 1;
120
+ currentUrl = nextUrl;
121
+ await response.body?.cancel();
122
+ }
123
+ }
124
+ //# sourceMappingURL=safe-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-fetch.js","sourceRoot":"","sources":["../../src/network/safe-fetch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=safe-fetch.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-fetch.test.d.ts","sourceRoot":"","sources":["../../src/network/safe-fetch.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,136 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { createExternalHttpsPolicy, createHfDocsPolicy } from './url-policy.js';
3
+ import { safeFetch } from './safe-fetch.js';
4
+ describe('safeFetch', () => {
5
+ afterEach(() => {
6
+ vi.clearAllMocks();
7
+ vi.unstubAllGlobals();
8
+ });
9
+ it('follows redirects manually and validates each hop', async () => {
10
+ const fetchMock = vi
11
+ .fn()
12
+ .mockResolvedValueOnce(new Response('', { status: 302, headers: { location: '/docs/next' } }))
13
+ .mockResolvedValueOnce(new Response('ok', { status: 200 }));
14
+ vi.stubGlobal('fetch', fetchMock);
15
+ const result = await safeFetch('https://huggingface.co/docs/start', {
16
+ urlPolicy: createHfDocsPolicy(),
17
+ externalOnly: true,
18
+ });
19
+ expect(result.redirectsFollowed).toBe(1);
20
+ expect(result.finalUrl.toString()).toBe('https://huggingface.co/docs/next');
21
+ expect(fetchMock).toHaveBeenCalledTimes(2);
22
+ expect(fetchMock.mock.calls[0]?.[0]).toBe('https://huggingface.co/docs/start');
23
+ expect(fetchMock.mock.calls[1]?.[0]).toBe('https://huggingface.co/docs/next');
24
+ });
25
+ it('rejects redirect to disallowed host', async () => {
26
+ const fetchMock = vi
27
+ .fn()
28
+ .mockResolvedValueOnce(new Response('', { status: 302, headers: { location: 'https://example.com/path' } }));
29
+ vi.stubGlobal('fetch', fetchMock);
30
+ await expect(safeFetch('https://huggingface.co/docs/start', {
31
+ urlPolicy: createHfDocsPolicy(),
32
+ externalOnly: true,
33
+ })).rejects.toThrow('URL hostname is not allowed');
34
+ expect(fetchMock).toHaveBeenCalledTimes(1);
35
+ });
36
+ it('enforces redirect limits', async () => {
37
+ const fetchMock = vi
38
+ .fn()
39
+ .mockResolvedValueOnce(new Response('', { status: 302, headers: { location: '/docs/a' } }))
40
+ .mockResolvedValueOnce(new Response('', { status: 302, headers: { location: '/docs/b' } }));
41
+ vi.stubGlobal('fetch', fetchMock);
42
+ await expect(safeFetch('https://huggingface.co/docs/start', {
43
+ urlPolicy: createHfDocsPolicy(),
44
+ maxRedirects: 1,
45
+ })).rejects.toThrow('Redirect limit exceeded');
46
+ });
47
+ it('enforces timeout', async () => {
48
+ const fetchMock = vi.fn().mockImplementation((_url, init) => new Promise((_, reject) => {
49
+ const signal = init?.signal;
50
+ signal?.addEventListener('abort', () => {
51
+ reject(new DOMException('aborted', 'AbortError'));
52
+ }, { once: true });
53
+ }));
54
+ vi.stubGlobal('fetch', fetchMock);
55
+ await expect(safeFetch('https://example.com/file.wav', {
56
+ urlPolicy: createExternalHttpsPolicy(),
57
+ timeoutMs: 5,
58
+ })).rejects.toThrow('Request timed out');
59
+ });
60
+ it('keeps caller abort signal active while streaming response body', async () => {
61
+ const fetchMock = vi.fn().mockImplementation((_url, init) => {
62
+ const stream = new ReadableStream({
63
+ start(controller) {
64
+ init?.signal?.addEventListener('abort', () => {
65
+ controller.error(new DOMException('aborted', 'AbortError'));
66
+ }, { once: true });
67
+ },
68
+ pull() {
69
+ return new Promise(() => { });
70
+ },
71
+ });
72
+ return Promise.resolve(new Response(stream, { status: 200 }));
73
+ });
74
+ vi.stubGlobal('fetch', fetchMock);
75
+ const controller = new AbortController();
76
+ const { response } = await safeFetch('https://example.com/file.wav', {
77
+ urlPolicy: createExternalHttpsPolicy(),
78
+ timeoutMs: 500,
79
+ requestInit: { signal: controller.signal },
80
+ });
81
+ const reader = response.body?.getReader();
82
+ if (!reader) {
83
+ throw new Error('Expected response body to exist');
84
+ }
85
+ const readPromise = Promise.race([
86
+ reader.read(),
87
+ new Promise((_, reject) => {
88
+ setTimeout(() => {
89
+ reject(new Error('stream read did not abort'));
90
+ }, 100);
91
+ }),
92
+ ]);
93
+ controller.abort();
94
+ await expect(readPromise).rejects.toMatchObject({ name: 'AbortError' });
95
+ });
96
+ it('enforces timeout while streaming response body', async () => {
97
+ const fetchMock = vi.fn().mockImplementation((_url, init) => {
98
+ const stream = new ReadableStream({
99
+ start(controller) {
100
+ init?.signal?.addEventListener('abort', () => {
101
+ controller.error(new DOMException('aborted', 'AbortError'));
102
+ }, { once: true });
103
+ },
104
+ pull() {
105
+ return new Promise(() => { });
106
+ },
107
+ });
108
+ return Promise.resolve(new Response(stream, { status: 200 }));
109
+ });
110
+ vi.stubGlobal('fetch', fetchMock);
111
+ const { response } = await safeFetch('https://example.com/file.wav', {
112
+ urlPolicy: createExternalHttpsPolicy(),
113
+ timeoutMs: 10,
114
+ });
115
+ const reader = response.body?.getReader();
116
+ if (!reader) {
117
+ throw new Error('Expected response body to exist');
118
+ }
119
+ const readPromise = Promise.race([
120
+ reader.read(),
121
+ new Promise((_, reject) => {
122
+ setTimeout(() => {
123
+ reject(new Error('stream read did not timeout'));
124
+ }, 200);
125
+ }),
126
+ ]);
127
+ await expect(readPromise).rejects.toMatchObject({ name: 'AbortError' });
128
+ });
129
+ it('blocks internal destinations when externalOnly is enabled', async () => {
130
+ await expect(safeFetch('https://127.0.0.1/x', {
131
+ urlPolicy: createExternalHttpsPolicy(),
132
+ externalOnly: true,
133
+ })).rejects.toThrow('Blocked internal or reserved address');
134
+ });
135
+ });
136
+ //# sourceMappingURL=safe-fetch.test.js.map