diff --git a/packages/json-schema-ref-parser/src/__tests__/bundle.test.ts b/packages/json-schema-ref-parser/src/__tests__/bundle.test.ts index 00b7ae0023..0a151c06cb 100644 --- a/packages/json-schema-ref-parser/src/__tests__/bundle.test.ts +++ b/packages/json-schema-ref-parser/src/__tests__/bundle.test.ts @@ -527,6 +527,89 @@ describe('bundle', () => { expect(pathKeys).toHaveLength(1); }); + it('prefixes operation-level security scheme names with source prefix', async () => { + const refParser = new $RefParser(); + const spec1 = { + components: { + securitySchemes: { + bearerAuth: { + bearerFormat: 'JWT', + scheme: 'bearer', + type: 'http', + }, + }, + }, + info: { title: 'Spec 1', version: '1.0.0' }, + openapi: '3.0.0', + paths: { + '/users': { + get: { + operationId: 'listUsers', + responses: { '200': { description: 'OK' } }, + security: [{ bearerAuth: [] }], + }, + }, + }, + }; + const spec2 = { + components: { + securitySchemes: { + bearerAuth: { + bearerFormat: 'JWT', + scheme: 'bearer', + type: 'http', + }, + oauth2: { + flows: { + authorizationCode: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: { read: 'read access', write: 'write access' }, + tokenUrl: 'https://example.com/oauth/token', + }, + }, + type: 'oauth2', + }, + }, + }, + info: { title: 'Spec 2', version: '1.0.0' }, + openapi: '3.0.0', + paths: { + '/orders': { + get: { + operationId: 'listOrders', + responses: { '200': { description: 'OK' } }, + security: [{ oauth2: ['read'] }, { bearerAuth: [] }], + }, + }, + }, + }; + + const merged = (await refParser.bundleMany({ + pathOrUrlOrSchemas: [ + { ...spec1, $id: 'file:///spec1.json' }, + { ...spec2, $id: 'file:///spec2.json' }, + ], + })) as any; + + const spec1Prefix = 'spec1'; + const spec2Prefix = 'spec2'; + + // Component security scheme names should be prefixed + expect(merged.components.securitySchemes[`${spec1Prefix}_bearerAuth`]).toBeDefined(); + expect(merged.components.securitySchemes[`${spec2Prefix}_bearerAuth`]).toBeDefined(); + expect(merged.components.securitySchemes[`${spec2Prefix}_oauth2`]).toBeDefined(); + + // Operation-level security references should also be prefixed + const usersSecurity = merged.paths['/users'].get.security; + expect(usersSecurity).toEqual([{ [`${spec1Prefix}_bearerAuth`]: [] }]); + + const ordersSecurity = merged.paths['/orders'].get.security; + expect(ordersSecurity).toEqual([ + { [`${spec2Prefix}_oauth2`]: ['read'] }, + { [`${spec2Prefix}_bearerAuth`]: [] }, + ]); + }); + it('adds prefix to path when HTTP methods conflict', async () => { const refParser = new $RefParser(); const spec1 = { diff --git a/packages/json-schema-ref-parser/src/index.ts b/packages/json-schema-ref-parser/src/index.ts index 1acf036591..a99ff5c411 100644 --- a/packages/json-schema-ref-parser/src/index.ts +++ b/packages/json-schema-ref-parser/src/index.ts @@ -42,7 +42,15 @@ export function getResolvedInput({ resolvedInput.path = url.fromFileSystemPath(resolvedInput.path); resolvedInput.type = 'file'; } else if (!resolvedInput.path && pathOrUrlOrSchema && typeof pathOrUrlOrSchema === 'object') { - if ('$id' in pathOrUrlOrSchema && pathOrUrlOrSchema.$id) { + if ( + ('openapi' in pathOrUrlOrSchema && pathOrUrlOrSchema.openapi) || + ('swagger' in pathOrUrlOrSchema && pathOrUrlOrSchema.swagger) + ) { + resolvedInput.schema = pathOrUrlOrSchema; + resolvedInput.type = 'json'; + if ('$id' in pathOrUrlOrSchema && pathOrUrlOrSchema.$id) + resolvedInput.path = pathOrUrlOrSchema.$id as string; + } else if ('$id' in pathOrUrlOrSchema && pathOrUrlOrSchema.$id) { // when schema id has defined an URL should use that hostname to request the references, // instead of using the current page URL const { hostname, protocol } = new URL(pathOrUrlOrSchema.$id as string); @@ -385,7 +393,8 @@ export class $RefParser { const baseName = (p: string) => { try { const withoutHash = p.split('#')[0]!; - const parts = withoutHash.split('/'); + const withoutTrailingSlash = withoutHash.replace(/\/+$/, ''); + const parts = withoutTrailingSlash.split('/'); const filename = parts[parts.length - 1] || 'schema'; const dot = filename.lastIndexOf('.'); const raw = dot > 0 ? filename.substring(0, dot) : filename; @@ -461,6 +470,14 @@ export class $RefParser { } } else if (k === 'tags' && Array.isArray(v) && v.every((x) => typeof x === 'string')) { out[k] = v.map((t) => tagMap.get(t) || t); + } else if (k === 'security' && Array.isArray(v)) { + out[k] = v.map((s) => { + const securityScheme: Record = {}; + for (const [key, value] of Object.entries(s)) { + securityScheme[`${opIdPrefix}_${key}`] = value; + } + return securityScheme; + }); } else if (k === 'operationId' && typeof v === 'string') { out[k] = unique(usedOpIds, `${opIdPrefix}_${v}`); } else {