openapi-typesafe 1.0.0-rc.3 → 1.0.0-rc.4

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 (448) hide show
  1. package/CHANGELOG.md +1831 -0
  2. package/dist/adapters/node-file-system/file-claims.cjs +105 -0
  3. package/dist/adapters/node-file-system/file-claims.cjs.map +1 -0
  4. package/dist/adapters/node-file-system/file-claims.mjs +95 -0
  5. package/dist/adapters/node-file-system/file-claims.mjs.map +1 -0
  6. package/dist/adapters/node-file-system/file-identity.cjs +82 -0
  7. package/dist/adapters/node-file-system/file-identity.cjs.map +1 -0
  8. package/dist/adapters/node-file-system/file-identity.mjs +74 -0
  9. package/dist/adapters/node-file-system/file-identity.mjs.map +1 -0
  10. package/dist/adapters/node-file-system/node-file-system.cjs +90 -14
  11. package/dist/adapters/node-file-system/node-file-system.cjs.map +1 -1
  12. package/dist/adapters/node-file-system/node-file-system.mjs +90 -14
  13. package/dist/adapters/node-file-system/node-file-system.mjs.map +1 -1
  14. package/dist/adapters/redocly-loader/bundled-mapping-copies.cjs +75 -0
  15. package/dist/adapters/redocly-loader/bundled-mapping-copies.cjs.map +1 -0
  16. package/dist/adapters/redocly-loader/bundled-mapping-copies.mjs +72 -0
  17. package/dist/adapters/redocly-loader/bundled-mapping-copies.mjs.map +1 -0
  18. package/dist/adapters/redocly-loader/bundled-ref-repair.cjs +1 -1
  19. package/dist/adapters/redocly-loader/bundled-ref-repair.cjs.map +1 -1
  20. package/dist/adapters/redocly-loader/bundled-ref-repair.mjs +2 -2
  21. package/dist/adapters/redocly-loader/bundled-ref-repair.mjs.map +1 -1
  22. package/dist/adapters/redocly-loader/loaded-documents.cjs +28 -0
  23. package/dist/adapters/redocly-loader/loaded-documents.cjs.map +1 -0
  24. package/dist/adapters/redocly-loader/loaded-documents.mjs +21 -0
  25. package/dist/adapters/redocly-loader/loaded-documents.mjs.map +1 -0
  26. package/dist/adapters/redocly-loader/redocly-schema-loader.cjs +14 -15
  27. package/dist/adapters/redocly-loader/redocly-schema-loader.cjs.map +1 -1
  28. package/dist/adapters/redocly-loader/redocly-schema-loader.mjs +12 -13
  29. package/dist/adapters/redocly-loader/redocly-schema-loader.mjs.map +1 -1
  30. package/dist/adapters/redocly-loader/ref-access.cjs +31 -3
  31. package/dist/adapters/redocly-loader/ref-access.cjs.map +1 -1
  32. package/dist/adapters/redocly-loader/ref-access.mjs +31 -3
  33. package/dist/adapters/redocly-loader/ref-access.mjs.map +1 -1
  34. package/dist/adapters/redocly-loader/ref-holders.cjs +2 -2
  35. package/dist/adapters/redocly-loader/ref-holders.cjs.map +1 -1
  36. package/dist/adapters/redocly-loader/ref-holders.mjs +3 -3
  37. package/dist/adapters/redocly-loader/ref-holders.mjs.map +1 -1
  38. package/dist/adapters/redocly-loader/remote-body-limit.cjs +50 -0
  39. package/dist/adapters/redocly-loader/remote-body-limit.cjs.map +1 -0
  40. package/dist/adapters/redocly-loader/remote-body-limit.mjs +48 -0
  41. package/dist/adapters/redocly-loader/remote-body-limit.mjs.map +1 -0
  42. package/dist/adapters/redocly-loader/remote-fetch.cjs +35 -6
  43. package/dist/adapters/redocly-loader/remote-fetch.cjs.map +1 -1
  44. package/dist/adapters/redocly-loader/remote-fetch.mjs +35 -6
  45. package/dist/adapters/redocly-loader/remote-fetch.mjs.map +1 -1
  46. package/dist/adapters/redocly-loader/remote-node-limit.cjs +47 -0
  47. package/dist/adapters/redocly-loader/remote-node-limit.cjs.map +1 -0
  48. package/dist/adapters/redocly-loader/remote-node-limit.mjs +44 -0
  49. package/dist/adapters/redocly-loader/remote-node-limit.mjs.map +1 -0
  50. package/dist/adapters/redocly-loader/schema-load-errors.cjs +32 -8
  51. package/dist/adapters/redocly-loader/schema-load-errors.cjs.map +1 -1
  52. package/dist/adapters/redocly-loader/schema-load-errors.mjs +32 -9
  53. package/dist/adapters/redocly-loader/schema-load-errors.mjs.map +1 -1
  54. package/dist/adapters/redocly-loader/schema-source.cjs +11 -7
  55. package/dist/adapters/redocly-loader/schema-source.cjs.map +1 -1
  56. package/dist/adapters/redocly-loader/schema-source.mjs +12 -8
  57. package/dist/adapters/redocly-loader/schema-source.mjs.map +1 -1
  58. package/dist/adapters/redocly-loader/unsupported-keys.cjs +3 -5
  59. package/dist/adapters/redocly-loader/unsupported-keys.cjs.map +1 -1
  60. package/dist/adapters/redocly-loader/unsupported-keys.mjs +3 -5
  61. package/dist/adapters/redocly-loader/unsupported-keys.mjs.map +1 -1
  62. package/dist/adapters/terminal-logger/terminal-logger.cjs +6 -4
  63. package/dist/adapters/terminal-logger/terminal-logger.cjs.map +1 -1
  64. package/dist/adapters/terminal-logger/terminal-logger.mjs +6 -4
  65. package/dist/adapters/terminal-logger/terminal-logger.mjs.map +1 -1
  66. package/dist/api/default-ports.cjs +14 -2
  67. package/dist/api/default-ports.cjs.map +1 -1
  68. package/dist/api/default-ports.mjs +14 -2
  69. package/dist/api/default-ports.mjs.map +1 -1
  70. package/dist/api/discriminator-enum-parent.cjs +2 -2
  71. package/dist/api/discriminator-enum-parent.cjs.map +1 -1
  72. package/dist/api/discriminator-enum-parent.mjs +3 -3
  73. package/dist/api/discriminator-enum-parent.mjs.map +1 -1
  74. package/dist/api/discriminator-other-owners.cjs +5 -3
  75. package/dist/api/discriminator-other-owners.cjs.map +1 -1
  76. package/dist/api/discriminator-other-owners.mjs +5 -3
  77. package/dist/api/discriminator-other-owners.mjs.map +1 -1
  78. package/dist/api/discriminator-patch.cjs +5 -1
  79. package/dist/api/discriminator-patch.cjs.map +1 -1
  80. package/dist/api/discriminator-patch.mjs +5 -1
  81. package/dist/api/discriminator-patch.mjs.map +1 -1
  82. package/dist/api/discriminator-scan.cjs +15 -8
  83. package/dist/api/discriminator-scan.cjs.map +1 -1
  84. package/dist/api/discriminator-scan.mjs +15 -8
  85. package/dist/api/discriminator-scan.mjs.map +1 -1
  86. package/dist/api/document-run.cjs +2 -2
  87. package/dist/api/document-run.cjs.map +1 -1
  88. package/dist/api/document-run.mjs +2 -2
  89. package/dist/api/document-run.mjs.map +1 -1
  90. package/dist/api/legacy-utils.cjs.map +1 -1
  91. package/dist/api/legacy-utils.mjs.map +1 -1
  92. package/dist/api/openapi-ts.cjs +8 -4
  93. package/dist/api/openapi-ts.cjs.map +1 -1
  94. package/dist/api/openapi-ts.mjs +8 -5
  95. package/dist/api/openapi-ts.mjs.map +1 -1
  96. package/dist/api/schema-callbacks.cjs +36 -12
  97. package/dist/api/schema-callbacks.cjs.map +1 -1
  98. package/dist/api/schema-callbacks.mjs +36 -12
  99. package/dist/api/schema-callbacks.mjs.map +1 -1
  100. package/dist/api/transform-context.cjs +7 -3
  101. package/dist/api/transform-context.cjs.map +1 -1
  102. package/dist/api/transform-context.mjs +7 -3
  103. package/dist/api/transform-context.mjs.map +1 -1
  104. package/dist/api/transform-schema.cjs +40 -10
  105. package/dist/api/transform-schema.cjs.map +1 -1
  106. package/dist/api/transform-schema.mjs +41 -11
  107. package/dist/api/transform-schema.mjs.map +1 -1
  108. package/dist/application/print/text-writer.cjs +10 -3
  109. package/dist/application/print/text-writer.cjs.map +1 -1
  110. package/dist/application/print/text-writer.mjs +10 -3
  111. package/dist/application/print/text-writer.mjs.map +1 -1
  112. package/dist/application/stack-overflow.cjs +2 -1
  113. package/dist/application/stack-overflow.cjs.map +1 -1
  114. package/dist/application/stack-overflow.mjs +2 -1
  115. package/dist/application/stack-overflow.mjs.map +1 -1
  116. package/dist/application/transform/document/components.cjs +1 -1
  117. package/dist/application/transform/document/components.cjs.map +1 -1
  118. package/dist/application/transform/document/components.mjs +2 -2
  119. package/dist/application/transform/document/components.mjs.map +1 -1
  120. package/dist/application/transform/document/declared-type-names.cjs +23 -0
  121. package/dist/application/transform/document/declared-type-names.cjs.map +1 -0
  122. package/dist/application/transform/document/declared-type-names.mjs +20 -0
  123. package/dist/application/transform/document/declared-type-names.mjs.map +1 -0
  124. package/dist/application/transform/document/defs-root.cjs +1 -1
  125. package/dist/application/transform/document/defs-root.cjs.map +1 -1
  126. package/dist/application/transform/document/defs-root.mjs +2 -2
  127. package/dist/application/transform/document/defs-root.mjs.map +1 -1
  128. package/dist/application/transform/document/document.cjs +4 -1
  129. package/dist/application/transform/document/document.cjs.map +1 -1
  130. package/dist/application/transform/document/document.mjs +3 -2
  131. package/dist/application/transform/document/document.mjs.map +1 -1
  132. package/dist/application/transform/document/enum-names-given-back.cjs +62 -0
  133. package/dist/application/transform/document/enum-names-given-back.cjs.map +1 -0
  134. package/dist/application/transform/document/enum-names-given-back.mjs +58 -0
  135. package/dist/application/transform/document/enum-names-given-back.mjs.map +1 -0
  136. package/dist/application/transform/document/global-type-references.cjs +1 -0
  137. package/dist/application/transform/document/global-type-references.cjs.map +1 -1
  138. package/dist/application/transform/document/global-type-references.mjs +1 -1
  139. package/dist/application/transform/document/global-type-references.mjs.map +1 -1
  140. package/dist/application/transform/document/media-type.cjs +2 -2
  141. package/dist/application/transform/document/media-type.cjs.map +1 -1
  142. package/dist/application/transform/document/media-type.mjs +3 -3
  143. package/dist/application/transform/document/media-type.mjs.map +1 -1
  144. package/dist/application/transform/document/operation-keys-written.cjs +26 -0
  145. package/dist/application/transform/document/operation-keys-written.cjs.map +1 -0
  146. package/dist/application/transform/document/operation-keys-written.mjs +24 -0
  147. package/dist/application/transform/document/operation-keys-written.mjs.map +1 -0
  148. package/dist/application/transform/document/operation.cjs +1 -1
  149. package/dist/application/transform/document/operation.cjs.map +1 -1
  150. package/dist/application/transform/document/operation.mjs +2 -2
  151. package/dist/application/transform/document/operation.mjs.map +1 -1
  152. package/dist/application/transform/document/parameter-entries.cjs +34 -0
  153. package/dist/application/transform/document/parameter-entries.cjs.map +1 -0
  154. package/dist/application/transform/document/parameter-entries.mjs +30 -0
  155. package/dist/application/transform/document/parameter-entries.mjs.map +1 -0
  156. package/dist/application/transform/document/parameter.cjs +12 -4
  157. package/dist/application/transform/document/parameter.cjs.map +1 -1
  158. package/dist/application/transform/document/parameter.mjs +13 -5
  159. package/dist/application/transform/document/parameter.mjs.map +1 -1
  160. package/dist/application/transform/document/parameters.cjs +6 -29
  161. package/dist/application/transform/document/parameters.cjs.map +1 -1
  162. package/dist/application/transform/document/parameters.mjs +6 -29
  163. package/dist/application/transform/document/parameters.mjs.map +1 -1
  164. package/dist/application/transform/document/path-item.cjs +11 -14
  165. package/dist/application/transform/document/path-item.cjs.map +1 -1
  166. package/dist/application/transform/document/path-item.mjs +12 -16
  167. package/dist/application/transform/document/path-item.mjs.map +1 -1
  168. package/dist/application/transform/document/paths-enum.cjs +1 -0
  169. package/dist/application/transform/document/paths-enum.cjs.map +1 -1
  170. package/dist/application/transform/document/paths-enum.mjs +1 -1
  171. package/dist/application/transform/document/paths-enum.mjs.map +1 -1
  172. package/dist/application/transform/document/paths.cjs +5 -1
  173. package/dist/application/transform/document/paths.cjs.map +1 -1
  174. package/dist/application/transform/document/paths.mjs +6 -2
  175. package/dist/application/transform/document/paths.mjs.map +1 -1
  176. package/dist/application/transform/document/referenced-parameter.cjs +80 -0
  177. package/dist/application/transform/document/referenced-parameter.cjs.map +1 -0
  178. package/dist/application/transform/document/referenced-parameter.mjs +78 -0
  179. package/dist/application/transform/document/referenced-parameter.mjs.map +1 -0
  180. package/dist/application/transform/document/request-body.cjs +1 -1
  181. package/dist/application/transform/document/request-body.cjs.map +1 -1
  182. package/dist/application/transform/document/request-body.mjs +2 -2
  183. package/dist/application/transform/document/request-body.mjs.map +1 -1
  184. package/dist/application/transform/document/response.cjs +2 -2
  185. package/dist/application/transform/document/response.cjs.map +1 -1
  186. package/dist/application/transform/document/response.mjs +3 -3
  187. package/dist/application/transform/document/response.mjs.map +1 -1
  188. package/dist/application/transform/document/responses.cjs +1 -1
  189. package/dist/application/transform/document/responses.cjs.map +1 -1
  190. package/dist/application/transform/document/responses.mjs +2 -2
  191. package/dist/application/transform/document/responses.mjs.map +1 -1
  192. package/dist/application/transform/document/root-type-aliases.cjs +8 -23
  193. package/dist/application/transform/document/root-type-aliases.cjs.map +1 -1
  194. package/dist/application/transform/document/root-type-aliases.mjs +8 -24
  195. package/dist/application/transform/document/root-type-aliases.mjs.map +1 -1
  196. package/dist/application/transform/document/type-resolution.cjs +3 -1
  197. package/dist/application/transform/document/type-resolution.cjs.map +1 -1
  198. package/dist/application/transform/document/type-resolution.mjs +3 -1
  199. package/dist/application/transform/document/type-resolution.mjs.map +1 -1
  200. package/dist/application/transform/document/webhooks.cjs +1 -1
  201. package/dist/application/transform/document/webhooks.cjs.map +1 -1
  202. package/dist/application/transform/document/webhooks.mjs +2 -2
  203. package/dist/application/transform/document/webhooks.mjs.map +1 -1
  204. package/dist/application/transform/footer/document-footer.cjs +10 -2
  205. package/dist/application/transform/footer/document-footer.cjs.map +1 -1
  206. package/dist/application/transform/footer/document-footer.mjs +10 -2
  207. package/dist/application/transform/footer/document-footer.mjs.map +1 -1
  208. package/dist/application/transform/footer/enum-name-claims.cjs +48 -0
  209. package/dist/application/transform/footer/enum-name-claims.cjs.map +1 -0
  210. package/dist/application/transform/footer/enum-name-claims.mjs +46 -0
  211. package/dist/application/transform/footer/enum-name-claims.mjs.map +1 -0
  212. package/dist/application/transform/footer/operation-keys.cjs +23 -0
  213. package/dist/application/transform/footer/operation-keys.cjs.map +1 -0
  214. package/dist/application/transform/footer/operation-keys.mjs +21 -0
  215. package/dist/application/transform/footer/operation-keys.mjs.map +1 -0
  216. package/dist/application/transform/footer/type-evaluation.cjs +4 -4
  217. package/dist/application/transform/footer/type-evaluation.cjs.map +1 -1
  218. package/dist/application/transform/footer/type-evaluation.mjs +4 -4
  219. package/dist/application/transform/footer/type-evaluation.mjs.map +1 -1
  220. package/dist/application/transform/helper-types.cjs +1 -1
  221. package/dist/application/transform/helper-types.cjs.map +1 -1
  222. package/dist/application/transform/helper-types.mjs +1 -1
  223. package/dist/application/transform/helper-types.mjs.map +1 -1
  224. package/dist/application/transform/numbered-names.cjs +9 -0
  225. package/dist/application/transform/numbered-names.cjs.map +1 -1
  226. package/dist/application/transform/numbered-names.mjs +9 -1
  227. package/dist/application/transform/numbered-names.mjs.map +1 -1
  228. package/dist/application/transform/ref-pointer/generated-location.cjs +1 -1
  229. package/dist/application/transform/ref-pointer/generated-location.cjs.map +1 -1
  230. package/dist/application/transform/ref-pointer/generated-location.mjs +2 -2
  231. package/dist/application/transform/ref-pointer/generated-location.mjs.map +1 -1
  232. package/dist/application/transform/ref-type.cjs +2 -1
  233. package/dist/application/transform/ref-type.cjs.map +1 -1
  234. package/dist/application/transform/ref-type.mjs +2 -1
  235. package/dist/application/transform/ref-type.mjs.map +1 -1
  236. package/dist/application/transform/schema/composition-type.cjs +1 -1
  237. package/dist/application/transform/schema/composition-type.cjs.map +1 -1
  238. package/dist/application/transform/schema/composition-type.mjs +2 -2
  239. package/dist/application/transform/schema/composition-type.mjs.map +1 -1
  240. package/dist/application/transform/schema/enum-rule.cjs +1 -1
  241. package/dist/application/transform/schema/enum-rule.cjs.map +1 -1
  242. package/dist/application/transform/schema/enum-rule.mjs +2 -2
  243. package/dist/application/transform/schema/enum-rule.mjs.map +1 -1
  244. package/dist/application/transform/schema/object-type.cjs +3 -3
  245. package/dist/application/transform/schema/object-type.cjs.map +1 -1
  246. package/dist/application/transform/schema/object-type.mjs +4 -4
  247. package/dist/application/transform/schema/object-type.mjs.map +1 -1
  248. package/dist/application/transform/schema/ref-siblings.cjs +47 -0
  249. package/dist/application/transform/schema/ref-siblings.cjs.map +1 -0
  250. package/dist/application/transform/schema/ref-siblings.mjs +45 -0
  251. package/dist/application/transform/schema/ref-siblings.mjs.map +1 -0
  252. package/dist/application/transform/schema/schema-transform.cjs +4 -2
  253. package/dist/application/transform/schema/schema-transform.cjs.map +1 -1
  254. package/dist/application/transform/schema/schema-transform.mjs +4 -2
  255. package/dist/application/transform/schema/schema-transform.mjs.map +1 -1
  256. package/dist/application/transform/schema-comment.cjs +5 -1
  257. package/dist/application/transform/schema-comment.cjs.map +1 -1
  258. package/dist/application/transform/schema-comment.mjs +5 -1
  259. package/dist/application/transform/schema-comment.mjs.map +1 -1
  260. package/dist/cli/api-settings.cjs +62 -6
  261. package/dist/cli/api-settings.cjs.map +1 -1
  262. package/dist/cli/api-settings.mjs +60 -7
  263. package/dist/cli/api-settings.mjs.map +1 -1
  264. package/dist/cli/api-writes.cjs +72 -0
  265. package/dist/cli/api-writes.cjs.map +1 -0
  266. package/dist/cli/api-writes.mjs +69 -0
  267. package/dist/cli/api-writes.mjs.map +1 -0
  268. package/dist/cli/cli-errors.cjs +12 -1
  269. package/dist/cli/cli-errors.cjs.map +1 -1
  270. package/dist/cli/cli-errors.mjs +12 -1
  271. package/dist/cli/cli-errors.mjs.map +1 -1
  272. package/dist/cli/cli-output.cjs +106 -1
  273. package/dist/cli/cli-output.cjs.map +1 -1
  274. package/dist/cli/cli-output.mjs +99 -2
  275. package/dist/cli/cli-output.mjs.map +1 -1
  276. package/dist/cli/main.cjs +5 -1
  277. package/dist/cli/main.cjs.map +1 -1
  278. package/dist/cli/main.mjs +6 -2
  279. package/dist/cli/main.mjs.map +1 -1
  280. package/dist/cli/redocly-apis.cjs +82 -36
  281. package/dist/cli/redocly-apis.cjs.map +1 -1
  282. package/dist/cli/redocly-apis.mjs +85 -39
  283. package/dist/cli/redocly-apis.mjs.map +1 -1
  284. package/dist/cli/run-cli.cjs +13 -7
  285. package/dist/cli/run-cli.cjs.map +1 -1
  286. package/dist/cli/run-cli.mjs +14 -8
  287. package/dist/cli/run-cli.mjs.map +1 -1
  288. package/dist/domain/append-all.cjs +10 -0
  289. package/dist/domain/append-all.cjs.map +1 -0
  290. package/dist/domain/append-all.mjs +8 -0
  291. package/dist/domain/append-all.mjs.map +1 -0
  292. package/dist/domain/cause-secrets.cjs +37 -0
  293. package/dist/domain/cause-secrets.cjs.map +1 -0
  294. package/dist/domain/cause-secrets.mjs +35 -0
  295. package/dist/domain/cause-secrets.mjs.map +1 -0
  296. package/dist/domain/errors.cjs +9 -3
  297. package/dist/domain/errors.cjs.map +1 -1
  298. package/dist/domain/errors.mjs +9 -3
  299. package/dist/domain/errors.mjs.map +1 -1
  300. package/dist/domain/ir/identifier-text.cjs.map +1 -1
  301. package/dist/domain/ir/identifier-text.mjs +1 -1
  302. package/dist/domain/ir/identifier-text.mjs.map +1 -1
  303. package/dist/domain/ir/ir-shape.cjs +187 -0
  304. package/dist/domain/ir/ir-shape.cjs.map +1 -0
  305. package/dist/domain/ir/ir-shape.mjs +184 -0
  306. package/dist/domain/ir/ir-shape.mjs.map +1 -0
  307. package/dist/domain/ir/newer-identifier-ranges.cjs +182 -29
  308. package/dist/domain/ir/newer-identifier-ranges.cjs.map +1 -1
  309. package/dist/domain/ir/newer-identifier-ranges.mjs +182 -29
  310. package/dist/domain/ir/newer-identifier-ranges.mjs.map +1 -1
  311. package/dist/domain/openapi/alias-expansion.cjs +3 -1
  312. package/dist/domain/openapi/alias-expansion.cjs.map +1 -1
  313. package/dist/domain/openapi/alias-expansion.mjs +3 -1
  314. package/dist/domain/openapi/alias-expansion.mjs.map +1 -1
  315. package/dist/domain/openapi/discriminated-union.cjs +3 -3
  316. package/dist/domain/openapi/discriminated-union.cjs.map +1 -1
  317. package/dist/domain/openapi/discriminated-union.mjs +4 -4
  318. package/dist/domain/openapi/discriminated-union.mjs.map +1 -1
  319. package/dist/domain/openapi/discriminator-conflicts.cjs +10 -6
  320. package/dist/domain/openapi/discriminator-conflicts.cjs.map +1 -1
  321. package/dist/domain/openapi/discriminator-conflicts.mjs +11 -7
  322. package/dist/domain/openapi/discriminator-conflicts.mjs.map +1 -1
  323. package/dist/domain/openapi/json-pointer.cjs +43 -1
  324. package/dist/domain/openapi/json-pointer.cjs.map +1 -1
  325. package/dist/domain/openapi/json-pointer.mjs +40 -2
  326. package/dist/domain/openapi/json-pointer.mjs.map +1 -1
  327. package/dist/domain/openapi/schema-dialect.cjs +15 -0
  328. package/dist/domain/openapi/schema-dialect.cjs.map +1 -0
  329. package/dist/domain/openapi/schema-dialect.mjs +12 -0
  330. package/dist/domain/openapi/schema-dialect.mjs.map +1 -0
  331. package/dist/domain/openapi/unsupported-features.cjs +1 -1
  332. package/dist/domain/openapi/unsupported-features.cjs.map +1 -1
  333. package/dist/domain/openapi/unsupported-features.mjs +2 -2
  334. package/dist/domain/openapi/unsupported-features.mjs.map +1 -1
  335. package/dist/domain/output-fixes.cjs +14 -0
  336. package/dist/domain/output-fixes.cjs.map +1 -1
  337. package/dist/domain/output-fixes.mjs +14 -0
  338. package/dist/domain/output-fixes.mjs.map +1 -1
  339. package/dist/domain/url-secrets.cjs +48 -0
  340. package/dist/domain/url-secrets.cjs.map +1 -0
  341. package/dist/domain/url-secrets.mjs +45 -0
  342. package/dist/domain/url-secrets.mjs.map +1 -0
  343. package/dist/index.d.cts +22 -4
  344. package/dist/index.d.mts +22 -4
  345. package/dist/index.d.ts +22 -4
  346. package/package.json +4 -5
  347. package/src/adapters/node-file-system/file-claims.ts +138 -0
  348. package/src/adapters/node-file-system/file-identity.ts +137 -0
  349. package/src/adapters/node-file-system/node-file-system.ts +145 -18
  350. package/src/adapters/redocly-loader/bundled-mapping-copies.ts +95 -0
  351. package/src/adapters/redocly-loader/bundled-ref-repair.ts +2 -2
  352. package/src/adapters/redocly-loader/loaded-documents.ts +46 -0
  353. package/src/adapters/redocly-loader/redocly-schema-loader.ts +18 -24
  354. package/src/adapters/redocly-loader/ref-access.ts +37 -3
  355. package/src/adapters/redocly-loader/ref-holders.ts +3 -3
  356. package/src/adapters/redocly-loader/remote-body-limit.ts +66 -0
  357. package/src/adapters/redocly-loader/remote-fetch.ts +74 -11
  358. package/src/adapters/redocly-loader/remote-node-limit.ts +62 -0
  359. package/src/adapters/redocly-loader/schema-load-errors.ts +52 -13
  360. package/src/adapters/redocly-loader/schema-source.ts +20 -8
  361. package/src/adapters/redocly-loader/unsupported-keys.ts +3 -7
  362. package/src/adapters/terminal-logger/terminal-logger.ts +10 -5
  363. package/src/api/default-ports.ts +33 -4
  364. package/src/api/discriminator-enum-parent.ts +3 -3
  365. package/src/api/discriminator-other-owners.ts +5 -3
  366. package/src/api/discriminator-patch.ts +5 -1
  367. package/src/api/discriminator-scan.ts +16 -8
  368. package/src/api/document-run.ts +4 -3
  369. package/src/api/legacy-utils.ts +5 -1
  370. package/src/api/openapi-ts.ts +20 -5
  371. package/src/api/run-context.ts +3 -0
  372. package/src/api/schema-callbacks.ts +60 -17
  373. package/src/api/transform-context.ts +7 -3
  374. package/src/api/transform-schema.ts +64 -12
  375. package/src/application/ports/file-system.ts +39 -0
  376. package/src/application/ports/schema-loader.ts +11 -1
  377. package/src/application/print/text-writer.ts +10 -3
  378. package/src/application/stack-overflow.ts +9 -5
  379. package/src/application/transform/document/components.ts +2 -2
  380. package/src/application/transform/document/declared-type-names.ts +36 -0
  381. package/src/application/transform/document/defs-root.ts +2 -2
  382. package/src/application/transform/document/document.ts +4 -3
  383. package/src/application/transform/document/enum-names-given-back.ts +91 -0
  384. package/src/application/transform/document/global-type-references.ts +1 -1
  385. package/src/application/transform/document/media-type.ts +4 -3
  386. package/src/application/transform/document/operation-keys-written.ts +27 -0
  387. package/src/application/transform/document/operation.ts +2 -2
  388. package/src/application/transform/document/parameter-entries.ts +70 -0
  389. package/src/application/transform/document/parameter.ts +21 -9
  390. package/src/application/transform/document/parameters.ts +11 -56
  391. package/src/application/transform/document/path-item.ts +13 -31
  392. package/src/application/transform/document/paths-enum.ts +1 -1
  393. package/src/application/transform/document/paths.ts +6 -2
  394. package/src/application/transform/document/referenced-parameter.ts +146 -0
  395. package/src/application/transform/document/request-body.ts +2 -2
  396. package/src/application/transform/document/response.ts +3 -3
  397. package/src/application/transform/document/responses.ts +2 -2
  398. package/src/application/transform/document/root-type-aliases.ts +17 -38
  399. package/src/application/transform/document/type-resolution.ts +2 -1
  400. package/src/application/transform/document/webhooks.ts +2 -2
  401. package/src/application/transform/footer/document-footer.ts +18 -1
  402. package/src/application/transform/footer/enum-name-claims.ts +62 -0
  403. package/src/application/transform/footer/operation-keys.ts +28 -0
  404. package/src/application/transform/footer/type-evaluation.ts +4 -4
  405. package/src/application/transform/helper-types.ts +6 -5
  406. package/src/application/transform/numbered-names.ts +15 -0
  407. package/src/application/transform/ref-pointer/generated-location.ts +2 -2
  408. package/src/application/transform/ref-type.ts +2 -1
  409. package/src/application/transform/schema/composition-type.ts +2 -2
  410. package/src/application/transform/schema/enum-rule.ts +6 -2
  411. package/src/application/transform/schema/object-type.ts +4 -4
  412. package/src/application/transform/schema/ref-siblings.ts +76 -0
  413. package/src/application/transform/schema/schema-context.ts +3 -0
  414. package/src/application/transform/schema/schema-transform.ts +4 -2
  415. package/src/application/transform/schema-comment.ts +5 -1
  416. package/src/application/transform/transform-context.ts +5 -0
  417. package/src/cli/api-settings.ts +82 -7
  418. package/src/cli/api-writes.ts +117 -0
  419. package/src/cli/cli-dependencies.ts +21 -4
  420. package/src/cli/cli-errors.ts +20 -1
  421. package/src/cli/cli-output.ts +186 -7
  422. package/src/cli/main.ts +6 -3
  423. package/src/cli/redocly-apis.ts +153 -48
  424. package/src/cli/run-cli.ts +37 -8
  425. package/src/domain/append-all.ts +10 -0
  426. package/src/domain/cause-secrets.ts +52 -0
  427. package/src/domain/errors.ts +14 -5
  428. package/src/domain/ir/identifier-text.ts +6 -5
  429. package/src/domain/ir/ir-shape.ts +299 -0
  430. package/src/domain/ir/newer-identifier-ranges.ts +183 -30
  431. package/src/domain/openapi/alias-expansion.ts +4 -1
  432. package/src/domain/openapi/discriminated-union.ts +6 -4
  433. package/src/domain/openapi/discriminator-conflicts.ts +21 -7
  434. package/src/domain/openapi/document-object.ts +2 -0
  435. package/src/domain/openapi/json-pointer.ts +96 -7
  436. package/src/domain/openapi/schema-dialect.ts +22 -0
  437. package/src/domain/openapi/unsupported-features.ts +2 -2
  438. package/src/domain/output-fixes.ts +21 -2
  439. package/src/domain/url-secrets.ts +72 -0
  440. package/src/types/generator-options.ts +1 -1
  441. package/dist/cli/main.d.cts +0 -2
  442. package/dist/cli/main.d.mts +0 -2
  443. package/dist/cli/main.d.ts +0 -2
  444. package/dist/domain/url-credentials.cjs +0 -10
  445. package/dist/domain/url-credentials.cjs.map +0 -1
  446. package/dist/domain/url-credentials.mjs +0 -8
  447. package/dist/domain/url-credentials.mjs.map +0 -1
  448. package/src/domain/url-credentials.ts +0 -18
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const schemaLoadErrors = require('./schema-load-errors.cjs');
4
+
5
+ class RemoteBodyLimit {
6
+ #limits;
7
+ #totalBytes = 0;
8
+ constructor(limits) {
9
+ this.#limits = limits;
10
+ }
11
+ /** The whole body of `response` from `url`; fails with `SCHEMA_REF_FORBIDDEN` past a limit. */
12
+ async read(response, url) {
13
+ const reader = response.body?.getReader();
14
+ if (reader === void 0) {
15
+ return new ArrayBuffer(0);
16
+ }
17
+ const chunks = [];
18
+ let documentBytes = 0;
19
+ for (let next = await reader.read(); !next.done; next = await reader.read()) {
20
+ documentBytes += next.value.byteLength;
21
+ this.#totalBytes += next.value.byteLength;
22
+ const refusal = this.#refusal(url, documentBytes);
23
+ if (refusal !== void 0) {
24
+ await reader.cancel();
25
+ throw refusal;
26
+ }
27
+ chunks.push(next.value);
28
+ }
29
+ return joined(chunks, documentBytes);
30
+ }
31
+ #refusal(url, documentBytes) {
32
+ const { maxDocumentBytes, maxTotalBytes } = this.#limits;
33
+ if (documentBytes > maxDocumentBytes) {
34
+ return schemaLoadErrors.refForbiddenError(url, { reason: "remote-document-too-large", maxDocumentBytes });
35
+ }
36
+ return this.#totalBytes > maxTotalBytes ? schemaLoadErrors.refForbiddenError(url, { reason: "remote-documents-too-large", maxTotalBytes }) : void 0;
37
+ }
38
+ }
39
+ function joined(chunks, byteLength) {
40
+ const body = new Uint8Array(byteLength);
41
+ let offset = 0;
42
+ for (const chunk of chunks) {
43
+ body.set(chunk, offset);
44
+ offset += chunk.byteLength;
45
+ }
46
+ return body.buffer;
47
+ }
48
+
49
+ exports.RemoteBodyLimit = RemoteBodyLimit;
50
+ //# sourceMappingURL=remote-body-limit.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-body-limit.cjs","sources":["../../../src/adapters/redocly-loader/remote-body-limit.ts"],"sourcesContent":["import type { OpenAPITypesafeError } from \"../../domain/errors.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/** The decoded size limits of the remote documents of one load (threat model T2). */\nexport interface RemoteBodyLimits {\n /** Bytes of one remote document's body after `Content-Encoding` is decoded. */\n readonly maxDocumentBytes: number;\n /** Bytes of the bodies of every remote document of one load together, decoded. */\n readonly maxTotalBytes: number;\n}\n\n/**\n * Reads remote bodies as a stream and stops at the limits. Fetch decodes `gzip`, `deflate` and\n * `br` before the body reaches the loader, so a body of a few megabytes on the wire can become\n * gigabytes in memory (a \"gzip bomb\"); counting the decoded bytes while they arrive stops it before\n * the memory is taken. One instance per load: the total covers every document of that load.\n */\nexport class RemoteBodyLimit {\n readonly #limits: RemoteBodyLimits;\n #totalBytes = 0;\n\n constructor(limits: RemoteBodyLimits) {\n this.#limits = limits;\n }\n\n /** The whole body of `response` from `url`; fails with `SCHEMA_REF_FORBIDDEN` past a limit. */\n async read(response: Response, url: string): Promise<ArrayBuffer> {\n const reader = response.body?.getReader();\n if (reader === undefined) {\n return new ArrayBuffer(0);\n }\n const chunks: Uint8Array[] = [];\n let documentBytes = 0;\n for (let next = await reader.read(); !next.done; next = await reader.read()) {\n documentBytes += next.value.byteLength;\n this.#totalBytes += next.value.byteLength;\n const refusal = this.#refusal(url, documentBytes);\n if (refusal !== undefined) {\n await reader.cancel();\n throw refusal;\n }\n chunks.push(next.value);\n }\n return joined(chunks, documentBytes);\n }\n\n #refusal(url: string, documentBytes: number): OpenAPITypesafeError | undefined {\n const { maxDocumentBytes, maxTotalBytes } = this.#limits;\n if (documentBytes > maxDocumentBytes) {\n return refForbiddenError(url, { reason: \"remote-document-too-large\", maxDocumentBytes });\n }\n return this.#totalBytes > maxTotalBytes\n ? refForbiddenError(url, { reason: \"remote-documents-too-large\", maxTotalBytes })\n : undefined;\n }\n}\n\nfunction joined(chunks: readonly Uint8Array[], byteLength: number): ArrayBuffer {\n const body = new Uint8Array(byteLength);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body.buffer;\n}\n"],"names":["refForbiddenError"],"mappings":";;;;AAiBO,MAAM,eAAA,CAAgB;AAAA,EAClB,OAAA;AAAA,EACT,WAAA,GAAc,CAAA;AAAA,EAEd,YAAY,MAAA,EAA0B;AACpC,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGA,MAAM,IAAA,CAAK,QAAA,EAAoB,GAAA,EAAmC;AAChE,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,EAAM,SAAA,EAAU;AACxC,IAAA,IAAI,WAAW,MAAA,EAAW;AACxB,MAAA,OAAO,IAAI,YAAY,CAAC,CAAA;AAAA,IAC1B;AACA,IAAA,MAAM,SAAuB,EAAC;AAC9B,IAAA,IAAI,aAAA,GAAgB,CAAA;AACpB,IAAA,KAAA,IAAS,IAAA,GAAO,MAAM,MAAA,CAAO,IAAA,EAAK,EAAG,CAAC,IAAA,CAAK,IAAA,EAAM,IAAA,GAAO,MAAM,MAAA,CAAO,IAAA,EAAK,EAAG;AAC3E,MAAA,aAAA,IAAiB,KAAK,KAAA,CAAM,UAAA;AAC5B,MAAA,IAAA,CAAK,WAAA,IAAe,KAAK,KAAA,CAAM,UAAA;AAC/B,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,EAAK,aAAa,CAAA;AAChD,MAAA,IAAI,YAAY,MAAA,EAAW;AACzB,QAAA,MAAM,OAAO,MAAA,EAAO;AACpB,QAAA,MAAM,OAAA;AAAA,MACR;AACA,MAAA,MAAA,CAAO,IAAA,CAAK,KAAK,KAAK,CAAA;AAAA,IACxB;AACA,IAAA,OAAO,MAAA,CAAO,QAAQ,aAAa,CAAA;AAAA,EACrC;AAAA,EAEA,QAAA,CAAS,KAAa,aAAA,EAAyD;AAC7E,IAAA,MAAM,EAAE,gBAAA,EAAkB,aAAA,EAAc,GAAI,IAAA,CAAK,OAAA;AACjD,IAAA,IAAI,gBAAgB,gBAAA,EAAkB;AACpC,MAAA,OAAOA,mCAAkB,GAAA,EAAK,EAAE,MAAA,EAAQ,2BAAA,EAA6B,kBAAkB,CAAA;AAAA,IACzF;AACA,IAAA,OAAO,IAAA,CAAK,WAAA,GAAc,aAAA,GACtBA,kCAAA,CAAkB,GAAA,EAAK,EAAE,MAAA,EAAQ,4BAAA,EAA8B,aAAA,EAAe,CAAA,GAC9E,MAAA;AAAA,EACN;AACF;AAEA,SAAS,MAAA,CAAO,QAA+B,UAAA,EAAiC;AAC9E,EAAA,MAAM,IAAA,GAAO,IAAI,UAAA,CAAW,UAAU,CAAA;AACtC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,IAAA,CAAK,GAAA,CAAI,OAAO,MAAM,CAAA;AACtB,IAAA,MAAA,IAAU,KAAA,CAAM,UAAA;AAAA,EAClB;AACA,EAAA,OAAO,IAAA,CAAK,MAAA;AACd;;"}
@@ -0,0 +1,48 @@
1
+ import { refForbiddenError } from './schema-load-errors.mjs';
2
+
3
+ class RemoteBodyLimit {
4
+ #limits;
5
+ #totalBytes = 0;
6
+ constructor(limits) {
7
+ this.#limits = limits;
8
+ }
9
+ /** The whole body of `response` from `url`; fails with `SCHEMA_REF_FORBIDDEN` past a limit. */
10
+ async read(response, url) {
11
+ const reader = response.body?.getReader();
12
+ if (reader === void 0) {
13
+ return new ArrayBuffer(0);
14
+ }
15
+ const chunks = [];
16
+ let documentBytes = 0;
17
+ for (let next = await reader.read(); !next.done; next = await reader.read()) {
18
+ documentBytes += next.value.byteLength;
19
+ this.#totalBytes += next.value.byteLength;
20
+ const refusal = this.#refusal(url, documentBytes);
21
+ if (refusal !== void 0) {
22
+ await reader.cancel();
23
+ throw refusal;
24
+ }
25
+ chunks.push(next.value);
26
+ }
27
+ return joined(chunks, documentBytes);
28
+ }
29
+ #refusal(url, documentBytes) {
30
+ const { maxDocumentBytes, maxTotalBytes } = this.#limits;
31
+ if (documentBytes > maxDocumentBytes) {
32
+ return refForbiddenError(url, { reason: "remote-document-too-large", maxDocumentBytes });
33
+ }
34
+ return this.#totalBytes > maxTotalBytes ? refForbiddenError(url, { reason: "remote-documents-too-large", maxTotalBytes }) : void 0;
35
+ }
36
+ }
37
+ function joined(chunks, byteLength) {
38
+ const body = new Uint8Array(byteLength);
39
+ let offset = 0;
40
+ for (const chunk of chunks) {
41
+ body.set(chunk, offset);
42
+ offset += chunk.byteLength;
43
+ }
44
+ return body.buffer;
45
+ }
46
+
47
+ export { RemoteBodyLimit };
48
+ //# sourceMappingURL=remote-body-limit.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-body-limit.mjs","sources":["../../../src/adapters/redocly-loader/remote-body-limit.ts"],"sourcesContent":["import type { OpenAPITypesafeError } from \"../../domain/errors.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/** The decoded size limits of the remote documents of one load (threat model T2). */\nexport interface RemoteBodyLimits {\n /** Bytes of one remote document's body after `Content-Encoding` is decoded. */\n readonly maxDocumentBytes: number;\n /** Bytes of the bodies of every remote document of one load together, decoded. */\n readonly maxTotalBytes: number;\n}\n\n/**\n * Reads remote bodies as a stream and stops at the limits. Fetch decodes `gzip`, `deflate` and\n * `br` before the body reaches the loader, so a body of a few megabytes on the wire can become\n * gigabytes in memory (a \"gzip bomb\"); counting the decoded bytes while they arrive stops it before\n * the memory is taken. One instance per load: the total covers every document of that load.\n */\nexport class RemoteBodyLimit {\n readonly #limits: RemoteBodyLimits;\n #totalBytes = 0;\n\n constructor(limits: RemoteBodyLimits) {\n this.#limits = limits;\n }\n\n /** The whole body of `response` from `url`; fails with `SCHEMA_REF_FORBIDDEN` past a limit. */\n async read(response: Response, url: string): Promise<ArrayBuffer> {\n const reader = response.body?.getReader();\n if (reader === undefined) {\n return new ArrayBuffer(0);\n }\n const chunks: Uint8Array[] = [];\n let documentBytes = 0;\n for (let next = await reader.read(); !next.done; next = await reader.read()) {\n documentBytes += next.value.byteLength;\n this.#totalBytes += next.value.byteLength;\n const refusal = this.#refusal(url, documentBytes);\n if (refusal !== undefined) {\n await reader.cancel();\n throw refusal;\n }\n chunks.push(next.value);\n }\n return joined(chunks, documentBytes);\n }\n\n #refusal(url: string, documentBytes: number): OpenAPITypesafeError | undefined {\n const { maxDocumentBytes, maxTotalBytes } = this.#limits;\n if (documentBytes > maxDocumentBytes) {\n return refForbiddenError(url, { reason: \"remote-document-too-large\", maxDocumentBytes });\n }\n return this.#totalBytes > maxTotalBytes\n ? refForbiddenError(url, { reason: \"remote-documents-too-large\", maxTotalBytes })\n : undefined;\n }\n}\n\nfunction joined(chunks: readonly Uint8Array[], byteLength: number): ArrayBuffer {\n const body = new Uint8Array(byteLength);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body.buffer;\n}\n"],"names":[],"mappings":";;AAiBO,MAAM,eAAA,CAAgB;AAAA,EAClB,OAAA;AAAA,EACT,WAAA,GAAc,CAAA;AAAA,EAEd,YAAY,MAAA,EAA0B;AACpC,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGA,MAAM,IAAA,CAAK,QAAA,EAAoB,GAAA,EAAmC;AAChE,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,EAAM,SAAA,EAAU;AACxC,IAAA,IAAI,WAAW,MAAA,EAAW;AACxB,MAAA,OAAO,IAAI,YAAY,CAAC,CAAA;AAAA,IAC1B;AACA,IAAA,MAAM,SAAuB,EAAC;AAC9B,IAAA,IAAI,aAAA,GAAgB,CAAA;AACpB,IAAA,KAAA,IAAS,IAAA,GAAO,MAAM,MAAA,CAAO,IAAA,EAAK,EAAG,CAAC,IAAA,CAAK,IAAA,EAAM,IAAA,GAAO,MAAM,MAAA,CAAO,IAAA,EAAK,EAAG;AAC3E,MAAA,aAAA,IAAiB,KAAK,KAAA,CAAM,UAAA;AAC5B,MAAA,IAAA,CAAK,WAAA,IAAe,KAAK,KAAA,CAAM,UAAA;AAC/B,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,EAAK,aAAa,CAAA;AAChD,MAAA,IAAI,YAAY,MAAA,EAAW;AACzB,QAAA,MAAM,OAAO,MAAA,EAAO;AACpB,QAAA,MAAM,OAAA;AAAA,MACR;AACA,MAAA,MAAA,CAAO,IAAA,CAAK,KAAK,KAAK,CAAA;AAAA,IACxB;AACA,IAAA,OAAO,MAAA,CAAO,QAAQ,aAAa,CAAA;AAAA,EACrC;AAAA,EAEA,QAAA,CAAS,KAAa,aAAA,EAAyD;AAC7E,IAAA,MAAM,EAAE,gBAAA,EAAkB,aAAA,EAAc,GAAI,IAAA,CAAK,OAAA;AACjD,IAAA,IAAI,gBAAgB,gBAAA,EAAkB;AACpC,MAAA,OAAO,kBAAkB,GAAA,EAAK,EAAE,MAAA,EAAQ,2BAAA,EAA6B,kBAAkB,CAAA;AAAA,IACzF;AACA,IAAA,OAAO,IAAA,CAAK,WAAA,GAAc,aAAA,GACtB,iBAAA,CAAkB,GAAA,EAAK,EAAE,MAAA,EAAQ,4BAAA,EAA8B,aAAA,EAAe,CAAA,GAC9E,MAAA;AAAA,EACN;AACF;AAEA,SAAS,MAAA,CAAO,QAA+B,UAAA,EAAiC;AAC9E,EAAA,MAAM,IAAA,GAAO,IAAI,UAAA,CAAW,UAAU,CAAA;AACtC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,IAAA,CAAK,GAAA,CAAI,OAAO,MAAM,CAAA;AACtB,IAAA,MAAA,IAAU,KAAA,CAAM,UAAA;AAAA,EAClB;AACA,EAAA,OAAO,IAAA,CAAK,MAAA;AACd;;"}
@@ -2,13 +2,14 @@
2
2
 
