Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5382810
fix(downgrader): a $ref cut at recursion in a copy reused later tells…
claude Nov 1, 2026
479b9ac
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
34ef504
fix(downgrader): keep a oneOf of object literals, and an additionalPr…
claude Nov 1, 2026
c750171
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
39f4ca4
fix(downgrader): follow a propertyNames $ref on only through $refs be…
claude Nov 1, 2026
f695074
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
c09da8c
fix(downgrader): rerun the conversion for a schema that loses a restr…
claude Nov 1, 2026
e9f7f25
fix(downgrader): follow an alias chain through inlined $refs and tagg…
claude Nov 1, 2026
905a75a
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
997f21b
perf(downgrader): skip the second pass when no not or oneOf kept its …
claude Nov 1, 2026
cb16050
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
7d46cbc
fix(downgrader): count an object that requires a readOnly or writeOnl…
claude Nov 1, 2026
9962db1
perf(downgrader): check one-way required properties without a closure…
claude Nov 1, 2026
9811aeb
refactor(downgrader): count any readOnly or writeOnly schema as loose
claude Nov 1, 2026
b320e25
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
6de99ef
refactor(downgrader): count a schema met again mid-cycle as loose, in…
claude Nov 1, 2026
11a7506
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
dd53f1c
refactor(downgrader): replace any $ref into items or additionalProper…
claude Nov 1, 2026
811283c
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
3c7ab4b
refactor(downgrader): follow a propertyNames $ref through one $ref only
claude Nov 1, 2026
8ba0f5a
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
37fdc08
fix(downgrader): replace a percent-encoded $ref with its target
claude Nov 1, 2026
c607746
docs(downgrader): note that a percent-encoded $ref is replaced by its…
claude Nov 1, 2026
a7b6f48
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
98c8199
fix(downgrader): write */* as the contentType of an Encoding Object t…
claude Nov 1, 2026
6270b78
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 1, 2026
ab1cc0e
fix(downgrader): leave out 3.2 security requirement names that are no…
claude Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/downgrader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Schema Objects inside the document convert as in [Schema](#schema-downgradeschem
| Example [`dataValue`][3.2-example-data-value] and [`serializedValue`][3.2-example-serialized-value] | Removed. Without `value` or `externalValue`, `dataValue`, or else `serializedValue`, fills `value`. |
| OAuth [`deviceAuthorization`][3.2-oauth-flows-device-authorization] flow | Removed. |
| Security Scheme [`oauth2MetadataUrl`][3.2-security-scheme-oauth2-metadata-url] and [`deprecated`][3.2-security-scheme-deprecated] | Removed. |
| [Security requirement][3.2-security-requirement] names other than a component name, such as a URI reference to a scheme | Removed, which asks for less. |

[3.2-self]: https://spec.openapis.org/oas/v3.2.1.html#oas-self
[3.2-json-schema-dialect]: https://spec.openapis.org/oas/v3.2.1.html#oas-json-schema-dialect
Expand All @@ -113,12 +114,14 @@ Schema Objects inside the document convert as in [Schema](#schema-downgradeschem
[3.2-oauth-flows-device-authorization]: https://spec.openapis.org/oas/v3.2.1.html#oauth-flows-device-authorization
[3.2-security-scheme-oauth2-metadata-url]: https://spec.openapis.org/oas/v3.2.1.html#security-scheme-oauth2-metadata-url
[3.2-security-scheme-deprecated]: https://spec.openapis.org/oas/v3.2.1.html#security-scheme-deprecated
[3.2-security-requirement]: https://spec.openapis.org/oas/v3.2.1.html#security-requirement-object

#### Limitations

- Other `$ref`s into a removed part, such as a schema `$ref` into `components.mediaTypes`, and Links or discriminator `mapping` values that point at a removed operation, are left as written and dangle.
- A `content` `$ref` that does not resolve is removed, which can leave a parameter or header without the one entry it needs.
- Security requirements that name a scheme by URI, and `$self`-relative references, pass through unchanged.
- A security requirement that names its scheme by URI reference loses it, so 3.1 tools can read the operation as needing no credentials for it.
- `$self`-relative references pass through unchanged.

### Schema (`downgradeSchemaV32ToV31`)

Expand Down
23 changes: 23 additions & 0 deletions packages/downgrader/src/v3.2-to-v3.1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
list,
map,
resolve,
resolvePointer,
} from './shared'

const V32_DIALECT_PREFIX = 'https://spec.openapis.org/oas/3.2/dialect/'
Expand Down Expand Up @@ -125,6 +126,7 @@ const SECURITY_SCHEME_FIELDS = defineFields({

const OPERATION_FIELDS = defineFields({
callbacks: map(refOr(convertCallback)),
security: convertSecurity,
parameters: list(convertParameterRef),
requestBody: refOr(convertRequestBody),
responses: map(refOr(convertResponse), isNotExtension),
Expand Down Expand Up @@ -159,11 +161,32 @@ const DOCUMENT_FIELDS = defineFields({
jsonSchemaDialect: (item) =>
typeof item === 'string' && item.startsWith(V32_DIALECT_PREFIX) ? V31_DIALECT : clone(item),
paths: map(convertPathItem, isPath),
security: convertSecurity,
servers: convertServers,
tags: list(convertTag),
webhooks: map(convertPathItem),
})

// 3.1 names a scheme in a security requirement only by its component name,
// where 3.2 also takes a URI reference to one. Any other name is left out,
// which asks for less.
function convertSecurity(value: unknown, ctx: Context): unknown {
if (!Array.isArray(value)) {
return clone(value)
}
const schemes = resolvePointer('#/components/securitySchemes', ctx)
const isScheme = (name: string): boolean => isRecord(schemes) && Object.hasOwn(schemes, name)
return value.map((requirement) =>
isRecord(requirement)
? Object.fromEntries(
Object.keys(requirement)
.filter((name) => requirement[name] !== undefined && isScheme(name))
.map((name) => [name, clone(requirement[name])]),
)
: clone(requirement),
)
}

/** Reference Objects are the same in 3.1, so they are copied as they are. */
function refOr(convert: Convert): Convert {
return (value, ctx) => (getRef(value) === undefined ? convert(value, ctx) : clone(value))
Expand Down
33 changes: 33 additions & 0 deletions packages/downgrader/tests/v3.2-to-v3.1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,39 @@ describe('downgradeSpecV32ToV31', () => {
})
})

