@uncefact/untp-utils 0.1.0 → 0.2.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 (220) hide show
  1. package/README.md +23 -1
  2. package/build/artefacts/detect-version-from-context.d.ts +46 -0
  3. package/build/artefacts/detect-version-from-context.js +77 -0
  4. package/build/artefacts/detect-version-from-context.js.map +1 -0
  5. package/build/artefacts/detect-version-from-context.test.d.ts +1 -0
  6. package/build/artefacts/detect-version-from-context.test.js +69 -0
  7. package/build/artefacts/detect-version-from-context.test.js.map +1 -0
  8. package/build/artefacts/index.d.ts +3 -0
  9. package/build/artefacts/index.js +4 -0
  10. package/build/artefacts/index.js.map +1 -0
  11. package/build/artefacts/urls.d.ts +79 -0
  12. package/build/artefacts/urls.js +133 -0
  13. package/build/artefacts/urls.js.map +1 -0
  14. package/build/artefacts/urls.test.d.ts +1 -0
  15. package/build/artefacts/urls.test.js +91 -0
  16. package/build/artefacts/urls.test.js.map +1 -0
  17. package/build/artefacts/version.d.ts +9 -0
  18. package/build/artefacts/version.js +17 -0
  19. package/build/artefacts/version.js.map +1 -0
  20. package/build/artefacts/version.test.d.ts +1 -0
  21. package/build/artefacts/version.test.js +16 -0
  22. package/build/artefacts/version.test.js.map +1 -0
  23. package/build/cache/in-memory-ttl-cache.d.ts +2 -0
  24. package/build/cache/in-memory-ttl-cache.js +76 -0
  25. package/build/cache/in-memory-ttl-cache.js.map +1 -0
  26. package/build/cache/in-memory-ttl-cache.test.d.ts +1 -0
  27. package/build/cache/in-memory-ttl-cache.test.js +236 -0
  28. package/build/cache/in-memory-ttl-cache.test.js.map +1 -0
  29. package/build/cache/index.d.ts +2 -0
  30. package/build/cache/index.js +2 -0
  31. package/build/cache/index.js.map +1 -0
  32. package/build/cache/ttl-cache.d.ts +22 -0
  33. package/build/cache/ttl-cache.js +2 -0
  34. package/build/cache/ttl-cache.js.map +1 -0
  35. package/build/common/as-non-empty-string.d.ts +7 -0
  36. package/build/common/as-non-empty-string.js +10 -0
  37. package/build/common/as-non-empty-string.js.map +1 -0
  38. package/build/common/as-non-empty-string.test.d.ts +1 -0
  39. package/build/common/as-non-empty-string.test.js +25 -0
  40. package/build/common/as-non-empty-string.test.js.map +1 -0
  41. package/build/common/require-string.d.ts +12 -0
  42. package/build/common/require-string.js +27 -0
  43. package/build/common/require-string.js.map +1 -0
  44. package/build/common/require-string.test.d.ts +1 -0
  45. package/build/common/require-string.test.js +53 -0
  46. package/build/common/require-string.test.js.map +1 -0
  47. package/build/conformity-vocabulary/codes.d.ts +10 -0
  48. package/build/conformity-vocabulary/codes.js +10 -0
  49. package/build/conformity-vocabulary/codes.js.map +1 -0
  50. package/build/conformity-vocabulary/errors.d.ts +44 -0
  51. package/build/conformity-vocabulary/errors.js +68 -0
  52. package/build/conformity-vocabulary/errors.js.map +1 -0
  53. package/build/conformity-vocabulary/index.d.ts +6 -0
  54. package/build/conformity-vocabulary/index.js +7 -0
  55. package/build/conformity-vocabulary/index.js.map +1 -0
  56. package/build/conformity-vocabulary/parse-conformity-catalogue.d.ts +26 -0
  57. package/build/conformity-vocabulary/parse-conformity-catalogue.js +106 -0
  58. package/build/conformity-vocabulary/parse-conformity-catalogue.js.map +1 -0
  59. package/build/conformity-vocabulary/parse-conformity-catalogue.test.d.ts +1 -0
  60. package/build/conformity-vocabulary/parse-conformity-catalogue.test.js +327 -0
  61. package/build/conformity-vocabulary/parse-conformity-catalogue.test.js.map +1 -0
  62. package/build/conformity-vocabulary/parse-conformity-scheme.d.ts +28 -0
  63. package/build/conformity-vocabulary/parse-conformity-scheme.js +47 -0
  64. package/build/conformity-vocabulary/parse-conformity-scheme.js.map +1 -0
  65. package/build/conformity-vocabulary/parse-conformity-scheme.test.d.ts +1 -0
  66. package/build/conformity-vocabulary/parse-conformity-scheme.test.js +284 -0
  67. package/build/conformity-vocabulary/parse-conformity-scheme.test.js.map +1 -0
  68. package/build/conformity-vocabulary/parsers/v0-7-0.parser.d.ts +19 -0
  69. package/build/conformity-vocabulary/parsers/v0-7-0.parser.js +219 -0
  70. package/build/conformity-vocabulary/parsers/v0-7-0.parser.js.map +1 -0
  71. package/build/conformity-vocabulary/types.d.ts +180 -0
  72. package/build/conformity-vocabulary/types.js +2 -0
  73. package/build/conformity-vocabulary/types.js.map +1 -0
  74. package/build/conformity-vocabulary/validate-conformity-claim.d.ts +26 -0
  75. package/build/conformity-vocabulary/validate-conformity-claim.js +165 -0
  76. package/build/conformity-vocabulary/validate-conformity-claim.js.map +1 -0
  77. package/build/conformity-vocabulary/validate-conformity-claim.test.d.ts +1 -0
  78. package/build/conformity-vocabulary/validate-conformity-claim.test.js +478 -0
  79. package/build/conformity-vocabulary/validate-conformity-claim.test.js.map +1 -0
  80. package/build/http-headers/index.d.ts +5 -0
  81. package/build/http-headers/index.js +6 -0
  82. package/build/http-headers/index.js.map +1 -0
  83. package/build/http-headers/is-safe-header-value.d.ts +10 -0
  84. package/build/http-headers/is-safe-header-value.js +15 -0
  85. package/build/http-headers/is-safe-header-value.js.map +1 -0
  86. package/build/http-headers/is-safe-header-value.test.d.ts +1 -0
  87. package/build/http-headers/is-safe-header-value.test.js +37 -0
  88. package/build/http-headers/is-safe-header-value.test.js.map +1 -0
  89. package/build/http-headers/parse-entity-tag.d.ts +9 -0
  90. package/build/http-headers/parse-entity-tag.js +18 -0
  91. package/build/http-headers/parse-entity-tag.js.map +1 -0
  92. package/build/http-headers/parse-entity-tag.test.d.ts +1 -0
  93. package/build/http-headers/parse-entity-tag.test.js +41 -0
  94. package/build/http-headers/parse-entity-tag.test.js.map +1 -0
  95. package/build/http-headers/parse-imf-date.d.ts +13 -0
  96. package/build/http-headers/parse-imf-date.js +23 -0
  97. package/build/http-headers/parse-imf-date.js.map +1 -0
  98. package/build/http-headers/parse-imf-date.test.d.ts +1 -0
  99. package/build/http-headers/parse-imf-date.test.js +31 -0
  100. package/build/http-headers/parse-imf-date.test.js.map +1 -0
  101. package/build/http-headers/parse-media-type.d.ts +8 -0
  102. package/build/http-headers/parse-media-type.js +21 -0
  103. package/build/http-headers/parse-media-type.js.map +1 -0
  104. package/build/http-headers/parse-media-type.test.d.ts +1 -0
  105. package/build/http-headers/parse-media-type.test.js +39 -0
  106. package/build/http-headers/parse-media-type.test.js.map +1 -0
  107. package/build/http-headers/user-agent.d.ts +35 -0
  108. package/build/http-headers/user-agent.js +45 -0
  109. package/build/http-headers/user-agent.js.map +1 -0
  110. package/build/http-headers/user-agent.test.d.ts +1 -0
  111. package/build/http-headers/user-agent.test.js +27 -0
  112. package/build/http-headers/user-agent.test.js.map +1 -0
  113. package/build/index.d.ts +1 -1
  114. package/build/index.js +1 -1
  115. package/build/index.js.map +1 -1
  116. package/build/loaders/errors.d.ts +21 -0
  117. package/build/loaders/errors.js +45 -0
  118. package/build/loaders/errors.js.map +1 -0
  119. package/build/loaders/index.d.ts +3 -0
  120. package/build/loaders/index.js +4 -0
  121. package/build/loaders/index.js.map +1 -0
  122. package/build/loaders/jsonld-document-loader.d.ts +46 -0
  123. package/build/loaders/jsonld-document-loader.js +34 -0
  124. package/build/loaders/jsonld-document-loader.js.map +1 -0
  125. package/build/loaders/jsonld-document-loader.test.d.ts +1 -0
  126. package/build/loaders/jsonld-document-loader.test.js +71 -0
  127. package/build/loaders/jsonld-document-loader.test.js.map +1 -0
  128. package/build/loaders/schema-loader.d.ts +21 -0
  129. package/build/loaders/schema-loader.js +67 -0
  130. package/build/loaders/schema-loader.js.map +1 -0
  131. package/build/loaders/schema-loader.ssrf.test.d.ts +1 -0
  132. package/build/loaders/schema-loader.ssrf.test.js +55 -0
  133. package/build/loaders/schema-loader.ssrf.test.js.map +1 -0
  134. package/build/loaders/schema-loader.test.d.ts +1 -0
  135. package/build/loaders/schema-loader.test.js +162 -0
  136. package/build/loaders/schema-loader.test.js.map +1 -0
  137. package/build/multibase-digest/multibase-digest.d.ts +23 -0
  138. package/build/multibase-digest/multibase-digest.js +40 -0
  139. package/build/multibase-digest/multibase-digest.js.map +1 -1
  140. package/build/multibase-digest/multibase-digest.test.js +46 -0
  141. package/build/multibase-digest/multibase-digest.test.js.map +1 -1
  142. package/build/node/errors.d.ts +41 -0
  143. package/build/node/errors.js +80 -0
  144. package/build/node/errors.js.map +1 -0
  145. package/build/node/index.d.ts +3 -0
  146. package/build/node/index.js +4 -0
  147. package/build/node/index.js.map +1 -0
  148. package/build/node/is-private-ip.d.ts +41 -0
  149. package/build/node/is-private-ip.js +138 -0
  150. package/build/node/is-private-ip.js.map +1 -0
  151. package/build/node/is-private-ip.test.d.ts +1 -0
  152. package/build/node/is-private-ip.test.js +96 -0
  153. package/build/node/is-private-ip.test.js.map +1 -0
  154. package/build/node/validate-public-url.d.ts +49 -0
  155. package/build/node/validate-public-url.js +94 -0
  156. package/build/node/validate-public-url.js.map +1 -0
  157. package/build/node/validate-public-url.test.d.ts +1 -0
  158. package/build/node/validate-public-url.test.js +177 -0
  159. package/build/node/validate-public-url.test.js.map +1 -0
  160. package/build/resolvers/errors.d.ts +43 -0
  161. package/build/resolvers/errors.js +99 -0
  162. package/build/resolvers/errors.js.map +1 -0
  163. package/build/resolvers/index.d.ts +5 -0
  164. package/build/resolvers/index.js +6 -0
  165. package/build/resolvers/index.js.map +1 -0
  166. package/build/resolvers/resolve-document-if-changed.d.ts +48 -0
  167. package/build/resolvers/resolve-document-if-changed.js +42 -0
  168. package/build/resolvers/resolve-document-if-changed.js.map +1 -0
  169. package/build/resolvers/resolve-document-if-changed.test.d.ts +1 -0
  170. package/build/resolvers/resolve-document-if-changed.test.js +109 -0
  171. package/build/resolvers/resolve-document-if-changed.test.js.map +1 -0
  172. package/build/resolvers/resolve-document.d.ts +86 -0
  173. package/build/resolvers/resolve-document.js +239 -0
  174. package/build/resolvers/resolve-document.js.map +1 -0
  175. package/build/resolvers/resolve-document.test.d.ts +1 -0
  176. package/build/resolvers/resolve-document.test.js +299 -0
  177. package/build/resolvers/resolve-document.test.js.map +1 -0
  178. package/build/resolvers/resolve-json-document.d.ts +40 -0
  179. package/build/resolvers/resolve-json-document.js +42 -0
  180. package/build/resolvers/resolve-json-document.js.map +1 -0
  181. package/build/resolvers/resolve-json-document.test.d.ts +1 -0
  182. package/build/resolvers/resolve-json-document.test.js +61 -0
  183. package/build/resolvers/resolve-json-document.test.js.map +1 -0
  184. package/build/structured-error.d.ts +49 -0
  185. package/build/structured-error.js +17 -0
  186. package/build/structured-error.js.map +1 -0
  187. package/build/structured-error.test.d.ts +1 -0
  188. package/build/structured-error.test.js +112 -0
  189. package/build/structured-error.test.js.map +1 -0
  190. package/build/validation/describe-jsonld-failure.d.ts +50 -0
  191. package/build/validation/describe-jsonld-failure.js +104 -0
  192. package/build/validation/describe-jsonld-failure.js.map +1 -0
  193. package/build/validation/describe-jsonld-failure.test.d.ts +1 -0
  194. package/build/validation/describe-jsonld-failure.test.js +141 -0
  195. package/build/validation/describe-jsonld-failure.test.js.map +1 -0
  196. package/build/validation/errors.d.ts +40 -0
  197. package/build/validation/errors.js +75 -0
  198. package/build/validation/errors.js.map +1 -0
  199. package/build/validation/index.d.ts +4 -0
  200. package/build/validation/index.js +5 -0
  201. package/build/validation/index.js.map +1 -0
  202. package/build/validation/validate-against-schemas.d.ts +22 -0
  203. package/build/validation/validate-against-schemas.js +65 -0
  204. package/build/validation/validate-against-schemas.js.map +1 -0
  205. package/build/validation/validate-against-schemas.test.d.ts +1 -0
  206. package/build/validation/validate-against-schemas.test.js +169 -0
  207. package/build/validation/validate-against-schemas.test.js.map +1 -0
  208. package/build/validation/validate-jsonld.context-cache.test.d.ts +1 -0
  209. package/build/validation/validate-jsonld.context-cache.test.js +32 -0
  210. package/build/validation/validate-jsonld.context-cache.test.js.map +1 -0
  211. package/build/validation/validate-jsonld.d.ts +33 -0
  212. package/build/validation/validate-jsonld.js +99 -0
  213. package/build/validation/validate-jsonld.js.map +1 -0
  214. package/build/validation/validate-jsonld.ssrf.test.d.ts +1 -0
  215. package/build/validation/validate-jsonld.ssrf.test.js +108 -0
  216. package/build/validation/validate-jsonld.ssrf.test.js.map +1 -0
  217. package/build/validation/validate-jsonld.test.d.ts +1 -0
  218. package/build/validation/validate-jsonld.test.js +200 -0
  219. package/build/validation/validate-jsonld.test.js.map +1 -0
  220. package/package.json +64 -3
@@ -0,0 +1,80 @@
1
+ import { StructuredError } from '../structured-error.js';
2
+ /**
3
+ * Base for every diagnostic from `@uncefact/untp-utils/node`. Catch to
4
+ * handle any URL-validation failure generically; catch a concrete subclass
5
+ * for specific handling.
6
+ */
7
+ export class UrlValidationError extends StructuredError {
8
+ }
9
+ /** The string passed could not be parsed as a URL by `new URL(...)`. */
10
+ export class InvalidUrlError extends UrlValidationError {
11
+ constructor(received, cause) {
12
+ super({ code: 'url.invalid', message: 'URL could not be parsed.', received, cause });
13
+ }
14
+ }
15
+ /** The URL's scheme is not in the allowed list (default `http`, `https`). */
16
+ export class UnsupportedSchemeError extends UrlValidationError {
17
+ constructor(scheme, allowedSchemes) {
18
+ super({
19
+ code: 'url.unsupported-scheme',
20
+ message: `URL scheme ${scheme} is not in the allowed list.`,
21
+ received: scheme,
22
+ expected: [...allowedSchemes],
23
+ remediation: `Use one of: ${allowedSchemes.join(', ')}.`,
24
+ });
25
+ }
26
+ }
27
+ /**
28
+ * The hostname is a known private/local name (`localhost`, `*.localhost`,
29
+ * `*.local`, `*.internal`, etc.), an IP literal in a non-public range, or
30
+ * empty.
31
+ */
32
+ export class PrivateHostnameError extends UrlValidationError {
33
+ constructor(hostname) {
34
+ super({
35
+ code: 'url.private-hostname',
36
+ message: `Hostname ${hostname || '(empty)'} names a private or local resource.`,
37
+ received: hostname,
38
+ remediation: 'Use a publicly-routable hostname.',
39
+ });
40
+ }
41
+ }
42
+ /** DNS resolution rejected (`ENOTFOUND`, `EAI_AGAIN`, etc.). */
43
+ export class ResolutionFailedError extends UrlValidationError {
44
+ constructor(hostname, cause) {
45
+ super({
46
+ code: 'url.resolution-failed',
47
+ message: `DNS resolution failed for ${hostname}.`,
48
+ received: cause instanceof Error ? cause.message : String(cause),
49
+ cause,
50
+ });
51
+ }
52
+ }
53
+ /** DNS resolution succeeded but returned no addresses. */
54
+ export class ResolutionEmptyError extends UrlValidationError {
55
+ constructor(hostname) {
56
+ super({
57
+ code: 'url.resolution-empty',
58
+ message: `DNS resolver returned no addresses for ${hostname}.`,
59
+ received: hostname,
60
+ });
61
+ }
62
+ }
63
+ /**
64
+ * At least one resolved IP is in a private / loopback / link-local /
65
+ * cloud-metadata range. The full list is exposed on
66
+ * {@link resolvedAddresses} for triage.
67
+ */
68
+ export class PrivateAddressError extends UrlValidationError {
69
+ resolvedAddresses;
70
+ constructor(hostname, resolvedAddresses) {
71
+ super({
72
+ code: 'url.private-address',
73
+ message: `Hostname ${hostname} resolved to a private address.`,
74
+ received: resolvedAddresses,
75
+ remediation: 'Verify the hostname does not resolve to private or reserved network space.',
76
+ });
77
+ this.resolvedAddresses = resolvedAddresses;
78
+ }
79
+ }
80
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/node/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,eAAe;CAAG;AAE1D,wEAAwE;AACxE,MAAM,OAAO,eAAgB,SAAQ,kBAAkB;IACrD,YAAY,QAAgB,EAAE,KAAc;QAC1C,KAAK,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,0BAA0B,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IACvF,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,OAAO,sBAAuB,SAAQ,kBAAkB;IAC5D,YAAY,MAAc,EAAE,cAAiC;QAC3D,KAAK,CAAC;YACJ,IAAI,EAAE,wBAAwB;YAC9B,OAAO,EAAE,cAAc,MAAM,8BAA8B;YAC3D,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,CAAC,GAAG,cAAc,CAAC;YAC7B,WAAW,EAAE,eAAe,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACzD,CAAC,CAAC;IACL,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,oBAAqB,SAAQ,kBAAkB;IAC1D,YAAY,QAAgB;QAC1B,KAAK,CAAC;YACJ,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,YAAY,QAAQ,IAAI,SAAS,qCAAqC;YAC/E,QAAQ,EAAE,QAAQ;YAClB,WAAW,EAAE,mCAAmC;SACjD,CAAC,CAAC;IACL,CAAC;CACF;AAED,gEAAgE;AAChE,MAAM,OAAO,qBAAsB,SAAQ,kBAAkB;IAC3D,YAAY,QAAgB,EAAE,KAAc;QAC1C,KAAK,CAAC;YACJ,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,6BAA6B,QAAQ,GAAG;YACjD,QAAQ,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YAChE,KAAK;SACN,CAAC,CAAC;IACL,CAAC;CACF;AAED,0DAA0D;AAC1D,MAAM,OAAO,oBAAqB,SAAQ,kBAAkB;IAC1D,YAAY,QAAgB;QAC1B,KAAK,CAAC;YACJ,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,0CAA0C,QAAQ,GAAG;YAC9D,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;IACL,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,mBAAoB,SAAQ,kBAAkB;IAChD,iBAAiB,CAAoB;IAC9C,YAAY,QAAgB,EAAE,iBAAoC;QAChE,KAAK,CAAC;YACJ,IAAI,EAAE,qBAAqB;YAC3B,OAAO,EAAE,YAAY,QAAQ,iCAAiC;YAC9D,QAAQ,EAAE,iBAAiB;YAC3B,WAAW,EAAE,4EAA4E;SAC1F,CAAC,CAAC;QACH,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ export { UrlValidationError, InvalidUrlError, UnsupportedSchemeError, PrivateHostnameError, ResolutionFailedError, ResolutionEmptyError, PrivateAddressError, } from './errors.js';
2
+ export { isPrivateHostname, isPrivateIpv4, isPrivateIpv6 } from './is-private-ip.js';
3
+ export { validatePublicUrl, type ValidatePublicUrlOptions, type ResolvedAddress } from './validate-public-url.js';
@@ -0,0 +1,4 @@
1
+ export { UrlValidationError, InvalidUrlError, UnsupportedSchemeError, PrivateHostnameError, ResolutionFailedError, ResolutionEmptyError, PrivateAddressError, } from './errors.js';
2
+ export { isPrivateHostname, isPrivateIpv4, isPrivateIpv6 } from './is-private-ip.js';
3
+ export { validatePublicUrl } from './validate-public-url.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAuD,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Returns `true` if `address` is an IPv4 address string that does not fall in
3
+ * the public unicast range, or that matches a known cloud-metadata IP. Use
4
+ * this to gate outbound connections to tenant-supplied addresses.
5
+ *
6
+ * Fails closed: if `ipaddr.js` cannot parse a value that passed
7
+ * `node:net`'s `isIPv4` (parser/grammar skew across versions), the function
8
+ * returns `true` rather than letting the address through.
9
+ *
10
+ * Returns `false` for non-IPv4 input (including unparseable strings).
11
+ * Callers handle parsing themselves and pass only IP literals. Never throws.
12
+ *
13
+ * @see https://datatracker.ietf.org/doc/html/rfc1918 Address Allocation for Private Internets
14
+ * @see https://datatracker.ietf.org/doc/html/rfc6598 IANA-Reserved IPv4 Prefix for Shared Address Space (CGNAT)
15
+ * @see https://datatracker.ietf.org/doc/html/rfc3927 Dynamic Configuration of IPv4 Link-Local Addresses
16
+ * @see https://datatracker.ietf.org/doc/html/rfc5735 Special Use IPv4 Addresses
17
+ */
18
+ export declare function isPrivateIpv4(address: string): boolean;
19
+ /**
20
+ * Returns `true` if `address` is an IPv6 address string that does not fall in
21
+ * the public unicast range. IPv4-mapped addresses (`::ffff:a.b.c.d`) are
22
+ * additionally re-checked against {@link isPrivateIpv4} so an IPv4 private
23
+ * address tunnelled as IPv6 cannot bypass the predicate.
24
+ *
25
+ * Fails closed on parser drift, mirroring {@link isPrivateIpv4}.
26
+ *
27
+ * Returns `false` for non-IPv6 input. Never throws.
28
+ *
29
+ * @see https://datatracker.ietf.org/doc/html/rfc4291 IP Version 6 Addressing Architecture
30
+ * @see https://datatracker.ietf.org/doc/html/rfc4193 Unique Local IPv6 Unicast Addresses
31
+ */
32
+ export declare function isPrivateIpv6(address: string): boolean;
33
+ /**
34
+ * Returns `true` if `host` always names a local resource (`localhost`,
35
+ * anything under `.localhost`, `.local`, `.internal`, etc.) or an IP literal
36
+ * in a non-public range.
37
+ *
38
+ * The empty string is treated as private (a URL with no hostname must never
39
+ * be allowed out).
40
+ */
41
+ export declare function isPrivateHostname(host: string): boolean;
@@ -0,0 +1,138 @@
1
+ import { isIPv4, isIPv6 } from 'node:net';
2
+ import ipaddr from 'ipaddr.js';
3
+ /**
4
+ * The single `ipaddr.js` range category that represents a publicly routable
5
+ * unicast address. Anything else (loopback, link-local, private, multicast,
6
+ * unique-local, reserved, 6to4, teredo, etc.) is rejected. An allowlist is
7
+ * safer than enumerating disallowed buckets: new range names introduced by
8
+ * future `ipaddr.js` versions default to "block" rather than "allow".
9
+ */
10
+ const PUBLIC_RANGE = 'unicast';
11
+ /**
12
+ * Cloud-metadata IPv4 addresses listed explicitly for belt-and-braces
13
+ * coverage. Both addresses are already classified as non-`unicast` by
14
+ * `ipaddr.js` (the AWS / GCP / Azure address via link-local
15
+ * `169.254.0.0/16`, the Alibaba address via CGNAT `100.64.0.0/10`), but the
16
+ * explicit listing means a future change to `ipaddr.js`'s range taxonomy
17
+ * cannot accidentally promote them to `unicast`.
18
+ *
19
+ * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
20
+ * @see https://help.aliyun.com/document_detail/49122.html
21
+ */
22
+ const ADDITIONAL_BLOCKED_IPV4 = new Set(['169.254.169.254', '100.100.100.200']);
23
+ /**
24
+ * Hostname suffixes that name resources reachable only on a local network and
25
+ * must not be allowed to leave the host. Matched case-insensitively against
26
+ * the full hostname (e.g. `foo.localhost`, `printer.local`, `db.internal`).
27
+ *
28
+ * `.localhost` is reserved by RFC 6761; `.local` is reserved by RFC 6762
29
+ * (Multicast DNS); `.internal` is on the IANA / ICANN reserved list for
30
+ * private use (per RFC 9499 and ICANN board resolution, 2024); the
31
+ * remainder (`.intranet`, `.lan`, `.home`, `.corp`, `.private`) are
32
+ * widely-used private-network conventions that are blocked from ICANN
33
+ * delegation. Treating them as private is a conservative default for an
34
+ * SSRF guard.
35
+ *
36
+ * @see https://datatracker.ietf.org/doc/html/rfc6761 Special-Use Domain Names
37
+ * @see https://datatracker.ietf.org/doc/html/rfc6762 Multicast DNS
38
+ * @see https://datatracker.ietf.org/doc/html/rfc9499 DNS Terminology
39
+ */
40
+ const PRIVATE_HOSTNAME_SUFFIXES = [
41
+ '.localhost',
42
+ '.local',
43
+ '.internal',
44
+ '.intranet',
45
+ '.lan',
46
+ '.home',
47
+ '.corp',
48
+ '.private',
49
+ ];
50
+ /**
51
+ * Returns `true` if `address` is an IPv4 address string that does not fall in
52
+ * the public unicast range, or that matches a known cloud-metadata IP. Use
53
+ * this to gate outbound connections to tenant-supplied addresses.
54
+ *
55
+ * Fails closed: if `ipaddr.js` cannot parse a value that passed
56
+ * `node:net`'s `isIPv4` (parser/grammar skew across versions), the function
57
+ * returns `true` rather than letting the address through.
58
+ *
59
+ * Returns `false` for non-IPv4 input (including unparseable strings).
60
+ * Callers handle parsing themselves and pass only IP literals. Never throws.
61
+ *
62
+ * @see https://datatracker.ietf.org/doc/html/rfc1918 Address Allocation for Private Internets
63
+ * @see https://datatracker.ietf.org/doc/html/rfc6598 IANA-Reserved IPv4 Prefix for Shared Address Space (CGNAT)
64
+ * @see https://datatracker.ietf.org/doc/html/rfc3927 Dynamic Configuration of IPv4 Link-Local Addresses
65
+ * @see https://datatracker.ietf.org/doc/html/rfc5735 Special Use IPv4 Addresses
66
+ */
67
+ export function isPrivateIpv4(address) {
68
+ if (!isIPv4(address))
69
+ return false;
70
+ if (ADDITIONAL_BLOCKED_IPV4.has(address))
71
+ return true;
72
+ try {
73
+ return ipaddr.parse(address).range() !== PUBLIC_RANGE;
74
+ }
75
+ catch {
76
+ return true; // Fail closed: cannot prove the address is public, so treat as private.
77
+ }
78
+ }
79
+ /**
80
+ * Returns `true` if `address` is an IPv6 address string that does not fall in
81
+ * the public unicast range. IPv4-mapped addresses (`::ffff:a.b.c.d`) are
82
+ * additionally re-checked against {@link isPrivateIpv4} so an IPv4 private
83
+ * address tunnelled as IPv6 cannot bypass the predicate.
84
+ *
85
+ * Fails closed on parser drift, mirroring {@link isPrivateIpv4}.
86
+ *
87
+ * Returns `false` for non-IPv6 input. Never throws.
88
+ *
89
+ * @see https://datatracker.ietf.org/doc/html/rfc4291 IP Version 6 Addressing Architecture
90
+ * @see https://datatracker.ietf.org/doc/html/rfc4193 Unique Local IPv6 Unicast Addresses
91
+ */
92
+ export function isPrivateIpv6(address) {
93
+ if (!isIPv6(address))
94
+ return false;
95
+ try {
96
+ const parsed = ipaddr.parse(address);
97
+ if (parsed.kind() !== 'ipv6')
98
+ return true; // Defensive: parser/version skew. Fail closed.
99
+ const v6 = parsed;
100
+ if (v6.isIPv4MappedAddress()) {
101
+ return isPrivateIpv4(v6.toIPv4Address().toString());
102
+ }
103
+ return v6.range() !== PUBLIC_RANGE;
104
+ }
105
+ catch {
106
+ return true; // Fail closed: cannot prove the address is public, so treat as private.
107
+ }
108
+ }
109
+ /**
110
+ * Returns `true` if `host` always names a local resource (`localhost`,
111
+ * anything under `.localhost`, `.local`, `.internal`, etc.) or an IP literal
112
+ * in a non-public range.
113
+ *
114
+ * The empty string is treated as private (a URL with no hostname must never
115
+ * be allowed out).
116
+ */
117
+ export function isPrivateHostname(host) {
118
+ if (!host)
119
+ return true;
120
+ // Strip leading/trailing brackets (URL.hostname wraps IPv6 literals like
121
+ // `[::1]`) and any trailing dot before further checks; without this, a
122
+ // direct caller passing `URL.hostname` for an IPv6 URL would silently
123
+ // miss the private-range match.
124
+ const lower = host
125
+ .toLowerCase()
126
+ .replace(/^\[|\]$/g, '')
127
+ .replace(/\.$/, '');
128
+ if (lower === 'localhost')
129
+ return true;
130
+ if (PRIVATE_HOSTNAME_SUFFIXES.some((suffix) => lower.endsWith(suffix)))
131
+ return true;
132
+ if (isIPv4(lower))
133
+ return isPrivateIpv4(lower);
134
+ if (isIPv6(lower))
135
+ return isPrivateIpv6(lower);
136
+ return false;
137
+ }
138
+ //# sourceMappingURL=is-private-ip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-private-ip.js","sourceRoot":"","sources":["../../src/node/is-private-ip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,MAAM,MAAM,WAAW,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,YAAY,GAAG,SAAS,CAAC;AAE/B;;;;;;;;;;GAUG;AACH,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,yBAAyB,GAAG;IAChC,YAAY;IACZ,QAAQ;IACR,WAAW;IACX,WAAW;IACX,MAAM;IACN,OAAO;IACP,OAAO;IACP,UAAU;CACX,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,uBAAuB,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,YAAY,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,wEAAwE;IACvF,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,CAAC,+CAA+C;QAC1F,MAAM,EAAE,GAAG,MAAqB,CAAC;QACjC,IAAI,EAAE,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC7B,OAAO,aAAa,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,YAAY,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,wEAAwE;IACvF,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,yEAAyE;IACzE,uEAAuE;IACvE,sEAAsE;IACtE,gCAAgC;IAChC,MAAM,KAAK,GAAG,IAAI;SACf,WAAW,EAAE;SACb,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtB,IAAI,KAAK,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,yBAAyB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpF,IAAI,MAAM,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ import { isPrivateHostname, isPrivateIpv4, isPrivateIpv6 } from './is-private-ip.js';
2
+ describe('isPrivateIpv4', () => {
3
+ it.each([['1.1.1.1'], ['8.8.8.8'], ['151.101.0.1']])('returns false for the unmistakeably public address %s', (addr) => {
4
+ expect(isPrivateIpv4(addr)).toBe(false);
5
+ });
6
+ it.each([
7
+ ['0.0.0.0'], // unspecified
8
+ ['10.0.0.1'], // RFC 1918
9
+ ['10.255.255.255'],
10
+ ['100.64.0.1'], // CGNAT
11
+ ['127.0.0.1'], // loopback
12
+ ['127.255.255.255'],
13
+ ['169.254.0.1'], // link-local
14
+ ['169.254.169.254'], // cloud metadata (AWS/GCP/Azure)
15
+ ['172.16.0.1'], // RFC 1918
16
+ ['172.31.255.255'],
17
+ ['192.168.0.1'], // RFC 1918
18
+ ['192.168.255.255'],
19
+ ['224.0.0.1'], // multicast
20
+ ['255.255.255.255'], // broadcast
21
+ ['198.18.0.1'], // benchmarking
22
+ ['203.0.113.1'], // TEST-NET-3 (reserved)
23
+ ['100.100.100.200'], // Alibaba Cloud metadata (publicly routable, but explicitly blocked)
24
+ ])('returns true for the private/reserved/metadata address %s', (addr) => {
25
+ expect(isPrivateIpv4(addr)).toBe(true);
26
+ });
27
+ it('returns false for non-IPv4 input', () => {
28
+ expect(isPrivateIpv4('not an ip')).toBe(false);
29
+ expect(isPrivateIpv4('::1')).toBe(false);
30
+ expect(isPrivateIpv4('')).toBe(false);
31
+ });
32
+ });
33
+ describe('isPrivateIpv6', () => {
34
+ it.each([
35
+ ['2606:4700:4700::1111'], // Cloudflare public DNS
36
+ ['2001:4860:4860::8888'], // Google public DNS
37
+ ])('returns false for the unmistakeably public address %s', (addr) => {
38
+ expect(isPrivateIpv6(addr)).toBe(false);
39
+ });
40
+ it.each([
41
+ ['::'], // unspecified
42
+ ['::1'], // loopback
43
+ ['fe80::1'], // link-local
44
+ ['fc00::1'], // unique local
45
+ ['fd00::1'], // unique local
46
+ ['ff02::1'], // multicast
47
+ ['2001:db8::1'], // documentation / reserved
48
+ ['2002::1'], // 6to4
49
+ ])('returns true for the non-public address %s', (addr) => {
50
+ expect(isPrivateIpv6(addr)).toBe(true);
51
+ });
52
+ it('treats IPv4-mapped IPv6 addresses by their embedded IPv4 status', () => {
53
+ expect(isPrivateIpv6('::ffff:127.0.0.1')).toBe(true);
54
+ expect(isPrivateIpv6('::ffff:169.254.169.254')).toBe(true);
55
+ expect(isPrivateIpv6('::ffff:1.1.1.1')).toBe(false);
56
+ });
57
+ it('returns false for non-IPv6 input', () => {
58
+ expect(isPrivateIpv6('not an ip')).toBe(false);
59
+ expect(isPrivateIpv6('10.0.0.1')).toBe(false);
60
+ expect(isPrivateIpv6('')).toBe(false);
61
+ });
62
+ });
63
+ describe('isPrivateHostname', () => {
64
+ it.each([
65
+ [''],
66
+ ['localhost'],
67
+ ['LOCALHOST'],
68
+ ['foo.localhost'],
69
+ ['bar.local'],
70
+ ['printer.local.'], // trailing dot is stripped
71
+ ['db.internal'],
72
+ ['svc.intranet'],
73
+ ['file-server.lan'],
74
+ ['router.home'],
75
+ ['app.corp'],
76
+ ['vault.private'],
77
+ ])('returns true for the private hostname %s', (host) => {
78
+ expect(isPrivateHostname(host)).toBe(true);
79
+ });
80
+ it.each([['example.com'], ['api.cloudflare.com'], ['google.com.']])('returns false for the public hostname %s', (host) => {
81
+ expect(isPrivateHostname(host)).toBe(false);
82
+ });
83
+ it('defers to IP predicates when the hostname is an IP literal', () => {
84
+ expect(isPrivateHostname('127.0.0.1')).toBe(true);
85
+ expect(isPrivateHostname('169.254.169.254')).toBe(true);
86
+ expect(isPrivateHostname('::1')).toBe(true);
87
+ expect(isPrivateHostname('1.1.1.1')).toBe(false);
88
+ expect(isPrivateHostname('2606:4700:4700::1111')).toBe(false);
89
+ });
90
+ it('handles IPv6 literals wrapped in brackets (as URL.hostname returns)', () => {
91
+ expect(isPrivateHostname('[::1]')).toBe(true);
92
+ expect(isPrivateHostname('[fc00::1]')).toBe(true);
93
+ expect(isPrivateHostname('[2606:4700:4700::1111]')).toBe(false);
94
+ });
95
+ });
96
+ //# sourceMappingURL=is-private-ip.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-private-ip.test.js","sourceRoot":"","sources":["../../src/node/is-private-ip.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Options for {@link validatePublicUrl}.
3
+ */
4
+ export interface ValidatePublicUrlOptions {
5
+ /**
6
+ * URL schemes that are allowed (e.g. `['http', 'https']`). Compared
7
+ * case-insensitively. Defaults to `['http', 'https']`.
8
+ */
9
+ allowedSchemes?: readonly string[];
10
+ /**
11
+ * IP family hint passed to `dns.lookup`. `0` (default) returns whichever
12
+ * family the resolver prefers, `4` forces IPv4-only, `6` forces IPv6-only.
13
+ */
14
+ family?: 0 | 4 | 6;
15
+ }
16
+ /**
17
+ * The resolved address that callers must use as the connect target so the
18
+ * connection lands on the IP that validation checked. Connecting via the
19
+ * hostname instead opens a DNS rebinding window.
20
+ */
21
+ export interface ResolvedAddress {
22
+ address: string;
23
+ family: 4 | 6;
24
+ }
25
+ /**
26
+ * Validates that `url` is a parseable HTTP(S) URL whose hostname resolves
27
+ * to publicly routable IP addresses, and returns one of those addresses
28
+ * pinned for the caller to use as the connect target.
29
+ *
30
+ * DNS resolution is performed with `all: true`; the URL is rejected if any
31
+ * resolved address is in a private / loopback / link-local /
32
+ * cloud-metadata range, so a mixed public/private DNS response cannot
33
+ * sneak a private record through.
34
+ *
35
+ * Per ADR-035, this function throws subclasses of {@link UrlValidationError}
36
+ * on failure. The structured payload (`code`, `message`, `received`,
37
+ * `expected`, `remediation`, `pointer`, `cause`) is available on the thrown
38
+ * instance.
39
+ *
40
+ * @see https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
41
+ * @see ../../../docs/adrs/035-utils-throws-structured-errors.md
42
+ * @throws {InvalidUrlError} `url` is not a parseable URL.
43
+ * @throws {UnsupportedSchemeError} the URL's scheme is not allowed.
44
+ * @throws {PrivateHostnameError} the hostname names a private resource.
45
+ * @throws {ResolutionFailedError} DNS resolution rejected.
46
+ * @throws {ResolutionEmptyError} DNS resolution returned no records.
47
+ * @throws {PrivateAddressError} any resolved record is private.
48
+ */
49
+ export declare function validatePublicUrl(url: string, options?: ValidatePublicUrlOptions): Promise<ResolvedAddress>;
@@ -0,0 +1,94 @@
1
+ import { isIP } from 'node:net';
2
+ import { lookup as dnsLookup } from 'node:dns/promises';
3
+ import { isPrivateHostname, isPrivateIpv4, isPrivateIpv6 } from './is-private-ip.js';
4
+ import { InvalidUrlError, PrivateAddressError, PrivateHostnameError, ResolutionEmptyError, ResolutionFailedError, UnsupportedSchemeError, } from './errors.js';
5
+ const DEFAULT_ALLOWED_SCHEMES = ['http', 'https'];
6
+ /**
7
+ * Validates that `url` is a parseable HTTP(S) URL whose hostname resolves
8
+ * to publicly routable IP addresses, and returns one of those addresses
9
+ * pinned for the caller to use as the connect target.
10
+ *
11
+ * DNS resolution is performed with `all: true`; the URL is rejected if any
12
+ * resolved address is in a private / loopback / link-local /
13
+ * cloud-metadata range, so a mixed public/private DNS response cannot
14
+ * sneak a private record through.
15
+ *
16
+ * Per ADR-035, this function throws subclasses of {@link UrlValidationError}
17
+ * on failure. The structured payload (`code`, `message`, `received`,
18
+ * `expected`, `remediation`, `pointer`, `cause`) is available on the thrown
19
+ * instance.
20
+ *
21
+ * @see https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
22
+ * @see ../../../docs/adrs/035-utils-throws-structured-errors.md
23
+ * @throws {InvalidUrlError} `url` is not a parseable URL.
24
+ * @throws {UnsupportedSchemeError} the URL's scheme is not allowed.
25
+ * @throws {PrivateHostnameError} the hostname names a private resource.
26
+ * @throws {ResolutionFailedError} DNS resolution rejected.
27
+ * @throws {ResolutionEmptyError} DNS resolution returned no records.
28
+ * @throws {PrivateAddressError} any resolved record is private.
29
+ */
30
+ export async function validatePublicUrl(url, options) {
31
+ let parsed;
32
+ try {
33
+ parsed = new URL(url);
34
+ }
35
+ catch (cause) {
36
+ throw new InvalidUrlError(url, cause);
37
+ }
38
+ const allowedSchemes = options?.allowedSchemes ?? DEFAULT_ALLOWED_SCHEMES;
39
+ const scheme = parsed.protocol.toLowerCase().replace(/:$/, '');
40
+ if (!allowedSchemes.some((s) => s.toLowerCase() === scheme)) {
41
+ throw new UnsupportedSchemeError(scheme, allowedSchemes);
42
+ }
43
+ // URL.hostname wraps IPv6 literals in brackets (e.g. `[::1]`); strip them
44
+ // so the hostname can be passed to predicates and DNS resolution. Note
45
+ // that `URL` already discards any `userinfo@` prefix from the hostname,
46
+ // so smuggling attempts like `http://evil.com@127.0.0.1/` resolve to
47
+ // `127.0.0.1` and are caught by isPrivateHostname below.
48
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
49
+ if (isPrivateHostname(hostname)) {
50
+ throw new PrivateHostnameError(hostname);
51
+ }
52
+ // If the hostname is already an IP literal, skip DNS resolution; the
53
+ // literal itself is the resolved address, and the private-range check
54
+ // above (via isPrivateHostname) has already validated it.
55
+ const literalFamily = isIP(hostname);
56
+ if (literalFamily === 4 || literalFamily === 6) {
57
+ return { address: hostname, family: literalFamily };
58
+ }
59
+ let records;
60
+ try {
61
+ records = await dnsLookup(hostname, { family: options?.family ?? 0, all: true });
62
+ }
63
+ catch (cause) {
64
+ throw new ResolutionFailedError(hostname, cause);
65
+ }
66
+ if (records.length === 0) {
67
+ throw new ResolutionEmptyError(hostname);
68
+ }
69
+ const privateRecords = [];
70
+ let firstPublicRecord = null;
71
+ for (const record of records) {
72
+ if (record.family !== 4 && record.family !== 6) {
73
+ throw new ResolutionFailedError(hostname, new Error(`unsupported address family ${record.family}`));
74
+ }
75
+ const isPrivate = record.family === 4 ? isPrivateIpv4(record.address) : isPrivateIpv6(record.address);
76
+ if (isPrivate) {
77
+ privateRecords.push(record.address);
78
+ }
79
+ else if (!firstPublicRecord) {
80
+ firstPublicRecord = { address: record.address, family: record.family };
81
+ }
82
+ }
83
+ if (privateRecords.length > 0) {
84
+ throw new PrivateAddressError(hostname, privateRecords);
85
+ }
86
+ // Defensive: `records.length > 0` and `privateRecords.length === 0`
87
+ // implies a public record was assigned in the loop. Throwing rather
88
+ // than `!`-asserting keeps the invariant explicit at the boundary.
89
+ if (!firstPublicRecord) {
90
+ throw new ResolutionEmptyError(hostname);
91
+ }
92
+ return firstPublicRecord;
93
+ }
94
+ //# sourceMappingURL=validate-public-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-public-url.js","sourceRoot":"","sources":["../../src/node/validate-public-url.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};