3
3
  const allowedHosts = require('./allowed-hosts.cjs');
4
4
  const configuredHeaders = require('./configured-headers.cjs');
5
+ const remoteBodyLimit = require('./remote-body-limit.cjs');
5
6
  const schemaLoadErrors = require('./schema-load-errors.cjs');
6
7
 
7
8
  const MAX_REDIRECTS = 20;
8
9
  const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
9
10
  const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
10
- async function bufferedResponse(response) {
11
- const body = await response.arrayBuffer();
11
+ async function bufferedResponse(response, url, bodyLimit) {
12
+ const body = await bodyLimit.read(response, url);
12
13
  const { status, statusText, headers } = response;
13
14
  return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {
14
15
  status,
@@ -16,17 +17,44 @@ async function bufferedResponse(response) {
16
17
  headers
17
18
  });
18
19
  }
19
- function fetchWithTimeout(timeoutMs) {
20
+ const CLOSED_CONNECTION_CODES = /* @__PURE__ */ new Set([
21
+ "ECONNRESET",
22
+ "EPIPE",
23
+ "UND_ERR_SOCKET"
24
+ ]);
25
+ const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
26
+ const DEFAULT_METHOD = "GET";
27
+ function isClosedConnection(error) {
28
+ const cause = error instanceof TypeError ? error.cause : void 0;
29
+ const code = cause instanceof Error && "code" in cause ? cause.code : void 0;
30
+ return typeof code === "string" && CLOSED_CONNECTION_CODES.has(code);
31
+ }
32
+ async function fetchOnOpenConnection(input, init) {
33
+ try {
34
+ return await fetch(input, init);
35
+ } catch (error) {
36
+ const method = (init.method ?? DEFAULT_METHOD).toUpperCase();
37
+ if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {
38
+ throw error;
39
+ }
40
+ return fetch(input, init);
41
+ }
42
+ }
43
+ function fetchWithTimeout(timeoutMs, bodyLimit) {
20
44
  return async (input, init = {}) => {
21
45
  try {
22
- const response = await fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
23
- return await bufferedResponse(response);
46
+ const signal = AbortSignal.timeout(timeoutMs);
47
+ const response = await fetchOnOpenConnection(input, { ...init, signal });
48
+ return await bufferedResponse(response, input, bodyLimit);
24
49
  } catch (cause) {
25
50
  const isTimeout = cause instanceof Error && cause.name === "TimeoutError";
26
51
  throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;
27
52
  }
28
53
  };
29
54
  }
55
+ function fetchWithBodyLimit(fetchImpl, bodyLimit) {
56
+ return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);
57
+ }
30
58
  function fetchWithCheckedRedirects(fetchImpl, policy) {
31
59
  return async (input, init = {}) => {
32
60
  let url = new URL(input);
@@ -71,7 +99,8 @@ function fetchWithConcurrencyLimit(fetchImpl, limit) {
71
99
  }
72
100
  function resolveConfigWithAccess(resolve, options) {
73
101
  const configured = resolve.http.customFetch;
74
- const base = configured ?? fetchWithTimeout(options.timeoutMs);
102
+ const bodyLimit = new remoteBodyLimit.RemoteBodyLimit(options.bodyLimits);
103
+ const base = configured === void 0 ? fetchWithTimeout(options.timeoutMs, bodyLimit) : fetchWithBodyLimit(configured, bodyLimit);
75
104
  const checked = fetchWithCheckedRedirects(base, {
76
105
  headers: resolve.http.headers,
77
106
  hosts: options.access.remoteHosts
@@ -1 +1 @@
1
- {"version":3,"file":"remote-fetch.cjs","sources":["../../../src/adapters/redocly-loader/remote-fetch.ts"],"sourcesContent":["import type { Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport type { RefAccess } from \"../../application/ports/schema-loader.js\";\nimport { isHostAllowed } from \"./allowed-hosts.js\";\nimport { type ConfiguredHeader, headersFor } from \"./configured-headers.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * How the loader fetches remote documents: a time limit, the host limit and the configured headers\n * checked on every redirect.\n */\n\ntype ResolveConfig = RedoclyConfig[\"resolve\"];\n\n/** The fetch Redocly calls for a remote document (`resolve.http.customFetch`). */\nexport type RemoteFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\n/**\n * Redirects followed for one remote document. The loader follows them itself, so that each hop is\n * checked; 20 is the limit of the Fetch standard, which fetch applied before.\n */\nexport const MAX_REDIRECTS = 20;\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Statuses whose responses have no body; `new Response()` refuses a body for them. */\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\n/** A response whose body is already read, so that reading it again cannot wait or fail. */\nasync function bufferedResponse(response: Response): Promise<Response> {\n const body = await response.arrayBuffer();\n const { status, statusText, headers } = response;\n return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {\n status,\n statusText,\n headers,\n });\n}\n\n/**\n * Fetch with a time limit for the whole response, body included: Node's own fetch waits five\n * minutes for a server that never answers, and a server that sends its body one byte at a time\n * never finishes. The body is read here, inside the limit; when Redocly read it, the abort\n * surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave\n * \"Cannot set property message of which has only a getter\" instead of the time limit.\n */\nfunction fetchWithTimeout(timeoutMs: number): RemoteFetch {\n return async (input, init = {}) => {\n try {\n const response = await fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });\n return await bufferedResponse(response);\n } catch (cause) {\n const isTimeout = cause instanceof Error && cause.name === \"TimeoutError\";\n throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;\n }\n };\n}\n\n/** What every hop of a remote fetch is checked against. */\nexport interface RedirectPolicy {\n /** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */\n readonly headers: readonly ConfiguredHeader[];\n /** `--remote-ref-hosts`; undefined allows every host. */\n readonly hosts: readonly string[] | undefined;\n}\n\n/**\n * Follow redirects one by one. Fetch drops only `Authorization`, `Cookie` and\n * `Proxy-Authorization` on a redirect to another origin and keeps every other header (an\n * `X-Api-Key`), so the configured headers are chosen for each hop's own URL instead, and a hop to\n * a host that is not listed is refused before it is fetched. The first URL was checked by the\n * resolver (or is the root document). Redocly passes only the configured headers in `init`, and\n * it gets an empty list of them (`resolveConfigWithAccess`), so `init.headers` is replaced.\n */\nexport function fetchWithCheckedRedirects(\n fetchImpl: RemoteFetch,\n policy: RedirectPolicy,\n): RemoteFetch {\n return async (input, init = {}) => {\n let url = new URL(input);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n if (hop > 0 && policy.hosts !== undefined && !isHostAllowed(url, policy.hosts)) {\n throw refForbiddenError(url.href, {\n reason: \"host-not-allowed\",\n allowedHosts: [...policy.hosts],\n });\n }\n const headers = await headersFor(url, policy.headers);\n const response = await fetchImpl(url.href, { ...init, headers, redirect: \"manual\" });\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n return response;\n }\n url = new URL(location, url);\n }\n throw new Error(`More than ${MAX_REDIRECTS} redirects loading ${input}`);\n };\n}\n\n/** Bounds on the remote documents of one load (threat model T2, ADR-033). */\nexport interface RemoteDocumentLimits {\n /** Remote documents one load may fetch, the root included; one more is refused. */\n readonly maxDocuments: number;\n /** Remote documents fetched at the same time; the others wait for a free slot. */\n readonly maxParallelFetches: number;\n}\n\n/**\n * At most `limit` calls of the fetch run at once; the others wait in order. A finished call hands\n * its slot to the next waiting one.\n */\nexport function fetchWithConcurrencyLimit(fetchImpl: RemoteFetch, limit: number): RemoteFetch {\n let running = 0;\n const waiting: (() => void)[] = [];\n return async (input, init) => {\n if (running < limit) {\n running += 1;\n } else {\n await new Promise<void>((resolve) => waiting.push(resolve));\n }\n try {\n return await fetchImpl(input, init);\n } finally {\n const next = waiting.shift();\n if (next === undefined) {\n running -= 1;\n } else {\n next();\n }\n }\n };\n}\n\nexport interface RemoteFetchOptions {\n /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */\n readonly timeoutMs: number;\n readonly access: RefAccess;\n readonly maxParallelFetches: number;\n}\n\n/**\n * The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is\n * kept and wrapped, so that each redirect is checked and only a few documents load at once. The\n * configured headers move into that wrapper: Redocly would match them against the raw URL and\n * send them on through redirects.\n */\nexport function resolveConfigWithAccess(\n resolve: ResolveConfig,\n options: RemoteFetchOptions,\n): ResolveConfig {\n // Redocly types `customFetch` as `Function`; it is called as fetch is.\n const configured = resolve.http.customFetch as RemoteFetch | undefined;\n const base = configured ?? fetchWithTimeout(options.timeoutMs);\n const checked = fetchWithCheckedRedirects(base, {\n headers: resolve.http.headers,\n hosts: options.access.remoteHosts,\n });\n // The limit is outside the redirects, so all hops of one document share one slot.\n const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);\n return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };\n}\n"],"names":["isHostAllowed","refForbiddenError","headersFor"],"mappings":"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"}
1
+ {"version":3,"file":"remote-fetch.cjs","sources":["../../../src/adapters/redocly-loader/remote-fetch.ts"],"sourcesContent":["import type { Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport type { RefAccess } from \"../../application/ports/schema-loader.js\";\nimport { isHostAllowed } from \"./allowed-hosts.js\";\nimport { type ConfiguredHeader, headersFor } from \"./configured-headers.js\";\nimport { RemoteBodyLimit, type RemoteBodyLimits } from \"./remote-body-limit.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * How the loader fetches remote documents: a time limit, size limits, the host limit and the\n * configured headers checked on every redirect.\n */\n\ntype ResolveConfig = RedoclyConfig[\"resolve\"];\n\n/** The fetch Redocly calls for a remote document (`resolve.http.customFetch`). */\nexport type RemoteFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\n/**\n * Redirects followed for one remote document. The loader follows them itself, so that each hop is\n * checked; 20 is the limit of the Fetch standard, which fetch applied before.\n */\nexport const MAX_REDIRECTS = 20;\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Statuses whose responses have no body; `new Response()` refuses a body for them. */\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\n/**\n * A response whose body is already read within the size limits, so that reading it again cannot\n * wait, fail or take more memory.\n */\nasync function bufferedResponse(\n response: Response,\n url: string,\n bodyLimit: RemoteBodyLimit,\n): Promise<Response> {\n const body = await bodyLimit.read(response, url);\n const { status, statusText, headers } = response;\n return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {\n status,\n statusText,\n headers,\n });\n}\n\n/**\n * Codes of a connection that closed before any response came. The server can close a kept-alive\n * connection while the process is busy (parsing a large document, generating another API) and\n * has not read the close yet; the next request on that connection then fails with one of these.\n */\nconst CLOSED_CONNECTION_CODES: ReadonlySet<string> = new Set([\n \"ECONNRESET\",\n \"EPIPE\",\n \"UND_ERR_SOCKET\",\n]);\n\n/** Methods that may be sent again without a second effect (RFC 9110 §9.2.2). */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\"]);\nconst DEFAULT_METHOD = \"GET\";\n\n/** Whether fetch failed because its connection closed before any response came. */\nfunction isClosedConnection(error: unknown): boolean {\n const cause = error instanceof TypeError ? error.cause : undefined;\n const code = cause instanceof Error && \"code\" in cause ? cause.code : undefined;\n return typeof code === \"string\" && CLOSED_CONNECTION_CODES.has(code);\n}\n\n/**\n * Fetch, and send the request once more when its connection closed before any response came, as\n * RFC 9112 §9.3.1 allows for an idempotent request. Fetch drops a connection that failed, so the\n * second attempt goes out on another one; a second failure is the server's answer and is thrown.\n */\nasync function fetchOnOpenConnection(input: string, init: RequestInit): Promise<Response> {\n try {\n return await fetch(input, init);\n } catch (error) {\n const method = (init.method ?? DEFAULT_METHOD).toUpperCase();\n if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {\n throw error;\n }\n return fetch(input, init);\n }\n}\n\n/**\n * Fetch with a time limit for the whole response, body included: Node's own fetch waits five\n * minutes for a server that never answers, and a server that sends its body one byte at a time\n * never finishes. The body is read here, inside the limit; when Redocly read it, the abort\n * surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave\n * \"Cannot set property message of which has only a getter\" instead of the time limit.\n */\nfunction fetchWithTimeout(timeoutMs: number, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init = {}) => {\n try {\n // One signal for both attempts, so that a request sent again stays within the same limit.\n const signal = AbortSignal.timeout(timeoutMs);\n const response = await fetchOnOpenConnection(input, { ...init, signal });\n return await bufferedResponse(response, input, bodyLimit);\n } catch (cause) {\n const isTimeout = cause instanceof Error && cause.name === \"TimeoutError\";\n throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;\n }\n };\n}\n\n/** A configured `customFetch` with its bodies read within the size limits. */\nfunction fetchWithBodyLimit(fetchImpl: RemoteFetch, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);\n}\n\n/** What every hop of a remote fetch is checked against. */\nexport interface RedirectPolicy {\n /** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */\n readonly headers: readonly ConfiguredHeader[];\n /** `--remote-ref-hosts`; undefined allows every host. */\n readonly hosts: readonly string[] | undefined;\n}\n\n/**\n * Follow redirects one by one. Fetch drops only `Authorization`, `Cookie` and\n * `Proxy-Authorization` on a redirect to another origin and keeps every other header (an\n * `X-Api-Key`), so the configured headers are chosen for each hop's own URL instead, and a hop to\n * a host that is not listed is refused before it is fetched. The first URL was checked by the\n * resolver (or is the root document). Redocly passes only the configured headers in `init`, and\n * it gets an empty list of them (`resolveConfigWithAccess`), so `init.headers` is replaced.\n */\nexport function fetchWithCheckedRedirects(\n fetchImpl: RemoteFetch,\n policy: RedirectPolicy,\n): RemoteFetch {\n return async (input, init = {}) => {\n let url = new URL(input);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n if (hop > 0 && policy.hosts !== undefined && !isHostAllowed(url, policy.hosts)) {\n throw refForbiddenError(url.href, {\n reason: \"host-not-allowed\",\n allowedHosts: [...policy.hosts],\n });\n }\n const headers = await headersFor(url, policy.headers);\n const response = await fetchImpl(url.href, { ...init, headers, redirect: \"manual\" });\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n return response;\n }\n url = new URL(location, url);\n }\n throw new Error(`More than ${MAX_REDIRECTS} redirects loading ${input}`);\n };\n}\n\n/** Bounds on the remote documents of one load (threat model T2, ADR-033). */\nexport interface RemoteDocumentLimits extends RemoteBodyLimits {\n /** Remote documents one load may fetch, the root included; one more is refused. */\n readonly maxDocuments: number;\n /** Remote documents fetched at the same time; the others wait for a free slot. */\n readonly maxParallelFetches: number;\n /** Nodes (keys and values) the remote documents of one load may parse into together. */\n readonly maxNodes: number;\n}\n\n/**\n * At most `limit` calls of the fetch run at once; the others wait in order. A finished call hands\n * its slot to the next waiting one.\n */\nexport function fetchWithConcurrencyLimit(fetchImpl: RemoteFetch, limit: number): RemoteFetch {\n let running = 0;\n const waiting: (() => void)[] = [];\n return async (input, init) => {\n if (running < limit) {\n running += 1;\n } else {\n await new Promise<void>((resolve) => waiting.push(resolve));\n }\n try {\n return await fetchImpl(input, init);\n } finally {\n const next = waiting.shift();\n if (next === undefined) {\n running -= 1;\n } else {\n next();\n }\n }\n };\n}\n\nexport interface RemoteFetchOptions {\n /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */\n readonly timeoutMs: number;\n readonly access: RefAccess;\n readonly maxParallelFetches: number;\n readonly bodyLimits: RemoteBodyLimits;\n}\n\n/**\n * The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is\n * kept and wrapped, so that each redirect is checked, each body is read within the size limits\n * and only a few documents load at once. The\n * configured headers move into that wrapper: Redocly would match them against the raw URL and\n * send them on through redirects.\n */\nexport function resolveConfigWithAccess(\n resolve: ResolveConfig,\n options: RemoteFetchOptions,\n): ResolveConfig {\n // Redocly types `customFetch` as `Function`; it is called as fetch is.\n const configured = resolve.http.customFetch as RemoteFetch | undefined;\n // One limit per load, so that the total covers every document this resolver fetches.\n const bodyLimit = new RemoteBodyLimit(options.bodyLimits);\n const base =\n configured === undefined\n ? fetchWithTimeout(options.timeoutMs, bodyLimit)\n : fetchWithBodyLimit(configured, bodyLimit);\n const checked = fetchWithCheckedRedirects(base, {\n headers: resolve.http.headers,\n hosts: options.access.remoteHosts,\n });\n // The limit is outside the redirects, so all hops of one document share one slot.\n const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);\n return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };\n}\n"],"names":["isHostAllowed","refForbiddenError","headersFor","RemoteBodyLimit"],"mappings":"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"}
@@ -1,12 +1,13 @@
1
1
  import { isHostAllowed } from './allowed-hosts.mjs';
2
2
  import { headersFor } from './configured-headers.mjs';
3
+ import { RemoteBodyLimit } from './remote-body-limit.mjs';
3
4
  import { refForbiddenError } from './schema-load-errors.mjs';
4
5
 
5
6
  const MAX_REDIRECTS = 20;
6
7
  const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
7
8
  const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
8
- async function bufferedResponse(response) {
9
- const body = await response.arrayBuffer();
9
+ async function bufferedResponse(response, url, bodyLimit) {
10
+ const body = await bodyLimit.read(response, url);
10
11
  const { status, statusText, headers } = response;
11
12
  return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {
12
13
  status,
@@ -14,17 +15,44 @@ async function bufferedResponse(response) {
14
15
  headers
15
16
  });
16
17
  }
17
- function fetchWithTimeout(timeoutMs) {
18
+ const CLOSED_CONNECTION_CODES = /* @__PURE__ */ new Set([
19
+ "ECONNRESET",
20
+ "EPIPE",
21
+ "UND_ERR_SOCKET"
22
+ ]);
23
+ const IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
24
+ const DEFAULT_METHOD = "GET";
25
+ function isClosedConnection(error) {
26
+ const cause = error instanceof TypeError ? error.cause : void 0;
27
+ const code = cause instanceof Error && "code" in cause ? cause.code : void 0;
28
+ return typeof code === "string" && CLOSED_CONNECTION_CODES.has(code);
29
+ }
30
+ async function fetchOnOpenConnection(input, init) {
31
+ try {
32
+ return await fetch(input, init);
33
+ } catch (error) {
34
+ const method = (init.method ?? DEFAULT_METHOD).toUpperCase();
35
+ if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {
36
+ throw error;
37
+ }
38
+ return fetch(input, init);
39
+ }
40
+ }
41
+ function fetchWithTimeout(timeoutMs, bodyLimit) {
18
42
  return async (input, init = {}) => {
19
43
  try {
20
- const response = await fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
21
- return await bufferedResponse(response);
44
+ const signal = AbortSignal.timeout(timeoutMs);
45
+ const response = await fetchOnOpenConnection(input, { ...init, signal });
46
+ return await bufferedResponse(response, input, bodyLimit);
22
47
  } catch (cause) {
23
48
  const isTimeout = cause instanceof Error && cause.name === "TimeoutError";
24
49
  throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;
25
50
  }
26
51
  };
27
52
  }
53
+ function fetchWithBodyLimit(fetchImpl, bodyLimit) {
54
+ return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);
55
+ }
28
56
  function fetchWithCheckedRedirects(fetchImpl, policy) {
29
57
  return async (input, init = {}) => {
30
58
  let url = new URL(input);
@@ -69,7 +97,8 @@ function fetchWithConcurrencyLimit(fetchImpl, limit) {
69
97
  }
70
98
  function resolveConfigWithAccess(resolve, options) {
71
99
  const configured = resolve.http.customFetch;
72
- const base = configured ?? fetchWithTimeout(options.timeoutMs);
100
+ const bodyLimit = new RemoteBodyLimit(options.bodyLimits);
101
+ const base = configured === void 0 ? fetchWithTimeout(options.timeoutMs, bodyLimit) : fetchWithBodyLimit(configured, bodyLimit);
73
102
  const checked = fetchWithCheckedRedirects(base, {
74
103
  headers: resolve.http.headers,
75
104
  hosts: options.access.remoteHosts
@@ -1 +1 @@
1
- {"version":3,"file":"remote-fetch.mjs","sources":["../../../src/adapters/redocly-loader/remote-fetch.ts"],"sourcesContent":["import type { Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport type { RefAccess } from \"../../application/ports/schema-loader.js\";\nimport { isHostAllowed } from \"./allowed-hosts.js\";\nimport { type ConfiguredHeader, headersFor } from \"./configured-headers.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * How the loader fetches remote documents: a time limit, the host limit and the configured headers\n * checked on every redirect.\n */\n\ntype ResolveConfig = RedoclyConfig[\"resolve\"];\n\n/** The fetch Redocly calls for a remote document (`resolve.http.customFetch`). */\nexport type RemoteFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\n/**\n * Redirects followed for one remote document. The loader follows them itself, so that each hop is\n * checked; 20 is the limit of the Fetch standard, which fetch applied before.\n */\nexport const MAX_REDIRECTS = 20;\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Statuses whose responses have no body; `new Response()` refuses a body for them. */\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\n/** A response whose body is already read, so that reading it again cannot wait or fail. */\nasync function bufferedResponse(response: Response): Promise<Response> {\n const body = await response.arrayBuffer();\n const { status, statusText, headers } = response;\n return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {\n status,\n statusText,\n headers,\n });\n}\n\n/**\n * Fetch with a time limit for the whole response, body included: Node's own fetch waits five\n * minutes for a server that never answers, and a server that sends its body one byte at a time\n * never finishes. The body is read here, inside the limit; when Redocly read it, the abort\n * surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave\n * \"Cannot set property message of which has only a getter\" instead of the time limit.\n */\nfunction fetchWithTimeout(timeoutMs: number): RemoteFetch {\n return async (input, init = {}) => {\n try {\n const response = await fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });\n return await bufferedResponse(response);\n } catch (cause) {\n const isTimeout = cause instanceof Error && cause.name === \"TimeoutError\";\n throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;\n }\n };\n}\n\n/** What every hop of a remote fetch is checked against. */\nexport interface RedirectPolicy {\n /** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */\n readonly headers: readonly ConfiguredHeader[];\n /** `--remote-ref-hosts`; undefined allows every host. */\n readonly hosts: readonly string[] | undefined;\n}\n\n/**\n * Follow redirects one by one. Fetch drops only `Authorization`, `Cookie` and\n * `Proxy-Authorization` on a redirect to another origin and keeps every other header (an\n * `X-Api-Key`), so the configured headers are chosen for each hop's own URL instead, and a hop to\n * a host that is not listed is refused before it is fetched. The first URL was checked by the\n * resolver (or is the root document). Redocly passes only the configured headers in `init`, and\n * it gets an empty list of them (`resolveConfigWithAccess`), so `init.headers` is replaced.\n */\nexport function fetchWithCheckedRedirects(\n fetchImpl: RemoteFetch,\n policy: RedirectPolicy,\n): RemoteFetch {\n return async (input, init = {}) => {\n let url = new URL(input);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n if (hop > 0 && policy.hosts !== undefined && !isHostAllowed(url, policy.hosts)) {\n throw refForbiddenError(url.href, {\n reason: \"host-not-allowed\",\n allowedHosts: [...policy.hosts],\n });\n }\n const headers = await headersFor(url, policy.headers);\n const response = await fetchImpl(url.href, { ...init, headers, redirect: \"manual\" });\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n return response;\n }\n url = new URL(location, url);\n }\n throw new Error(`More than ${MAX_REDIRECTS} redirects loading ${input}`);\n };\n}\n\n/** Bounds on the remote documents of one load (threat model T2, ADR-033). */\nexport interface RemoteDocumentLimits {\n /** Remote documents one load may fetch, the root included; one more is refused. */\n readonly maxDocuments: number;\n /** Remote documents fetched at the same time; the others wait for a free slot. */\n readonly maxParallelFetches: number;\n}\n\n/**\n * At most `limit` calls of the fetch run at once; the others wait in order. A finished call hands\n * its slot to the next waiting one.\n */\nexport function fetchWithConcurrencyLimit(fetchImpl: RemoteFetch, limit: number): RemoteFetch {\n let running = 0;\n const waiting: (() => void)[] = [];\n return async (input, init) => {\n if (running < limit) {\n running += 1;\n } else {\n await new Promise<void>((resolve) => waiting.push(resolve));\n }\n try {\n return await fetchImpl(input, init);\n } finally {\n const next = waiting.shift();\n if (next === undefined) {\n running -= 1;\n } else {\n next();\n }\n }\n };\n}\n\nexport interface RemoteFetchOptions {\n /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */\n readonly timeoutMs: number;\n readonly access: RefAccess;\n readonly maxParallelFetches: number;\n}\n\n/**\n * The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is\n * kept and wrapped, so that each redirect is checked and only a few documents load at once. The\n * configured headers move into that wrapper: Redocly would match them against the raw URL and\n * send them on through redirects.\n */\nexport function resolveConfigWithAccess(\n resolve: ResolveConfig,\n options: RemoteFetchOptions,\n): ResolveConfig {\n // Redocly types `customFetch` as `Function`; it is called as fetch is.\n const configured = resolve.http.customFetch as RemoteFetch | undefined;\n const base = configured ?? fetchWithTimeout(options.timeoutMs);\n const checked = fetchWithCheckedRedirects(base, {\n headers: resolve.http.headers,\n hosts: options.access.remoteHosts,\n });\n // The limit is outside the redirects, so all hops of one document share one slot.\n const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);\n return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"remote-fetch.mjs","sources":["../../../src/adapters/redocly-loader/remote-fetch.ts"],"sourcesContent":["import type { Config as RedoclyConfig } from \"@redocly/openapi-core\";\nimport type { RefAccess } from \"../../application/ports/schema-loader.js\";\nimport { isHostAllowed } from \"./allowed-hosts.js\";\nimport { type ConfiguredHeader, headersFor } from \"./configured-headers.js\";\nimport { RemoteBodyLimit, type RemoteBodyLimits } from \"./remote-body-limit.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * How the loader fetches remote documents: a time limit, size limits, the host limit and the\n * configured headers checked on every redirect.\n */\n\ntype ResolveConfig = RedoclyConfig[\"resolve\"];\n\n/** The fetch Redocly calls for a remote document (`resolve.http.customFetch`). */\nexport type RemoteFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\n/**\n * Redirects followed for one remote document. The loader follows them itself, so that each hop is\n * checked; 20 is the limit of the Fetch standard, which fetch applied before.\n */\nexport const MAX_REDIRECTS = 20;\n\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Statuses whose responses have no body; `new Response()` refuses a body for them. */\nconst NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);\n\n/**\n * A response whose body is already read within the size limits, so that reading it again cannot\n * wait, fail or take more memory.\n */\nasync function bufferedResponse(\n response: Response,\n url: string,\n bodyLimit: RemoteBodyLimit,\n): Promise<Response> {\n const body = await bodyLimit.read(response, url);\n const { status, statusText, headers } = response;\n return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {\n status,\n statusText,\n headers,\n });\n}\n\n/**\n * Codes of a connection that closed before any response came. The server can close a kept-alive\n * connection while the process is busy (parsing a large document, generating another API) and\n * has not read the close yet; the next request on that connection then fails with one of these.\n */\nconst CLOSED_CONNECTION_CODES: ReadonlySet<string> = new Set([\n \"ECONNRESET\",\n \"EPIPE\",\n \"UND_ERR_SOCKET\",\n]);\n\n/** Methods that may be sent again without a second effect (RFC 9110 §9.2.2). */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\"]);\nconst DEFAULT_METHOD = \"GET\";\n\n/** Whether fetch failed because its connection closed before any response came. */\nfunction isClosedConnection(error: unknown): boolean {\n const cause = error instanceof TypeError ? error.cause : undefined;\n const code = cause instanceof Error && \"code\" in cause ? cause.code : undefined;\n return typeof code === \"string\" && CLOSED_CONNECTION_CODES.has(code);\n}\n\n/**\n * Fetch, and send the request once more when its connection closed before any response came, as\n * RFC 9112 §9.3.1 allows for an idempotent request. Fetch drops a connection that failed, so the\n * second attempt goes out on another one; a second failure is the server's answer and is thrown.\n */\nasync function fetchOnOpenConnection(input: string, init: RequestInit): Promise<Response> {\n try {\n return await fetch(input, init);\n } catch (error) {\n const method = (init.method ?? DEFAULT_METHOD).toUpperCase();\n if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {\n throw error;\n }\n return fetch(input, init);\n }\n}\n\n/**\n * Fetch with a time limit for the whole response, body included: Node's own fetch waits five\n * minutes for a server that never answers, and a server that sends its body one byte at a time\n * never finishes. The body is read here, inside the limit; when Redocly read it, the abort\n * surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave\n * \"Cannot set property message of which has only a getter\" instead of the time limit.\n */\nfunction fetchWithTimeout(timeoutMs: number, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init = {}) => {\n try {\n // One signal for both attempts, so that a request sent again stays within the same limit.\n const signal = AbortSignal.timeout(timeoutMs);\n const response = await fetchOnOpenConnection(input, { ...init, signal });\n return await bufferedResponse(response, input, bodyLimit);\n } catch (cause) {\n const isTimeout = cause instanceof Error && cause.name === \"TimeoutError\";\n throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;\n }\n };\n}\n\n/** A configured `customFetch` with its bodies read within the size limits. */\nfunction fetchWithBodyLimit(fetchImpl: RemoteFetch, bodyLimit: RemoteBodyLimit): RemoteFetch {\n return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);\n}\n\n/** What every hop of a remote fetch is checked against. */\nexport interface RedirectPolicy {\n /** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */\n readonly headers: readonly ConfiguredHeader[];\n /** `--remote-ref-hosts`; undefined allows every host. */\n readonly hosts: readonly string[] | undefined;\n}\n\n/**\n * Follow redirects one by one. Fetch drops only `Authorization`, `Cookie` and\n * `Proxy-Authorization` on a redirect to another origin and keeps every other header (an\n * `X-Api-Key`), so the configured headers are chosen for each hop's own URL instead, and a hop to\n * a host that is not listed is refused before it is fetched. The first URL was checked by the\n * resolver (or is the root document). Redocly passes only the configured headers in `init`, and\n * it gets an empty list of them (`resolveConfigWithAccess`), so `init.headers` is replaced.\n */\nexport function fetchWithCheckedRedirects(\n fetchImpl: RemoteFetch,\n policy: RedirectPolicy,\n): RemoteFetch {\n return async (input, init = {}) => {\n let url = new URL(input);\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n if (hop > 0 && policy.hosts !== undefined && !isHostAllowed(url, policy.hosts)) {\n throw refForbiddenError(url.href, {\n reason: \"host-not-allowed\",\n allowedHosts: [...policy.hosts],\n });\n }\n const headers = await headersFor(url, policy.headers);\n const response = await fetchImpl(url.href, { ...init, headers, redirect: \"manual\" });\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n return response;\n }\n url = new URL(location, url);\n }\n throw new Error(`More than ${MAX_REDIRECTS} redirects loading ${input}`);\n };\n}\n\n/** Bounds on the remote documents of one load (threat model T2, ADR-033). */\nexport interface RemoteDocumentLimits extends RemoteBodyLimits {\n /** Remote documents one load may fetch, the root included; one more is refused. */\n readonly maxDocuments: number;\n /** Remote documents fetched at the same time; the others wait for a free slot. */\n readonly maxParallelFetches: number;\n /** Nodes (keys and values) the remote documents of one load may parse into together. */\n readonly maxNodes: number;\n}\n\n/**\n * At most `limit` calls of the fetch run at once; the others wait in order. A finished call hands\n * its slot to the next waiting one.\n */\nexport function fetchWithConcurrencyLimit(fetchImpl: RemoteFetch, limit: number): RemoteFetch {\n let running = 0;\n const waiting: (() => void)[] = [];\n return async (input, init) => {\n if (running < limit) {\n running += 1;\n } else {\n await new Promise<void>((resolve) => waiting.push(resolve));\n }\n try {\n return await fetchImpl(input, init);\n } finally {\n const next = waiting.shift();\n if (next === undefined) {\n running -= 1;\n } else {\n next();\n }\n }\n };\n}\n\nexport interface RemoteFetchOptions {\n /** Time limit of one fetch and its body, unless the Redocly config has its own `customFetch`. */\n readonly timeoutMs: number;\n readonly access: RefAccess;\n readonly maxParallelFetches: number;\n readonly bodyLimits: RemoteBodyLimits;\n}\n\n/**\n * The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is\n * kept and wrapped, so that each redirect is checked, each body is read within the size limits\n * and only a few documents load at once. The\n * configured headers move into that wrapper: Redocly would match them against the raw URL and\n * send them on through redirects.\n */\nexport function resolveConfigWithAccess(\n resolve: ResolveConfig,\n options: RemoteFetchOptions,\n): ResolveConfig {\n // Redocly types `customFetch` as `Function`; it is called as fetch is.\n const configured = resolve.http.customFetch as RemoteFetch | undefined;\n // One limit per load, so that the total covers every document this resolver fetches.\n const bodyLimit = new RemoteBodyLimit(options.bodyLimits);\n const base =\n configured === undefined\n ? fetchWithTimeout(options.timeoutMs, bodyLimit)\n : fetchWithBodyLimit(configured, bodyLimit);\n const checked = fetchWithCheckedRedirects(base, {\n headers: resolve.http.headers,\n hosts: options.access.remoteHosts,\n });\n // The limit is outside the redirects, so all hops of one document share one slot.\n const customFetch = fetchWithConcurrencyLimit(checked, options.maxParallelFetches);\n return { ...resolve, http: { ...resolve.http, headers: [], customFetch } };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const openapiCore = require('@redocly/openapi-core');
4
+ const schemaLoadErrors = require('./schema-load-errors.cjs');
5
+
6
+ const PARSED_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".yml", ".yaml"]);
7
+ const PARSED_MIME_TYPE = /(json|yaml|openapi)/;
8
+ function isParsedSource(source, isRoot) {
9
+ const { absoluteRef, mimeType } = source;
10
+ const extension = absoluteRef.slice(absoluteRef.lastIndexOf("."));
11
+ return isRoot || PARSED_EXTENSIONS.has(extension) || PARSED_MIME_TYPE.test(mimeType ?? "");
12
+ }
13
+ const NODE_OPENED = "open";
14
+ class RemoteNodeLimit {
15
+ #maxNodes;
16
+ #nodes = 0;
17
+ constructor(maxNodes) {
18
+ this.#maxNodes = maxNodes;
19
+ }
20
+ /**
21
+ * The parsed document of `source`, as Redocly parses it. Fails with `SCHEMA_REF_FORBIDDEN`
22
+ * (`remote-documents-too-many-nodes`) once the nodes of the load pass the limit.
23
+ */
24
+ parse(source) {
25
+ const listener = (event) => {
26
+ if (event !== NODE_OPENED) {
27
+ return;
28
+ }
29
+ this.#nodes += 1;
30
+ const refusal = this.#refusal(source.absoluteRef);
31
+ if (refusal !== void 0) {
32
+ throw refusal;
33
+ }
34
+ };
35
+ return { source, parsed: openapiCore.parseYaml(source.body, { filename: source.absoluteRef, listener }) };
36
+ }
37
+ #refusal(url) {
38
+ return this.#nodes > this.#maxNodes ? schemaLoadErrors.refForbiddenError(url, {
39
+ reason: "remote-documents-too-many-nodes",
40
+ maxNodes: this.#maxNodes
41
+ }) : void 0;
42
+ }
43
+ }
44
+
45
+ exports.RemoteNodeLimit = RemoteNodeLimit;
46
+ exports.isParsedSource = isParsedSource;
47
+ //# sourceMappingURL=remote-node-limit.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-node-limit.cjs","sources":["../../../src/adapters/redocly-loader/remote-node-limit.ts"],"sourcesContent":["import { type Document, parseYaml, type Source } from \"@redocly/openapi-core\";\nimport type { OpenAPITypesafeError } from \"../../domain/errors.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * Bounds the parsed size of the remote documents of one load (threat model T2). The byte limits\n * do not bound memory: parsing multiplies it by the density of the text, and 250 MiB of `{},`\n * (87 million empty objects, under a quarter of a megabyte as gzip) took the process past its\n * 4 GB heap. The parser reports every node it builds, keys included, and the parse stops when\n * the nodes of all remote documents of the load pass the limit, before their memory is taken.\n */\n\n/** The extensions that Redocly 1.34's `BaseResolver.parseDocument` parses as YAML or JSON. */\nconst PARSED_EXTENSIONS: ReadonlySet<string> = new Set([\".json\", \".yml\", \".yaml\"]);\nconst PARSED_MIME_TYPE = /(json|yaml|openapi)/;\n\n/** Whether Redocly parses this source, or keeps its text: the same rule as its own resolver. */\nexport function isParsedSource(source: Source, isRoot: boolean): boolean {\n const { absoluteRef, mimeType } = source;\n const extension = absoluteRef.slice(absoluteRef.lastIndexOf(\".\"));\n return isRoot || PARSED_EXTENSIONS.has(extension) || PARSED_MIME_TYPE.test(mimeType ?? \"\");\n}\n\n/** The parser's event for a node it starts to build. */\nconst NODE_OPENED = \"open\";\n\n/** One instance per load: the count covers every remote document of that load. */\nexport class RemoteNodeLimit {\n readonly #maxNodes: number;\n #nodes = 0;\n\n constructor(maxNodes: number) {\n this.#maxNodes = maxNodes;\n }\n\n /**\n * The parsed document of `source`, as Redocly parses it. Fails with `SCHEMA_REF_FORBIDDEN`\n * (`remote-documents-too-many-nodes`) once the nodes of the load pass the limit.\n */\n parse(source: Source): Document {\n const listener = (event: string): void => {\n if (event !== NODE_OPENED) {\n return;\n }\n this.#nodes += 1;\n const refusal = this.#refusal(source.absoluteRef);\n if (refusal !== undefined) {\n throw refusal;\n }\n };\n return { source, parsed: parseYaml(source.body, { filename: source.absoluteRef, listener }) };\n }\n\n #refusal(url: string): OpenAPITypesafeError | undefined {\n return this.#nodes > this.#maxNodes\n ? refForbiddenError(url, {\n reason: \"remote-documents-too-many-nodes\",\n maxNodes: this.#maxNodes,\n })\n : undefined;\n }\n}\n"],"names":["parseYaml","refForbiddenError"],"mappings":";;;;;AAaA,MAAM,oCAAyC,IAAI,GAAA,CAAI,CAAC,OAAA,EAAS,MAAA,EAAQ,OAAO,CAAC,CAAA;AACjF,MAAM,gBAAA,GAAmB,qBAAA;AAGlB,SAAS,cAAA,CAAe,QAAgB,MAAA,EAA0B;AACvE,EAAA,MAAM,EAAE,WAAA,EAAa,QAAA,EAAS,GAAI,MAAA;AAClC,EAAA,MAAM,YAAY,WAAA,CAAY,KAAA,CAAM,WAAA,CAAY,WAAA,CAAY,GAAG,CAAC,CAAA;AAChE,EAAA,OAAO,MAAA,IAAU,kBAAkB,GAAA,CAAI,SAAS,KAAK,gBAAA,CAAiB,IAAA,CAAK,YAAY,EAAE,CAAA;AAC3F;AAGA,MAAM,WAAA,GAAc,MAAA;AAGb,MAAM,eAAA,CAAgB;AAAA,EAClB,SAAA;AAAA,EACT,MAAA,GAAS,CAAA;AAAA,EAET,YAAY,QAAA,EAAkB;AAC5B,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAA,EAA0B;AAC9B,IAAA,MAAM,QAAA,GAAW,CAAC,KAAA,KAAwB;AACxC,MAAA,IAAI,UAAU,WAAA,EAAa;AACzB,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,MAAA,IAAU,CAAA;AACf,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,WAAW,CAAA;AAChD,MAAA,IAAI,YAAY,MAAA,EAAW;AACzB,QAAA,MAAM,OAAA;AAAA,MACR;AAAA,IACF,CAAA;AACA,IAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQA,qBAAA,CAAU,MAAA,CAAO,IAAA,EAAM,EAAE,QAAA,EAAU,MAAA,CAAO,WAAA,EAAa,QAAA,EAAU,CAAA,EAAE;AAAA,EAC9F;AAAA,EAEA,SAAS,GAAA,EAA+C;AACtD,IAAA,OAAO,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,SAAA,GACtBC,mCAAkB,GAAA,EAAK;AAAA,MACrB,MAAA,EAAQ,iCAAA;AAAA,MACR,UAAU,IAAA,CAAK;AAAA,KAChB,CAAA,GACD,MAAA;AAAA,EACN;AACF;;;"}
@@ -0,0 +1,44 @@
1
+ import { parseYaml } from '@redocly/openapi-core';
2
+ import { refForbiddenError } from './schema-load-errors.mjs';
3
+
4
+ const PARSED_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".yml", ".yaml"]);
5
+ const PARSED_MIME_TYPE = /(json|yaml|openapi)/;
6
+ function isParsedSource(source, isRoot) {
7
+ const { absoluteRef, mimeType } = source;
8
+ const extension = absoluteRef.slice(absoluteRef.lastIndexOf("."));
9
+ return isRoot || PARSED_EXTENSIONS.has(extension) || PARSED_MIME_TYPE.test(mimeType ?? "");
10
+ }
11
+ const NODE_OPENED = "open";
12
+ class RemoteNodeLimit {
13
+ #maxNodes;
14
+ #nodes = 0;
15
+ constructor(maxNodes) {
16
+ this.#maxNodes = maxNodes;
17
+ }
18
+ /**
19
+ * The parsed document of `source`, as Redocly parses it. Fails with `SCHEMA_REF_FORBIDDEN`
20
+ * (`remote-documents-too-many-nodes`) once the nodes of the load pass the limit.
21
+ */
22
+ parse(source) {
23
+ const listener = (event) => {
24
+ if (event !== NODE_OPENED) {
25
+ return;
26
+ }
27
+ this.#nodes += 1;
28
+ const refusal = this.#refusal(source.absoluteRef);
29
+ if (refusal !== void 0) {
30
+ throw refusal;
31
+ }
32
+ };
33
+ return { source, parsed: parseYaml(source.body, { filename: source.absoluteRef, listener }) };
34
+ }
35
+ #refusal(url) {
36
+ return this.#nodes > this.#maxNodes ? refForbiddenError(url, {
37
+ reason: "remote-documents-too-many-nodes",
38
+ maxNodes: this.#maxNodes
39
+ }) : void 0;
40
+ }
41
+ }
42
+
43
+ export { RemoteNodeLimit, isParsedSource };
44
+ //# sourceMappingURL=remote-node-limit.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-node-limit.mjs","sources":["../../../src/adapters/redocly-loader/remote-node-limit.ts"],"sourcesContent":["import { type Document, parseYaml, type Source } from \"@redocly/openapi-core\";\nimport type { OpenAPITypesafeError } from \"../../domain/errors.js\";\nimport { refForbiddenError } from \"./schema-load-errors.js\";\n\n/**\n * Bounds the parsed size of the remote documents of one load (threat model T2). The byte limits\n * do not bound memory: parsing multiplies it by the density of the text, and 250 MiB of `{},`\n * (87 million empty objects, under a quarter of a megabyte as gzip) took the process past its\n * 4 GB heap. The parser reports every node it builds, keys included, and the parse stops when\n * the nodes of all remote documents of the load pass the limit, before their memory is taken.\n */\n\n/** The extensions that Redocly 1.34's `BaseResolver.parseDocument` parses as YAML or JSON. */\nconst PARSED_EXTENSIONS: ReadonlySet<string> = new Set([\".json\", \".yml\", \".yaml\"]);\nconst PARSED_MIME_TYPE = /(json|yaml|openapi)/;\n\n/** Whether Redocly parses this source, or keeps its text: the same rule as its own resolver. */\nexport function isParsedSource(source: Source, isRoot: boolean): boolean {\n const { absoluteRef, mimeType } = source;\n const extension = absoluteRef.slice(absoluteRef.lastIndexOf(\".\"));\n return isRoot || PARSED_EXTENSIONS.has(extension) || PARSED_MIME_TYPE.test(mimeType ?? \"\");\n}\n\n/** The parser's event for a node it starts to build. */\nconst NODE_OPENED = \"open\";\n\n/** One instance per load: the count covers every remote document of that load. */\nexport class RemoteNodeLimit {\n readonly #maxNodes: number;\n #nodes = 0;\n\n constructor(maxNodes: number) {\n this.#maxNodes = maxNodes;\n }\n\n /**\n * The parsed document of `source`, as Redocly parses it. Fails with `SCHEMA_REF_FORBIDDEN`\n * (`remote-documents-too-many-nodes`) once the nodes of the load pass the limit.\n */\n parse(source: Source): Document {\n const listener = (event: string): void => {\n if (event !== NODE_OPENED) {\n return;\n }\n this.#nodes += 1;\n const refusal = this.#refusal(source.absoluteRef);\n if (refusal !== undefined) {\n throw refusal;\n }\n };\n return { source, parsed: parseYaml(source.body, { filename: source.absoluteRef, listener }) };\n }\n\n #refusal(url: string): OpenAPITypesafeError | undefined {\n return this.#nodes > this.#maxNodes\n ? refForbiddenError(url, {\n reason: \"remote-documents-too-many-nodes\",\n maxNodes: this.#maxNodes,\n })\n : undefined;\n }\n}\n"],"names":[],"mappings":";;;AAaA,MAAM,oCAAyC,IAAI,GAAA,CAAI,CAAC,OAAA,EAAS,MAAA,EAAQ,OAAO,CAAC,CAAA;AACjF,MAAM,gBAAA,GAAmB,qBAAA;AAGlB,SAAS,cAAA,CAAe,QAAgB,MAAA,EAA0B;AACvE,EAAA,MAAM,EAAE,WAAA,EAAa,QAAA,EAAS,GAAI,MAAA;AAClC,EAAA,MAAM,YAAY,WAAA,CAAY,KAAA,CAAM,WAAA,CAAY,WAAA,CAAY,GAAG,CAAC,CAAA;AAChE,EAAA,OAAO,MAAA,IAAU,kBAAkB,GAAA,CAAI,SAAS,KAAK,gBAAA,CAAiB,IAAA,CAAK,YAAY,EAAE,CAAA;AAC3F;AAGA,MAAM,WAAA,GAAc,MAAA;AAGb,MAAM,eAAA,CAAgB;AAAA,EAClB,SAAA;AAAA,EACT,MAAA,GAAS,CAAA;AAAA,EAET,YAAY,QAAA,EAAkB;AAC5B,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAA,EAA0B;AAC9B,IAAA,MAAM,QAAA,GAAW,CAAC,KAAA,KAAwB;AACxC,MAAA,IAAI,UAAU,WAAA,EAAa;AACzB,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,MAAA,IAAU,CAAA;AACf,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,WAAW,CAAA;AAChD,MAAA,IAAI,YAAY,MAAA,EAAW;AACzB,QAAA,MAAM,OAAA;AAAA,MACR;AAAA,IACF,CAAA;AACA,IAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,SAAA,CAAU,MAAA,CAAO,IAAA,EAAM,EAAE,QAAA,EAAU,MAAA,CAAO,WAAA,EAAa,QAAA,EAAU,CAAA,EAAE;AAAA,EAC9F;AAAA,EAEA,SAAS,GAAA,EAA+C;AACtD,IAAA,OAAO,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,SAAA,GACtB,kBAAkB,GAAA,EAAK;AAAA,MACrB,MAAA,EAAQ,iCAAA;AAAA,MACR,UAAU,IAAA,CAAK;AAAA,KAChB,CAAA,GACD,MAAA;AAAA,EACN;AACF;;"}
@@ -2,7 +2,7 @@
2
2
 
3
3
  const assertNever = require('../../domain/assert-never.cjs');
4
4
  const errors = require('../../domain/errors.cjs');
5
- const urlCredentials = require('../../domain/url-credentials.cjs');
5
+ const urlSecrets = require('../../domain/url-secrets.cjs');
6
6
 
7
7
  function sourceEmptyError(message) {
8
8
  return new errors.OpenAPITypesafeError({ code: errors.ERROR_CODES.SCHEMA_SOURCE_EMPTY, message });
@@ -15,23 +15,32 @@ function sourceInvalidError(source) {
15
15
  details: { received }
16
16
  });
17
17
  }
18
+ function urlInvalidError(text) {
19
+ return new errors.OpenAPITypesafeError({
20
+ code: errors.ERROR_CODES.SCHEMA_SOURCE_INVALID,
21
+ message: `The string ${urlSecrets.redactUnparsedUrl(text)} starts like a URL but is not a valid URL`,
22
+ details: { reason: "url", received: "string" }
23
+ });
24
+ }
18
25
  function causeMessage(cause) {
19
26
  return cause instanceof Error ? cause.message : String(cause);
20
27
  }
21
28
  const SOURCE_TEXT_IN_MESSAGE = 200;
22
- function pathStringError(text) {
29
+ const LOAD_FILE_ADVICE = "To load a file, pass a URL: openapiTS(pathToFileURL(path)) from node:url.";
30
+ function pathStringError(text, origin) {
23
31
  const shown = text.length > SOURCE_TEXT_IN_MESSAGE ? `${text.slice(0, SOURCE_TEXT_IN_MESSAGE)}\u2026` : text;
32
+ const message = origin === "string" ? `The string "${shown}" was parsed as YAML text, not loaded as a file. ${LOAD_FILE_ADVICE}` : `The stream held only the YAML string "${shown}", not an OpenAPI document. ` + LOAD_FILE_ADVICE;
24
33
  return new errors.OpenAPITypesafeError({
25
34
  code: errors.ERROR_CODES.SCHEMA_SOURCE_INVALID,
26
- message: `The string "${shown}" was parsed as YAML text, not loaded as a file. To load a file, pass a URL: openapiTS(pathToFileURL(path)) from node:url.`,
27
- details: { reason: "path", received: "string" }
35
+ message,
36
+ details: { reason: "path", received: origin, text: shown }
28
37
  });
29
38
  }
30
39
  function loadFailedError(absoluteRef, cause) {
31
40
  return new errors.OpenAPITypesafeError({
32
41
  code: errors.ERROR_CODES.SCHEMA_LOAD_FAILED,
33
42
  message: causeMessage(cause),
34
- details: { source: urlCredentials.redactUrlCredentials(absoluteRef) },
43
+ details: { source: urlSecrets.redactUrlSecrets(absoluteRef) },
35
44
  cause
36
45
  });
37
46
  }
@@ -39,7 +48,7 @@ function parseFailedError(format, absoluteRef, cause) {
39
48
  return new errors.OpenAPITypesafeError({
40
49
  code: errors.ERROR_CODES.SCHEMA_PARSE_FAILED,
41
50
  message: causeMessage(cause),
42
- details: { format, source: urlCredentials.redactUrlCredentials(absoluteRef) },
51
+ details: { format, source: urlSecrets.redactUrlSecrets(absoluteRef) },
43
52
  cause
44
53
  });
45
54
  }
@@ -54,7 +63,7 @@ function schemaInvalidError(problems) {
54
63
  return new errors.OpenAPITypesafeError({
55
64
  code: errors.ERROR_CODES.SCHEMA_INVALID,
56
65
  message: problems.at(-1) ?? "",
57
- details: { problems: problems.map(urlCredentials.redactUrlCredentials) }
66
+ details: { problems: problems.map(urlSecrets.redactUrlSecrets) }
58
67
  });
59
68
  }
60
69
  function refCycleError(cause) {
@@ -68,6 +77,10 @@ function refCycleError(cause) {
68
77
  function hostListText(hosts) {
69
78
  return hosts.length === 0 ? "none" : hosts.join(", ");
70
79
  }
80
+ const BYTES_PER_MEBIBYTE = 1024 * 1024;
81
+ function sizeText(bytes) {
82
+ return bytes % BYTES_PER_MEBIBYTE === 0 ? `${bytes / BYTES_PER_MEBIBYTE} MiB` : `${bytes} bytes`;
83
+ }
71
84
  function limitText(details) {
72
85
  switch (details.reason) {
73
86
  case "outside-local-root":
@@ -80,6 +93,16 @@ function limitText(details) {
80
93
  return `it is not an http(s) URL, and the $ref is in the remote document ${details.base}`;
81
94
  case "too-many-remote-documents":
82
95
  return `the document refers to more than ${details.maxDocuments} remote documents`;
96
+ case "remote-document-too-large":
97
+ return `its body is larger than ${sizeText(details.maxDocumentBytes)} once decoded`;
98
+ case "remote-documents-too-large": {
99
+ const limit = sizeText(details.maxTotalBytes);
100
+ return `the remote documents of this run are larger than ${limit} together once decoded`;
101
+ }
102
+ case "remote-documents-too-many-nodes": {
103
+ const limit = details.maxNodes;
104
+ return `the remote documents of this run parse into more than ${limit} nodes together`;
105
+ }
83
106
  default:
84
107
  return assertNever.assertNever(details);
85
108
  }
@@ -89,7 +112,7 @@ function refForbiddenError(ref, details) {
89
112
  return new errors.OpenAPITypesafeError({
90
113
  code: errors.ERROR_CODES.SCHEMA_REF_FORBIDDEN,
91
114
  message: `Refused to load ${ref}: ${limit}`,
92
- details: { ref: urlCredentials.redactUrlCredentials(ref), ...details }
115
+ details: { ref: urlSecrets.redactUrlSecrets(ref), ...details }
93
116
  });
94
117
  }
95
118
 
@@ -101,5 +124,6 @@ exports.refForbiddenError = refForbiddenError;
101
124
  exports.schemaInvalidError = schemaInvalidError;
102
125
  exports.sourceEmptyError = sourceEmptyError;
103
126
  exports.sourceInvalidError = sourceInvalidError;
127
+ exports.urlInvalidError = urlInvalidError;
104
128
  exports.versionUnsupportedError = versionUnsupportedError;
105
129
  //# sourceMappingURL=schema-load-errors.cjs.map