it('leaves out security requirement names that are not a component name', () => {
const key = { type: 'apiKey', in: 'header', name: 'X-Key' } as const
const out = convert({
security: [{ '#/components/securitySchemes/key': [] }],
paths: {
'/a': {
get: {
security: [
{ '#/components/securitySchemes/key': [], 'key': [] },
// A component name is read as one, whatever it looks like.
{ '#/components/securitySchemes/other': ['read'] },
{ 'https://example.com/openapi.json#/components/securitySchemes/key': [] },
],
},
},
},
components: { securitySchemes: { 'key': key, '#/components/securitySchemes/other': key } },
})
expect(out.security).toEqual([{}])
expect(out.paths?.['/a']?.get?.security).toEqual([
{ key: [] },
{ '#/components/securitySchemes/other': ['read'] },
{},
])
// Without any schemes, every name is left out, and malformed values stay.
const malformed = convert({
security: 'all',
paths: { '/a': { get: { security: [null, { key: [] }] } } },
} as any)
expect(malformed.security).toBe('all')
expect(malformed.paths?.['/a']?.get?.security).toEqual([null, {}])
})

it('converts schemas everywhere in the document', () => {
const schema: OpenAPIV3_2.SchemaObject = { type: 'string', xml: { nodeType: 'attribute' } }
const out = convert({
Expand Down
Loading