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.
- package/CHANGELOG.md +1831 -0
- package/dist/adapters/node-file-system/file-claims.cjs +105 -0
- package/dist/adapters/node-file-system/file-claims.cjs.map +1 -0
- package/dist/adapters/node-file-system/file-claims.mjs +95 -0
- package/dist/adapters/node-file-system/file-claims.mjs.map +1 -0
- package/dist/adapters/node-file-system/file-identity.cjs +82 -0
- package/dist/adapters/node-file-system/file-identity.cjs.map +1 -0
- package/dist/adapters/node-file-system/file-identity.mjs +74 -0
- package/dist/adapters/node-file-system/file-identity.mjs.map +1 -0
- package/dist/adapters/node-file-system/node-file-system.cjs +90 -14
- package/dist/adapters/node-file-system/node-file-system.cjs.map +1 -1
- package/dist/adapters/node-file-system/node-file-system.mjs +90 -14
- package/dist/adapters/node-file-system/node-file-system.mjs.map +1 -1
- package/dist/adapters/redocly-loader/bundled-mapping-copies.cjs +75 -0
- package/dist/adapters/redocly-loader/bundled-mapping-copies.cjs.map +1 -0
- package/dist/adapters/redocly-loader/bundled-mapping-copies.mjs +72 -0
- package/dist/adapters/redocly-loader/bundled-mapping-copies.mjs.map +1 -0
- package/dist/adapters/redocly-loader/bundled-ref-repair.cjs +1 -1
- package/dist/adapters/redocly-loader/bundled-ref-repair.cjs.map +1 -1
- package/dist/adapters/redocly-loader/bundled-ref-repair.mjs +2 -2
- package/dist/adapters/redocly-loader/bundled-ref-repair.mjs.map +1 -1
- package/dist/adapters/redocly-loader/loaded-documents.cjs +28 -0
- package/dist/adapters/redocly-loader/loaded-documents.cjs.map +1 -0
- package/dist/adapters/redocly-loader/loaded-documents.mjs +21 -0
- package/dist/adapters/redocly-loader/loaded-documents.mjs.map +1 -0
- package/dist/adapters/redocly-loader/redocly-schema-loader.cjs +14 -15
- package/dist/adapters/redocly-loader/redocly-schema-loader.cjs.map +1 -1
- package/dist/adapters/redocly-loader/redocly-schema-loader.mjs +12 -13
- package/dist/adapters/redocly-loader/redocly-schema-loader.mjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-access.cjs +31 -3
- package/dist/adapters/redocly-loader/ref-access.cjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-access.mjs +31 -3
- package/dist/adapters/redocly-loader/ref-access.mjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-holders.cjs +2 -2
- package/dist/adapters/redocly-loader/ref-holders.cjs.map +1 -1
- package/dist/adapters/redocly-loader/ref-holders.mjs +3 -3
- package/dist/adapters/redocly-loader/ref-holders.mjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-body-limit.cjs +50 -0
- package/dist/adapters/redocly-loader/remote-body-limit.cjs.map +1 -0
- package/dist/adapters/redocly-loader/remote-body-limit.mjs +48 -0
- package/dist/adapters/redocly-loader/remote-body-limit.mjs.map +1 -0
- package/dist/adapters/redocly-loader/remote-fetch.cjs +35 -6
- package/dist/adapters/redocly-loader/remote-fetch.cjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-fetch.mjs +35 -6
- package/dist/adapters/redocly-loader/remote-fetch.mjs.map +1 -1
- package/dist/adapters/redocly-loader/remote-node-limit.cjs +47 -0
- package/dist/adapters/redocly-loader/remote-node-limit.cjs.map +1 -0
- package/dist/adapters/redocly-loader/remote-node-limit.mjs +44 -0
- package/dist/adapters/redocly-loader/remote-node-limit.mjs.map +1 -0
- package/dist/adapters/redocly-loader/schema-load-errors.cjs +32 -8
- package/dist/adapters/redocly-loader/schema-load-errors.cjs.map +1 -1
- package/dist/adapters/redocly-loader/schema-load-errors.mjs +32 -9
- package/dist/adapters/redocly-loader/schema-load-errors.mjs.map +1 -1
- package/dist/adapters/redocly-loader/schema-source.cjs +11 -7
- package/dist/adapters/redocly-loader/schema-source.cjs.map +1 -1
- package/dist/adapters/redocly-loader/schema-source.mjs +12 -8
- package/dist/adapters/redocly-loader/schema-source.mjs.map +1 -1
- package/dist/adapters/redocly-loader/unsupported-keys.cjs +3 -5
- package/dist/adapters/redocly-loader/unsupported-keys.cjs.map +1 -1
- package/dist/adapters/redocly-loader/unsupported-keys.mjs +3 -5
- package/dist/adapters/redocly-loader/unsupported-keys.mjs.map +1 -1
- package/dist/adapters/terminal-logger/terminal-logger.cjs +6 -4
- package/dist/adapters/terminal-logger/terminal-logger.cjs.map +1 -1
- package/dist/adapters/terminal-logger/terminal-logger.mjs +6 -4
- package/dist/adapters/terminal-logger/terminal-logger.mjs.map +1 -1
- package/dist/api/default-ports.cjs +14 -2
- package/dist/api/default-ports.cjs.map +1 -1
- package/dist/api/default-ports.mjs +14 -2
- package/dist/api/default-ports.mjs.map +1 -1
- package/dist/api/discriminator-enum-parent.cjs +2 -2
- package/dist/api/discriminator-enum-parent.cjs.map +1 -1
- package/dist/api/discriminator-enum-parent.mjs +3 -3
- package/dist/api/discriminator-enum-parent.mjs.map +1 -1
- package/dist/api/discriminator-other-owners.cjs +5 -3
- package/dist/api/discriminator-other-owners.cjs.map +1 -1
- package/dist/api/discriminator-other-owners.mjs +5 -3
- package/dist/api/discriminator-other-owners.mjs.map +1 -1
- package/dist/api/discriminator-patch.cjs +5 -1
- package/dist/api/discriminator-patch.cjs.map +1 -1
- package/dist/api/discriminator-patch.mjs +5 -1
- package/dist/api/discriminator-patch.mjs.map +1 -1
- package/dist/api/discriminator-scan.cjs +15 -8
- package/dist/api/discriminator-scan.cjs.map +1 -1
- package/dist/api/discriminator-scan.mjs +15 -8
- package/dist/api/discriminator-scan.mjs.map +1 -1
- package/dist/api/document-run.cjs +2 -2
- package/dist/api/document-run.cjs.map +1 -1
- package/dist/api/document-run.mjs +2 -2
- package/dist/api/document-run.mjs.map +1 -1
- package/dist/api/legacy-utils.cjs.map +1 -1
- package/dist/api/legacy-utils.mjs.map +1 -1
- package/dist/api/openapi-ts.cjs +8 -4
- package/dist/api/openapi-ts.cjs.map +1 -1
- package/dist/api/openapi-ts.mjs +8 -5
- package/dist/api/openapi-ts.mjs.map +1 -1
- package/dist/api/schema-callbacks.cjs +36 -12
- package/dist/api/schema-callbacks.cjs.map +1 -1
- package/dist/api/schema-callbacks.mjs +36 -12
- package/dist/api/schema-callbacks.mjs.map +1 -1
- package/dist/api/transform-context.cjs +7 -3
- package/dist/api/transform-context.cjs.map +1 -1
- package/dist/api/transform-context.mjs +7 -3
- package/dist/api/transform-context.mjs.map +1 -1
- package/dist/api/transform-schema.cjs +40 -10
- package/dist/api/transform-schema.cjs.map +1 -1
- package/dist/api/transform-schema.mjs +41 -11
- package/dist/api/transform-schema.mjs.map +1 -1
- package/dist/application/print/text-writer.cjs +10 -3
- package/dist/application/print/text-writer.cjs.map +1 -1
- package/dist/application/print/text-writer.mjs +10 -3
- package/dist/application/print/text-writer.mjs.map +1 -1
- package/dist/application/stack-overflow.cjs +2 -1
- package/dist/application/stack-overflow.cjs.map +1 -1
- package/dist/application/stack-overflow.mjs +2 -1
- package/dist/application/stack-overflow.mjs.map +1 -1
- package/dist/application/transform/document/components.cjs +1 -1
- package/dist/application/transform/document/components.cjs.map +1 -1
- package/dist/application/transform/document/components.mjs +2 -2
- package/dist/application/transform/document/components.mjs.map +1 -1
- package/dist/application/transform/document/declared-type-names.cjs +23 -0
- package/dist/application/transform/document/declared-type-names.cjs.map +1 -0
- package/dist/application/transform/document/declared-type-names.mjs +20 -0
- package/dist/application/transform/document/declared-type-names.mjs.map +1 -0
- package/dist/application/transform/document/defs-root.cjs +1 -1
- package/dist/application/transform/document/defs-root.cjs.map +1 -1
- package/dist/application/transform/document/defs-root.mjs +2 -2
- package/dist/application/transform/document/defs-root.mjs.map +1 -1
- package/dist/application/transform/document/document.cjs +4 -1
- package/dist/application/transform/document/document.cjs.map +1 -1
- package/dist/application/transform/document/document.mjs +3 -2
- package/dist/application/transform/document/document.mjs.map +1 -1
- package/dist/application/transform/document/enum-names-given-back.cjs +62 -0
- package/dist/application/transform/document/enum-names-given-back.cjs.map +1 -0
- package/dist/application/transform/document/enum-names-given-back.mjs +58 -0
- package/dist/application/transform/document/enum-names-given-back.mjs.map +1 -0
- package/dist/application/transform/document/global-type-references.cjs +1 -0
- package/dist/application/transform/document/global-type-references.cjs.map +1 -1
- package/dist/application/transform/document/global-type-references.mjs +1 -1
- package/dist/application/transform/document/global-type-references.mjs.map +1 -1
- package/dist/application/transform/document/media-type.cjs +2 -2
- package/dist/application/transform/document/media-type.cjs.map +1 -1
- package/dist/application/transform/document/media-type.mjs +3 -3
- package/dist/application/transform/document/media-type.mjs.map +1 -1
- package/dist/application/transform/document/operation-keys-written.cjs +26 -0
- package/dist/application/transform/document/operation-keys-written.cjs.map +1 -0
- package/dist/application/transform/document/operation-keys-written.mjs +24 -0
- package/dist/application/transform/document/operation-keys-written.mjs.map +1 -0
- package/dist/application/transform/document/operation.cjs +1 -1
- package/dist/application/transform/document/operation.cjs.map +1 -1
- package/dist/application/transform/document/operation.mjs +2 -2
- package/dist/application/transform/document/operation.mjs.map +1 -1
- package/dist/application/transform/document/parameter-entries.cjs +34 -0
- package/dist/application/transform/document/parameter-entries.cjs.map +1 -0
- package/dist/application/transform/document/parameter-entries.mjs +30 -0
- package/dist/application/transform/document/parameter-entries.mjs.map +1 -0
- package/dist/application/transform/document/parameter.cjs +12 -4
- package/dist/application/transform/document/parameter.cjs.map +1 -1
- package/dist/application/transform/document/parameter.mjs +13 -5
- package/dist/application/transform/document/parameter.mjs.map +1 -1
- package/dist/application/transform/document/parameters.cjs +6 -29
- package/dist/application/transform/document/parameters.cjs.map +1 -1
- package/dist/application/transform/document/parameters.mjs +6 -29
- package/dist/application/transform/document/parameters.mjs.map +1 -1
- package/dist/application/transform/document/path-item.cjs +11 -14
- package/dist/application/transform/document/path-item.cjs.map +1 -1
- package/dist/application/transform/document/path-item.mjs +12 -16
- package/dist/application/transform/document/path-item.mjs.map +1 -1
- package/dist/application/transform/document/paths-enum.cjs +1 -0
- package/dist/application/transform/document/paths-enum.cjs.map +1 -1
- package/dist/application/transform/document/paths-enum.mjs +1 -1
- package/dist/application/transform/document/paths-enum.mjs.map +1 -1
- package/dist/application/transform/document/paths.cjs +5 -1
- package/dist/application/transform/document/paths.cjs.map +1 -1
- package/dist/application/transform/document/paths.mjs +6 -2
- package/dist/application/transform/document/paths.mjs.map +1 -1
- package/dist/application/transform/document/referenced-parameter.cjs +80 -0
- package/dist/application/transform/document/referenced-parameter.cjs.map +1 -0
- package/dist/application/transform/document/referenced-parameter.mjs +78 -0
- package/dist/application/transform/document/referenced-parameter.mjs.map +1 -0
- package/dist/application/transform/document/request-body.cjs +1 -1
- package/dist/application/transform/document/request-body.cjs.map +1 -1
- package/dist/application/transform/document/request-body.mjs +2 -2
- package/dist/application/transform/document/request-body.mjs.map +1 -1
- package/dist/application/transform/document/response.cjs +2 -2
- package/dist/application/transform/document/response.cjs.map +1 -1
- package/dist/application/transform/document/response.mjs +3 -3
- package/dist/application/transform/document/response.mjs.map +1 -1
- package/dist/application/transform/document/responses.cjs +1 -1
- package/dist/application/transform/document/responses.cjs.map +1 -1
- package/dist/application/transform/document/responses.mjs +2 -2
- package/dist/application/transform/document/responses.mjs.map +1 -1
- package/dist/application/transform/document/root-type-aliases.cjs +8 -23
- package/dist/application/transform/document/root-type-aliases.cjs.map +1 -1
- package/dist/application/transform/document/root-type-aliases.mjs +8 -24
- package/dist/application/transform/document/root-type-aliases.mjs.map +1 -1
- package/dist/application/transform/document/type-resolution.cjs +3 -1
- package/dist/application/transform/document/type-resolution.cjs.map +1 -1
- package/dist/application/transform/document/type-resolution.mjs +3 -1
- package/dist/application/transform/document/type-resolution.mjs.map +1 -1
- package/dist/application/transform/document/webhooks.cjs +1 -1
- package/dist/application/transform/document/webhooks.cjs.map +1 -1
- package/dist/application/transform/document/webhooks.mjs +2 -2
- package/dist/application/transform/document/webhooks.mjs.map +1 -1
- package/dist/application/transform/footer/document-footer.cjs +10 -2
- package/dist/application/transform/footer/document-footer.cjs.map +1 -1
- package/dist/application/transform/footer/document-footer.mjs +10 -2
- package/dist/application/transform/footer/document-footer.mjs.map +1 -1
- package/dist/application/transform/footer/enum-name-claims.cjs +48 -0
- package/dist/application/transform/footer/enum-name-claims.cjs.map +1 -0
- package/dist/application/transform/footer/enum-name-claims.mjs +46 -0
- package/dist/application/transform/footer/enum-name-claims.mjs.map +1 -0
- package/dist/application/transform/footer/operation-keys.cjs +23 -0
- package/dist/application/transform/footer/operation-keys.cjs.map +1 -0
- package/dist/application/transform/footer/operation-keys.mjs +21 -0
- package/dist/application/transform/footer/operation-keys.mjs.map +1 -0
- package/dist/application/transform/footer/type-evaluation.cjs +4 -4
- package/dist/application/transform/footer/type-evaluation.cjs.map +1 -1
- package/dist/application/transform/footer/type-evaluation.mjs +4 -4
- package/dist/application/transform/footer/type-evaluation.mjs.map +1 -1
- package/dist/application/transform/helper-types.cjs +1 -1
- package/dist/application/transform/helper-types.cjs.map +1 -1
- package/dist/application/transform/helper-types.mjs +1 -1
- package/dist/application/transform/helper-types.mjs.map +1 -1
- package/dist/application/transform/numbered-names.cjs +9 -0
- package/dist/application/transform/numbered-names.cjs.map +1 -1
- package/dist/application/transform/numbered-names.mjs +9 -1
- package/dist/application/transform/numbered-names.mjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-location.cjs +1 -1
- package/dist/application/transform/ref-pointer/generated-location.cjs.map +1 -1
- package/dist/application/transform/ref-pointer/generated-location.mjs +2 -2
- package/dist/application/transform/ref-pointer/generated-location.mjs.map +1 -1
- package/dist/application/transform/ref-type.cjs +2 -1
- package/dist/application/transform/ref-type.cjs.map +1 -1
- package/dist/application/transform/ref-type.mjs +2 -1
- package/dist/application/transform/ref-type.mjs.map +1 -1
- package/dist/application/transform/schema/composition-type.cjs +1 -1
- package/dist/application/transform/schema/composition-type.cjs.map +1 -1
- package/dist/application/transform/schema/composition-type.mjs +2 -2
- package/dist/application/transform/schema/composition-type.mjs.map +1 -1
- package/dist/application/transform/schema/enum-rule.cjs +1 -1
- package/dist/application/transform/schema/enum-rule.cjs.map +1 -1
- package/dist/application/transform/schema/enum-rule.mjs +2 -2
- package/dist/application/transform/schema/enum-rule.mjs.map +1 -1
- package/dist/application/transform/schema/object-type.cjs +3 -3
- package/dist/application/transform/schema/object-type.cjs.map +1 -1
- package/dist/application/transform/schema/object-type.mjs +4 -4
- package/dist/application/transform/schema/object-type.mjs.map +1 -1
- package/dist/application/transform/schema/ref-siblings.cjs +47 -0
- package/dist/application/transform/schema/ref-siblings.cjs.map +1 -0
- package/dist/application/transform/schema/ref-siblings.mjs +45 -0
- package/dist/application/transform/schema/ref-siblings.mjs.map +1 -0
- package/dist/application/transform/schema/schema-transform.cjs +4 -2
- package/dist/application/transform/schema/schema-transform.cjs.map +1 -1
- package/dist/application/transform/schema/schema-transform.mjs +4 -2
- package/dist/application/transform/schema/schema-transform.mjs.map +1 -1
- package/dist/application/transform/schema-comment.cjs +5 -1
- package/dist/application/transform/schema-comment.cjs.map +1 -1
- package/dist/application/transform/schema-comment.mjs +5 -1
- package/dist/application/transform/schema-comment.mjs.map +1 -1
- package/dist/cli/api-settings.cjs +62 -6
- package/dist/cli/api-settings.cjs.map +1 -1
- package/dist/cli/api-settings.mjs +60 -7
- package/dist/cli/api-settings.mjs.map +1 -1
- package/dist/cli/api-writes.cjs +72 -0
- package/dist/cli/api-writes.cjs.map +1 -0
- package/dist/cli/api-writes.mjs +69 -0
- package/dist/cli/api-writes.mjs.map +1 -0
- package/dist/cli/cli-errors.cjs +12 -1
- package/dist/cli/cli-errors.cjs.map +1 -1
- package/dist/cli/cli-errors.mjs +12 -1
- package/dist/cli/cli-errors.mjs.map +1 -1
- package/dist/cli/cli-output.cjs +106 -1
- package/dist/cli/cli-output.cjs.map +1 -1
- package/dist/cli/cli-output.mjs +99 -2
- package/dist/cli/cli-output.mjs.map +1 -1
- package/dist/cli/main.cjs +5 -1
- package/dist/cli/main.cjs.map +1 -1
- package/dist/cli/main.mjs +6 -2
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/redocly-apis.cjs +82 -36
- package/dist/cli/redocly-apis.cjs.map +1 -1
- package/dist/cli/redocly-apis.mjs +85 -39
- package/dist/cli/redocly-apis.mjs.map +1 -1
- package/dist/cli/run-cli.cjs +13 -7
- package/dist/cli/run-cli.cjs.map +1 -1
- package/dist/cli/run-cli.mjs +14 -8
- package/dist/cli/run-cli.mjs.map +1 -1
- package/dist/domain/append-all.cjs +10 -0
- package/dist/domain/append-all.cjs.map +1 -0
- package/dist/domain/append-all.mjs +8 -0
- package/dist/domain/append-all.mjs.map +1 -0
- package/dist/domain/cause-secrets.cjs +37 -0
- package/dist/domain/cause-secrets.cjs.map +1 -0
- package/dist/domain/cause-secrets.mjs +35 -0
- package/dist/domain/cause-secrets.mjs.map +1 -0
- package/dist/domain/errors.cjs +9 -3
- package/dist/domain/errors.cjs.map +1 -1
- package/dist/domain/errors.mjs +9 -3
- package/dist/domain/errors.mjs.map +1 -1
- package/dist/domain/ir/identifier-text.cjs.map +1 -1
- package/dist/domain/ir/identifier-text.mjs +1 -1
- package/dist/domain/ir/identifier-text.mjs.map +1 -1
- package/dist/domain/ir/ir-shape.cjs +187 -0
- package/dist/domain/ir/ir-shape.cjs.map +1 -0
- package/dist/domain/ir/ir-shape.mjs +184 -0
- package/dist/domain/ir/ir-shape.mjs.map +1 -0
- package/dist/domain/ir/newer-identifier-ranges.cjs +182 -29
- package/dist/domain/ir/newer-identifier-ranges.cjs.map +1 -1
- package/dist/domain/ir/newer-identifier-ranges.mjs +182 -29
- package/dist/domain/ir/newer-identifier-ranges.mjs.map +1 -1
- package/dist/domain/openapi/alias-expansion.cjs +3 -1
- package/dist/domain/openapi/alias-expansion.cjs.map +1 -1
- package/dist/domain/openapi/alias-expansion.mjs +3 -1
- package/dist/domain/openapi/alias-expansion.mjs.map +1 -1
- package/dist/domain/openapi/discriminated-union.cjs +3 -3
- package/dist/domain/openapi/discriminated-union.cjs.map +1 -1
- package/dist/domain/openapi/discriminated-union.mjs +4 -4
- package/dist/domain/openapi/discriminated-union.mjs.map +1 -1
- package/dist/domain/openapi/discriminator-conflicts.cjs +10 -6
- package/dist/domain/openapi/discriminator-conflicts.cjs.map +1 -1
- package/dist/domain/openapi/discriminator-conflicts.mjs +11 -7
- package/dist/domain/openapi/discriminator-conflicts.mjs.map +1 -1
- package/dist/domain/openapi/json-pointer.cjs +43 -1
- package/dist/domain/openapi/json-pointer.cjs.map +1 -1
- package/dist/domain/openapi/json-pointer.mjs +40 -2
- package/dist/domain/openapi/json-pointer.mjs.map +1 -1
- package/dist/domain/openapi/schema-dialect.cjs +15 -0
- package/dist/domain/openapi/schema-dialect.cjs.map +1 -0
- package/dist/domain/openapi/schema-dialect.mjs +12 -0
- package/dist/domain/openapi/schema-dialect.mjs.map +1 -0
- package/dist/domain/openapi/unsupported-features.cjs +1 -1
- package/dist/domain/openapi/unsupported-features.cjs.map +1 -1
- package/dist/domain/openapi/unsupported-features.mjs +2 -2
- package/dist/domain/openapi/unsupported-features.mjs.map +1 -1
- package/dist/domain/output-fixes.cjs +14 -0
- package/dist/domain/output-fixes.cjs.map +1 -1
- package/dist/domain/output-fixes.mjs +14 -0
- package/dist/domain/output-fixes.mjs.map +1 -1
- package/dist/domain/url-secrets.cjs +48 -0
- package/dist/domain/url-secrets.cjs.map +1 -0
- package/dist/domain/url-secrets.mjs +45 -0
- package/dist/domain/url-secrets.mjs.map +1 -0
- package/dist/index.d.cts +22 -4
- package/dist/index.d.mts +22 -4
- package/dist/index.d.ts +22 -4
- package/package.json +4 -5
- package/src/adapters/node-file-system/file-claims.ts +138 -0
- package/src/adapters/node-file-system/file-identity.ts +137 -0
- package/src/adapters/node-file-system/node-file-system.ts +145 -18
- package/src/adapters/redocly-loader/bundled-mapping-copies.ts +95 -0
- package/src/adapters/redocly-loader/bundled-ref-repair.ts +2 -2
- package/src/adapters/redocly-loader/loaded-documents.ts +46 -0
- package/src/adapters/redocly-loader/redocly-schema-loader.ts +18 -24
- package/src/adapters/redocly-loader/ref-access.ts +37 -3
- package/src/adapters/redocly-loader/ref-holders.ts +3 -3
- package/src/adapters/redocly-loader/remote-body-limit.ts +66 -0
- package/src/adapters/redocly-loader/remote-fetch.ts +74 -11
- package/src/adapters/redocly-loader/remote-node-limit.ts +62 -0
- package/src/adapters/redocly-loader/schema-load-errors.ts +52 -13
- package/src/adapters/redocly-loader/schema-source.ts +20 -8
- package/src/adapters/redocly-loader/unsupported-keys.ts +3 -7
- package/src/adapters/terminal-logger/terminal-logger.ts +10 -5
- package/src/api/default-ports.ts +33 -4
- package/src/api/discriminator-enum-parent.ts +3 -3
- package/src/api/discriminator-other-owners.ts +5 -3
- package/src/api/discriminator-patch.ts +5 -1
- package/src/api/discriminator-scan.ts +16 -8
- package/src/api/document-run.ts +4 -3
- package/src/api/legacy-utils.ts +5 -1
- package/src/api/openapi-ts.ts +20 -5
- package/src/api/run-context.ts +3 -0
- package/src/api/schema-callbacks.ts +60 -17
- package/src/api/transform-context.ts +7 -3
- package/src/api/transform-schema.ts +64 -12
- package/src/application/ports/file-system.ts +39 -0
- package/src/application/ports/schema-loader.ts +11 -1
- package/src/application/print/text-writer.ts +10 -3
- package/src/application/stack-overflow.ts +9 -5
- package/src/application/transform/document/components.ts +2 -2
- package/src/application/transform/document/declared-type-names.ts +36 -0
- package/src/application/transform/document/defs-root.ts +2 -2
- package/src/application/transform/document/document.ts +4 -3
- package/src/application/transform/document/enum-names-given-back.ts +91 -0
- package/src/application/transform/document/global-type-references.ts +1 -1
- package/src/application/transform/document/media-type.ts +4 -3
- package/src/application/transform/document/operation-keys-written.ts +27 -0
- package/src/application/transform/document/operation.ts +2 -2
- package/src/application/transform/document/parameter-entries.ts +70 -0
- package/src/application/transform/document/parameter.ts +21 -9
- package/src/application/transform/document/parameters.ts +11 -56
- package/src/application/transform/document/path-item.ts +13 -31
- package/src/application/transform/document/paths-enum.ts +1 -1
- package/src/application/transform/document/paths.ts +6 -2
- package/src/application/transform/document/referenced-parameter.ts +146 -0
- package/src/application/transform/document/request-body.ts +2 -2
- package/src/application/transform/document/response.ts +3 -3
- package/src/application/transform/document/responses.ts +2 -2
- package/src/application/transform/document/root-type-aliases.ts +17 -38
- package/src/application/transform/document/type-resolution.ts +2 -1
- package/src/application/transform/document/webhooks.ts +2 -2
- package/src/application/transform/footer/document-footer.ts +18 -1
- package/src/application/transform/footer/enum-name-claims.ts +62 -0
- package/src/application/transform/footer/operation-keys.ts +28 -0
- package/src/application/transform/footer/type-evaluation.ts +4 -4
- package/src/application/transform/helper-types.ts +6 -5
- package/src/application/transform/numbered-names.ts +15 -0
- package/src/application/transform/ref-pointer/generated-location.ts +2 -2
- package/src/application/transform/ref-type.ts +2 -1
- package/src/application/transform/schema/composition-type.ts +2 -2
- package/src/application/transform/schema/enum-rule.ts +6 -2
- package/src/application/transform/schema/object-type.ts +4 -4
- package/src/application/transform/schema/ref-siblings.ts +76 -0
- package/src/application/transform/schema/schema-context.ts +3 -0
- package/src/application/transform/schema/schema-transform.ts +4 -2
- package/src/application/transform/schema-comment.ts +5 -1
- package/src/application/transform/transform-context.ts +5 -0
- package/src/cli/api-settings.ts +82 -7
- package/src/cli/api-writes.ts +117 -0
- package/src/cli/cli-dependencies.ts +21 -4
- package/src/cli/cli-errors.ts +20 -1
- package/src/cli/cli-output.ts +186 -7
- package/src/cli/main.ts +6 -3
- package/src/cli/redocly-apis.ts +153 -48
- package/src/cli/run-cli.ts +37 -8
- package/src/domain/append-all.ts +10 -0
- package/src/domain/cause-secrets.ts +52 -0
- package/src/domain/errors.ts +14 -5
- package/src/domain/ir/identifier-text.ts +6 -5
- package/src/domain/ir/ir-shape.ts +299 -0
- package/src/domain/ir/newer-identifier-ranges.ts +183 -30
- package/src/domain/openapi/alias-expansion.ts +4 -1
- package/src/domain/openapi/discriminated-union.ts +6 -4
- package/src/domain/openapi/discriminator-conflicts.ts +21 -7
- package/src/domain/openapi/document-object.ts +2 -0
- package/src/domain/openapi/json-pointer.ts +96 -7
- package/src/domain/openapi/schema-dialect.ts +22 -0
- package/src/domain/openapi/unsupported-features.ts +2 -2
- package/src/domain/output-fixes.ts +21 -2
- package/src/domain/url-secrets.ts +72 -0
- package/src/types/generator-options.ts +1 -1
- package/dist/cli/main.d.cts +0 -2
- package/dist/cli/main.d.mts +0 -2
- package/dist/cli/main.d.ts +0 -2
- package/dist/domain/url-credentials.cjs +0 -10
- package/dist/domain/url-credentials.cjs.map +0 -1
- package/dist/domain/url-credentials.mjs +0 -8
- package/dist/domain/url-credentials.mjs.map +0 -1
- package/src/domain/url-credentials.ts +0 -18
|
@@ -8,11 +8,17 @@ import {
|
|
|
8
8
|
type Config as RedoclyConfig,
|
|
9
9
|
} from "@redocly/openapi-core";
|
|
10
10
|
import type { Logger } from "../../application/ports/logger.js";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
LoadedSchema,
|
|
13
|
+
SchemaLoader,
|
|
14
|
+
SchemaLoadRequest,
|
|
15
|
+
} from "../../application/ports/schema-loader.js";
|
|
12
16
|
import { withStackOverflowReportedAsync } from "../../application/stack-overflow.js";
|
|
13
17
|
import { WARNING_CODES, warningText } from "../../domain/warnings.js";
|
|
14
18
|
import type { OpenAPI3 } from "../../types.js";
|
|
19
|
+
import { removeBundledMappingCopies, schemaKeysOf } from "./bundled-mapping-copies.js";
|
|
15
20
|
import { repairBundledRefs } from "./bundled-ref-repair.js";
|
|
21
|
+
import { loadedDocuments, localFilesRead } from "./loaded-documents.js";
|
|
16
22
|
import { findMalformedRefs, malformedRefsError } from "./malformed-refs.js";
|
|
17
23
|
import { RefAccessResolver } from "./ref-access.js";
|
|
18
24
|
import { checkRefsResolve } from "./ref-resolution-check.js";
|
|
@@ -25,11 +31,7 @@ import {
|
|
|
25
31
|
versionUnsupportedError,
|
|
26
32
|
} from "./schema-load-errors.js";
|
|
27
33
|
import { parseSchema } from "./schema-source.js";
|
|
28
|
-
import {
|
|
29
|
-
findUnsupportedKeys,
|
|
30
|
-
type LoadedDocument,
|
|
31
|
-
unsupportedKeysError,
|
|
32
|
-
} from "./unsupported-keys.js";
|
|
34
|
+
import { findUnsupportedKeys, unsupportedKeysError } from "./unsupported-keys.js";
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* The SchemaLoader port on Redocly 1.34 (ADR-005): parse any inherited source type, then lint and
|
|
@@ -107,21 +109,6 @@ function sourceFilePath(source: URL): string {
|
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
/**
|
|
111
|
-
* The root document and every other document the resolver loaded, as far as they parsed. A root
|
|
112
|
-
* read from a file or URL is in the resolver cache too, and is listed once.
|
|
113
|
-
*/
|
|
114
|
-
async function loadedDocuments(root: Document, resolver: BaseResolver): Promise<LoadedDocument[]> {
|
|
115
|
-
const cached = await Promise.all(resolver.cache.values());
|
|
116
|
-
const bySource = new Map<string, unknown>([[root.source.absoluteRef, root.parsed]]);
|
|
117
|
-
for (const document of cached) {
|
|
118
|
-
if ("parsed" in document && !bySource.has(document.source.absoluteRef)) {
|
|
119
|
-
bySource.set(document.source.absoluteRef, document.parsed);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return [...bySource].map(([source, parsed]) => ({ source, parsed }));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
112
|
/** Report the document problems Redocly throws as typed errors; anything else passes through. */
|
|
126
113
|
async function rethrowDocumentError(
|
|
127
114
|
error: unknown,
|
|
@@ -148,7 +135,7 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
148
135
|
this.#options = options;
|
|
149
136
|
}
|
|
150
137
|
|
|
151
|
-
async load(request: SchemaLoadRequest): Promise<
|
|
138
|
+
async load(request: SchemaLoadRequest): Promise<LoadedSchema> {
|
|
152
139
|
const resolver = this.#resolver(request);
|
|
153
140
|
// Parsing is guarded too: `JSON.stringify` of a deep object source overflows the stack on
|
|
154
141
|
// Node.js 22 and 24, before linting starts.
|
|
@@ -164,7 +151,7 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
164
151
|
return bundled;
|
|
165
152
|
}
|
|
166
153
|
|
|
167
|
-
async #parseAndBundle(request: SchemaLoadRequest, resolver: BaseResolver): Promise<
|
|
154
|
+
async #parseAndBundle(request: SchemaLoadRequest, resolver: BaseResolver): Promise<LoadedSchema> {
|
|
168
155
|
const { logger, now } = this.#options;
|
|
169
156
|
const configLoadedAt = now();
|
|
170
157
|
logger.debug("Loaded Redoc config", "redoc", now() - configLoadedAt);
|
|
@@ -173,7 +160,10 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
173
160
|
const document = await parseSchema(request.source, { absoluteRef, resolver });
|
|
174
161
|
logger.debug("Parsed schema", "redoc", now() - parseStartedAt);
|
|
175
162
|
assertOpenAPI3(document.parsed);
|
|
176
|
-
|
|
163
|
+
const bundled = await this.#lintAndBundle(document, resolver, request.shouldLint);
|
|
164
|
+
const isRootAFile = request.source instanceof URL && request.source.protocol === "file:";
|
|
165
|
+
const localFiles = await localFilesRead(document, resolver, isRootAFile);
|
|
166
|
+
return { document: bundled, localFiles };
|
|
177
167
|
}
|
|
178
168
|
|
|
179
169
|
#resolver(request: SchemaLoadRequest): RefAccessResolver {
|
|
@@ -182,10 +172,12 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
182
172
|
timeoutMs: remoteRefTimeoutMs,
|
|
183
173
|
access: request.refAccess,
|
|
184
174
|
maxParallelFetches: remoteDocumentLimits.maxParallelFetches,
|
|
175
|
+
bodyLimits: remoteDocumentLimits,
|
|
185
176
|
});
|
|
186
177
|
return new RefAccessResolver(config, {
|
|
187
178
|
access: request.refAccess,
|
|
188
179
|
maxRemoteDocuments: remoteDocumentLimits.maxDocuments,
|
|
180
|
+
maxRemoteNodes: remoteDocumentLimits.maxNodes,
|
|
189
181
|
});
|
|
190
182
|
}
|
|
191
183
|
|
|
@@ -216,6 +208,7 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
216
208
|
|
|
217
209
|
async #bundle(document: Document, resolver: BaseResolver): Promise<OpenAPI3> {
|
|
218
210
|
const startedAt = this.#options.now();
|
|
211
|
+
const schemaKeys = schemaKeysOf(document.parsed);
|
|
219
212
|
// The lint resolver: documents it already loaded are not fetched again.
|
|
220
213
|
const bundled = await bundle({
|
|
221
214
|
config: this.#options.redoc,
|
|
@@ -224,6 +217,7 @@ class RedoclySchemaLoader implements SchemaLoader {
|
|
|
224
217
|
externalRefResolver: resolver,
|
|
225
218
|
});
|
|
226
219
|
this.#report(bundled.problems);
|
|
220
|
+
removeBundledMappingCopies(bundled.bundle.parsed, schemaKeys);
|
|
227
221
|
repairBundledRefs(bundled.bundle.parsed);
|
|
228
222
|
this.#options.logger.debug("Bundled schema", "bundle", this.#options.now() - startedAt);
|
|
229
223
|
return bundled.bundle.parsed;
|
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
} from "@redocly/openapi-core";
|
|
11
11
|
import type { RefAccess } from "../../application/ports/schema-loader.js";
|
|
12
12
|
import { ERROR_CODES, type OpenAPITypesafeError } from "../../domain/errors.js";
|
|
13
|
-
import {
|
|
13
|
+
import { redactUrlSecrets } from "../../domain/url-secrets.js";
|
|
14
14
|
import { aliasExpansionFailure } from "./alias-limit.js";
|
|
15
15
|
import { isHostAllowed } from "./allowed-hosts.js";
|
|
16
|
+
import { isParsedSource, RemoteNodeLimit } from "./remote-node-limit.js";
|
|
16
17
|
import { refForbiddenError } from "./schema-load-errors.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -92,15 +93,20 @@ function localFromRemoteRefusal(
|
|
|
92
93
|
if (!isRemoteRef(base) || isRemoteRef(absoluteRef)) {
|
|
93
94
|
return undefined;
|
|
94
95
|
}
|
|
95
|
-
const shownBase =
|
|
96
|
+
const shownBase = redactUrlSecrets(base);
|
|
96
97
|
return refForbiddenError(absoluteRef, { reason: "local-ref-from-remote", base: shownBase });
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
/** The scheme of an `http(s)` URL, in any case. */
|
|
101
|
+
const REMOTE_SCHEME_RE = /^https?:(?=\/\/)/i;
|
|
102
|
+
|
|
99
103
|
/** The limits one resolver checks; each load gets its own resolver. */
|
|
100
104
|
export interface RefAccessLimits {
|
|
101
105
|
readonly access: RefAccess;
|
|
102
106
|
/** Remote documents one load may fetch, the root included (threat model T2). */
|
|
103
107
|
readonly maxRemoteDocuments: number;
|
|
108
|
+
/** Nodes the remote documents of one load may parse into together (threat model T2). */
|
|
109
|
+
readonly maxRemoteNodes: number;
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
/**
|
|
@@ -110,6 +116,7 @@ export interface RefAccessLimits {
|
|
|
110
116
|
*/
|
|
111
117
|
export class RefAccessResolver extends BaseResolver {
|
|
112
118
|
readonly #limits: RefAccessLimits;
|
|
119
|
+
readonly #nodeLimit: RemoteNodeLimit;
|
|
113
120
|
readonly #refusals: OpenAPITypesafeError[] = [];
|
|
114
121
|
/** The root document's ref: it is chosen by the developer, so it is not limited. */
|
|
115
122
|
#rootRef: string | undefined;
|
|
@@ -119,6 +126,7 @@ export class RefAccessResolver extends BaseResolver {
|
|
|
119
126
|
constructor(config: ResolveConfig, limits: RefAccessLimits) {
|
|
120
127
|
super(config);
|
|
121
128
|
this.#limits = limits;
|
|
129
|
+
this.#nodeLimit = new RemoteNodeLimit(limits.maxRemoteNodes);
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
/** The first document that was refused, if any. */
|
|
@@ -126,6 +134,17 @@ export class RefAccessResolver extends BaseResolver {
|
|
|
126
134
|
return this.#refusals[0];
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
/**
|
|
138
|
+
* A scheme is the same in any case (RFC 3986, section 3.1), but Redocly takes only `http://` and
|
|
139
|
+
* `https://` as URLs: it read `HTTP://host/x.yaml` as a file path, and its "not found" message
|
|
140
|
+
* showed the user info and the query of the URL, which no longer looked like one. The scheme
|
|
141
|
+
* is written in lowercase first, so the `$ref` loads as the URL it is, within the same limits.
|
|
142
|
+
*/
|
|
143
|
+
override resolveExternalRef(base: string | null, ref: string): string {
|
|
144
|
+
const normalized = ref.replace(REMOTE_SCHEME_RE, (scheme) => scheme.toLowerCase());
|
|
145
|
+
return super.resolveExternalRef(base, normalized);
|
|
146
|
+
}
|
|
147
|
+
|
|
129
148
|
override resolveDocument(
|
|
130
149
|
base: string | null,
|
|
131
150
|
ref: string,
|
|
@@ -145,7 +164,7 @@ export class RefAccessResolver extends BaseResolver {
|
|
|
145
164
|
|
|
146
165
|
/** Refuse a YAML alias bomb before Redocly walks it (pre-beta test plan 10.2). */
|
|
147
166
|
override parseDocument(source: Source, isRoot = false): Document {
|
|
148
|
-
const document =
|
|
167
|
+
const document = this.#parsed(source, isRoot);
|
|
149
168
|
const failure = aliasExpansionFailure(document.parsed);
|
|
150
169
|
if (failure !== undefined) {
|
|
151
170
|
throw new YamlParseError(failure, source);
|
|
@@ -153,6 +172,21 @@ export class RefAccessResolver extends BaseResolver {
|
|
|
153
172
|
return document;
|
|
154
173
|
}
|
|
155
174
|
|
|
175
|
+
/** A remote document is parsed within the node limit; any other one as Redocly parses it. */
|
|
176
|
+
#parsed(source: Source, isRoot: boolean): Document {
|
|
177
|
+
if (!isRemoteRef(source.absoluteRef) || !isParsedSource(source, isRoot)) {
|
|
178
|
+
return super.parseDocument(source, isRoot);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
return this.#nodeLimit.parse(source);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (isRefusal(error)) {
|
|
184
|
+
this.#refusals.push(error);
|
|
185
|
+
}
|
|
186
|
+
throw new YamlParseError(error as Error, source);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
156
190
|
override async loadExternalRef(absoluteRef: string): Promise<Source> {
|
|
157
191
|
const refusal = this.#countRefusal(absoluteRef) ?? (await this.#accessRefusal(absoluteRef));
|
|
158
192
|
if (refusal !== undefined) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { childPointer, ROOT_POINTER } from "../../domain/openapi/json-pointer.js";
|
|
2
2
|
|
|
3
3
|
/** An object with a string `$ref`, and its JSON Pointer in the document. */
|
|
4
4
|
export interface RefHolder {
|
|
@@ -30,13 +30,13 @@ export function findRefHolders(document: unknown): RefHolder[] {
|
|
|
30
30
|
pending.push({ value, pointer });
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
-
enqueue(document,
|
|
33
|
+
enqueue(document, ROOT_POINTER);
|
|
34
34
|
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
35
35
|
if (hasStringRef(next.value)) {
|
|
36
36
|
found.push({ node: next.value, pointer: next.pointer });
|
|
37
37
|
}
|
|
38
38
|
for (const [key, child] of Object.entries(next.value)) {
|
|
39
|
-
enqueue(child,
|
|
39
|
+
enqueue(child, childPointer(next.pointer, key));
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
return found;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { OpenAPITypesafeError } from "../../domain/errors.js";
|
|
2
|
+
import { refForbiddenError } from "./schema-load-errors.js";
|
|
3
|
+
|
|
4
|
+
/** The decoded size limits of the remote documents of one load (threat model T2). */
|
|
5
|
+
export interface RemoteBodyLimits {
|
|
6
|
+
/** Bytes of one remote document's body after `Content-Encoding` is decoded. */
|
|
7
|
+
readonly maxDocumentBytes: number;
|
|
8
|
+
/** Bytes of the bodies of every remote document of one load together, decoded. */
|
|
9
|
+
readonly maxTotalBytes: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reads remote bodies as a stream and stops at the limits. Fetch decodes `gzip`, `deflate` and
|
|
14
|
+
* `br` before the body reaches the loader, so a body of a few megabytes on the wire can become
|
|
15
|
+
* gigabytes in memory (a "gzip bomb"); counting the decoded bytes while they arrive stops it before
|
|
16
|
+
* the memory is taken. One instance per load: the total covers every document of that load.
|
|
17
|
+
*/
|
|
18
|
+
export class RemoteBodyLimit {
|
|
19
|
+
readonly #limits: RemoteBodyLimits;
|
|
20
|
+
#totalBytes = 0;
|
|
21
|
+
|
|
22
|
+
constructor(limits: RemoteBodyLimits) {
|
|
23
|
+
this.#limits = limits;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The whole body of `response` from `url`; fails with `SCHEMA_REF_FORBIDDEN` past a limit. */
|
|
27
|
+
async read(response: Response, url: string): Promise<ArrayBuffer> {
|
|
28
|
+
const reader = response.body?.getReader();
|
|
29
|
+
if (reader === undefined) {
|
|
30
|
+
return new ArrayBuffer(0);
|
|
31
|
+
}
|
|
32
|
+
const chunks: Uint8Array[] = [];
|
|
33
|
+
let documentBytes = 0;
|
|
34
|
+
for (let next = await reader.read(); !next.done; next = await reader.read()) {
|
|
35
|
+
documentBytes += next.value.byteLength;
|
|
36
|
+
this.#totalBytes += next.value.byteLength;
|
|
37
|
+
const refusal = this.#refusal(url, documentBytes);
|
|
38
|
+
if (refusal !== undefined) {
|
|
39
|
+
await reader.cancel();
|
|
40
|
+
throw refusal;
|
|
41
|
+
}
|
|
42
|
+
chunks.push(next.value);
|
|
43
|
+
}
|
|
44
|
+
return joined(chunks, documentBytes);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#refusal(url: string, documentBytes: number): OpenAPITypesafeError | undefined {
|
|
48
|
+
const { maxDocumentBytes, maxTotalBytes } = this.#limits;
|
|
49
|
+
if (documentBytes > maxDocumentBytes) {
|
|
50
|
+
return refForbiddenError(url, { reason: "remote-document-too-large", maxDocumentBytes });
|
|
51
|
+
}
|
|
52
|
+
return this.#totalBytes > maxTotalBytes
|
|
53
|
+
? refForbiddenError(url, { reason: "remote-documents-too-large", maxTotalBytes })
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function joined(chunks: readonly Uint8Array[], byteLength: number): ArrayBuffer {
|
|
59
|
+
const body = new Uint8Array(byteLength);
|
|
60
|
+
let offset = 0;
|
|
61
|
+
for (const chunk of chunks) {
|
|
62
|
+
body.set(chunk, offset);
|
|
63
|
+
offset += chunk.byteLength;
|
|
64
|
+
}
|
|
65
|
+
return body.buffer;
|
|
66
|
+
}
|
|
@@ -2,11 +2,12 @@ import type { Config as RedoclyConfig } from "@redocly/openapi-core";
|
|
|
2
2
|
import type { RefAccess } from "../../application/ports/schema-loader.js";
|
|
3
3
|
import { isHostAllowed } from "./allowed-hosts.js";
|
|
4
4
|
import { type ConfiguredHeader, headersFor } from "./configured-headers.js";
|
|
5
|
+
import { RemoteBodyLimit, type RemoteBodyLimits } from "./remote-body-limit.js";
|
|
5
6
|
import { refForbiddenError } from "./schema-load-errors.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* How the loader fetches remote documents: a time limit, the host limit and the
|
|
9
|
-
* checked on every redirect.
|
|
9
|
+
* How the loader fetches remote documents: a time limit, size limits, the host limit and the
|
|
10
|
+
* configured headers checked on every redirect.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
type ResolveConfig = RedoclyConfig["resolve"];
|
|
@@ -25,9 +26,16 @@ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
|
25
26
|
/** Statuses whose responses have no body; `new Response()` refuses a body for them. */
|
|
26
27
|
const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* A response whose body is already read within the size limits, so that reading it again cannot
|
|
31
|
+
* wait, fail or take more memory.
|
|
32
|
+
*/
|
|
33
|
+
async function bufferedResponse(
|
|
34
|
+
response: Response,
|
|
35
|
+
url: string,
|
|
36
|
+
bodyLimit: RemoteBodyLimit,
|
|
37
|
+
): Promise<Response> {
|
|
38
|
+
const body = await bodyLimit.read(response, url);
|
|
31
39
|
const { status, statusText, headers } = response;
|
|
32
40
|
return new Response(NULL_BODY_STATUSES.has(status) ? null : body, {
|
|
33
41
|
status,
|
|
@@ -36,6 +44,45 @@ async function bufferedResponse(response: Response): Promise<Response> {
|
|
|
36
44
|
});
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Codes of a connection that closed before any response came. The server can close a kept-alive
|
|
49
|
+
* connection while the process is busy (parsing a large document, generating another API) and
|
|
50
|
+
* has not read the close yet; the next request on that connection then fails with one of these.
|
|
51
|
+
*/
|
|
52
|
+
const CLOSED_CONNECTION_CODES: ReadonlySet<string> = new Set([
|
|
53
|
+
"ECONNRESET",
|
|
54
|
+
"EPIPE",
|
|
55
|
+
"UND_ERR_SOCKET",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/** Methods that may be sent again without a second effect (RFC 9110 §9.2.2). */
|
|
59
|
+
const IDEMPOTENT_METHODS: ReadonlySet<string> = new Set(["GET", "HEAD"]);
|
|
60
|
+
const DEFAULT_METHOD = "GET";
|
|
61
|
+
|
|
62
|
+
/** Whether fetch failed because its connection closed before any response came. */
|
|
63
|
+
function isClosedConnection(error: unknown): boolean {
|
|
64
|
+
const cause = error instanceof TypeError ? error.cause : undefined;
|
|
65
|
+
const code = cause instanceof Error && "code" in cause ? cause.code : undefined;
|
|
66
|
+
return typeof code === "string" && CLOSED_CONNECTION_CODES.has(code);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fetch, and send the request once more when its connection closed before any response came, as
|
|
71
|
+
* RFC 9112 §9.3.1 allows for an idempotent request. Fetch drops a connection that failed, so the
|
|
72
|
+
* second attempt goes out on another one; a second failure is the server's answer and is thrown.
|
|
73
|
+
*/
|
|
74
|
+
async function fetchOnOpenConnection(input: string, init: RequestInit): Promise<Response> {
|
|
75
|
+
try {
|
|
76
|
+
return await fetch(input, init);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const method = (init.method ?? DEFAULT_METHOD).toUpperCase();
|
|
79
|
+
if (!(IDEMPOTENT_METHODS.has(method) && isClosedConnection(error))) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return fetch(input, init);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
39
86
|
/**
|
|
40
87
|
* Fetch with a time limit for the whole response, body included: Node's own fetch waits five
|
|
41
88
|
* minutes for a server that never answers, and a server that sends its body one byte at a time
|
|
@@ -43,11 +90,13 @@ async function bufferedResponse(response: Response): Promise<Response> {
|
|
|
43
90
|
* surfaced as a `DOMException` whose read-only `message` Redocly tried to change, which gave
|
|
44
91
|
* "Cannot set property message of which has only a getter" instead of the time limit.
|
|
45
92
|
*/
|
|
46
|
-
function fetchWithTimeout(timeoutMs: number): RemoteFetch {
|
|
93
|
+
function fetchWithTimeout(timeoutMs: number, bodyLimit: RemoteBodyLimit): RemoteFetch {
|
|
47
94
|
return async (input, init = {}) => {
|
|
48
95
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
96
|
+
// One signal for both attempts, so that a request sent again stays within the same limit.
|
|
97
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
98
|
+
const response = await fetchOnOpenConnection(input, { ...init, signal });
|
|
99
|
+
return await bufferedResponse(response, input, bodyLimit);
|
|
51
100
|
} catch (cause) {
|
|
52
101
|
const isTimeout = cause instanceof Error && cause.name === "TimeoutError";
|
|
53
102
|
throw isTimeout ? new Error(`Timed out after ${timeoutMs} ms loading ${input}`) : cause;
|
|
@@ -55,6 +104,11 @@ function fetchWithTimeout(timeoutMs: number): RemoteFetch {
|
|
|
55
104
|
};
|
|
56
105
|
}
|
|
57
106
|
|
|
107
|
+
/** A configured `customFetch` with its bodies read within the size limits. */
|
|
108
|
+
function fetchWithBodyLimit(fetchImpl: RemoteFetch, bodyLimit: RemoteBodyLimit): RemoteFetch {
|
|
109
|
+
return async (input, init) => bufferedResponse(await fetchImpl(input, init), input, bodyLimit);
|
|
110
|
+
}
|
|
111
|
+
|
|
58
112
|
/** What every hop of a remote fetch is checked against. */
|
|
59
113
|
export interface RedirectPolicy {
|
|
60
114
|
/** `resolve.http.headers` of the Redocly config; chosen again for the URL of every hop. */
|
|
@@ -97,11 +151,13 @@ export function fetchWithCheckedRedirects(
|
|
|
97
151
|
}
|
|
98
152
|
|
|
99
153
|
/** Bounds on the remote documents of one load (threat model T2, ADR-033). */
|
|
100
|
-
export interface RemoteDocumentLimits {
|
|
154
|
+
export interface RemoteDocumentLimits extends RemoteBodyLimits {
|
|
101
155
|
/** Remote documents one load may fetch, the root included; one more is refused. */
|
|
102
156
|
readonly maxDocuments: number;
|
|
103
157
|
/** Remote documents fetched at the same time; the others wait for a free slot. */
|
|
104
158
|
readonly maxParallelFetches: number;
|
|
159
|
+
/** Nodes (keys and values) the remote documents of one load may parse into together. */
|
|
160
|
+
readonly maxNodes: number;
|
|
105
161
|
}
|
|
106
162
|
|
|
107
163
|
/**
|
|
@@ -135,11 +191,13 @@ export interface RemoteFetchOptions {
|
|
|
135
191
|
readonly timeoutMs: number;
|
|
136
192
|
readonly access: RefAccess;
|
|
137
193
|
readonly maxParallelFetches: number;
|
|
194
|
+
readonly bodyLimits: RemoteBodyLimits;
|
|
138
195
|
}
|
|
139
196
|
|
|
140
197
|
/**
|
|
141
198
|
* The resolve config with the fetch the loader uses. A `customFetch` of the Redocly config is
|
|
142
|
-
* kept and wrapped, so that each redirect is checked
|
|
199
|
+
* kept and wrapped, so that each redirect is checked, each body is read within the size limits
|
|
200
|
+
* and only a few documents load at once. The
|
|
143
201
|
* configured headers move into that wrapper: Redocly would match them against the raw URL and
|
|
144
202
|
* send them on through redirects.
|
|
145
203
|
*/
|
|
@@ -149,7 +207,12 @@ export function resolveConfigWithAccess(
|
|
|
149
207
|
): ResolveConfig {
|
|
150
208
|
// Redocly types `customFetch` as `Function`; it is called as fetch is.
|
|
151
209
|
const configured = resolve.http.customFetch as RemoteFetch | undefined;
|
|
152
|
-
|
|
210
|
+
// One limit per load, so that the total covers every document this resolver fetches.
|
|
211
|
+
const bodyLimit = new RemoteBodyLimit(options.bodyLimits);
|
|
212
|
+
const base =
|
|
213
|
+
configured === undefined
|
|
214
|
+
? fetchWithTimeout(options.timeoutMs, bodyLimit)
|
|
215
|
+
: fetchWithBodyLimit(configured, bodyLimit);
|
|
153
216
|
const checked = fetchWithCheckedRedirects(base, {
|
|
154
217
|
headers: resolve.http.headers,
|
|
155
218
|
hosts: options.access.remoteHosts,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Document, parseYaml, type Source } from "@redocly/openapi-core";
|
|
2
|
+
import type { OpenAPITypesafeError } from "../../domain/errors.js";
|
|
3
|
+
import { refForbiddenError } from "./schema-load-errors.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bounds the parsed size of the remote documents of one load (threat model T2). The byte limits
|
|
7
|
+
* do not bound memory: parsing multiplies it by the density of the text, and 250 MiB of `{},`
|
|
8
|
+
* (87 million empty objects, under a quarter of a megabyte as gzip) took the process past its
|
|
9
|
+
* 4 GB heap. The parser reports every node it builds, keys included, and the parse stops when
|
|
10
|
+
* the nodes of all remote documents of the load pass the limit, before their memory is taken.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The extensions that Redocly 1.34's `BaseResolver.parseDocument` parses as YAML or JSON. */
|
|
14
|
+
const PARSED_EXTENSIONS: ReadonlySet<string> = new Set([".json", ".yml", ".yaml"]);
|
|
15
|
+
const PARSED_MIME_TYPE = /(json|yaml|openapi)/;
|
|
16
|
+
|
|
17
|
+
/** Whether Redocly parses this source, or keeps its text: the same rule as its own resolver. */
|
|
18
|
+
export function isParsedSource(source: Source, isRoot: boolean): boolean {
|
|
19
|
+
const { absoluteRef, mimeType } = source;
|
|
20
|
+
const extension = absoluteRef.slice(absoluteRef.lastIndexOf("."));
|
|
21
|
+
return isRoot || PARSED_EXTENSIONS.has(extension) || PARSED_MIME_TYPE.test(mimeType ?? "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The parser's event for a node it starts to build. */
|
|
25
|
+
const NODE_OPENED = "open";
|
|
26
|
+
|
|
27
|
+
/** One instance per load: the count covers every remote document of that load. */
|
|
28
|
+
export class RemoteNodeLimit {
|
|
29
|
+
readonly #maxNodes: number;
|
|
30
|
+
#nodes = 0;
|
|
31
|
+
|
|
32
|
+
constructor(maxNodes: number) {
|
|
33
|
+
this.#maxNodes = maxNodes;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The parsed document of `source`, as Redocly parses it. Fails with `SCHEMA_REF_FORBIDDEN`
|
|
38
|
+
* (`remote-documents-too-many-nodes`) once the nodes of the load pass the limit.
|
|
39
|
+
*/
|
|
40
|
+
parse(source: Source): Document {
|
|
41
|
+
const listener = (event: string): void => {
|
|
42
|
+
if (event !== NODE_OPENED) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.#nodes += 1;
|
|
46
|
+
const refusal = this.#refusal(source.absoluteRef);
|
|
47
|
+
if (refusal !== undefined) {
|
|
48
|
+
throw refusal;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return { source, parsed: parseYaml(source.body, { filename: source.absoluteRef, listener }) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#refusal(url: string): OpenAPITypesafeError | undefined {
|
|
55
|
+
return this.#nodes > this.#maxNodes
|
|
56
|
+
? refForbiddenError(url, {
|
|
57
|
+
reason: "remote-documents-too-many-nodes",
|
|
58
|
+
maxNodes: this.#maxNodes,
|
|
59
|
+
})
|
|
60
|
+
: undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assertNever } from "../../domain/assert-never.js";
|
|
2
2
|
import { ERROR_CODES, OpenAPITypesafeError } from "../../domain/errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { redactUnparsedUrl, redactUrlSecrets } from "../../domain/url-secrets.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The typed errors of loading, validating and bundling a document (ADR-011). Messages are the
|
|
@@ -23,6 +23,15 @@ export function sourceInvalidError(source: unknown): OpenAPITypesafeError {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** A source string starts with a URL scheme but does not parse as a URL. */
|
|
27
|
+
export function urlInvalidError(text: string): OpenAPITypesafeError {
|
|
28
|
+
return new OpenAPITypesafeError({
|
|
29
|
+
code: ERROR_CODES.SCHEMA_SOURCE_INVALID,
|
|
30
|
+
message: `The string ${redactUnparsedUrl(text)} starts like a URL but is not a valid URL`,
|
|
31
|
+
details: { reason: "url", received: "string" },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
function causeMessage(cause: unknown): string {
|
|
27
36
|
return cause instanceof Error ? cause.message : String(cause);
|
|
28
37
|
}
|
|
@@ -30,19 +39,29 @@ function causeMessage(cause: unknown): string {
|
|
|
30
39
|
/** Enough of a source string to recognise it in the message. */
|
|
31
40
|
const SOURCE_TEXT_IN_MESSAGE = 200;
|
|
32
41
|
|
|
42
|
+
/** Where document text that is only a YAML string came from. */
|
|
43
|
+
export type TextOrigin = "string" | "stream";
|
|
44
|
+
|
|
45
|
+
const LOAD_FILE_ADVICE =
|
|
46
|
+
"To load a file, pass a URL: openapiTS(pathToFileURL(path)) from node:url.";
|
|
47
|
+
|
|
33
48
|
/**
|
|
34
49
|
* A string source is document text, so a file path parses as a YAML string scalar, not as a
|
|
35
|
-
* document (docs/node.md). The message says how to load the file instead.
|
|
50
|
+
* document (docs/node.md). The message says how to load the file instead. Text read from a
|
|
51
|
+
* stream (stdin in the CLI) is named as such; `details.text` lets the CLI give its own advice.
|
|
36
52
|
*/
|
|
37
|
-
export function pathStringError(text: string): OpenAPITypesafeError {
|
|
53
|
+
export function pathStringError(text: string, origin: TextOrigin): OpenAPITypesafeError {
|
|
38
54
|
const shown =
|
|
39
55
|
text.length > SOURCE_TEXT_IN_MESSAGE ? `${text.slice(0, SOURCE_TEXT_IN_MESSAGE)}…` : text;
|
|
56
|
+
const message =
|
|
57
|
+
origin === "string"
|
|
58
|
+
? `The string "${shown}" was parsed as YAML text, not loaded as a file. ${LOAD_FILE_ADVICE}`
|
|
59
|
+
: `The stream held only the YAML string "${shown}", not an OpenAPI document. ` +
|
|
60
|
+
LOAD_FILE_ADVICE;
|
|
40
61
|
return new OpenAPITypesafeError({
|
|
41
62
|
code: ERROR_CODES.SCHEMA_SOURCE_INVALID,
|
|
42
|
-
message
|
|
43
|
-
|
|
44
|
-
"To load a file, pass a URL: openapiTS(pathToFileURL(path)) from node:url.",
|
|
45
|
-
details: { reason: "path", received: "string" },
|
|
63
|
+
message,
|
|
64
|
+
details: { reason: "path", received: origin, text: shown },
|
|
46
65
|
});
|
|
47
66
|
}
|
|
48
67
|
|
|
@@ -51,7 +70,7 @@ export function loadFailedError(absoluteRef: string, cause: unknown): OpenAPITyp
|
|
|
51
70
|
return new OpenAPITypesafeError({
|
|
52
71
|
code: ERROR_CODES.SCHEMA_LOAD_FAILED,
|
|
53
72
|
message: causeMessage(cause),
|
|
54
|
-
details: { source:
|
|
73
|
+
details: { source: redactUrlSecrets(absoluteRef) },
|
|
55
74
|
cause,
|
|
56
75
|
});
|
|
57
76
|
}
|
|
@@ -65,7 +84,7 @@ export function parseFailedError(
|
|
|
65
84
|
return new OpenAPITypesafeError({
|
|
66
85
|
code: ERROR_CODES.SCHEMA_PARSE_FAILED,
|
|
67
86
|
message: causeMessage(cause),
|
|
68
|
-
details: { format, source:
|
|
87
|
+
details: { format, source: redactUrlSecrets(absoluteRef) },
|
|
69
88
|
cause,
|
|
70
89
|
});
|
|
71
90
|
}
|
|
@@ -90,7 +109,7 @@ export function schemaInvalidError(problems: readonly string[]): OpenAPITypesafe
|
|
|
90
109
|
return new OpenAPITypesafeError({
|
|
91
110
|
code: ERROR_CODES.SCHEMA_INVALID,
|
|
92
111
|
message: problems.at(-1) ?? "",
|
|
93
|
-
details: { problems: problems.map(
|
|
112
|
+
details: { problems: problems.map(redactUrlSecrets) },
|
|
94
113
|
});
|
|
95
114
|
}
|
|
96
115
|
|
|
@@ -113,7 +132,17 @@ export type RefRefusalDetails =
|
|
|
113
132
|
| { readonly reason: "outside-local-root"; readonly localRoot: string }
|
|
114
133
|
| { readonly reason: "host-not-allowed"; readonly allowedHosts: readonly string[] }
|
|
115
134
|
| { readonly reason: "local-ref-from-remote"; readonly base: string }
|
|
116
|
-
| { readonly reason: "too-many-remote-documents"; readonly maxDocuments: number }
|
|
135
|
+
| { readonly reason: "too-many-remote-documents"; readonly maxDocuments: number }
|
|
136
|
+
| { readonly reason: "remote-document-too-large"; readonly maxDocumentBytes: number }
|
|
137
|
+
| { readonly reason: "remote-documents-too-large"; readonly maxTotalBytes: number }
|
|
138
|
+
| { readonly reason: "remote-documents-too-many-nodes"; readonly maxNodes: number };
|
|
139
|
+
|
|
140
|
+
const BYTES_PER_MEBIBYTE = 1024 * 1024;
|
|
141
|
+
|
|
142
|
+
/** A byte count in MiB, as the limits are chosen: `256 MiB`; a smaller limit in bytes. */
|
|
143
|
+
function sizeText(bytes: number): string {
|
|
144
|
+
return bytes % BYTES_PER_MEBIBYTE === 0 ? `${bytes / BYTES_PER_MEBIBYTE} MiB` : `${bytes} bytes`;
|
|
145
|
+
}
|
|
117
146
|
|
|
118
147
|
function limitText(details: RefRefusalDetails): string {
|
|
119
148
|
switch (details.reason) {
|
|
@@ -127,6 +156,16 @@ function limitText(details: RefRefusalDetails): string {
|
|
|
127
156
|
return `it is not an http(s) URL, and the $ref is in the remote document ${details.base}`;
|
|
128
157
|
case "too-many-remote-documents":
|
|
129
158
|
return `the document refers to more than ${details.maxDocuments} remote documents`;
|
|
159
|
+
case "remote-document-too-large":
|
|
160
|
+
return `its body is larger than ${sizeText(details.maxDocumentBytes)} once decoded`;
|
|
161
|
+
case "remote-documents-too-large": {
|
|
162
|
+
const limit = sizeText(details.maxTotalBytes);
|
|
163
|
+
return `the remote documents of this run are larger than ${limit} together once decoded`;
|
|
164
|
+
}
|
|
165
|
+
case "remote-documents-too-many-nodes": {
|
|
166
|
+
const limit = details.maxNodes;
|
|
167
|
+
return `the remote documents of this run parse into more than ${limit} nodes together`;
|
|
168
|
+
}
|
|
130
169
|
default:
|
|
131
170
|
return assertNever(details);
|
|
132
171
|
}
|
|
@@ -134,13 +173,13 @@ function limitText(details: RefRefusalDetails): string {
|
|
|
134
173
|
|
|
135
174
|
/**
|
|
136
175
|
* A `$ref` is outside the local root, its host is not allowed (threat model R1, R2), a remote
|
|
137
|
-
* document refers to a local file (T3), or there are too many remote documents (T2).
|
|
176
|
+
* document refers to a local file (T3), or there are too many or too large remote documents (T2).
|
|
138
177
|
*/
|
|
139
178
|
export function refForbiddenError(ref: string, details: RefRefusalDetails): OpenAPITypesafeError {
|
|
140
179
|
const limit = limitText(details);
|
|
141
180
|
return new OpenAPITypesafeError({
|
|
142
181
|
code: ERROR_CODES.SCHEMA_REF_FORBIDDEN,
|
|
143
182
|
message: `Refused to load ${ref}: ${limit}`,
|
|
144
|
-
details: { ref:
|
|
183
|
+
details: { ref: redactUrlSecrets(ref), ...details },
|
|
145
184
|
});
|
|
146
185
|
}
|