From ffd3ed2fb30f3b8cb5cb90c83ed2707a4e522a0d Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Tue, 2 Jun 2026 23:52:09 +0300 Subject: [PATCH 1/9] feat: add spread-security-to-operations decorator --- .../spread-security-to-operations/README.md | 125 ++++++++++++++++++ .../decorator.js | 15 +++ .../spread-security-to-operations/plugin.js | 12 ++ .../redocly.yaml | 6 + 4 files changed, 158 insertions(+) create mode 100644 custom-plugin-decorators/spread-security-to-operations/README.md create mode 100644 custom-plugin-decorators/spread-security-to-operations/decorator.js create mode 100644 custom-plugin-decorators/spread-security-to-operations/plugin.js create mode 100644 custom-plugin-decorators/spread-security-to-operations/redocly.yaml diff --git a/custom-plugin-decorators/spread-security-to-operations/README.md b/custom-plugin-decorators/spread-security-to-operations/README.md new file mode 100644 index 0000000..8da4285 --- /dev/null +++ b/custom-plugin-decorators/spread-security-to-operations/README.md @@ -0,0 +1,125 @@ +# Spread root-level security to operations after join + +Authors: + +- [`@Daryna-del`](https://github.com/Daryna-del), Daryna Pastushenko (Redocly) + +## What this does and why + +When you use `redocly join` to combine multiple API descriptions into one, root-level `security` is not automatically inherited across the joined specs. This is by design — silently applying security requirements from one file to operations defined in another would change their behavior without an explicit declaration. + +A common scenario is when one spec (for example, `foo.yaml`) defines shared infrastructure — security schemes and root-level `security` — but has no paths of its own, while another spec (`bar.yaml`) defines all the paths but has no `security` at all. After joining, the operations from `bar.yaml` end up with no security applied. + +This decorator (`spread-security-to-operations`) solves that: it reads the root-level `security` from a specified source file and applies it to any operation that doesn't already define its own `security`. It runs as a post-join `bundle` step, giving you full control over which security gets applied and where. + +## Code + +The `security-plugin` plugin defines the `decorator` section and the plugin `id`: + +```javascript +import spreadSecurityToOperations from "./decorator"; + +export default function plugin() { + return { + id: "security-plugin", + decorators: { + oas3: { + "spread-security-to-operations": spreadSecurityToOperations, + }, + }, + }; +} +``` + +Here's the main part of the decorator (from `decorator.js`): + +```javascript +export default function spreadSecurityToOperations({ pathSecurityFile } = {}) { + return { + Operation: { + leave(operation, { config }) { + const absolutePath = path.isAbsolute(pathSecurityFile) + ? pathSecurityFile + : path.resolve(path.dirname(config.configPath), pathSecurityFile); + const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8')); + + if (doc?.security === undefined || operation.security !== undefined) return; + operation.security = doc?.security; + }, + }, + }; +}; +``` + +Put this file alongside your `redocly.yaml` file, and add the following configuration to `redocly.yaml`: + +```yaml +plugins: + - './plugin.js' + +decorators: + security-plugin/spread-security-to-operations: + pathSecurityFile: ./foo.yaml +``` + +The `pathSecurityFile` parameter is the path to the spec file that contains the root-level `security` you want to spread. + +## Examples + +Given two specs: + +**foo.yaml** — defines root-level security, no paths: +```yaml +openapi: 3.1.0 +info: + title: Foo + version: 1.0.0 +security: + - oauth2: [] +components: + securitySchemes: + oauth2: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: {} +paths: {} +``` + +**bar.yaml** — defines paths, no security: +```yaml +openapi: 3.1.0 +info: + title: Bar + version: 1.0.0 +paths: + /pets: + get: + summary: Get pets example + operationId: getPetsExample + responses: + '200': + description: OK + '400': + description: Bad request +``` + +Run the two-step workflow: + +```bash +# Step 1: join the specs +redocly join foo.yaml bar.yaml -o joined.yaml + +# Step 2: bundle with the decorator to spread security +redocly bundle joined.yaml -o result.yaml +``` + +The resulting `result.yaml` will have `security: [oauth2: []]` applied to the `/pets` GET operation, because it had no security of its own. + +## References + +- [Redocly join command](https://redocly.com/docs/cli/commands/join) +- [Custom decorators in plugins](https://redocly.com/docs/cli/custom-plugins/custom-decorators) +- [Security requirement object (OpenAPI)](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) diff --git a/custom-plugin-decorators/spread-security-to-operations/decorator.js b/custom-plugin-decorators/spread-security-to-operations/decorator.js new file mode 100644 index 0000000..93f9e01 --- /dev/null +++ b/custom-plugin-decorators/spread-security-to-operations/decorator.js @@ -0,0 +1,15 @@ +export default function spreadSecurityToOperations({ pathSecurityFile } = {}) { + return { + Operation: { + leave(operation, { config }) { + const absolutePath = path.isAbsolute(pathSecurityFile) + ? pathSecurityFile + : path.resolve(path.dirname(config.configPath), pathSecurityFile); + const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8')); + + if (doc?.security === undefined || operation.security !== undefined) return; + operation.security = doc?.security; + }, + }, + }; +}; diff --git a/custom-plugin-decorators/spread-security-to-operations/plugin.js b/custom-plugin-decorators/spread-security-to-operations/plugin.js new file mode 100644 index 0000000..a29244c --- /dev/null +++ b/custom-plugin-decorators/spread-security-to-operations/plugin.js @@ -0,0 +1,12 @@ +import spreadSecurityToOperations from "./decorator"; + +export default function plugin() { + return { + id: "security-plugin", + decorators: { + oas3: { + "spread-security-to-operations": spreadSecurityToOperations, + }, + }, + }; +} diff --git a/custom-plugin-decorators/spread-security-to-operations/redocly.yaml b/custom-plugin-decorators/spread-security-to-operations/redocly.yaml new file mode 100644 index 0000000..7fef90d --- /dev/null +++ b/custom-plugin-decorators/spread-security-to-operations/redocly.yaml @@ -0,0 +1,6 @@ +plugins: + - './plugin.js' + +decorators: + security-plugin/spread-security-to-operations: + pathSecurityFile: ./foo.yaml From 5dd896c0ab2ccc87cf46c9ad754c020b5f6fc649 Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Thu, 4 Jun 2026 17:24:08 +0300 Subject: [PATCH 2/9] chore: address to comments --- .../README.md | 56 ++++++++----------- .../spread-root-security/plugin.js | 24 ++++++++ .../redocly.yaml | 2 +- .../decorator.js | 15 ----- .../spread-security-to-operations/plugin.js | 12 ---- 5 files changed, 47 insertions(+), 62 deletions(-) rename custom-plugin-decorators/{spread-security-to-operations => spread-root-security}/README.md (60%) create mode 100644 custom-plugin-decorators/spread-root-security/plugin.js rename custom-plugin-decorators/{spread-security-to-operations => spread-root-security}/redocly.yaml (59%) delete mode 100644 custom-plugin-decorators/spread-security-to-operations/decorator.js delete mode 100644 custom-plugin-decorators/spread-security-to-operations/plugin.js diff --git a/custom-plugin-decorators/spread-security-to-operations/README.md b/custom-plugin-decorators/spread-root-security/README.md similarity index 60% rename from custom-plugin-decorators/spread-security-to-operations/README.md rename to custom-plugin-decorators/spread-root-security/README.md index 8da4285..3ccfcd2 100644 --- a/custom-plugin-decorators/spread-security-to-operations/README.md +++ b/custom-plugin-decorators/spread-root-security/README.md @@ -10,47 +10,39 @@ When you use `redocly join` to combine multiple API descriptions into one, root- A common scenario is when one spec (for example, `foo.yaml`) defines shared infrastructure — security schemes and root-level `security` — but has no paths of its own, while another spec (`bar.yaml`) defines all the paths but has no `security` at all. After joining, the operations from `bar.yaml` end up with no security applied. -This decorator (`spread-security-to-operations`) solves that: it reads the root-level `security` from a specified source file and applies it to any operation that doesn't already define its own `security`. It runs as a post-join `bundle` step, giving you full control over which security gets applied and where. +This decorator (`spread-root-security`) solves that: it reads the root-level `security` from a specified source file (for example `foo.yaml`) and sets it as root-level `security` on the document you are bundling when that document does not already define its own. It runs as a `bundle` step, giving you full control over which file supplies the requirement. ## Code -The `security-plugin` plugin defines the `decorator` section and the plugin `id`: +The following code snippet shows the decorator, in a file named `plugin.js`: ```javascript -import spreadSecurityToOperations from "./decorator"; - export default function plugin() { return { id: "security-plugin", decorators: { oas3: { - "spread-security-to-operations": spreadSecurityToOperations, + "spread-root-security": ({ pathSecurityFile }) => { + return { + Root: { + leave(root, { config }) { + const absolutePath = path.isAbsolute(pathSecurityFile) + ? pathSecurityFile + : path.resolve(path.dirname(config.configPath), pathSecurityFile); + const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8')); + + if (doc?.security === undefined || root.security !== undefined) return; + root.security = doc?.security; + }, + }, + }; + }, }, }, - }; + } } ``` -Here's the main part of the decorator (from `decorator.js`): - -```javascript -export default function spreadSecurityToOperations({ pathSecurityFile } = {}) { - return { - Operation: { - leave(operation, { config }) { - const absolutePath = path.isAbsolute(pathSecurityFile) - ? pathSecurityFile - : path.resolve(path.dirname(config.configPath), pathSecurityFile); - const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8')); - - if (doc?.security === undefined || operation.security !== undefined) return; - operation.security = doc?.security; - }, - }, - }; -}; -``` - Put this file alongside your `redocly.yaml` file, and add the following configuration to `redocly.yaml`: ```yaml @@ -58,7 +50,7 @@ plugins: - './plugin.js' decorators: - security-plugin/spread-security-to-operations: + security-plugin/spread-root-security: pathSecurityFile: ./foo.yaml ``` @@ -106,17 +98,13 @@ paths: description: Bad request ``` -Run the two-step workflow: +Run: ```bash -# Step 1: join the specs -redocly join foo.yaml bar.yaml -o joined.yaml - -# Step 2: bundle with the decorator to spread security -redocly bundle joined.yaml -o result.yaml +redocly bundle bar.yaml -o result.yaml ``` -The resulting `result.yaml` will have `security: [oauth2: []]` applied to the `/pets` GET operation, because it had no security of its own. +The resulting `result.yaml` will have `security: [oauth2: []]` spreaded at the root. ## References diff --git a/custom-plugin-decorators/spread-root-security/plugin.js b/custom-plugin-decorators/spread-root-security/plugin.js new file mode 100644 index 0000000..0ca4ffd --- /dev/null +++ b/custom-plugin-decorators/spread-root-security/plugin.js @@ -0,0 +1,24 @@ +export default function plugin() { + return { + id: "security-plugin", + decorators: { + oas3: { + "spread-root-security": ({ pathSecurityFile }) => { + return { + Root: { + leave(root, { config }) { + const absolutePath = path.isAbsolute(pathSecurityFile) + ? pathSecurityFile + : path.resolve(path.dirname(config.configPath), pathSecurityFile); + const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8')); + + if (doc?.security === undefined || root.security !== undefined) return; + root.security = doc?.security; + }, + }, + }; + }, + }, + }, + } +} diff --git a/custom-plugin-decorators/spread-security-to-operations/redocly.yaml b/custom-plugin-decorators/spread-root-security/redocly.yaml similarity index 59% rename from custom-plugin-decorators/spread-security-to-operations/redocly.yaml rename to custom-plugin-decorators/spread-root-security/redocly.yaml index 7fef90d..d3bc93d 100644 --- a/custom-plugin-decorators/spread-security-to-operations/redocly.yaml +++ b/custom-plugin-decorators/spread-root-security/redocly.yaml @@ -2,5 +2,5 @@ plugins: - './plugin.js' decorators: - security-plugin/spread-security-to-operations: + security-plugin/spread-root-security: pathSecurityFile: ./foo.yaml diff --git a/custom-plugin-decorators/spread-security-to-operations/decorator.js b/custom-plugin-decorators/spread-security-to-operations/decorator.js deleted file mode 100644 index 93f9e01..0000000 --- a/custom-plugin-decorators/spread-security-to-operations/decorator.js +++ /dev/null @@ -1,15 +0,0 @@ -export default function spreadSecurityToOperations({ pathSecurityFile } = {}) { - return { - Operation: { - leave(operation, { config }) { - const absolutePath = path.isAbsolute(pathSecurityFile) - ? pathSecurityFile - : path.resolve(path.dirname(config.configPath), pathSecurityFile); - const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8')); - - if (doc?.security === undefined || operation.security !== undefined) return; - operation.security = doc?.security; - }, - }, - }; -}; diff --git a/custom-plugin-decorators/spread-security-to-operations/plugin.js b/custom-plugin-decorators/spread-security-to-operations/plugin.js deleted file mode 100644 index a29244c..0000000 --- a/custom-plugin-decorators/spread-security-to-operations/plugin.js +++ /dev/null @@ -1,12 +0,0 @@ -import spreadSecurityToOperations from "./decorator"; - -export default function plugin() { - return { - id: "security-plugin", - decorators: { - oas3: { - "spread-security-to-operations": spreadSecurityToOperations, - }, - }, - }; -} From 8723d556c735cc1baeb59428a7e95d75996dbc24 Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Tue, 9 Jun 2026 16:34:00 +0300 Subject: [PATCH 3/9] fix: apply securitySchema --- .../spread-root-security/README.md | 24 ++++++++++++------- .../spread-root-security/plugin.js | 20 +++++++++++----- .../spread-root-security/redocly.yaml | 2 +- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/custom-plugin-decorators/spread-root-security/README.md b/custom-plugin-decorators/spread-root-security/README.md index 3ccfcd2..40a44ac 100644 --- a/custom-plugin-decorators/spread-root-security/README.md +++ b/custom-plugin-decorators/spread-root-security/README.md @@ -10,7 +10,7 @@ When you use `redocly join` to combine multiple API descriptions into one, root- A common scenario is when one spec (for example, `foo.yaml`) defines shared infrastructure — security schemes and root-level `security` — but has no paths of its own, while another spec (`bar.yaml`) defines all the paths but has no `security` at all. After joining, the operations from `bar.yaml` end up with no security applied. -This decorator (`spread-root-security`) solves that: it reads the root-level `security` from a specified source file (for example `foo.yaml`) and sets it as root-level `security` on the document you are bundling when that document does not already define its own. It runs as a `bundle` step, giving you full control over which file supplies the requirement. +This decorator (`apply-root-security`) solves that: it reads the root-level `security` from a specified source file (for example `foo.yaml`) and sets it as root-level `security` on the document you are bundling when that document does not already define its own. It runs as a `bundle` step, giving you full control over which file supplies the requirement. ## Code @@ -22,17 +22,25 @@ export default function plugin() { id: "security-plugin", decorators: { oas3: { - "spread-root-security": ({ pathSecurityFile }) => { + 'apply-root-security': ({ pathSecurityFile } = {}) => { return { Root: { leave(root, { config }) { - const absolutePath = path.isAbsolute(pathSecurityFile) - ? pathSecurityFile - : path.resolve(path.dirname(config.configPath), pathSecurityFile); - const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8')); + const doc = resolvePath(pathSecurityFile, config); - if (doc?.security === undefined || root.security !== undefined) return; + if (doc?.security !== undefined || root.security === undefined){ root.security = doc?.security; + } + + if (doc.components?.securitySchemes !== undefined) { + if (!root.components) { + root.components = {}; + } + root.components.securitySchemes = { + ...root.components.securitySchemes, + ...doc.components.securitySchemes, + }; + } }, }, }; @@ -104,7 +112,7 @@ Run: redocly bundle bar.yaml -o result.yaml ``` -The resulting `result.yaml` will have `security: [oauth2: []]` spreaded at the root. +The resulting `result.yaml` will have `security: [oauth2: []]` and `securitySchema` applied. ## References diff --git a/custom-plugin-decorators/spread-root-security/plugin.js b/custom-plugin-decorators/spread-root-security/plugin.js index 0ca4ffd..2988a22 100644 --- a/custom-plugin-decorators/spread-root-security/plugin.js +++ b/custom-plugin-decorators/spread-root-security/plugin.js @@ -3,17 +3,25 @@ export default function plugin() { id: "security-plugin", decorators: { oas3: { - "spread-root-security": ({ pathSecurityFile }) => { + 'apply-root-security': ({ pathSecurityFile } = {}) => { return { Root: { leave(root, { config }) { - const absolutePath = path.isAbsolute(pathSecurityFile) - ? pathSecurityFile - : path.resolve(path.dirname(config.configPath), pathSecurityFile); - const doc = yaml.load(fs.readFileSync(absolutePath, 'utf8')); + const doc = resolvePath(pathSecurityFile, config); - if (doc?.security === undefined || root.security !== undefined) return; + if (doc?.security !== undefined || root.security === undefined){ root.security = doc?.security; + } + + if (doc.components?.securitySchemes !== undefined) { + if (!root.components) { + root.components = {}; + } + root.components.securitySchemes = { + ...root.components.securitySchemes, + ...doc.components.securitySchemes, + }; + } }, }, }; diff --git a/custom-plugin-decorators/spread-root-security/redocly.yaml b/custom-plugin-decorators/spread-root-security/redocly.yaml index d3bc93d..985deac 100644 --- a/custom-plugin-decorators/spread-root-security/redocly.yaml +++ b/custom-plugin-decorators/spread-root-security/redocly.yaml @@ -2,5 +2,5 @@ plugins: - './plugin.js' decorators: - security-plugin/spread-root-security: + security-plugin/apply-root-security: pathSecurityFile: ./foo.yaml From cfc6070aa7d13821e9f168fbfced76ea1d2b789b Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Tue, 9 Jun 2026 17:27:01 +0300 Subject: [PATCH 4/9] fix: fix override root security and formatting --- custom-plugin-decorators/spread-root-security/README.md | 4 ++-- custom-plugin-decorators/spread-root-security/plugin.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom-plugin-decorators/spread-root-security/README.md b/custom-plugin-decorators/spread-root-security/README.md index 40a44ac..ce60967 100644 --- a/custom-plugin-decorators/spread-root-security/README.md +++ b/custom-plugin-decorators/spread-root-security/README.md @@ -28,8 +28,8 @@ export default function plugin() { leave(root, { config }) { const doc = resolvePath(pathSecurityFile, config); - if (doc?.security !== undefined || root.security === undefined){ - root.security = doc?.security; + if (doc?.security !== undefined && root.security === undefined){ + root.security = doc?.security; } if (doc.components?.securitySchemes !== undefined) { diff --git a/custom-plugin-decorators/spread-root-security/plugin.js b/custom-plugin-decorators/spread-root-security/plugin.js index 2988a22..51fceeb 100644 --- a/custom-plugin-decorators/spread-root-security/plugin.js +++ b/custom-plugin-decorators/spread-root-security/plugin.js @@ -9,8 +9,8 @@ export default function plugin() { leave(root, { config }) { const doc = resolvePath(pathSecurityFile, config); - if (doc?.security !== undefined || root.security === undefined){ - root.security = doc?.security; + if (doc?.security !== undefined && root.security === undefined){ + root.security = doc?.security; } if (doc.components?.securitySchemes !== undefined) { From 5529a31f13c57f2ea75ab079beaf00bd413c6d9e Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Thu, 11 Jun 2026 13:35:22 +0300 Subject: [PATCH 5/9] chore: address to comments and add oas2 --- .../apply-root-security/README.md | 234 ++++++++++++++++++ .../apply-root-security/decorator.js | 54 ++++ .../apply-root-security/plugin.js | 11 + .../redocly.yaml | 0 .../spread-root-security/README.md | 121 --------- .../spread-root-security/plugin.js | 32 --- 6 files changed, 299 insertions(+), 153 deletions(-) create mode 100644 custom-plugin-decorators/apply-root-security/README.md create mode 100644 custom-plugin-decorators/apply-root-security/decorator.js create mode 100644 custom-plugin-decorators/apply-root-security/plugin.js rename custom-plugin-decorators/{spread-root-security => apply-root-security}/redocly.yaml (100%) delete mode 100644 custom-plugin-decorators/spread-root-security/README.md delete mode 100644 custom-plugin-decorators/spread-root-security/plugin.js diff --git a/custom-plugin-decorators/apply-root-security/README.md b/custom-plugin-decorators/apply-root-security/README.md new file mode 100644 index 0000000..235b0a4 --- /dev/null +++ b/custom-plugin-decorators/apply-root-security/README.md @@ -0,0 +1,234 @@ +# Apply root-level security + +Authors: + +- [`@Daryna-del`](https://github.com/Daryna-del), Daryna Pastushenko (Redocly) + +## What this does and why + +When you use `redocly join` to combine multiple API descriptions into one, root-level `security` is not automatically inherited across the joined specs. This is by design — silently applying security requirements from one file to operations defined in another would change their behavior without an explicit declaration. + +A common scenario is when one spec (for example, `foo.yaml`) defines shared infrastructure — security schemes and root-level `security` — but has no paths of its own, while another spec (`bar.yaml`) defines all the paths but has no `security` at all. After joining, the operations from `bar.yaml` end up with no security applied. + +This decorator (`apply-root-security`) solves that: it reads the root-level `security` from a specified source file (for example `foo.yaml`) and sets it as root-level `security` on the document you are bundling when that document does not already define its own. It runs as a `bundle` step, giving you full control over which file supplies the requirement. + +Supported spec types and what gets merged: + +| Spec | Root security | Security definitions | +| ------------- | ---------------------------------- | ---------------------------------------- | +| OAS3 / OAS3.1 | Merged into `root.security` | Merged into `components.securitySchemes` | +| OAS2 | Merged into `root.security` | Merged into `securityDefinitions` | + +## Code + +The `security-plugin` plugin defines the `decorator` section and the plugin `id`: + +```javascript +export default function plugin() { + return { + id: "security-plugin", + decorators: { + oas3: {'apply-root-security': applyRootSecurity }, + oas2: {'apply-root-security': applyRootSecurity }, + }, + } +} +``` + +Here's the main part of the decorator (from `decorator.js`): + +```javascript +const applyRootSecurity = ({ pathSecurityFile } = {}) => { + return { + Root: { + leave(root, { config, specVersion }) { + const doc = resolvePath(pathSecurityFile, config); + + validateOpenapiSpecification(pathSecurityFile, doc, specVersion); + + if (specVersion === 'oas2') { + mergeSecurityRequirements(root, doc); + if (doc?.securityDefinitions !== undefined) { + root.securityDefinitions = { ...root.securityDefinitions, ...doc.securityDefinitions }; + } + } else { + mergeSecurityRequirements(root, doc); + mergeSecuritySchemes(root, doc); + } + }, + }, + }; +}; + +``` + +The `resolvePath` function resolves the path to the security file and returns its parsed content: + +```javascript +function resolvePath(pathSecurityFile, config) { + const base = config.configPath ? path.dirname(config.configPath) : process.cwd(); + const absolutePath = path.isAbsolute(pathSecurityFile) ? pathSecurityFile : path.resolve(base, pathSecurityFile); + return yaml.load(fs.readFileSync(absolutePath, 'utf8')); +}; +``` + +The `validateOpenapiSpecification` function checks that the security file format matches the target spec version and throws a descriptive error if not — for example, if an OAS2 file is used with an OAS3 target: + +```javascript +function validateOpenapiSpecification(pathSecurityFile, doc, specVersion) { + if (specVersion === 'oas2' && doc?.components?.securitySchemes !== undefined && doc?.securityDefinitions === undefined) { + throw new Error( + `apply-root-security: "${pathSecurityFile}" uses OAS3 components.securitySchemes but the target spec is OAS2. Use securityDefinitions instead.` + ); + } + if (specVersion !== 'oas2' && doc?.securityDefinitions !== undefined && doc?.components?.securitySchemes === undefined) { + throw new Error( + `apply-root-security: "${pathSecurityFile}" uses OAS2 securityDefinitions but the target spec is ${specVersion}. Use components.securitySchemes instead.` + ); + } +}; +``` + +The `mergeSecurityRequirements` function appends root-level security requirements from the source file into the target document. If the target already has security requirements defined, the entries are appended rather than replaced: + +```javascript +function mergeSecurityRequirements(root, doc) { + if (!Array.isArray(doc?.security)) return; + root.security = [...(root.security || []), ...doc.security]; +}; +``` + +The `mergeSecuritySchemes` function merges the security scheme definitions from the source file into `components.securitySchemes` on the target document. If the target already has schemes defined, they are preserved and the new ones are added alongside them: + +```javascript +function mergeSecuritySchemes(root, doc) { + if (doc?.components?.securitySchemes === undefined) return; + if (!root.components) root.components = {}; + root.components.securitySchemes = { + ...root.components.securitySchemes, + ...doc.components.securitySchemes, + }; +}; +``` + +Add the following to `redocly.yaml`: + +```yaml +plugins: + - './plugin.js' + +decorators: + security-plugin/apply-root-security: + pathSecurityFile: ./foo.yaml +``` + +The `pathSecurityFile` must be in the same format as the spec you are bundling — an OAS3 file for OAS3 targets, an OAS2 file for OAS2 targets. + +## Examples + +### OAS3 + +Given two specs: + +**foo.yaml** — defines root-level security, no paths: +```yaml +openapi: 3.1.0 +info: + title: Foo + version: 1.0.0 +security: + - oauth2: [] +components: + securitySchemes: + oauth2: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: {} +paths: {} +``` + +**bar.yaml** — defines paths, no security: +```yaml +openapi: 3.1.0 +info: + title: Bar + version: 1.0.0 +paths: + /pets: + get: + summary: Get pets example + operationId: getPetsExample + responses: + '200': + description: OK + '400': + description: Bad request +``` + +Run: + +```bash +redocly bundle bar.yaml -o result.yaml +``` + +`result.yaml` will have `security: [{oauth2: []}]` and `components.securitySchemes.oauth2` applied. + +### OAS2 + +Given two specs: + +**foo.yaml** — defines root-level security, no paths: +```yaml +swagger: "2.0" +info: + title: Foo + version: 1.0.0 +host: example.com +basePath: / +schemes: + - https +security: + - oauth2: [] +securityDefinitions: + oauth2: + type: oauth2 + flow: accessCode + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: {} +paths: {} +``` + +**bar.yaml** — defines paths, no security: +```yaml +swagger: "2.0" +info: + title: Bar + version: 1.0.0 +host: example.com +basePath: / +schemes: + - https +paths: + /pets: + get: + summary: Get pets example + operationId: getPetsExample + responses: + 200: + description: OK + 400: + description: Bad request +``` + +`result.yaml` will have `security: [{oauth2: []}]` and `securityDefinitions.oauth2` applied. + +## References + +- [Redocly join command](https://redocly.com/docs/cli/commands/join) +- [Custom decorators in plugins](https://redocly.com/docs/cli/custom-plugins/custom-decorators) +- [Security requirement object (OpenAPI)](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) +- [Security requirement object (OpenAPI 2 / Swagger)](https://swagger.io/specification/v2/#security-requirement-object) diff --git a/custom-plugin-decorators/apply-root-security/decorator.js b/custom-plugin-decorators/apply-root-security/decorator.js new file mode 100644 index 0000000..395d361 --- /dev/null +++ b/custom-plugin-decorators/apply-root-security/decorator.js @@ -0,0 +1,54 @@ +const applyRootSecurity = ({ pathSecurityFile } = {}) => { + return { + Root: { + leave(root, { config, specVersion }) { + const doc = resolvePath(pathSecurityFile, config); + + validateOpenapiSpecification(pathSecurityFile, doc, specVersion); + + if (specVersion === 'oas2') { + mergeSecurityRequirements(root, doc); + if (doc?.securityDefinitions !== undefined) { + root.securityDefinitions = { ...root.securityDefinitions, ...doc.securityDefinitions }; + } + } else { + mergeSecurityRequirements(root, doc); + mergeSecuritySchemes(root, doc); + } + }, + }, + }; +}; + +function resolvePath(pathSecurityFile, config) { + const base = config.configPath ? path.dirname(config.configPath) : process.cwd(); + const absolutePath = path.isAbsolute(pathSecurityFile) ? pathSecurityFile : path.resolve(base, pathSecurityFile); + return yaml.load(fs.readFileSync(absolutePath, 'utf8')); +}; + +function validateOpenapiSpecification(pathSecurityFile, doc, specVersion) { + if (specVersion === 'oas2' && doc?.components?.securitySchemes !== undefined && doc?.securityDefinitions === undefined) { + throw new Error( + `apply-root-security: "${pathSecurityFile}" uses OAS3 components.securitySchemes but the target spec is OAS2. Use securityDefinitions instead.` + ); + } + if (specVersion !== 'oas2' && doc?.securityDefinitions !== undefined && doc?.components?.securitySchemes === undefined) { + throw new Error( + `apply-root-security: "${pathSecurityFile}" uses OAS2 securityDefinitions but the target spec is ${specVersion}. Use components.securitySchemes instead.` + ); + } +}; + +function mergeSecurityRequirements(root, doc) { + if (!Array.isArray(doc?.security)) return; + root.security = [...(root.security || []), ...doc.security]; +}; + +function mergeSecuritySchemes(root, doc) { + if (doc?.components?.securitySchemes === undefined) return; + if (!root.components) root.components = {}; + root.components.securitySchemes = { + ...root.components.securitySchemes, + ...doc.components.securitySchemes, + }; +}; \ No newline at end of file diff --git a/custom-plugin-decorators/apply-root-security/plugin.js b/custom-plugin-decorators/apply-root-security/plugin.js new file mode 100644 index 0000000..4f1b020 --- /dev/null +++ b/custom-plugin-decorators/apply-root-security/plugin.js @@ -0,0 +1,11 @@ +import applyRootSecurity from "./decorator.js"; + +export default function plugin() { + return { + id: "security-plugin", + decorators: { + oas3: {'apply-root-security': applyRootSecurity }, + oas2: {'apply-root-security': applyRootSecurity }, + }, + } +} diff --git a/custom-plugin-decorators/spread-root-security/redocly.yaml b/custom-plugin-decorators/apply-root-security/redocly.yaml similarity index 100% rename from custom-plugin-decorators/spread-root-security/redocly.yaml rename to custom-plugin-decorators/apply-root-security/redocly.yaml diff --git a/custom-plugin-decorators/spread-root-security/README.md b/custom-plugin-decorators/spread-root-security/README.md deleted file mode 100644 index ce60967..0000000 --- a/custom-plugin-decorators/spread-root-security/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# Spread root-level security to operations after join - -Authors: - -- [`@Daryna-del`](https://github.com/Daryna-del), Daryna Pastushenko (Redocly) - -## What this does and why - -When you use `redocly join` to combine multiple API descriptions into one, root-level `security` is not automatically inherited across the joined specs. This is by design — silently applying security requirements from one file to operations defined in another would change their behavior without an explicit declaration. - -A common scenario is when one spec (for example, `foo.yaml`) defines shared infrastructure — security schemes and root-level `security` — but has no paths of its own, while another spec (`bar.yaml`) defines all the paths but has no `security` at all. After joining, the operations from `bar.yaml` end up with no security applied. - -This decorator (`apply-root-security`) solves that: it reads the root-level `security` from a specified source file (for example `foo.yaml`) and sets it as root-level `security` on the document you are bundling when that document does not already define its own. It runs as a `bundle` step, giving you full control over which file supplies the requirement. - -## Code - -The following code snippet shows the decorator, in a file named `plugin.js`: - -```javascript -export default function plugin() { - return { - id: "security-plugin", - decorators: { - oas3: { - 'apply-root-security': ({ pathSecurityFile } = {}) => { - return { - Root: { - leave(root, { config }) { - const doc = resolvePath(pathSecurityFile, config); - - if (doc?.security !== undefined && root.security === undefined){ - root.security = doc?.security; - } - - if (doc.components?.securitySchemes !== undefined) { - if (!root.components) { - root.components = {}; - } - root.components.securitySchemes = { - ...root.components.securitySchemes, - ...doc.components.securitySchemes, - }; - } - }, - }, - }; - }, - }, - }, - } -} -``` - -Put this file alongside your `redocly.yaml` file, and add the following configuration to `redocly.yaml`: - -```yaml -plugins: - - './plugin.js' - -decorators: - security-plugin/spread-root-security: - pathSecurityFile: ./foo.yaml -``` - -The `pathSecurityFile` parameter is the path to the spec file that contains the root-level `security` you want to spread. - -## Examples - -Given two specs: - -**foo.yaml** — defines root-level security, no paths: -```yaml -openapi: 3.1.0 -info: - title: Foo - version: 1.0.0 -security: - - oauth2: [] -components: - securitySchemes: - oauth2: - type: oauth2 - flows: - authorizationCode: - authorizationUrl: https://example.com/oauth/authorize - tokenUrl: https://example.com/oauth/token - scopes: {} -paths: {} -``` - -**bar.yaml** — defines paths, no security: -```yaml -openapi: 3.1.0 -info: - title: Bar - version: 1.0.0 -paths: - /pets: - get: - summary: Get pets example - operationId: getPetsExample - responses: - '200': - description: OK - '400': - description: Bad request -``` - -Run: - -```bash -redocly bundle bar.yaml -o result.yaml -``` - -The resulting `result.yaml` will have `security: [oauth2: []]` and `securitySchema` applied. - -## References - -- [Redocly join command](https://redocly.com/docs/cli/commands/join) -- [Custom decorators in plugins](https://redocly.com/docs/cli/custom-plugins/custom-decorators) -- [Security requirement object (OpenAPI)](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) diff --git a/custom-plugin-decorators/spread-root-security/plugin.js b/custom-plugin-decorators/spread-root-security/plugin.js deleted file mode 100644 index 51fceeb..0000000 --- a/custom-plugin-decorators/spread-root-security/plugin.js +++ /dev/null @@ -1,32 +0,0 @@ -export default function plugin() { - return { - id: "security-plugin", - decorators: { - oas3: { - 'apply-root-security': ({ pathSecurityFile } = {}) => { - return { - Root: { - leave(root, { config }) { - const doc = resolvePath(pathSecurityFile, config); - - if (doc?.security !== undefined && root.security === undefined){ - root.security = doc?.security; - } - - if (doc.components?.securitySchemes !== undefined) { - if (!root.components) { - root.components = {}; - } - root.components.securitySchemes = { - ...root.components.securitySchemes, - ...doc.components.securitySchemes, - }; - } - }, - }, - }; - }, - }, - }, - } -} From 307e3fe07e25f0b231c31150d4b919b17eaf7ecc Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Thu, 11 Jun 2026 13:39:46 +0300 Subject: [PATCH 6/9] fix: add empty line at the end --- custom-plugin-decorators/apply-root-security/decorator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom-plugin-decorators/apply-root-security/decorator.js b/custom-plugin-decorators/apply-root-security/decorator.js index 395d361..b20bea5 100644 --- a/custom-plugin-decorators/apply-root-security/decorator.js +++ b/custom-plugin-decorators/apply-root-security/decorator.js @@ -51,4 +51,4 @@ function mergeSecuritySchemes(root, doc) { ...root.components.securitySchemes, ...doc.components.securitySchemes, }; -}; \ No newline at end of file +}; From 261cde136a404b18f2ecda64561dff8d05560a83 Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Fri, 12 Jun 2026 11:21:32 +0300 Subject: [PATCH 7/9] chore: remove oas2 and address to comments --- .../apply-root-security/README.md | 117 +++--------------- .../apply-root-security/decorator.js | 49 +++----- .../apply-root-security/plugin.js | 3 +- 3 files changed, 29 insertions(+), 140 deletions(-) diff --git a/custom-plugin-decorators/apply-root-security/README.md b/custom-plugin-decorators/apply-root-security/README.md index 235b0a4..3d73f2c 100644 --- a/custom-plugin-decorators/apply-root-security/README.md +++ b/custom-plugin-decorators/apply-root-security/README.md @@ -12,13 +12,6 @@ A common scenario is when one spec (for example, `foo.yaml`) defines shared infr This decorator (`apply-root-security`) solves that: it reads the root-level `security` from a specified source file (for example `foo.yaml`) and sets it as root-level `security` on the document you are bundling when that document does not already define its own. It runs as a `bundle` step, giving you full control over which file supplies the requirement. -Supported spec types and what gets merged: - -| Spec | Root security | Security definitions | -| ------------- | ---------------------------------- | ---------------------------------------- | -| OAS3 / OAS3.1 | Merged into `root.security` | Merged into `components.securitySchemes` | -| OAS2 | Merged into `root.security` | Merged into `securityDefinitions` | - ## Code The `security-plugin` plugin defines the `decorator` section and the plugin `id`: @@ -29,7 +22,6 @@ export default function plugin() { id: "security-plugin", decorators: { oas3: {'apply-root-security': applyRootSecurity }, - oas2: {'apply-root-security': applyRootSecurity }, }, } } @@ -38,28 +30,18 @@ export default function plugin() { Here's the main part of the decorator (from `decorator.js`): ```javascript -const applyRootSecurity = ({ pathSecurityFile } = {}) => { +export const applyRootSecurity = ({ pathSecurityFile } = {}) => { return { Root: { - leave(root, { config, specVersion }) { + leave(root, { config }) { const doc = resolvePath(pathSecurityFile, config); - validateOpenapiSpecification(pathSecurityFile, doc, specVersion); - - if (specVersion === 'oas2') { - mergeSecurityRequirements(root, doc); - if (doc?.securityDefinitions !== undefined) { - root.securityDefinitions = { ...root.securityDefinitions, ...doc.securityDefinitions }; - } - } else { - mergeSecurityRequirements(root, doc); - mergeSecuritySchemes(root, doc); - } + mergeSecurityRequirements(root, doc); + mergeSecuritySchemes(root, doc); }, }, }; }; - ``` The `resolvePath` function resolves the path to the security file and returns its parsed content: @@ -72,41 +54,25 @@ function resolvePath(pathSecurityFile, config) { }; ``` -The `validateOpenapiSpecification` function checks that the security file format matches the target spec version and throws a descriptive error if not — for example, if an OAS2 file is used with an OAS3 target: - -```javascript -function validateOpenapiSpecification(pathSecurityFile, doc, specVersion) { - if (specVersion === 'oas2' && doc?.components?.securitySchemes !== undefined && doc?.securityDefinitions === undefined) { - throw new Error( - `apply-root-security: "${pathSecurityFile}" uses OAS3 components.securitySchemes but the target spec is OAS2. Use securityDefinitions instead.` - ); - } - if (specVersion !== 'oas2' && doc?.securityDefinitions !== undefined && doc?.components?.securitySchemes === undefined) { - throw new Error( - `apply-root-security: "${pathSecurityFile}" uses OAS2 securityDefinitions but the target spec is ${specVersion}. Use components.securitySchemes instead.` - ); - } -}; -``` - The `mergeSecurityRequirements` function appends root-level security requirements from the source file into the target document. If the target already has security requirements defined, the entries are appended rather than replaced: ```javascript -function mergeSecurityRequirements(root, doc) { - if (!Array.isArray(doc?.security)) return; - root.security = [...(root.security || []), ...doc.security]; +function mergeSecurityRequirements(target, source){ + if (!Array.isArray(source?.security) + || JSON.stringify(target.security) === JSON.stringify(source?.security)) return; + target.security = [...(target.security || []), ...source.security]; }; ``` The `mergeSecuritySchemes` function merges the security scheme definitions from the source file into `components.securitySchemes` on the target document. If the target already has schemes defined, they are preserved and the new ones are added alongside them: ```javascript -function mergeSecuritySchemes(root, doc) { - if (doc?.components?.securitySchemes === undefined) return; - if (!root.components) root.components = {}; - root.components.securitySchemes = { - ...root.components.securitySchemes, - ...doc.components.securitySchemes, +function mergeSecuritySchemes(target, source) { + if (source?.components?.securitySchemes === undefined) return; + if (!target.components) target.components = {}; + target.components.securitySchemes = { + ...target.components.securitySchemes, + ...source.components.securitySchemes, }; }; ``` @@ -122,12 +88,8 @@ decorators: pathSecurityFile: ./foo.yaml ``` -The `pathSecurityFile` must be in the same format as the spec you are bundling — an OAS3 file for OAS3 targets, an OAS2 file for OAS2 targets. - ## Examples -### OAS3 - Given two specs: **foo.yaml** — defines root-level security, no paths: @@ -176,59 +138,8 @@ redocly bundle bar.yaml -o result.yaml `result.yaml` will have `security: [{oauth2: []}]` and `components.securitySchemes.oauth2` applied. -### OAS2 - -Given two specs: - -**foo.yaml** — defines root-level security, no paths: -```yaml -swagger: "2.0" -info: - title: Foo - version: 1.0.0 -host: example.com -basePath: / -schemes: - - https -security: - - oauth2: [] -securityDefinitions: - oauth2: - type: oauth2 - flow: accessCode - authorizationUrl: https://example.com/oauth/authorize - tokenUrl: https://example.com/oauth/token - scopes: {} -paths: {} -``` - -**bar.yaml** — defines paths, no security: -```yaml -swagger: "2.0" -info: - title: Bar - version: 1.0.0 -host: example.com -basePath: / -schemes: - - https -paths: - /pets: - get: - summary: Get pets example - operationId: getPetsExample - responses: - 200: - description: OK - 400: - description: Bad request -``` - -`result.yaml` will have `security: [{oauth2: []}]` and `securityDefinitions.oauth2` applied. - ## References - [Redocly join command](https://redocly.com/docs/cli/commands/join) - [Custom decorators in plugins](https://redocly.com/docs/cli/custom-plugins/custom-decorators) - [Security requirement object (OpenAPI)](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) -- [Security requirement object (OpenAPI 2 / Swagger)](https://swagger.io/specification/v2/#security-requirement-object) diff --git a/custom-plugin-decorators/apply-root-security/decorator.js b/custom-plugin-decorators/apply-root-security/decorator.js index b20bea5..29e08c0 100644 --- a/custom-plugin-decorators/apply-root-security/decorator.js +++ b/custom-plugin-decorators/apply-root-security/decorator.js @@ -1,20 +1,11 @@ -const applyRootSecurity = ({ pathSecurityFile } = {}) => { +export const applyRootSecurity = ({ pathSecurityFile } = {}) => { return { Root: { - leave(root, { config, specVersion }) { + leave(root, { config }) { const doc = resolvePath(pathSecurityFile, config); - validateOpenapiSpecification(pathSecurityFile, doc, specVersion); - - if (specVersion === 'oas2') { - mergeSecurityRequirements(root, doc); - if (doc?.securityDefinitions !== undefined) { - root.securityDefinitions = { ...root.securityDefinitions, ...doc.securityDefinitions }; - } - } else { - mergeSecurityRequirements(root, doc); - mergeSecuritySchemes(root, doc); - } + mergeSecurityRequirements(root, doc); + mergeSecuritySchemes(root, doc); }, }, }; @@ -26,29 +17,17 @@ function resolvePath(pathSecurityFile, config) { return yaml.load(fs.readFileSync(absolutePath, 'utf8')); }; -function validateOpenapiSpecification(pathSecurityFile, doc, specVersion) { - if (specVersion === 'oas2' && doc?.components?.securitySchemes !== undefined && doc?.securityDefinitions === undefined) { - throw new Error( - `apply-root-security: "${pathSecurityFile}" uses OAS3 components.securitySchemes but the target spec is OAS2. Use securityDefinitions instead.` - ); - } - if (specVersion !== 'oas2' && doc?.securityDefinitions !== undefined && doc?.components?.securitySchemes === undefined) { - throw new Error( - `apply-root-security: "${pathSecurityFile}" uses OAS2 securityDefinitions but the target spec is ${specVersion}. Use components.securitySchemes instead.` - ); - } -}; - -function mergeSecurityRequirements(root, doc) { - if (!Array.isArray(doc?.security)) return; - root.security = [...(root.security || []), ...doc.security]; +function mergeSecurityRequirements(target, source){ + if (!Array.isArray(source?.security) + || JSON.stringify(target.security) === JSON.stringify(source?.security)) return; + target.security = [...(target.security || []), ...source.security]; }; -function mergeSecuritySchemes(root, doc) { - if (doc?.components?.securitySchemes === undefined) return; - if (!root.components) root.components = {}; - root.components.securitySchemes = { - ...root.components.securitySchemes, - ...doc.components.securitySchemes, +function mergeSecuritySchemes(target, source) { + if (source?.components?.securitySchemes === undefined) return; + if (!target.components) target.components = {}; + target.components.securitySchemes = { + ...target.components.securitySchemes, + ...source.components.securitySchemes, }; }; diff --git a/custom-plugin-decorators/apply-root-security/plugin.js b/custom-plugin-decorators/apply-root-security/plugin.js index 4f1b020..bbb1597 100644 --- a/custom-plugin-decorators/apply-root-security/plugin.js +++ b/custom-plugin-decorators/apply-root-security/plugin.js @@ -1,11 +1,10 @@ -import applyRootSecurity from "./decorator.js"; +import { applyRootSecurity } from "./decorator.js"; export default function plugin() { return { id: "security-plugin", decorators: { oas3: {'apply-root-security': applyRootSecurity }, - oas2: {'apply-root-security': applyRootSecurity }, }, } } From e65e84cc11207b1d884ee9531b50579e174777c4 Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Fri, 12 Jun 2026 12:10:12 +0300 Subject: [PATCH 8/9] chore: improve logic --- .../apply-root-security/README.md | 51 +++++++++---------- .../apply-root-security/decorator.js | 37 +++++++------- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/custom-plugin-decorators/apply-root-security/README.md b/custom-plugin-decorators/apply-root-security/README.md index 3d73f2c..796f027 100644 --- a/custom-plugin-decorators/apply-root-security/README.md +++ b/custom-plugin-decorators/apply-root-security/README.md @@ -31,19 +31,37 @@ Here's the main part of the decorator (from `decorator.js`): ```javascript export const applyRootSecurity = ({ pathSecurityFile } = {}) => { + let source = null; + return { Root: { - leave(root, { config }) { - const doc = resolvePath(pathSecurityFile, config); - - mergeSecurityRequirements(root, doc); - mergeSecuritySchemes(root, doc); + enter(_root, { config }) { + source = resolvePath(pathSecurityFile, config); + }, + leave(root) { + if (!Array.isArray(source?.security) + || JSON.stringify(root.security) === JSON.stringify(source?.security)) return; + root.security = [...(root.security || []), ...source.security]; }, }, + Components(components) { + if (source?.components?.securitySchemes) { + components.securitySchemes = { + ...components.securitySchemes, + ...source.components.securitySchemes, + }; + } + } }; }; ``` +In summary, this decorator does the following: + +1. Load the source security file once in `Root.enter` and share it across all visitor hooks. +2. Visit the `Root` node and apply any security requirements from the source file that are not already present in the target document. +3. Visit the `Components` node and merge security scheme definitions from the source file into the target, preserving existing schemes. + The `resolvePath` function resolves the path to the security file and returns its parsed content: ```javascript @@ -54,29 +72,6 @@ function resolvePath(pathSecurityFile, config) { }; ``` -The `mergeSecurityRequirements` function appends root-level security requirements from the source file into the target document. If the target already has security requirements defined, the entries are appended rather than replaced: - -```javascript -function mergeSecurityRequirements(target, source){ - if (!Array.isArray(source?.security) - || JSON.stringify(target.security) === JSON.stringify(source?.security)) return; - target.security = [...(target.security || []), ...source.security]; -}; -``` - -The `mergeSecuritySchemes` function merges the security scheme definitions from the source file into `components.securitySchemes` on the target document. If the target already has schemes defined, they are preserved and the new ones are added alongside them: - -```javascript -function mergeSecuritySchemes(target, source) { - if (source?.components?.securitySchemes === undefined) return; - if (!target.components) target.components = {}; - target.components.securitySchemes = { - ...target.components.securitySchemes, - ...source.components.securitySchemes, - }; -}; -``` - Add the following to `redocly.yaml`: ```yaml diff --git a/custom-plugin-decorators/apply-root-security/decorator.js b/custom-plugin-decorators/apply-root-security/decorator.js index 29e08c0..9b92484 100644 --- a/custom-plugin-decorators/apply-root-security/decorator.js +++ b/custom-plugin-decorators/apply-root-security/decorator.js @@ -1,13 +1,25 @@ export const applyRootSecurity = ({ pathSecurityFile } = {}) => { + let source = null; + return { Root: { - leave(root, { config }) { - const doc = resolvePath(pathSecurityFile, config); - - mergeSecurityRequirements(root, doc); - mergeSecuritySchemes(root, doc); + enter(_root, { config }) { + source = resolvePath(pathSecurityFile, config); + }, + leave(root) { + if (!Array.isArray(source?.security) + || JSON.stringify(root.security) === JSON.stringify(source?.security)) return; + root.security = [...(root.security || []), ...source.security]; }, }, + Components(components) { + if (source?.components?.securitySchemes) { + components.securitySchemes = { + ...components.securitySchemes, + ...source.components.securitySchemes, + }; + } + } }; }; @@ -16,18 +28,3 @@ function resolvePath(pathSecurityFile, config) { const absolutePath = path.isAbsolute(pathSecurityFile) ? pathSecurityFile : path.resolve(base, pathSecurityFile); return yaml.load(fs.readFileSync(absolutePath, 'utf8')); }; - -function mergeSecurityRequirements(target, source){ - if (!Array.isArray(source?.security) - || JSON.stringify(target.security) === JSON.stringify(source?.security)) return; - target.security = [...(target.security || []), ...source.security]; -}; - -function mergeSecuritySchemes(target, source) { - if (source?.components?.securitySchemes === undefined) return; - if (!target.components) target.components = {}; - target.components.securitySchemes = { - ...target.components.securitySchemes, - ...source.components.securitySchemes, - }; -}; From 79aca50238c128962dabced84a34e01229844d52 Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Fri, 12 Jun 2026 16:37:31 +0300 Subject: [PATCH 9/9] chore: adjust summary --- custom-plugin-decorators/apply-root-security/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/custom-plugin-decorators/apply-root-security/README.md b/custom-plugin-decorators/apply-root-security/README.md index 796f027..c6beb2f 100644 --- a/custom-plugin-decorators/apply-root-security/README.md +++ b/custom-plugin-decorators/apply-root-security/README.md @@ -58,9 +58,8 @@ export const applyRootSecurity = ({ pathSecurityFile } = {}) => { In summary, this decorator does the following: -1. Load the source security file once in `Root.enter` and share it across all visitor hooks. -2. Visit the `Root` node and apply any security requirements from the source file that are not already present in the target document. -3. Visit the `Components` node and merge security scheme definitions from the source file into the target, preserving existing schemes. +1. Visit the `Root` node and apply any security requirements from the source file that are not already present in the target document. +2. Visit the `Components` node and merge security scheme definitions from the source file into the target, preserving existing schemes. The `resolvePath` function resolves the path to the security file and returns its parsed content: