@uncefact/untp-utils 0.1.0 → 0.3.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 (389) hide show
  1. package/README.md +52 -2
  2. package/artefacts/context/untp/0.6.0/dcc.json +1241 -0
  3. package/artefacts/context/untp/0.6.0/dfr.json +619 -0
  4. package/artefacts/context/untp/0.6.0/dia.json +162 -0
  5. package/artefacts/context/untp/0.6.0/dpp.json +1078 -0
  6. package/artefacts/context/untp/0.6.0/dte.json +1285 -0
  7. package/artefacts/context/untp/0.6.1/dcc.json +1388 -0
  8. package/artefacts/context/untp/0.6.1/dfr.json +690 -0
  9. package/artefacts/context/untp/0.6.1/dia.json +196 -0
  10. package/artefacts/context/untp/0.6.1/dpp.json +1164 -0
  11. package/artefacts/context/untp/0.6.1/dte.json +1340 -0
  12. package/artefacts/context/untp/0.7.0/untp.json +3493 -0
  13. package/artefacts/context/vcdm/2/credentials.json +301 -0
  14. package/artefacts/manifest.json +214 -0
  15. package/artefacts/schema/untp/0.6.0/dcc.json +1131 -0
  16. package/artefacts/schema/untp/0.6.0/dfr.json +1028 -0
  17. package/artefacts/schema/untp/0.6.0/dia.json +236 -0
  18. package/artefacts/schema/untp/0.6.0/dpp.json +1219 -0
  19. package/artefacts/schema/untp/0.6.0/dte.json +845 -0
  20. package/artefacts/schema/untp/0.6.1/dcc.json +1196 -0
  21. package/artefacts/schema/untp/0.6.1/dfr.json +1037 -0
  22. package/artefacts/schema/untp/0.6.1/dia.json +236 -0
  23. package/artefacts/schema/untp/0.6.1/dpp.json +1229 -0
  24. package/artefacts/schema/untp/0.6.1/dte.json +845 -0
  25. package/artefacts/schema/untp/0.7.0/cvc.json +701 -0
  26. package/artefacts/schema/untp/0.7.0/dcc.json +1133 -0
  27. package/artefacts/schema/untp/0.7.0/dfr.json +1245 -0
  28. package/artefacts/schema/untp/0.7.0/dia.json +443 -0
  29. package/artefacts/schema/untp/0.7.0/dpp.json +1300 -0
  30. package/artefacts/schema/untp/0.7.0/dte.json +1226 -0
  31. package/artefacts/schema/untp/0.7.0/linkset.json +119 -0
  32. package/artefacts/schema/vcdm/2/verifiable-credential.json +318 -0
  33. package/build/artefacts/detect-version-from-context.d.ts +46 -0
  34. package/build/artefacts/detect-version-from-context.js +77 -0
  35. package/build/artefacts/detect-version-from-context.js.map +1 -0
  36. package/build/artefacts/detect-version-from-context.test.d.ts +1 -0
  37. package/build/artefacts/detect-version-from-context.test.js +69 -0
  38. package/build/artefacts/detect-version-from-context.test.js.map +1 -0
  39. package/build/artefacts/index.d.ts +3 -0
  40. package/build/artefacts/index.js +4 -0
  41. package/build/artefacts/index.js.map +1 -0
  42. package/build/artefacts/urls.d.ts +85 -0
  43. package/build/artefacts/urls.js +141 -0
  44. package/build/artefacts/urls.js.map +1 -0
  45. package/build/artefacts/urls.test.d.ts +1 -0
  46. package/build/artefacts/urls.test.js +99 -0
  47. package/build/artefacts/urls.test.js.map +1 -0
  48. package/build/artefacts/version.d.ts +9 -0
  49. package/build/artefacts/version.js +17 -0
  50. package/build/artefacts/version.js.map +1 -0
  51. package/build/artefacts/version.test.d.ts +1 -0
  52. package/build/artefacts/version.test.js +16 -0
  53. package/build/artefacts/version.test.js.map +1 -0
  54. package/build/bundle/artefacts/context/untp/0.6.0/dcc.d.ts +2 -0
  55. package/build/bundle/artefacts/context/untp/0.6.0/dcc.js +1244 -0
  56. package/build/bundle/artefacts/context/untp/0.6.0/dcc.js.map +1 -0
  57. package/build/bundle/artefacts/context/untp/0.6.0/dfr.d.ts +2 -0
  58. package/build/bundle/artefacts/context/untp/0.6.0/dfr.js +622 -0
  59. package/build/bundle/artefacts/context/untp/0.6.0/dfr.js.map +1 -0
  60. package/build/bundle/artefacts/context/untp/0.6.0/dia.d.ts +2 -0
  61. package/build/bundle/artefacts/context/untp/0.6.0/dia.js +165 -0
  62. package/build/bundle/artefacts/context/untp/0.6.0/dia.js.map +1 -0
  63. package/build/bundle/artefacts/context/untp/0.6.0/dpp.d.ts +2 -0
  64. package/build/bundle/artefacts/context/untp/0.6.0/dpp.js +1081 -0
  65. package/build/bundle/artefacts/context/untp/0.6.0/dpp.js.map +1 -0
  66. package/build/bundle/artefacts/context/untp/0.6.0/dte.d.ts +2 -0
  67. package/build/bundle/artefacts/context/untp/0.6.0/dte.js +1288 -0
  68. package/build/bundle/artefacts/context/untp/0.6.0/dte.js.map +1 -0
  69. package/build/bundle/artefacts/context/untp/0.6.1/dcc.d.ts +2 -0
  70. package/build/bundle/artefacts/context/untp/0.6.1/dcc.js +1391 -0
  71. package/build/bundle/artefacts/context/untp/0.6.1/dcc.js.map +1 -0
  72. package/build/bundle/artefacts/context/untp/0.6.1/dfr.d.ts +2 -0
  73. package/build/bundle/artefacts/context/untp/0.6.1/dfr.js +693 -0
  74. package/build/bundle/artefacts/context/untp/0.6.1/dfr.js.map +1 -0
  75. package/build/bundle/artefacts/context/untp/0.6.1/dia.d.ts +2 -0
  76. package/build/bundle/artefacts/context/untp/0.6.1/dia.js +199 -0
  77. package/build/bundle/artefacts/context/untp/0.6.1/dia.js.map +1 -0
  78. package/build/bundle/artefacts/context/untp/0.6.1/dpp.d.ts +2 -0
  79. package/build/bundle/artefacts/context/untp/0.6.1/dpp.js +1167 -0
  80. package/build/bundle/artefacts/context/untp/0.6.1/dpp.js.map +1 -0
  81. package/build/bundle/artefacts/context/untp/0.6.1/dte.d.ts +2 -0
  82. package/build/bundle/artefacts/context/untp/0.6.1/dte.js +1343 -0
  83. package/build/bundle/artefacts/context/untp/0.6.1/dte.js.map +1 -0
  84. package/build/bundle/artefacts/context/untp/0.7.0/untp.d.ts +2 -0
  85. package/build/bundle/artefacts/context/untp/0.7.0/untp.js +3496 -0
  86. package/build/bundle/artefacts/context/untp/0.7.0/untp.js.map +1 -0
  87. package/build/bundle/artefacts/context/vcdm/2/credentials.d.ts +2 -0
  88. package/build/bundle/artefacts/context/vcdm/2/credentials.js +304 -0
  89. package/build/bundle/artefacts/context/vcdm/2/credentials.js.map +1 -0
  90. package/build/bundle/artefacts/schema/untp/0.6.0/dcc.d.ts +2 -0
  91. package/build/bundle/artefacts/schema/untp/0.6.0/dcc.js +1134 -0
  92. package/build/bundle/artefacts/schema/untp/0.6.0/dcc.js.map +1 -0
  93. package/build/bundle/artefacts/schema/untp/0.6.0/dfr.d.ts +2 -0
  94. package/build/bundle/artefacts/schema/untp/0.6.0/dfr.js +1031 -0
  95. package/build/bundle/artefacts/schema/untp/0.6.0/dfr.js.map +1 -0
  96. package/build/bundle/artefacts/schema/untp/0.6.0/dia.d.ts +2 -0
  97. package/build/bundle/artefacts/schema/untp/0.6.0/dia.js +239 -0
  98. package/build/bundle/artefacts/schema/untp/0.6.0/dia.js.map +1 -0
  99. package/build/bundle/artefacts/schema/untp/0.6.0/dpp.d.ts +2 -0
  100. package/build/bundle/artefacts/schema/untp/0.6.0/dpp.js +1222 -0
  101. package/build/bundle/artefacts/schema/untp/0.6.0/dpp.js.map +1 -0
  102. package/build/bundle/artefacts/schema/untp/0.6.0/dte.d.ts +2 -0
  103. package/build/bundle/artefacts/schema/untp/0.6.0/dte.js +848 -0
  104. package/build/bundle/artefacts/schema/untp/0.6.0/dte.js.map +1 -0
  105. package/build/bundle/artefacts/schema/untp/0.6.1/dcc.d.ts +2 -0
  106. package/build/bundle/artefacts/schema/untp/0.6.1/dcc.js +1199 -0
  107. package/build/bundle/artefacts/schema/untp/0.6.1/dcc.js.map +1 -0
  108. package/build/bundle/artefacts/schema/untp/0.6.1/dfr.d.ts +2 -0
  109. package/build/bundle/artefacts/schema/untp/0.6.1/dfr.js +1040 -0
  110. package/build/bundle/artefacts/schema/untp/0.6.1/dfr.js.map +1 -0
  111. package/build/bundle/artefacts/schema/untp/0.6.1/dia.d.ts +2 -0
  112. package/build/bundle/artefacts/schema/untp/0.6.1/dia.js +239 -0
  113. package/build/bundle/artefacts/schema/untp/0.6.1/dia.js.map +1 -0
  114. package/build/bundle/artefacts/schema/untp/0.6.1/dpp.d.ts +2 -0
  115. package/build/bundle/artefacts/schema/untp/0.6.1/dpp.js +1232 -0
  116. package/build/bundle/artefacts/schema/untp/0.6.1/dpp.js.map +1 -0
  117. package/build/bundle/artefacts/schema/untp/0.6.1/dte.d.ts +2 -0
  118. package/build/bundle/artefacts/schema/untp/0.6.1/dte.js +848 -0
  119. package/build/bundle/artefacts/schema/untp/0.6.1/dte.js.map +1 -0
  120. package/build/bundle/artefacts/schema/untp/0.7.0/cvc.d.ts +2 -0
  121. package/build/bundle/artefacts/schema/untp/0.7.0/cvc.js +704 -0
  122. package/build/bundle/artefacts/schema/untp/0.7.0/cvc.js.map +1 -0
  123. package/build/bundle/artefacts/schema/untp/0.7.0/dcc.d.ts +2 -0
  124. package/build/bundle/artefacts/schema/untp/0.7.0/dcc.js +1136 -0
  125. package/build/bundle/artefacts/schema/untp/0.7.0/dcc.js.map +1 -0
  126. package/build/bundle/artefacts/schema/untp/0.7.0/dfr.d.ts +2 -0
  127. package/build/bundle/artefacts/schema/untp/0.7.0/dfr.js +1248 -0
  128. package/build/bundle/artefacts/schema/untp/0.7.0/dfr.js.map +1 -0
  129. package/build/bundle/artefacts/schema/untp/0.7.0/dia.d.ts +2 -0
  130. package/build/bundle/artefacts/schema/untp/0.7.0/dia.js +446 -0
  131. package/build/bundle/artefacts/schema/untp/0.7.0/dia.js.map +1 -0
  132. package/build/bundle/artefacts/schema/untp/0.7.0/dpp.d.ts +2 -0
  133. package/build/bundle/artefacts/schema/untp/0.7.0/dpp.js +1303 -0
  134. package/build/bundle/artefacts/schema/untp/0.7.0/dpp.js.map +1 -0
  135. package/build/bundle/artefacts/schema/untp/0.7.0/dte.d.ts +2 -0
  136. package/build/bundle/artefacts/schema/untp/0.7.0/dte.js +1229 -0
  137. package/build/bundle/artefacts/schema/untp/0.7.0/dte.js.map +1 -0
  138. package/build/bundle/artefacts/schema/untp/0.7.0/linkset.d.ts +2 -0
  139. package/build/bundle/artefacts/schema/untp/0.7.0/linkset.js +122 -0
  140. package/build/bundle/artefacts/schema/untp/0.7.0/linkset.js.map +1 -0
  141. package/build/bundle/artefacts/schema/vcdm/2/verifiable-credential.d.ts +2 -0
  142. package/build/bundle/artefacts/schema/vcdm/2/verifiable-credential.js +321 -0
  143. package/build/bundle/artefacts/schema/vcdm/2/verifiable-credential.js.map +1 -0
  144. package/build/bundle/fallback.d.ts +41 -0
  145. package/build/bundle/fallback.js +62 -0
  146. package/build/bundle/fallback.js.map +1 -0
  147. package/build/bundle/fallback.test.d.ts +1 -0
  148. package/build/bundle/fallback.test.js +124 -0
  149. package/build/bundle/fallback.test.js.map +1 -0
  150. package/build/bundle/index.d.ts +4 -0
  151. package/build/bundle/index.js +77 -0
  152. package/build/bundle/index.js.map +1 -0
  153. package/build/bundle/lookup.d.ts +14 -0
  154. package/build/bundle/lookup.js +40 -0
  155. package/build/bundle/lookup.js.map +1 -0
  156. package/build/bundle/lookup.test.d.ts +1 -0
  157. package/build/bundle/lookup.test.js +70 -0
  158. package/build/bundle/lookup.test.js.map +1 -0
  159. package/build/bundle/public.d.ts +25 -0
  160. package/build/bundle/public.js +56 -0
  161. package/build/bundle/public.js.map +1 -0
  162. package/build/bundle/public.test.d.ts +1 -0
  163. package/build/bundle/public.test.js +35 -0
  164. package/build/bundle/public.test.js.map +1 -0
  165. package/build/bundle/refresh-classify.test.d.ts +1 -0
  166. package/build/bundle/refresh-classify.test.js +20 -0
  167. package/build/bundle/refresh-classify.test.js.map +1 -0
  168. package/build/bundle/refresh-main.test.d.ts +1 -0
  169. package/build/bundle/refresh-main.test.js +98 -0
  170. package/build/bundle/refresh-main.test.js.map +1 -0
  171. package/build/cache/in-memory-ttl-cache.d.ts +2 -0
  172. package/build/cache/in-memory-ttl-cache.js +76 -0
  173. package/build/cache/in-memory-ttl-cache.js.map +1 -0
  174. package/build/cache/in-memory-ttl-cache.test.d.ts +1 -0
  175. package/build/cache/in-memory-ttl-cache.test.js +236 -0
  176. package/build/cache/in-memory-ttl-cache.test.js.map +1 -0
  177. package/build/cache/index.d.ts +2 -0
  178. package/build/cache/index.js +2 -0
  179. package/build/cache/index.js.map +1 -0
  180. package/build/cache/ttl-cache.d.ts +22 -0
  181. package/build/cache/ttl-cache.js +2 -0
  182. package/build/cache/ttl-cache.js.map +1 -0
  183. package/build/common/as-date-time.d.ts +13 -0
  184. package/build/common/as-date-time.js +44 -0
  185. package/build/common/as-date-time.js.map +1 -0
  186. package/build/common/as-date-time.test.d.ts +1 -0
  187. package/build/common/as-date-time.test.js +42 -0
  188. package/build/common/as-date-time.test.js.map +1 -0
  189. package/build/common/as-non-empty-string.d.ts +14 -0
  190. package/build/common/as-non-empty-string.js +20 -0
  191. package/build/common/as-non-empty-string.js.map +1 -0
  192. package/build/common/as-non-empty-string.test.d.ts +1 -0
  193. package/build/common/as-non-empty-string.test.js +30 -0
  194. package/build/common/as-non-empty-string.test.js.map +1 -0
  195. package/build/common/index.d.ts +4 -0
  196. package/build/common/index.js +5 -0
  197. package/build/common/index.js.map +1 -0
  198. package/build/common/require-string.d.ts +12 -0
  199. package/build/common/require-string.js +27 -0
  200. package/build/common/require-string.js.map +1 -0
  201. package/build/common/require-string.test.d.ts +1 -0
  202. package/build/common/require-string.test.js +53 -0
  203. package/build/common/require-string.test.js.map +1 -0
  204. package/build/common/validity-window.d.ts +45 -0
  205. package/build/common/validity-window.js +68 -0
  206. package/build/common/validity-window.js.map +1 -0
  207. package/build/common/validity-window.test.d.ts +1 -0
  208. package/build/common/validity-window.test.js +67 -0
  209. package/build/common/validity-window.test.js.map +1 -0
  210. package/build/conformity-vocabulary/codes.d.ts +10 -0
  211. package/build/conformity-vocabulary/codes.js +10 -0
  212. package/build/conformity-vocabulary/codes.js.map +1 -0
  213. package/build/conformity-vocabulary/errors.d.ts +44 -0
  214. package/build/conformity-vocabulary/errors.js +68 -0
  215. package/build/conformity-vocabulary/errors.js.map +1 -0
  216. package/build/conformity-vocabulary/index.d.ts +6 -0
  217. package/build/conformity-vocabulary/index.js +7 -0
  218. package/build/conformity-vocabulary/index.js.map +1 -0
  219. package/build/conformity-vocabulary/parse-conformity-catalogue.d.ts +26 -0
  220. package/build/conformity-vocabulary/parse-conformity-catalogue.js +106 -0
  221. package/build/conformity-vocabulary/parse-conformity-catalogue.js.map +1 -0
  222. package/build/conformity-vocabulary/parse-conformity-catalogue.test.d.ts +1 -0
  223. package/build/conformity-vocabulary/parse-conformity-catalogue.test.js +327 -0
  224. package/build/conformity-vocabulary/parse-conformity-catalogue.test.js.map +1 -0
  225. package/build/conformity-vocabulary/parse-conformity-scheme.d.ts +28 -0
  226. package/build/conformity-vocabulary/parse-conformity-scheme.js +47 -0
  227. package/build/conformity-vocabulary/parse-conformity-scheme.js.map +1 -0
  228. package/build/conformity-vocabulary/parse-conformity-scheme.test.d.ts +1 -0
  229. package/build/conformity-vocabulary/parse-conformity-scheme.test.js +284 -0
  230. package/build/conformity-vocabulary/parse-conformity-scheme.test.js.map +1 -0
  231. package/build/conformity-vocabulary/parsers/v0-7-0.parser.d.ts +19 -0
  232. package/build/conformity-vocabulary/parsers/v0-7-0.parser.js +219 -0
  233. package/build/conformity-vocabulary/parsers/v0-7-0.parser.js.map +1 -0
  234. package/build/conformity-vocabulary/types.d.ts +180 -0
  235. package/build/conformity-vocabulary/types.js +2 -0
  236. package/build/conformity-vocabulary/types.js.map +1 -0
  237. package/build/conformity-vocabulary/validate-conformity-claim.d.ts +26 -0
  238. package/build/conformity-vocabulary/validate-conformity-claim.js +165 -0
  239. package/build/conformity-vocabulary/validate-conformity-claim.js.map +1 -0
  240. package/build/conformity-vocabulary/validate-conformity-claim.test.d.ts +1 -0
  241. package/build/conformity-vocabulary/validate-conformity-claim.test.js +478 -0
  242. package/build/conformity-vocabulary/validate-conformity-claim.test.js.map +1 -0
  243. package/build/http-headers/index.d.ts +5 -0
  244. package/build/http-headers/index.js +6 -0
  245. package/build/http-headers/index.js.map +1 -0
  246. package/build/http-headers/is-safe-header-value.d.ts +10 -0
  247. package/build/http-headers/is-safe-header-value.js +15 -0
  248. package/build/http-headers/is-safe-header-value.js.map +1 -0
  249. package/build/http-headers/is-safe-header-value.test.d.ts +1 -0
  250. package/build/http-headers/is-safe-header-value.test.js +37 -0
  251. package/build/http-headers/is-safe-header-value.test.js.map +1 -0
  252. package/build/http-headers/parse-entity-tag.d.ts +9 -0
  253. package/build/http-headers/parse-entity-tag.js +18 -0
  254. package/build/http-headers/parse-entity-tag.js.map +1 -0
  255. package/build/http-headers/parse-entity-tag.test.d.ts +1 -0
  256. package/build/http-headers/parse-entity-tag.test.js +41 -0
  257. package/build/http-headers/parse-entity-tag.test.js.map +1 -0
  258. package/build/http-headers/parse-imf-date.d.ts +13 -0
  259. package/build/http-headers/parse-imf-date.js +23 -0
  260. package/build/http-headers/parse-imf-date.js.map +1 -0
  261. package/build/http-headers/parse-imf-date.test.d.ts +1 -0
  262. package/build/http-headers/parse-imf-date.test.js +31 -0
  263. package/build/http-headers/parse-imf-date.test.js.map +1 -0
  264. package/build/http-headers/parse-media-type.d.ts +8 -0
  265. package/build/http-headers/parse-media-type.js +21 -0
  266. package/build/http-headers/parse-media-type.js.map +1 -0
  267. package/build/http-headers/parse-media-type.test.d.ts +1 -0
  268. package/build/http-headers/parse-media-type.test.js +39 -0
  269. package/build/http-headers/parse-media-type.test.js.map +1 -0
  270. package/build/http-headers/user-agent.d.ts +35 -0
  271. package/build/http-headers/user-agent.js +45 -0
  272. package/build/http-headers/user-agent.js.map +1 -0
  273. package/build/http-headers/user-agent.test.d.ts +1 -0
  274. package/build/http-headers/user-agent.test.js +27 -0
  275. package/build/http-headers/user-agent.test.js.map +1 -0
  276. package/build/index.d.ts +1 -1
  277. package/build/index.js +1 -1
  278. package/build/index.js.map +1 -1
  279. package/build/loaders/errors.d.ts +21 -0
  280. package/build/loaders/errors.js +45 -0
  281. package/build/loaders/errors.js.map +1 -0
  282. package/build/loaders/index.d.ts +5 -0
  283. package/build/loaders/index.js +5 -0
  284. package/build/loaders/index.js.map +1 -0
  285. package/build/loaders/jsonld-document-loader.d.ts +48 -0
  286. package/build/loaders/jsonld-document-loader.js +43 -0
  287. package/build/loaders/jsonld-document-loader.js.map +1 -0
  288. package/build/loaders/jsonld-document-loader.test.d.ts +1 -0
  289. package/build/loaders/jsonld-document-loader.test.js +111 -0
  290. package/build/loaders/jsonld-document-loader.test.js.map +1 -0
  291. package/build/loaders/schema-loader.d.ts +31 -0
  292. package/build/loaders/schema-loader.js +83 -0
  293. package/build/loaders/schema-loader.js.map +1 -0
  294. package/build/loaders/schema-loader.ssrf.test.d.ts +1 -0
  295. package/build/loaders/schema-loader.ssrf.test.js +55 -0
  296. package/build/loaders/schema-loader.ssrf.test.js.map +1 -0
  297. package/build/loaders/schema-loader.test.d.ts +1 -0
  298. package/build/loaders/schema-loader.test.js +217 -0
  299. package/build/loaders/schema-loader.test.js.map +1 -0
  300. package/build/multibase-digest/multibase-digest.d.ts +23 -0
  301. package/build/multibase-digest/multibase-digest.js +40 -0
  302. package/build/multibase-digest/multibase-digest.js.map +1 -1
  303. package/build/multibase-digest/multibase-digest.test.js +46 -0
  304. package/build/multibase-digest/multibase-digest.test.js.map +1 -1
  305. package/build/node/errors.d.ts +47 -0
  306. package/build/node/errors.js +86 -0
  307. package/build/node/errors.js.map +1 -0
  308. package/build/node/index.d.ts +3 -0
  309. package/build/node/index.js +4 -0
  310. package/build/node/index.js.map +1 -0
  311. package/build/node/is-private-ip.d.ts +45 -0
  312. package/build/node/is-private-ip.fail-closed.test.d.ts +1 -0
  313. package/build/node/is-private-ip.fail-closed.test.js +36 -0
  314. package/build/node/is-private-ip.fail-closed.test.js.map +1 -0
  315. package/build/node/is-private-ip.js +203 -0
  316. package/build/node/is-private-ip.js.map +1 -0
  317. package/build/node/is-private-ip.test.d.ts +1 -0
  318. package/build/node/is-private-ip.test.js +124 -0
  319. package/build/node/is-private-ip.test.js.map +1 -0
  320. package/build/node/validate-public-url.d.ts +81 -0
  321. package/build/node/validate-public-url.js +128 -0
  322. package/build/node/validate-public-url.js.map +1 -0
  323. package/build/node/validate-public-url.test.d.ts +1 -0
  324. package/build/node/validate-public-url.test.js +324 -0
  325. package/build/node/validate-public-url.test.js.map +1 -0
  326. package/build/resolvers/errors.d.ts +73 -0
  327. package/build/resolvers/errors.js +133 -0
  328. package/build/resolvers/errors.js.map +1 -0
  329. package/build/resolvers/index.d.ts +5 -0
  330. package/build/resolvers/index.js +6 -0
  331. package/build/resolvers/index.js.map +1 -0
  332. package/build/resolvers/resolve-document-if-changed.d.ts +48 -0
  333. package/build/resolvers/resolve-document-if-changed.js +42 -0
  334. package/build/resolvers/resolve-document-if-changed.js.map +1 -0
  335. package/build/resolvers/resolve-document-if-changed.test.d.ts +1 -0
  336. package/build/resolvers/resolve-document-if-changed.test.js +116 -0
  337. package/build/resolvers/resolve-document-if-changed.test.js.map +1 -0
  338. package/build/resolvers/resolve-document.d.ts +106 -0
  339. package/build/resolvers/resolve-document.integration.test.d.ts +1 -0
  340. package/build/resolvers/resolve-document.integration.test.js +108 -0
  341. package/build/resolvers/resolve-document.integration.test.js.map +1 -0
  342. package/build/resolvers/resolve-document.js +319 -0
  343. package/build/resolvers/resolve-document.js.map +1 -0
  344. package/build/resolvers/resolve-document.test.d.ts +1 -0
  345. package/build/resolvers/resolve-document.test.js +749 -0
  346. package/build/resolvers/resolve-document.test.js.map +1 -0
  347. package/build/resolvers/resolve-json-document.d.ts +43 -0
  348. package/build/resolvers/resolve-json-document.js +43 -0
  349. package/build/resolvers/resolve-json-document.js.map +1 -0
  350. package/build/resolvers/resolve-json-document.test.d.ts +1 -0
  351. package/build/resolvers/resolve-json-document.test.js +78 -0
  352. package/build/resolvers/resolve-json-document.test.js.map +1 -0
  353. package/build/structured-error.d.ts +49 -0
  354. package/build/structured-error.js +17 -0
  355. package/build/structured-error.js.map +1 -0
  356. package/build/structured-error.test.d.ts +1 -0
  357. package/build/structured-error.test.js +112 -0
  358. package/build/structured-error.test.js.map +1 -0
  359. package/build/validation/describe-jsonld-failure.d.ts +90 -0
  360. package/build/validation/describe-jsonld-failure.js +171 -0
  361. package/build/validation/describe-jsonld-failure.js.map +1 -0
  362. package/build/validation/describe-jsonld-failure.test.d.ts +1 -0
  363. package/build/validation/describe-jsonld-failure.test.js +226 -0
  364. package/build/validation/describe-jsonld-failure.test.js.map +1 -0
  365. package/build/validation/errors.d.ts +40 -0
  366. package/build/validation/errors.js +75 -0
  367. package/build/validation/errors.js.map +1 -0
  368. package/build/validation/index.d.ts +5 -0
  369. package/build/validation/index.js +5 -0
  370. package/build/validation/index.js.map +1 -0
  371. package/build/validation/validate-against-schemas.d.ts +22 -0
  372. package/build/validation/validate-against-schemas.js +65 -0
  373. package/build/validation/validate-against-schemas.js.map +1 -0
  374. package/build/validation/validate-against-schemas.test.d.ts +1 -0
  375. package/build/validation/validate-against-schemas.test.js +169 -0
  376. package/build/validation/validate-against-schemas.test.js.map +1 -0
  377. package/build/validation/validate-jsonld.context-cache.test.d.ts +1 -0
  378. package/build/validation/validate-jsonld.context-cache.test.js +32 -0
  379. package/build/validation/validate-jsonld.context-cache.test.js.map +1 -0
  380. package/build/validation/validate-jsonld.d.ts +59 -0
  381. package/build/validation/validate-jsonld.js +126 -0
  382. package/build/validation/validate-jsonld.js.map +1 -0
  383. package/build/validation/validate-jsonld.ssrf.test.d.ts +1 -0
  384. package/build/validation/validate-jsonld.ssrf.test.js +108 -0
  385. package/build/validation/validate-jsonld.ssrf.test.js.map +1 -0
  386. package/build/validation/validate-jsonld.test.d.ts +1 -0
  387. package/build/validation/validate-jsonld.test.js +257 -0
  388. package/build/validation/validate-jsonld.test.js.map +1 -0
  389. package/package.json +86 -5
@@ -0,0 +1,749 @@
1
+ import { jest } from '@jest/globals';
2
+ import { ReadableStream } from 'node:stream/web';
3
+ import { MultibaseDigest } from '../multibase-digest/index.js';
4
+ import { PrivateAddressError, ResolutionFailedError } from '../node/index.js';
5
+ import { ResolverHttpError, ResolverNetworkError, ResolverRedirectMissingLocationError, ResolverTimedOutError, ResolverTooLargeError, ResolverTooManyRedirectsError, } from './errors.js';
6
+ const undiciFetch = jest.fn();
7
+ const NativeDOMException = globalThis.DOMException;
8
+ let lastAgentClose = jest.fn(() => Promise.resolve());
9
+ let agentConstructionError;
10
+ let agentCloseImplementation = () => Promise.resolve();
11
+ // Constructor options of every Agent created during a test, in creation
12
+ // order. The `connect.lookup` inside is the IP pin under test.
13
+ let agentOptions = [];
14
+ // The FakeAgent instances themselves, so tests can assert the dispatcher
15
+ // passed to fetch IS the pinned agent, not merely that a pinned agent exists.
16
+ let agentInstances = [];
17
+ class FakeAgent {
18
+ close;
19
+ constructor(options) {
20
+ if (agentConstructionError !== undefined)
21
+ throw agentConstructionError;
22
+ agentOptions.push(options);
23
+ agentInstances.push(this);
24
+ this.close = jest.fn(() => agentCloseImplementation());
25
+ lastAgentClose = this.close;
26
+ }
27
+ }
28
+ jest.unstable_mockModule('undici', () => ({
29
+ Agent: FakeAgent,
30
+ fetch: undiciFetch,
31
+ }));
32
+ const validatePublicUrl = jest.fn();
33
+ jest.unstable_mockModule('../node/index.js', () => ({
34
+ validatePublicUrl,
35
+ PrivateAddressError,
36
+ }));
37
+ const { resolveDocument } = await import('./resolve-document.js');
38
+ const { DEFAULT_USER_AGENT, USER_AGENT_ENV_VAR } = await import('../http-headers/index.js');
39
+ function makeResponse(opts) {
40
+ const status = opts.status ?? 200;
41
+ const headers = new Headers(opts.headers ?? {});
42
+ let bodyStream;
43
+ if (opts.body === null || opts.body === undefined) {
44
+ bodyStream = null;
45
+ }
46
+ else {
47
+ const bytes = typeof opts.body === 'string' ? new TextEncoder().encode(opts.body) : opts.body;
48
+ bodyStream = new ReadableStream({
49
+ start(controller) {
50
+ controller.enqueue(bytes);
51
+ controller.close();
52
+ },
53
+ });
54
+ }
55
+ return {
56
+ status,
57
+ ok: opts.ok ?? (status >= 200 && status < 300),
58
+ headers,
59
+ body: bodyStream,
60
+ };
61
+ }
62
+ /**
63
+ * The shape `validatePublicUrl` returns for a single validated address:
64
+ * `address` / `family` repeat the first entry of `addresses`, and the
65
+ * resolver hands the whole of `addresses` to the connector.
66
+ */
67
+ function resolvedAddress(address = '1.1.1.1', family = 4) {
68
+ return { address, family, addresses: [{ address, family }] };
69
+ }
70
+ describe('resolveDocument', () => {
71
+ beforeEach(() => {
72
+ agentOptions = [];
73
+ agentInstances = [];
74
+ agentConstructionError = undefined;
75
+ agentCloseImplementation = () => Promise.resolve();
76
+ undiciFetch.mockReset();
77
+ validatePublicUrl.mockReset();
78
+ });
79
+ describe('SSRF guard', () => {
80
+ it('propagates validatePublicUrl errors unwrapped and never calls fetch', async () => {
81
+ validatePublicUrl.mockRejectedValue(new PrivateAddressError('https://attacker.example/', ['10.0.0.1']));
82
+ await expect(resolveDocument('https://attacker.example/')).rejects.toBeInstanceOf(PrivateAddressError);
83
+ expect(undiciFetch).not.toHaveBeenCalled();
84
+ });
85
+ });
86
+ describe('happy path', () => {
87
+ it('keeps the default DNS lookup path unchanged when no lookup is supplied', async () => {
88
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
89
+ undiciFetch.mockResolvedValue(makeResponse({ body: 'ok' }));
90
+ await resolveDocument('https://example.com/doc');
91
+ expect(validatePublicUrl).toHaveBeenCalledWith('https://example.com/doc', {
92
+ allowedSchemes: undefined,
93
+ allowPrivateAddresses: undefined,
94
+ });
95
+ });
96
+ it('forwards a supplied lookup to validation and pins its result into the connector', async () => {
97
+ const lookup = jest
98
+ .fn()
99
+ .mockResolvedValueOnce([{ address: '203.0.113.10', family: 4 }])
100
+ .mockResolvedValueOnce([{ address: '203.0.113.20', family: 4 }]);
101
+ validatePublicUrl.mockImplementation(async (_url, options) => {
102
+ const records = await options.lookup('example.com', { family: 0, all: true });
103
+ const [first] = records;
104
+ return { address: first.address, family: first.family, addresses: records };
105
+ });
106
+ undiciFetch.mockResolvedValue(makeResponse({ body: 'ok' }));
107
+ await resolveDocument('https://example.com/doc', { lookup });
108
+ expect(validatePublicUrl).toHaveBeenCalledWith('https://example.com/doc', expect.objectContaining({ lookup }));
109
+ const connectorLookup = agentOptions[0].connect.lookup;
110
+ const pinned = await new Promise((resolve, reject) => {
111
+ connectorLookup('example.com', {}, (error, addresses) => (error ? reject(error) : resolve(addresses)));
112
+ });
113
+ expect(pinned).toEqual([{ address: '203.0.113.10', family: 4 }]);
114
+ expect(lookup).toHaveBeenCalledTimes(1);
115
+ });
116
+ it('returns a LoadResult with body, digest, and the allowlisted headers', async () => {
117
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
118
+ undiciFetch.mockResolvedValue(makeResponse({
119
+ body: '{"hello":"world"}',
120
+ headers: {
121
+ 'content-type': 'application/json',
122
+ etag: '"abc"',
123
+ 'last-modified': 'Wed, 21 May 2026 12:00:00 GMT',
124
+ 'set-cookie': 'session=secret', // must NOT appear on the LoadResult
125
+ 'x-internal-trace': 'noisy', // ditto
126
+ },
127
+ }));
128
+ const result = await resolveDocument('https://example.com/doc.json');
129
+ expect(result.status).toBe(200);
130
+ expect(result.finalUrl).toBe('https://example.com/doc.json');
131
+ expect(result.etag).toBe('"abc"');
132
+ expect(result.lastModified).toBe('Wed, 21 May 2026 12:00:00 GMT');
133
+ expect(result.contentType).toBe('application/json');
134
+ expect(new TextDecoder().decode(result.body)).toBe('{"hello":"world"}');
135
+ expect(result.bodyDigest.toString()).toMatch(/^z[1-9A-HJ-NP-Za-km-z]+$/); // multibase base58btc
136
+ // Arbitrary upstream headers must NOT be echoed onto the LoadResult.
137
+ const valueAsRecord = result;
138
+ expect(valueAsRecord['set-cookie']).toBeUndefined();
139
+ expect(valueAsRecord['x-internal-trace']).toBeUndefined();
140
+ });
141
+ });
142
+ describe('outbound headers', () => {
143
+ it('sends only Accept-free defaults with a User-Agent and no correlation header', async () => {
144
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
145
+ undiciFetch.mockResolvedValue(makeResponse({ body: 'ok' }));
146
+ await resolveDocument('https://example.com/doc');
147
+ const headers = undiciFetch.mock.calls[0][1].headers;
148
+ const names = Object.keys(headers).map((name) => name.toLowerCase());
149
+ expect(names).toContain('user-agent');
150
+ expect(names).not.toContain('x-correlation-id');
151
+ });
152
+ });
153
+ describe('HTTP errors', () => {
154
+ it('throws ResolverHttpError for a 4xx response with status attached', async () => {
155
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
156
+ undiciFetch.mockResolvedValue(makeResponse({ status: 404, ok: false, body: 'not found' }));
157
+ const error = (await resolveDocument('https://example.com/missing').catch((e) => e));
158
+ expect(error).toBeInstanceOf(ResolverHttpError);
159
+ expect(error.status).toBe(404);
160
+ expect(error.url).toBe('https://example.com/missing');
161
+ });
162
+ it('carries the failing hop URL, not the original, when a redirect ends in an HTTP error', async () => {
163
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
164
+ undiciFetch
165
+ .mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'https://example.com/moved' }, body: null }))
166
+ .mockResolvedValueOnce(makeResponse({ status: 404, ok: false, body: 'gone' }));
167
+ const error = (await resolveDocument('https://example.com/start').catch((e) => e));
168
+ expect(error).toBeInstanceOf(ResolverHttpError);
169
+ expect(error.url).toBe('https://example.com/moved');
170
+ });
171
+ it('throws ResolverHttpError for a 5xx response', async () => {
172
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
173
+ undiciFetch.mockResolvedValue(makeResponse({ status: 503, ok: false, body: 'busy' }));
174
+ const error = (await resolveDocument('https://example.com/busy').catch((e) => e));
175
+ expect(error).toBeInstanceOf(ResolverHttpError);
176
+ expect(error.status).toBe(503);
177
+ });
178
+ });
179
+ describe('network errors and timeouts', () => {
180
+ it('throws ResolverNetworkError on fetch rejection', async () => {
181
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
182
+ undiciFetch.mockRejectedValue(new Error('ECONNREFUSED'));
183
+ const error = (await resolveDocument('https://example.com/').catch((e) => e));
184
+ expect(error).toBeInstanceOf(ResolverNetworkError);
185
+ expect(error.cause).toBeInstanceOf(Error);
186
+ });
187
+ it('throws ResolverNetworkError when the response body stream rejects mid-read', async () => {
188
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
189
+ const erroringStream = new ReadableStream({
190
+ pull(controller) {
191
+ controller.error(new Error('connection reset by peer'));
192
+ },
193
+ });
194
+ undiciFetch.mockResolvedValue({
195
+ status: 200,
196
+ ok: true,
197
+ headers: new Headers(),
198
+ body: erroringStream,
199
+ });
200
+ await expect(resolveDocument('https://example.com/')).rejects.toBeInstanceOf(ResolverNetworkError);
201
+ });
202
+ // A foreign abort-shaped rejection, synthesised here rather than observed
203
+ // from undici: any abort that is not ours can arrive while our budget is
204
+ // still running. Classifying on the name alone would report it as "timed
205
+ // out after 10000ms" when our signal never fired. Fails if the deadline
206
+ // check is dropped.
207
+ it('reports an abort-named fetch rejection as a network error while our deadline has not passed', async () => {
208
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
209
+ const abort = new NativeDOMException('The operation was aborted.', 'AbortError');
210
+ undiciFetch.mockRejectedValue(abort);
211
+ const error = (await resolveDocument('https://example.com/').catch((e) => e));
212
+ expect(error).toBeInstanceOf(ResolverNetworkError);
213
+ expect(error.cause).toBe(abort);
214
+ });
215
+ // Same rule at the body-read await: an upstream body timeout is not our
216
+ // budget expiring. Fails if `readWithLimit` classifies on the name alone.
217
+ it('reports an abort-named body-read rejection as a network error while our deadline has not passed', async () => {
218
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
219
+ const abort = new NativeDOMException('The operation was aborted.', 'AbortError');
220
+ const erroringStream = new ReadableStream({
221
+ pull(controller) {
222
+ controller.error(abort);
223
+ },
224
+ });
225
+ undiciFetch.mockResolvedValue({
226
+ status: 200,
227
+ ok: true,
228
+ headers: new Headers(),
229
+ body: erroringStream,
230
+ });
231
+ const error = (await resolveDocument('https://example.com/').catch((e) => e));
232
+ expect(error).toBeInstanceOf(ResolverNetworkError);
233
+ expect(error.cause).toBe(abort);
234
+ });
235
+ it('reports an abort-named body-read rejection as a timeout once our deadline has passed', async () => {
236
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
237
+ const abort = new NativeDOMException('The operation was aborted.', 'AbortError');
238
+ const reader = {
239
+ read: jest.fn(() => new Promise((_resolve, reject) => setTimeout(() => reject(abort), 40))),
240
+ cancel: jest.fn(() => Promise.resolve()),
241
+ };
242
+ undiciFetch.mockResolvedValue({
243
+ status: 200,
244
+ ok: true,
245
+ headers: new Headers(),
246
+ body: { getReader: () => reader },
247
+ });
248
+ const error = (await resolveDocument('https://example.com/', { totalTimeoutMs: 10 }).catch((e) => e));
249
+ expect(error).toBeInstanceOf(ResolverTimedOutError);
250
+ expect(error.cause).toBe(abort);
251
+ });
252
+ it('times out while the first-hop guard is unresolved without creating an Agent or fetching', async () => {
253
+ let releaseGuard;
254
+ const guard = new Promise((resolve) => {
255
+ releaseGuard = resolve;
256
+ });
257
+ validatePublicUrl.mockReturnValue(guard);
258
+ const resolution = resolveDocument('https://slow-dns.example/', { totalTimeoutMs: 20 });
259
+ await expect(resolution).rejects.toBeInstanceOf(ResolverTimedOutError);
260
+ expect(agentInstances).toHaveLength(0);
261
+ expect(undiciFetch).not.toHaveBeenCalled();
262
+ releaseGuard(resolvedAddress());
263
+ await Promise.resolve();
264
+ });
265
+ it('does not report a late guard rejection as unhandled after timeout', async () => {
266
+ let rejectGuard;
267
+ const guard = new Promise((_resolve, reject) => {
268
+ rejectGuard = reject;
269
+ });
270
+ validatePublicUrl.mockReturnValue(guard);
271
+ await expect(resolveDocument('https://slow-dns.example/', { totalTimeoutMs: 20 })).rejects.toBeInstanceOf(ResolverTimedOutError);
272
+ const unhandledRejection = jest.fn();
273
+ process.on('unhandledRejection', unhandledRejection);
274
+ try {
275
+ rejectGuard(new Error('late DNS failure'));
276
+ await new Promise((resolve) => setImmediate(resolve));
277
+ }
278
+ finally {
279
+ process.off('unhandledRejection', unhandledRejection);
280
+ }
281
+ expect(unhandledRejection).not.toHaveBeenCalled();
282
+ });
283
+ it('keeps the on-time DNS rejection unchanged', async () => {
284
+ const dnsError = new ResolutionFailedError('example.com', new Error('ENOTFOUND'));
285
+ validatePublicUrl.mockRejectedValue(dnsError);
286
+ await expect(resolveDocument('https://example.com/')).rejects.toBe(dnsError);
287
+ expect(undiciFetch).not.toHaveBeenCalled();
288
+ });
289
+ // The paired case for the fetch await: the same abort-shaped rejection is
290
+ // our timeout once the signal has fired.
291
+ it('classifies an in-flight fetch abort as a timeout', async () => {
292
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
293
+ const abort = new NativeDOMException('The operation was aborted.', 'AbortError');
294
+ undiciFetch.mockImplementation((_url, rawInit) => {
295
+ const init = rawInit;
296
+ return new Promise((_resolve, reject) => {
297
+ init.signal.addEventListener('abort', () => reject(abort), { once: true });
298
+ });
299
+ });
300
+ const error = (await resolveDocument('https://example.com/', { totalTimeoutMs: 20 }).catch((e) => e));
301
+ expect(error).toBeInstanceOf(ResolverTimedOutError);
302
+ expect(error.cause).toBe(abort);
303
+ });
304
+ });
305
+ describe('defect propagation', () => {
306
+ it('propagates a header-processing defect after a successful fetch unchanged', async () => {
307
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
308
+ const sentinel = new TypeError('header parser defect');
309
+ const headers = {
310
+ get: jest.fn(() => {
311
+ throw sentinel;
312
+ }),
313
+ };
314
+ undiciFetch.mockResolvedValue({ status: 200, ok: true, headers, body: null });
315
+ await expect(resolveDocument('https://example.com/')).rejects.toBe(sentinel);
316
+ expect(lastAgentClose).toHaveBeenCalled();
317
+ });
318
+ it('propagates an Agent construction defect unchanged', async () => {
319
+ const sentinel = new TypeError('agent construction defect');
320
+ agentConstructionError = sentinel;
321
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
322
+ await expect(resolveDocument('https://example.com/')).rejects.toBe(sentinel);
323
+ expect(undiciFetch).not.toHaveBeenCalled();
324
+ });
325
+ it('propagates a digest defect unchanged', async () => {
326
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
327
+ undiciFetch.mockResolvedValue(makeResponse({ body: 'ok' }));
328
+ const sentinel = new Error('digest defect');
329
+ const digestSpy = jest.spyOn(MultibaseDigest, 'fromData').mockRejectedValue(sentinel);
330
+ try {
331
+ await expect(resolveDocument('https://example.com/')).rejects.toBe(sentinel);
332
+ }
333
+ finally {
334
+ digestSpy.mockRestore();
335
+ }
336
+ expect(lastAgentClose).toHaveBeenCalled();
337
+ });
338
+ it('propagates a request-header construction defect before dispatch unchanged', async () => {
339
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
340
+ const sentinel = new TypeError('request header defect');
341
+ const headers = Object.defineProperty({}, 'User-Agent', {
342
+ enumerable: true,
343
+ get: () => {
344
+ throw sentinel;
345
+ },
346
+ });
347
+ await expect(resolveDocument('https://example.com/', { headers })).rejects.toBe(sentinel);
348
+ expect(undiciFetch).not.toHaveBeenCalled();
349
+ // The Agent is constructed before the headers are materialised, so the
350
+ // defect must still leave through the dispatcher's finally. Fails if the
351
+ // header work moves outside that try and leaks the Agent.
352
+ expect(lastAgentClose).toHaveBeenCalled();
353
+ });
354
+ // The sibling case above supplies an explicit `User-Agent`, which takes the
355
+ // early-return branch of withUserAgent. This one has no caller
356
+ // `User-Agent`, so the implicit branch spreads the caller's object to add
357
+ // the generated header, and must read the throwing `Accept` getter there.
358
+ // Fails if that branch stops materialising the caller's headers, which
359
+ // would defer the defect into the transport instead of raising it here.
360
+ it('propagates a request-header defect from the implicit User-Agent branch unchanged', async () => {
361
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
362
+ const sentinel = new TypeError('request header defect');
363
+ const headers = Object.defineProperty({}, 'Accept', {
364
+ enumerable: true,
365
+ get: () => {
366
+ throw sentinel;
367
+ },
368
+ });
369
+ await expect(resolveDocument('https://example.com/', { headers })).rejects.toBe(sentinel);
370
+ expect(undiciFetch).not.toHaveBeenCalled();
371
+ expect(lastAgentClose).toHaveBeenCalled();
372
+ });
373
+ it('does not classify an abort-named processing defect as a timeout', async () => {
374
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
375
+ const sentinel = new TypeError('processing defect');
376
+ sentinel.name = 'AbortError';
377
+ const headers = {
378
+ get: jest.fn(() => {
379
+ throw sentinel;
380
+ }),
381
+ };
382
+ undiciFetch.mockResolvedValue({ status: 200, ok: true, headers, body: null });
383
+ await expect(resolveDocument('https://example.com/')).rejects.toBe(sentinel);
384
+ });
385
+ it('propagates a reader acquisition defect unchanged', async () => {
386
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
387
+ const sentinel = new TypeError('reader acquisition defect');
388
+ undiciFetch.mockResolvedValue({
389
+ status: 200,
390
+ ok: true,
391
+ headers: new Headers(),
392
+ body: {
393
+ getReader: () => {
394
+ throw sentinel;
395
+ },
396
+ },
397
+ });
398
+ await expect(resolveDocument('https://example.com/')).rejects.toBe(sentinel);
399
+ });
400
+ it('propagates a chunk-processing defect unchanged', async () => {
401
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
402
+ const sentinel = new TypeError('chunk processing defect');
403
+ const value = Object.defineProperty({}, 'byteLength', {
404
+ get: () => {
405
+ throw sentinel;
406
+ },
407
+ });
408
+ const reader = { read: jest.fn(), cancel: jest.fn() };
409
+ reader.read.mockResolvedValue({ value, done: false });
410
+ reader.cancel.mockResolvedValue(undefined);
411
+ undiciFetch.mockResolvedValue({
412
+ status: 200,
413
+ ok: true,
414
+ headers: new Headers(),
415
+ body: { getReader: () => reader },
416
+ });
417
+ await expect(resolveDocument('https://example.com/')).rejects.toBe(sentinel);
418
+ expect(reader.cancel).toHaveBeenCalledTimes(1);
419
+ });
420
+ });
421
+ describe('size limit', () => {
422
+ it('throws ResolverTooLargeError with limit attached when the body exceeds maxResponseBytes', async () => {
423
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
424
+ const big = new Uint8Array(2048);
425
+ undiciFetch.mockResolvedValue(makeResponse({ body: big }));
426
+ const error = (await resolveDocument('https://example.com/big', { maxResponseBytes: 1024 }).catch((e) => e));
427
+ expect(error).toBeInstanceOf(ResolverTooLargeError);
428
+ expect(error.limit).toBe(1024);
429
+ });
430
+ // The relaxed setting permits private destinations and nothing else. Fails
431
+ // if the size cap is bypassed or widened when allowPrivateAddresses is on.
432
+ it('applies the same size cap with allowPrivateAddresses on', async () => {
433
+ validatePublicUrl.mockResolvedValue(resolvedAddress('10.0.0.1'));
434
+ const big = new Uint8Array(2048);
435
+ undiciFetch.mockResolvedValue(makeResponse({ body: big }));
436
+ const error = (await resolveDocument('http://db.internal/big', {
437
+ maxResponseBytes: 1024,
438
+ allowPrivateAddresses: true,
439
+ }).catch((e) => e));
440
+ expect(error).toBeInstanceOf(ResolverTooLargeError);
441
+ expect(error.limit).toBe(1024);
442
+ });
443
+ });
444
+ describe('redirects', () => {
445
+ it('follows a redirect chain within the cap', async () => {
446
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
447
+ undiciFetch
448
+ .mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'https://example.com/next' }, body: null }))
449
+ .mockResolvedValueOnce(makeResponse({ body: 'final' }));
450
+ const result = await resolveDocument('https://example.com/start');
451
+ expect(result.finalUrl).toBe('https://example.com/next');
452
+ expect(new TextDecoder().decode(result.body)).toBe('final');
453
+ expect(undiciFetch).toHaveBeenCalledTimes(2);
454
+ });
455
+ it('pins each hop connection to the addresses validatePublicUrl resolved', async () => {
456
+ validatePublicUrl
457
+ .mockResolvedValueOnce(resolvedAddress('203.0.113.10', 4))
458
+ .mockResolvedValueOnce(resolvedAddress('2606:4700:4700::1111', 6));
459
+ undiciFetch
460
+ .mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'https://example.com/next' }, body: null }))
461
+ .mockResolvedValueOnce(makeResponse({ body: 'final' }));
462
+ await resolveDocument('https://example.com/start');
463
+ expect(agentOptions).toHaveLength(2);
464
+ // Each hop's request must be dispatched through the agent that holds
465
+ // that hop's pin; constructing a pinned agent and fetching without it
466
+ // would satisfy the lookup assertions alone.
467
+ expect(undiciFetch.mock.calls[0][1]).toMatchObject({ dispatcher: agentInstances[0] });
468
+ expect(undiciFetch.mock.calls[1][1]).toMatchObject({ dispatcher: agentInstances[1] });
469
+ const pins = await Promise.all(agentOptions.map((options) => new Promise((resolve, reject) => {
470
+ const lookup = options.connect.lookup;
471
+ lookup('example.com', {}, (err, addresses) => (err ? reject(err) : resolve(addresses)));
472
+ })));
473
+ // Each hop's lookup must return exactly the addresses its own
474
+ // validatePublicUrl call resolved, never a fresh DNS answer.
475
+ expect(pins[0]).toEqual([{ address: '203.0.113.10', family: 4 }]);
476
+ expect(pins[1]).toEqual([{ address: '2606:4700:4700::1111', family: 6 }]);
477
+ });
478
+ // The dual-stack `localhost` case: the guard validated both addresses, so
479
+ // both must reach the connector, which is what lets Node's default
480
+ // autoSelectFamily try the second when the first refuses the connection.
481
+ it('hands the connector every address the guard validated for a hop', async () => {
482
+ validatePublicUrl.mockResolvedValueOnce({
483
+ address: '::1',
484
+ family: 6,
485
+ addresses: [
486
+ { address: '::1', family: 6 },
487
+ { address: '127.0.0.1', family: 4 },
488
+ ],
489
+ });
490
+ undiciFetch.mockResolvedValueOnce(makeResponse({ body: 'final' }));
491
+ await resolveDocument('http://localhost/doc', { allowPrivateAddresses: true });
492
+ expect(agentOptions).toHaveLength(1);
493
+ expect(undiciFetch.mock.calls[0][1]).toMatchObject({ dispatcher: agentInstances[0] });
494
+ const pin = await new Promise((resolve, reject) => {
495
+ const lookup = agentOptions[0].connect.lookup;
496
+ lookup('localhost', {}, (err, addresses) => (err ? reject(err) : resolve(addresses)));
497
+ });
498
+ expect(pin).toEqual([
499
+ { address: '::1', family: 6 },
500
+ { address: '127.0.0.1', family: 4 },
501
+ ]);
502
+ });
503
+ it('forwards private permission and pins each redirect hop through the guard', async () => {
504
+ validatePublicUrl
505
+ .mockResolvedValueOnce(resolvedAddress('10.0.0.1', 4))
506
+ .mockResolvedValueOnce(resolvedAddress('10.0.0.2', 4));
507
+ undiciFetch
508
+ .mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'http://db.internal/next' }, body: null }))
509
+ .mockResolvedValueOnce(makeResponse({ body: 'final' }));
510
+ await resolveDocument('http://localhost/start', { allowPrivateAddresses: true });
511
+ expect(validatePublicUrl).toHaveBeenNthCalledWith(1, 'http://localhost/start', expect.objectContaining({ allowPrivateAddresses: true }));
512
+ expect(validatePublicUrl).toHaveBeenNthCalledWith(2, 'http://db.internal/next', expect.objectContaining({ allowPrivateAddresses: true }));
513
+ // Relaxed mode must dispatch through the pinned Agents too, exactly as
514
+ // the strict sibling above asserts. Constructing a pinned Agent and then
515
+ // fetching without it would satisfy the pin assertions below alone.
516
+ expect(undiciFetch.mock.calls[0][1]).toMatchObject({ dispatcher: agentInstances[0] });
517
+ expect(undiciFetch.mock.calls[1][1]).toMatchObject({ dispatcher: agentInstances[1] });
518
+ const pins = await Promise.all(agentOptions.map((options) => new Promise((resolve, reject) => {
519
+ const lookup = options.connect.lookup;
520
+ lookup('ignored.example', {}, (err, addresses) => err ? reject(err) : resolve(addresses));
521
+ })));
522
+ expect(pins).toEqual([[{ address: '10.0.0.1', family: 4 }], [{ address: '10.0.0.2', family: 4 }]]);
523
+ });
524
+ it('re-validates each redirect target through validatePublicUrl', async () => {
525
+ validatePublicUrl
526
+ .mockResolvedValueOnce(resolvedAddress('1.1.1.1'))
527
+ .mockRejectedValueOnce(new PrivateAddressError('https://internal.example/', ['10.0.0.1']));
528
+ undiciFetch.mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'https://internal.example/' }, body: null }));
529
+ await expect(resolveDocument('https://public.example/')).rejects.toBeInstanceOf(PrivateAddressError);
530
+ expect(validatePublicUrl).toHaveBeenCalledTimes(2);
531
+ // Second hop's URL must be the Location target, not the original.
532
+ expect(validatePublicUrl).toHaveBeenNthCalledWith(2, 'https://internal.example/', expect.anything());
533
+ expect(undiciFetch).toHaveBeenCalledTimes(1);
534
+ });
535
+ it('throws ResolverRedirectMissingLocationError when Location is not a parseable URL', async () => {
536
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
537
+ const malformed = 'http://[';
538
+ undiciFetch.mockResolvedValue(makeResponse({ status: 301, headers: { location: malformed }, body: null }));
539
+ const error = (await resolveDocument('https://example.com/').catch((e) => e));
540
+ expect(error).toBeInstanceOf(ResolverRedirectMissingLocationError);
541
+ expect(error.received).toBe(malformed);
542
+ });
543
+ it('throws ResolverTooManyRedirectsError with limit attached when the chain exceeds the cap', async () => {
544
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
545
+ undiciFetch
546
+ .mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'https://example.com/loop' }, body: null }))
547
+ .mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'https://example.com/final' }, body: null }));
548
+ const error = (await resolveDocument('https://example.com/start', { maxRedirects: 1 }).catch((e) => e));
549
+ expect(error).toBeInstanceOf(ResolverTooManyRedirectsError);
550
+ expect(error.limit).toBe(1);
551
+ expect(error.lastHopUrl).toBe('https://example.com/loop');
552
+ // The message names where the chain began and `.lastHopUrl` names who
553
+ // answered last. Fails if the two are transposed, which would report the hop that
554
+ // redirected as the start of the chain.
555
+ expect(error.message).toContain('starting from https://example.com/start');
556
+ expect(error.message).not.toContain('https://example.com/loop');
557
+ });
558
+ // Same cap, same responder, with the relaxed setting on. Fails if the hop
559
+ // cap is bypassed or overridden when allowPrivateAddresses is on.
560
+ it('applies the same redirect cap with allowPrivateAddresses on', async () => {
561
+ validatePublicUrl.mockResolvedValue(resolvedAddress('10.0.0.1'));
562
+ undiciFetch
563
+ .mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'http://db.internal/loop' }, body: null }))
564
+ .mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'http://db.internal/final' }, body: null }));
565
+ const error = (await resolveDocument('http://db.internal/start', {
566
+ maxRedirects: 1,
567
+ allowPrivateAddresses: true,
568
+ }).catch((e) => e));
569
+ expect(error).toBeInstanceOf(ResolverTooManyRedirectsError);
570
+ expect(error.limit).toBe(1);
571
+ expect(error.lastHopUrl).toBe('http://db.internal/loop');
572
+ expect(undiciFetch).toHaveBeenCalledTimes(2);
573
+ });
574
+ // R6: the disclosed default of three additional hops, exercised rather
575
+ // than restated as a constant. Four consecutive redirects must exhaust it,
576
+ // and the error must name the hop that answered the fourth request. Fails
577
+ // if the default changes or the responder is transposed.
578
+ it('exhausts the default redirect cap of three additional hops', async () => {
579
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
580
+ for (const location of [
581
+ 'https://example.com/r1',
582
+ 'https://example.com/r2',
583
+ 'https://example.com/r3',
584
+ 'https://example.com/r4',
585
+ ]) {
586
+ undiciFetch.mockResolvedValueOnce(makeResponse({ status: 301, headers: { location }, body: null }));
587
+ }
588
+ const error = (await resolveDocument('https://example.com/start').catch((e) => e));
589
+ expect(error).toBeInstanceOf(ResolverTooManyRedirectsError);
590
+ expect(error.limit).toBe(3);
591
+ expect(undiciFetch).toHaveBeenCalledTimes(4);
592
+ expect(undiciFetch.mock.calls[3][0]).toBe('https://example.com/r3');
593
+ expect(error.lastHopUrl).toBe('https://example.com/r3');
594
+ });
595
+ // The post-loop throw is reachable only when the caller supplies a
596
+ // negative limit, so no hop was ever validated and there is no responder
597
+ // to name. Fails if that path is given a default `lastHopUrl`, which would
598
+ // claim a response that never happened, or if the loop runs at all.
599
+ it('throws ResolverTooManyRedirectsError with no lastHopUrl when the caller supplies a negative limit', async () => {
600
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
601
+ const error = (await resolveDocument('https://example.com/start', { maxRedirects: -1 }).catch((e) => e));
602
+ expect(error).toBeInstanceOf(ResolverTooManyRedirectsError);
603
+ expect(error.lastHopUrl).toBeUndefined();
604
+ expect(error.limit).toBe(-1);
605
+ expect(error.message).toContain('https://example.com/start');
606
+ expect(validatePublicUrl).not.toHaveBeenCalled();
607
+ expect(undiciFetch).not.toHaveBeenCalled();
608
+ });
609
+ it('does not call the second-hop guard after redirect cleanup consumes the timeout budget', async () => {
610
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
611
+ undiciFetch.mockResolvedValue(makeResponse({ status: 301, headers: { location: 'https://example.com/next' }, body: null }));
612
+ agentCloseImplementation = () => new Promise((resolve) => setTimeout(resolve, 30));
613
+ await expect(resolveDocument('https://example.com/start', { totalTimeoutMs: 5 })).rejects.toBeInstanceOf(ResolverTimedOutError);
614
+ expect(validatePublicUrl).toHaveBeenCalledTimes(1);
615
+ expect(agentInstances).toHaveLength(1);
616
+ expect(undiciFetch).toHaveBeenCalledTimes(1);
617
+ });
618
+ it('uses the live redirect hop when a second-hop guard remains unresolved at timeout', async () => {
619
+ let releaseGuard;
620
+ const guard = new Promise((resolve) => {
621
+ releaseGuard = resolve;
622
+ });
623
+ validatePublicUrl.mockResolvedValueOnce(resolvedAddress()).mockReturnValueOnce(guard);
624
+ undiciFetch.mockResolvedValueOnce(makeResponse({ status: 301, headers: { location: 'https://example.com/next' }, body: null }));
625
+ const error = (await resolveDocument('https://example.com/start', { totalTimeoutMs: 20 }).catch((e) => e));
626
+ expect(error).toBeInstanceOf(ResolverTimedOutError);
627
+ expect(error.message).toContain('https://example.com/next');
628
+ expect(validatePublicUrl).toHaveBeenCalledTimes(2);
629
+ expect(agentInstances).toHaveLength(1);
630
+ expect(undiciFetch).toHaveBeenCalledTimes(1);
631
+ releaseGuard(resolvedAddress());
632
+ await Promise.resolve();
633
+ });
634
+ it('throws ResolverRedirectMissingLocationError when a 3xx has no Location', async () => {
635
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
636
+ undiciFetch.mockResolvedValue(makeResponse({ status: 302, body: null }));
637
+ await expect(resolveDocument('https://example.com/')).rejects.toBeInstanceOf(ResolverRedirectMissingLocationError);
638
+ });
639
+ });
640
+ describe('dispatcher lifecycle', () => {
641
+ it('closes the dispatcher only after the response body has been fully read', async () => {
642
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
643
+ const bodyPull = jest.fn();
644
+ const slowStream = new ReadableStream({
645
+ async pull(controller) {
646
+ bodyPull();
647
+ controller.enqueue(new TextEncoder().encode('chunk'));
648
+ controller.close();
649
+ },
650
+ });
651
+ undiciFetch.mockResolvedValue({
652
+ status: 200,
653
+ ok: true,
654
+ headers: new Headers(),
655
+ body: slowStream,
656
+ });
657
+ await resolveDocument('https://example.com/');
658
+ expect(bodyPull).toHaveBeenCalled();
659
+ expect(lastAgentClose).toHaveBeenCalled();
660
+ const pullOrder = bodyPull.mock.invocationCallOrder[0];
661
+ const closeOrder = lastAgentClose.mock.invocationCallOrder[0];
662
+ expect(closeOrder).toBeGreaterThan(pullOrder);
663
+ });
664
+ });
665
+ describe('User-Agent', () => {
666
+ afterEach(() => {
667
+ delete process.env[USER_AGENT_ENV_VAR];
668
+ });
669
+ function sentHeaders(callIndex = 0) {
670
+ const init = undiciFetch.mock.calls[callIndex][1];
671
+ return init.headers ?? {};
672
+ }
673
+ it('sends the default User-Agent when the caller supplies none', async () => {
674
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
675
+ undiciFetch.mockResolvedValue(makeResponse({ body: '{}' }));
676
+ await resolveDocument('https://example.com/doc.json', { headers: { Accept: 'application/json' } });
677
+ expect(sentHeaders()['User-Agent']).toBe(DEFAULT_USER_AGENT);
678
+ expect(sentHeaders()['Accept']).toBe('application/json');
679
+ });
680
+ it('sends the default User-Agent when no headers are supplied at all', async () => {
681
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
682
+ undiciFetch.mockResolvedValue(makeResponse({ body: '{}' }));
683
+ await resolveDocument('https://example.com/doc.json');
684
+ expect(sentHeaders()['User-Agent']).toBe(DEFAULT_USER_AGENT);
685
+ });
686
+ it('prefers the RI_HTTP_USER_AGENT environment override to the default', async () => {
687
+ process.env[USER_AGENT_ENV_VAR] = 'acme-operator/1.0';
688
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
689
+ undiciFetch.mockResolvedValue(makeResponse({ body: '{}' }));
690
+ await resolveDocument('https://example.com/doc.json');
691
+ expect(sentHeaders()['User-Agent']).toBe('acme-operator/1.0');
692
+ });
693
+ it('falls back to the default when the environment override is blank (blank is treated as unset)', async () => {
694
+ process.env[USER_AGENT_ENV_VAR] = ' ';
695
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
696
+ undiciFetch.mockResolvedValue(makeResponse({ body: '{}' }));
697
+ await resolveDocument('https://example.com/doc.json');
698
+ expect(sentHeaders()['User-Agent']).toBe(DEFAULT_USER_AGENT);
699
+ });
700
+ it('passes a non-blank override through unvalidated (boot-time validation owns rejection)', async () => {
701
+ process.env[USER_AGENT_ENV_VAR] = 'operator-agent/1.0';
702
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
703
+ undiciFetch.mockResolvedValue(makeResponse({ body: '{}' }));
704
+ await resolveDocument('https://example.com/doc.json');
705
+ expect(sentHeaders()['User-Agent']).toBe('operator-agent/1.0');
706
+ });
707
+ it('passes an invalid override through raw rather than silently substituting the default', async () => {
708
+ process.env[USER_AGENT_ENV_VAR] = 'evil\r\nX-Injected: 1';
709
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
710
+ undiciFetch.mockResolvedValue(makeResponse({ body: '{}' }));
711
+ await resolveDocument('https://example.com/doc.json');
712
+ // The load-bearing rule: no request-time fallback. The raw value reaches
713
+ // the fetch layer, which rejects it loudly in real undici; deployments
714
+ // that want fail-fast validate at boot instead.
715
+ expect(sentHeaders()['User-Agent']).toBe('evil\r\nX-Injected: 1');
716
+ });
717
+ it.each(['user-agent', 'User-Agent', 'USER-AGENT', 'uSeR-aGeNt'])('never overrides a caller-supplied %s header', async (headerName) => {
718
+ process.env[USER_AGENT_ENV_VAR] = 'acme-operator/1.0';
719
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
720
+ undiciFetch.mockResolvedValue(makeResponse({ body: '{}' }));
721
+ await resolveDocument('https://example.com/doc.json', { headers: { [headerName]: 'caller/2.0' } });
722
+ const headers = sentHeaders();
723
+ expect(headers[headerName]).toBe('caller/2.0');
724
+ const uaKeys = Object.keys(headers).filter((k) => k.toLowerCase() === 'user-agent');
725
+ expect(uaKeys).toEqual([headerName]);
726
+ });
727
+ it('sends the User-Agent on every redirect hop', async () => {
728
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
729
+ undiciFetch
730
+ .mockResolvedValueOnce(makeResponse({ status: 302, headers: { location: 'https://example.com/final' }, body: null }))
731
+ .mockResolvedValueOnce(makeResponse({ body: '{}' }));
732
+ await resolveDocument('https://example.com/doc.json');
733
+ expect(undiciFetch).toHaveBeenCalledTimes(2);
734
+ expect(sentHeaders(0)['User-Agent']).toBe(DEFAULT_USER_AGENT);
735
+ expect(sentHeaders(1)['User-Agent']).toBe(DEFAULT_USER_AGENT);
736
+ });
737
+ });
738
+ describe('304 Not Modified', () => {
739
+ it('returns a LoadResult with status 304 and an empty body', async () => {
740
+ validatePublicUrl.mockResolvedValue(resolvedAddress());
741
+ undiciFetch.mockResolvedValue(makeResponse({ status: 304, body: null, headers: { etag: '"abc"' } }));
742
+ const result = await resolveDocument('https://example.com/');
743
+ expect(result.status).toBe(304);
744
+ expect(result.body.byteLength).toBe(0);
745
+ expect(result.etag).toBe('"abc"');
746
+ });
747
+ });
748
+ });
749
+ //# sourceMappingURL=resolve-document.test.js.map