Repository: Redocly/redocly-cli
Pull request #1259 (2023-09-13 17:16:16)

fix: update redocly.yaml validation
## What/Why/How?

Redocly config validation update. 
Copied and pasted validation schemas from the Portal, and added additional 'rules' validation (`metadata-schema` and `custom-fields-schema`). 


## Reference

## Testing

Tested on Portal playground's and Rebilly redocly.yaml files.

## Screenshots (optional)

## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows (internal)
- [ ] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: c5744f6c20977f6b9578fb0884a61b2072d564cd

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name                  | Type  |
| --------------------- | ----- |
| @redocly/openapi-core | Patch |
| @redocly/cli          | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/Redocly/redocly-cli/new/fix/update-redocly-yaml-validation?filename=.changeset/late-crews-enjoy.md&value=---%0A%22%40redocly%2Fopenapi-core%22%3A%20patch%0A---%0A%0Afix%3A%20update%20redocly.yaml%20validation%0A)


github-actions[bot]: | Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `redocly lint packages/core/src/benchmark/benches/rebilly.yaml` | 1.766 ± 0.045 | 1.705 | 1.859 | 1.00 |
| `redocly-next lint packages/core/src/benchmark/benches/rebilly.yaml` | 1.859 ± 0.047 | 1.813 | 1.944 | 1.05 ± 0.04 |

<!-- thollander/actions-comment-pull-request "hyperfine-comparison" -->
github-actions[bot]: <!-- jest coverage report action for options with hash 3339ae53a38c44382c0fb4491bdb282d -->

## Coverage report 



| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category   | Percentage | Covered / Total |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :--------- | :--------- | :-------------: |
|                                                                           🟡                                                                           | Statements | 75.44%     |    3793/5028    |
|                                                                           🟡                                                                           | Branches   | 66.3%      |    2103/3172    |
|                                                                           🟡                                                                           | Functions  | 68.6%      |     638/930     |
|                                                                           🟡                                                                           | Lines      | 75.59%     |    3543/4687    |

<details><summary>Show new covered files 🐣</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                                                      | Statements | Branches | Functions | Lines |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------- | :--------- | :------- | :-------- | :---- |
|                                                                           🟢                                                                           | <div title="core/src/types/portal-config-schema.ts">`...` / portal-config-schema.ts</div> | 100%       | 100%     | 100%      | 100%  |
|                                                                           🟢                                                                           | <div title="core/src/types/theme-config.ts">`...` / theme-config.ts</div>                 | 100%       | 100%     | 100%      | 100%  |

</details>


<details><summary>Show files with reduced coverage 🔻</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File                                                                      | Statements                                                     | Branches                                                       | Functions | Lines                                                         |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------ | :------------------------------------------------------------- | :------------------------------------------------------------- | :-------- | :------------------------------------------------------------ |
|                                                                           🟡                                                                           | <div title="core/src/types/redocly-yaml.ts">`...` / redocly-yaml.ts</div> | <div title="Base coverage is: 80.37%">79.28% (-1.09% 🔻)</div> | <div title="Base coverage is: 39.29%">34.38% (-4.91% 🔻)</div> | 55.56%    | <div title="Base coverage is: 80.19%">79.09% (-1.1% 🔻)</div> |

</details>

## Test suite run success
625 tests passing in 91 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from c5744f6c20977f6b9578fb0884a61b2072d564cd</p>

lornajane: Are we asking docs in this pull request, or capturing in an issue what we need to add later? Link the issue if its separate
tatomyr: @lornajane I don't think there's something to be documented here. We are only catching up with the Portal config, no changes to the CLI config itself. 
lornajane: Got it. I thought we were also adopting the rules and would need docs. Now I see it's just the validation
tatomyr: @SmoliyY I see your point, but those don't make sense in the CLI itself as they are not quite rules and are only related to the Portal. 

Patch:
diff --git a/.changeset/long-rabbits-camp.md b/.changeset/long-rabbits-camp.md
new file mode 100644
index 00000000..8a8f8f56
--- /dev/null
+++ b/.changeset/long-rabbits-camp.md
@@ -0,0 +1,6 @@
+---
+'@redocly/openapi-core': patch
+'@redocly/cli': patch
+---
+
+Updated Redocly config validation.
diff --git a/packages/core/src/types/portal-config-schema.ts b/packages/core/src/types/portal-config-schema.ts
new file mode 100644
index 00000000..182d5108
--- /dev/null
+++ b/packages/core/src/types/portal-config-schema.ts
@@ -0,0 +1,343 @@
+import {
+  ApigeeDevOnboardingIntegrationAuthType,
+  AuthProviderType,
+  DEFAULT_TEAM_CLAIM_NAME,
+} from '../config';
+
+const oidcIssuerMetadataSchema = {
+  type: 'object',
+  properties: {
+    end_session_endpoint: { type: 'string' },
+    token_endpoint: { type: 'string' },
+    authorization_endpoint: { type: 'string' },
+    jwks_uri: { type: 'string' },
+  },
+  required: ['token_endpoint', 'authorization_endpoint'],
+  additionalProperties: true,
+} as const;
+
+const oidcProviderConfigSchema = {
+  type: 'object',
+  properties: {
+    type: { type: 'string', const: AuthProviderType.OIDC },
+    title: { type: 'string' },
+    configurationUrl: { type: 'string', minLength: 1 },
+    configuration: oidcIssuerMetadataSchema,
+    clientId: { type: 'string', minLength: 1 },
+    clientSecret: { type: 'string', minLength: 1 },
+    teamsClaimName: { type: 'string' },
+    teamsClaimMap: { type: 'object', additionalProperties: { type: 'string' } },
+    defaultTeams: { type: 'array', items: { type: 'string' } },
+    scopes: { type: 'array', items: { type: 'string' } },
+    tokenExpirationTime: { type: 'number' },
+    authorizationRequestCustomParams: { type: 'object', additionalProperties: { type: 'string' } },
+    tokenRequestCustomParams: { type: 'object', additionalProperties: { type: 'string' } },
+    audience: { type: 'array', items: { type: 'string' } },
+  },
+  required: ['type', 'clientId', 'clientSecret'],
+  oneOf: [{ required: ['configurationUrl'] }, { required: ['configuration'] }],
+  additionalProperties: false,
+} as const;
+
+const saml2ProviderConfigSchema = {
+  type: 'object',
+  properties: {
+    type: { type: 'string', const: AuthProviderType.SAML2 },
+    title: { type: 'string' },
+    issuerId: { type: 'string' },
+    entityId: { type: 'string' },
+    ssoUrl: { type: 'string' },
+    x509PublicCert: { type: 'string' },
+    teamsAttributeName: { type: 'string', default: DEFAULT_TEAM_CLAIM_NAME },
+    teamsAttributeMap: { type: 'object', additionalProperties: { type: 'string' } },
+    defaultTeams: { type: 'array', items: { type: 'string' } },
+  },
+  additionalProperties: false,
+  required: ['type', 'issuerId', 'ssoUrl', 'x509PublicCert'],
+} as const;
+
+const basicAuthProviderConfigSchema = {
+  type: 'object',
+  properties: {
+    type: { type: 'string', const: AuthProviderType.BASIC },
+    title: { type: 'string' },
+    credentials: {
+      type: 'array',
+      items: {
+        type: 'object',
+        properties: {
+          username: { type: 'string' },
+          password: { type: 'string' },
+          passwordHash: { type: 'string' },
+          teams: { type: 'array', items: { type: 'string' } },
+        },
+        required: ['username'],
+        additionalProperties: false,
+      },
+    },
+  },
+  required: ['type', 'credentials'],
+  additionalProperties: false,
+} as const;
+
+const authProviderConfigSchema = {
+  oneOf: [oidcProviderConfigSchema, saml2ProviderConfigSchema, basicAuthProviderConfigSchema],
+  discriminator: { propertyName: 'type' },
+} as const;
+
+export const ssoConfigSchema = {
+  type: 'object',
+  additionalProperties: authProviderConfigSchema,
+} as const;
+
+const redirectConfigSchema = {
+  type: 'object',
+  properties: {
+    to: { type: 'string' },
+    type: { type: 'number', default: 301 },
+  },
+  required: ['to'],
+  additionalProperties: false,
+} as const;
+
+export const apiConfigSchema = {
+  type: 'object',
+  properties: {
+    root: { type: 'string' },
+    rbac: { type: 'object', additionalProperties: true },
+    theme: {
+      type: 'object',
+      properties: {
+        openapi: { type: 'object', additionalProperties: true },
+      },
+      additionalProperties: false,
+    },
+    title: { type: 'string' },
+    metadata: { type: 'object', additionalProperties: true },
+  },
+  additionalProperties: true,
+  required: ['root'],
+} as const;
+
+const metadataConfigSchema = {
+  type: 'object',
+  additionalProperties: true,
+} as const;
+
+const seoConfigSchema = {
+  type: 'object',
+  properties: {
+    title: { type: 'string' },
+    description: { type: 'string' },
+    siteUrl: { type: 'string' },
+    image: { type: 'string' },
+    keywords: { type: 'array', items: { type: 'string' } },
+    lang: { type: 'string' },
+    jsonLd: { type: 'object' },
+    meta: {
+      type: 'array',
+      items: {
+        type: 'object',
+        properties: {
+          name: { type: 'string' },
+          content: { type: 'string' },
+        },
+        required: ['name', 'content'],
+        additionalProperties: false,
+      },
+    },
+  },
+  additionalProperties: false,
+} as const;
+
+const rbacScopeItemsSchema = { type: 'object', additionalProperties: { type: 'string' } } as const;
+
+const rbacConfigSchema = {
+  type: 'object',
+  properties: {
+    defaults: rbacScopeItemsSchema,
+  },
+  additionalProperties: rbacScopeItemsSchema,
+} as const;
+
+const graviteeAdapterConfigSchema = {
+  type: 'object',
+  properties: {
+    type: { type: 'string', const: 'GRAVITEE' },
+    apiBaseUrl: { type: 'string' },
+    env: { type: 'string' },
+    allowApiProductsOutsideCatalog: { type: 'boolean', default: false },
+    stage: { type: 'string', default: 'non-production' },
+
+    auth: { type: 'object', properties: { static: { type: 'string' } } },
+  },
+  additionalProperties: false,
+  required: ['type', 'apiBaseUrl'],
+} as const;
+
+const apigeeAdapterAuthOauth2Schema = {
+  type: 'object',
+  properties: {
+    type: { type: 'string', const: ApigeeDevOnboardingIntegrationAuthType.OAUTH2 },
+    tokenEndpoint: { type: 'string' },
+    clientId: { type: 'string' },
+    clientSecret: { type: 'string' },
+  },
+  additionalProperties: false,
+  required: ['type', 'tokenEndpoint', 'clientId', 'clientSecret'],
+} as const;
+
+const apigeeAdapterAuthServiceAccountSchema = {
+  type: 'object',
+  properties: {
+    type: { type: 'string', const: ApigeeDevOnboardingIntegrationAuthType.SERVICE_ACCOUNT },
+    serviceAccountEmail: { type: 'string' },
+    serviceAccountPrivateKey: { type: 'string' },
+  },
+  additionalProperties: false,
+  required: ['type', 'serviceAccountEmail', 'serviceAccountPrivateKey'],
+} as const;
+
+const apigeeXAdapterConfigSchema = {
+  type: 'object',
+  properties: {
+    type: { type: 'string', const: 'APIGEE_X' },
+    apiUrl: { type: 'string' },
+    stage: { type: 'string', default: 'non-production' },
+    organizationName: { type: 'string' },
+    ignoreApiProducts: { type: 'array', items: { type: 'string' } },
+    allowApiProductsOutsideCatalog: { type: 'boolean', default: false },
+    auth: {
+      type: 'object',
+      oneOf: [apigeeAdapterAuthOauth2Schema, apigeeAdapterAuthServiceAccountSchema],
+      discriminator: { propertyName: 'type' },
+    },
+  },
+  additionalProperties: false,
+  required: ['type', 'organizationName', 'auth'],
+} as const;
+
+const apigeeEdgeAdapterConfigSchema = {
+  ...apigeeXAdapterConfigSchema,
+  properties: {
+    ...apigeeXAdapterConfigSchema.properties,
+    type: { type: 'string', const: 'APIGEE_EDGE' },
+  },
+} as const;
+
+const devOnboardingAdapterConfigSchema = {
+  type: 'object',
+  oneOf: [apigeeXAdapterConfigSchema, apigeeEdgeAdapterConfigSchema, graviteeAdapterConfigSchema],
+  discriminator: { propertyName: 'type' },
+} as const;
+
+const devOnboardingConfigSchema = {
+  type: 'object',
+  required: ['adapters'],
+  additionalProperties: false,
+  properties: {
+    adapters: {
+      type: 'array',
+      items: devOnboardingAdapterConfigSchema,
+    },
+  },
+} as const;
+
+const responseHeaderSchema = {
+  type: 'object',
+  properties: {
+    name: { type: 'string' },
+    value: { type: 'string' },
+  },
+  additionalProperties: false,
+  required: ['name', 'value'],
+} as const;
+
+export const redoclyConfigSchema = {
+  type: 'object',
+  properties: {
+    licenseKey: { type: 'string' },
+    theme: { type: 'object', default: {} }, // ThemeConfig
+    redirects: { type: 'object', additionalProperties: redirectConfigSchema, default: {} },
+    seo: seoConfigSchema,
+    rbac: rbacConfigSchema,
+    responseHeaders: {
+      type: 'object',
+      additionalProperties: {
+        type: 'array',
+        items: responseHeaderSchema,
+      },
+    },
+    mockServer: {
+      type: 'object',
+      properties: {
+        off: { type: 'boolean', default: false },
+        position: { type: 'string', enum: ['first', 'last', 'replace', 'off'], default: 'first' },
+        strictExamples: { type: 'boolean', default: false },
+        errorIfForcedExampleNotFound: { type: 'boolean', default: false },
+        description: { type: 'string' },
+      },
+    },
+    apis: {
+      type: 'object',
+      additionalProperties: apiConfigSchema,
+    },
+    sso: ssoConfigSchema,
+    developerOnboarding: devOnboardingConfigSchema,
+    i18n: {
+      type: 'object',
+      properties: {
+        defaultLocale: {
+          type: 'string',
+        },
+        locales: {
+          type: 'array',
+          items: {
+            type: 'object',
+            properties: {
+              code: {
+                type: 'string',
+              },
+              name: {
+                type: 'string',
+              },
+            },
+            required: ['code'],
+          },
+        },
+      },
+      additionalProperties: false,
+      required: ['defaultLocale', 'locales'],
+    },
+    metadata: metadataConfigSchema,
+  },
+  default: {},
+  additionalProperties: true,
+} as const;
+
+export const environmentSchema = {
+  oneOf: [
+    { ...redoclyConfigSchema, additionalProperties: false },
+    {
+      type: 'object',
+      properties: {
+        $ref: { type: 'string' },
+      },
+      required: ['$ref'],
+      additionalProperties: false,
+    },
+  ],
+} as const;
+
+export const rootRedoclyConfigSchema = {
+  ...redoclyConfigSchema,
+  properties: {
+    ...redoclyConfigSchema.properties,
+    env: {
+      type: 'object',
+      additionalProperties: environmentSchema,
+    },
+  },
+  default: {},
+  required: ['redirects'],
+} as const;
diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts
index e19dd277..ee20912e 100644
--- a/packages/core/src/types/redocly-yaml.ts
+++ b/packages/core/src/types/redocly-yaml.ts
@@ -1,15 +1,7 @@
-import {
-  devOnboardingConfigSchema,
-  i18nConfigSchema,
-  mockServerConfigSchema,
-  rbacConfigSchema,
-  redirectConfigSchema,
-  responseHeaderSchema,
-  seoConfigSchema,
-  ssoConfigSchema,
-  themeConfigSchema,
-} from './config-external-schemas';
+import { rootRedoclyConfigSchema, apiConfigSchema } from './portal-config-schema';
+import { themeConfigSchema } from './theme-config';
 import { NodeType, listOf } from '.';
+import { Oas3_1Types } from './oas3_1';
 import { omitObjectProps, pickObjectProps, isCustomRuleId } from '../utils';
 
 const builtInRulesList = [
@@ -68,6 +60,7 @@ const builtInRulesList = [
   'spec-strict-refs',
   'component-name-unique',
 ];
+
 const nodeTypesList = [
   'any',
   'Root',
@@ -165,12 +158,13 @@ const RootConfigStyleguide: NodeType = {
 
 const ConfigRoot: NodeType = {
   properties: {
-    organization: { type: 'string' },
-    apis: 'ConfigApis',
+    ...rootRedoclyConfigSchema.properties,
     ...RootConfigStyleguide.properties,
+    apis: 'ConfigApis',
     theme: 'ConfigRootTheme',
     'features.openapi': 'ConfigReferenceDocs', // deprecated
     'features.mockServer': 'ConfigMockServer', // deprecated
+    organization: { type: 'string' },
     region: { enum: ['us', 'eu'] },
     telemetry: { enum: ['on', 'off'] },
     resolve: {
@@ -185,15 +179,6 @@ const ConfigRoot: NodeType = {
         type: 'string',
       },
     },
-    redirects: { type: 'object', additionalProperties: redirectConfigSchema },
-    licenseKey: { type: 'string' },
-    seo: seoConfigSchema,
-    rbac: rbacConfigSchema,
-    responseHeaders: responseHeaderSchema,
-    mockServer: mockServerConfigSchema,
-    sso: ssoConfigSchema,
-    developerOnboarding: devOnboardingConfigSchema,
-    i18n: i18nConfigSchema,
   },
 };
 
@@ -204,6 +189,7 @@ const ConfigApis: NodeType = {
 
 const ConfigApisProperties: NodeType = {
   properties: {
+    ...apiConfigSchema.properties,
     root: { type: 'string' },
     labels: {
       type: 'array',
@@ -223,9 +209,6 @@ const ConfigApisProperties: NodeType = {
         type: 'string',
       },
     },
-    title: { type: 'string' },
-    rbac: { type: 'object' },
-    metadata: { type: 'object' },
   },
   required: ['root'],
 };
@@ -263,6 +246,8 @@ const Rules: NodeType = {
       } else {
         return 'ObjectRule';
       }
+    } else if (key === 'metadata-schema' || key === 'custom-fields-schema') {
+      return 'Schema';
     }
     // Otherwise is considered as invalid
     return;
@@ -572,7 +557,7 @@ const TryItButton: NodeType = {
   },
 };
 
-const Components: NodeType = {
+const ConfigThemeComponents: NodeType = {
   properties: {
     buttons: 'ButtonsConfig',
     httpBadges: 'HttpBadgesConfig',
@@ -619,7 +604,7 @@ const SchemaColorsConfig: NodeType = {
   },
 };
 
-const Schema: NodeType = {
+const ConfigThemeSchema: NodeType = {
   properties: {
     breakFieldNames: { type: 'boolean' },
     caretColor: { type: 'string' },
@@ -759,7 +744,7 @@ const CodeBlock: NodeType = {
   },
 };
 
-const Logo: NodeType = {
+const ConfigThemeLogo: NodeType = {
   properties: {
     gutter: { type: 'string' },
     maxHeight: { type: 'string' },
@@ -815,13 +800,13 @@ const ConfigTheme: NodeType = {
     breakpoints: 'Breakpoints',
     codeBlock: 'CodeBlock',
     colors: 'ThemeColors',
-    components: 'Components',
+    components: 'ConfigThemeComponents',
     layout: 'Layout',
-    logo: 'Logo',
+    logo: 'ConfigThemeLogo',
     fab: 'Fab',
     overrides: 'Overrides',
     rightPanel: 'RightPanel',
-    schema: 'Schema',
+    schema: 'ConfigThemeSchema',
     shape: 'Shape',
     sidebar: 'Sidebar',
     spacing: 'ThemeSpacing',
@@ -973,6 +958,7 @@ const ConfigMockServer: NodeType = {
 };
 
 export const ConfigTypes: Record<string, NodeType> = {
+  ...Oas3_1Types,
   Assert,
   ConfigRoot,
   ConfigApis,
@@ -1018,7 +1004,7 @@ export const ConfigTypes: Record<string, NodeType> = {
   LinksConfig,
   TokenProps,
   CodeBlock,
-  Logo,
+  ConfigThemeLogo,
   Fab,
   ButtonOverrides,
   Overrides,
@@ -1029,9 +1015,9 @@ export const ConfigTypes: Record<string, NodeType> = {
   ThemeSpacing,
   GenerateCodeSamples,
   GroupItemsConfig,
-  Components,
+  ConfigThemeComponents,
   Layout,
-  Schema,
+  ConfigThemeSchema,
   Sidebar,
   Heading,
   Typography,
diff --git a/packages/core/src/types/config-external-schemas.ts b/packages/core/src/types/theme-config.ts
similarity index 65%
rename from packages/core/src/types/config-external-schemas.ts
rename to packages/core/src/types/theme-config.ts
index 7c0a9700..f679b755 100644
--- a/packages/core/src/types/config-external-schemas.ts
+++ b/packages/core/src/types/theme-config.ts
@@ -1,271 +1,8 @@
-import {
-  ApigeeDevOnboardingIntegrationAuthType,
-  AuthProviderType,
-  DEFAULT_TEAM_CLAIM_NAME,
-} from '../config';
-
-const oidcIssuerMetadataSchema = {
-  type: 'object',
-  properties: {
-    end_session_endpoint: { type: 'string' },
-    token_endpoint: { type: 'string' },
-    authorization_endpoint: { type: 'string' },
-  },
-  required: ['token_endpoint', 'authorization_endpoint'],
-  additionalProperties: true,
-} as const;
-
-const oidcProviderConfigSchema = {
-  type: 'object',
-  properties: {
-    type: { type: 'string', const: AuthProviderType.OIDC },
-    title: { type: 'string' },
-    configurationUrl: { type: 'string', minLength: 1 },
-    configuration: oidcIssuerMetadataSchema,
-    clientId: { type: 'string', minLength: 1 },
-    clientSecret: { type: 'string', minLength: 1 },
-    teamsClaimName: { type: 'string' },
-    defaultTeams: { type: 'array', items: { type: 'string' } },
-    scopes: { type: 'array', items: { type: 'string' } },
-    tokenExpirationTime: { type: 'number' },
-    authorizationRequestCustomParams: { type: 'object', additionalProperties: { type: 'string' } },
-    tokenRequestCustomParams: { type: 'object', additionalProperties: { type: 'string' } },
-  },
-  required: ['type', 'clientId', 'clientSecret'],
-  oneOf: [{ required: ['configurationUrl'] }, { required: ['configuration'] }],
-  additionalProperties: false,
-} as const;
-
-const saml2ProviderConfigSchema = {
-  type: 'object',
-  properties: {
-    type: { type: 'string', const: AuthProviderType.SAML2 },
-    title: { type: 'string' },
-    issuerId: { type: 'string' },
-    entityId: { type: 'string' },
-    ssoUrl: { type: 'string' },
-    x509PublicCert: { type: 'string' },
-    teamsAttributeName: { type: 'string', default: DEFAULT_TEAM_CLAIM_NAME },
-    teamsAttributeMap: { type: 'object', additionalProperties: { type: 'string' } },
-    defaultTeams: { type: 'array', items: { type: 'string' } },
-  },
-  additionalProperties: false,
-  required: ['type', 'issuerId', 'ssoUrl', 'x509PublicCert'],
-} as const;
-
-const basicAuthProviderConfigSchema = {
-  type: 'object',
-  properties: {
-    type: { type: 'string', const: AuthProviderType.BASIC },
-    title: { type: 'string' },
-    credentials: {
-      type: 'array',
-      items: {
-        type: 'object',
-        properties: {
-          username: { type: 'string' },
-          password: { type: 'string' },
-          passwordHash: { type: 'string' },
-          teams: { type: 'array', items: { type: 'string' } },
-        },
-        required: ['username'],
-        additionalProperties: false,
-      },
-    },
-  },
-  required: ['type', 'credentials'],
-  additionalProperties: false,
-} as const;
-
-const authProviderConfigSchema = {
-  oneOf: [oidcProviderConfigSchema, saml2ProviderConfigSchema, basicAuthProviderConfigSchema],
-  discriminator: { propertyName: 'type' },
-} as const;
-
-const rbacScopeItemsSchema = { type: 'object', additionalProperties: { type: 'string' } } as const;
-
-export const rbacConfigSchema = {
-  type: 'object',
-  properties: {
-    defaults: rbacScopeItemsSchema,
-  },
-  additionalProperties: rbacScopeItemsSchema,
-} as const;
-
-export const ssoConfigSchema = {
-  type: 'object',
-  additionalProperties: authProviderConfigSchema,
-} as const;
-
-export const redirectConfigSchema = {
-  type: 'object',
-  properties: {
-    to: { type: 'string' },
-    type: { type: 'number', default: 301 },
-  },
-  required: ['to'],
-  additionalProperties: false,
-} as const;
-
-export const seoConfigSchema = {
-  type: 'object',
-  properties: {
-    title: { type: 'string' },
-    description: { type: 'string' },
-    siteUrl: { type: 'string' },
-    image: { type: 'string' },
-    keywords: { type: 'array', items: { type: 'string' } },
-    lang: { type: 'string' },
-    jsonLd: { type: 'object' },
-    meta: {
-      type: 'array',
-      items: {
-        type: 'object',
-        properties: {
-          name: { type: 'string' },
-          content: { type: 'string' },
-        },
-        required: ['name', 'content'],
-        additionalProperties: false,
-      },
-    },
-  },
-  additionalProperties: false,
-} as const;
-
-const apigeeAdapterAuthOauth2Schema = {
-  type: 'object',
-  properties: {
-    type: { type: 'string', const: ApigeeDevOnboardingIntegrationAuthType.OAUTH2 },
-    tokenEndpoint: { type: 'string' },
-    clientId: { type: 'string' },
-    clientSecret: { type: 'string' },
-  },
-  additionalProperties: false,
-  required: ['type', 'tokenEndpoint', 'clientId', 'clientSecret'],
-} as const;
-
-const apigeeAdapterAuthServiceAccountSchema = {
-  type: 'object',
-  properties: {
-    type: { type: 'string', const: ApigeeDevOnboardingIntegrationAuthType.SERVICE_ACCOUNT },
-    serviceAccountEmail: { type: 'string' },
-    serviceAccountPrivateKey: { type: 'string' },
-  },
-  additionalProperties: false,
-  required: ['type', 'serviceAccountEmail', 'serviceAccountPrivateKey'],
-} as const;
-
-const apigeeXAdapterConfigSchema = {
-  type: 'object',
-  properties: {
-    type: { type: 'string', const: 'APIGEE_X' },
-    apiUrl: { type: 'string' },
-    stage: { type: 'string', default: 'non-production' },
-    organizationName: { type: 'string' },
-    ignoreApiProducts: { type: 'array', items: { type: 'string' } },
-    allowApiProductsOutsideCatalog: { type: 'boolean', default: false },
-    auth: {
-      type: 'object',
-      oneOf: [apigeeAdapterAuthOauth2Schema, apigeeAdapterAuthServiceAccountSchema],
-      discriminator: { propertyName: 'type' },
-    },
-  },
-  additionalProperties: false,
-  required: ['type', 'organizationName', 'auth'],
-} as const;
-
-const apigeeEdgeAdapterConfigSchema = {
-  ...apigeeXAdapterConfigSchema,
-  properties: {
-    ...apigeeXAdapterConfigSchema.properties,
-    type: { type: 'string', const: 'APIGEE_EDGE' },
-  },
-} as const;
-
-const graviteeAdapterConfigSchema = {
-  type: 'object',
-  properties: {
-    type: { type: 'string', const: 'GRAVITEE' },
-    apiBaseUrl: { type: 'string' },
-    env: { type: 'string' },
-    allowApiProductsOutsideCatalog: { type: 'boolean', default: false },
-    stage: { type: 'string', default: 'non-production' },
-
-    auth: { type: 'object', properties: { static: { type: 'string' } } },
-  },
-  additionalProperties: false,
-  required: ['type', 'apiBaseUrl'],
-} as const;
-
-const devOnboardingAdapterConfigSchema = {
-  type: 'object',
-  oneOf: [apigeeXAdapterConfigSchema, apigeeEdgeAdapterConfigSchema, graviteeAdapterConfigSchema],
-  discriminator: { propertyName: 'type' },
-} as const;
-
-export const devOnboardingConfigSchema = {
-  type: 'object',
-  required: ['adapters'],
-  additionalProperties: false,
-  properties: {
-    adapters: {
-      type: 'array',
-      items: devOnboardingAdapterConfigSchema,
-    },
-  },
-} as const;
-
-export const responseHeaderSchema = {
-  type: 'object',
-  properties: {
-    name: { type: 'string' },
-    value: { type: 'string' },
-  },
-  additionalProperties: false,
-  required: ['name', 'value'],
-} as const;
-
-export const i18nConfigSchema = {
-  type: 'object',
-  properties: {
-    defaultLocale: {
-      type: 'string',
-    },
-    locales: {
-      type: 'array',
-      items: {
-        type: 'object',
-        properties: {
-          code: {
-            type: 'string',
-          },
-          name: {
-            type: 'string',
-          },
-        },
-        required: ['code'],
-      },
-    },
-  },
-  required: ['defaultLocale', 'locales'],
-} as const;
-
-export const mockServerConfigSchema = {
-  type: 'object',
-  properties: {
-    off: { type: 'boolean', default: false },
-    position: { type: 'string', enum: ['first', 'last', 'replace', 'off'], default: 'first' },
-    strictExamples: { type: 'boolean', default: false },
-    errorIfForcedExampleNotFound: { type: 'boolean', default: false },
-    description: { type: 'string' },
-  },
-} as const;
-
 const logoConfigSchema = {
   type: 'object',
   properties: {
     image: { type: 'string' },
+    srcSet: { type: 'string' },
     altText: { type: 'string' },
     link: { type: 'string' },
     favicon: { type: 'string' },
@@ -359,8 +96,6 @@ const markdownConfigSchema = {
       type: 'object',
       properties: {
         baseUrl: { type: 'string' },
-        icon: { type: 'string' },
-        text: { type: 'string', default: 'Edit this page' },
         ...hideConfigSchema.properties,
       },
       additionalProperties: false,
@@ -457,37 +192,17 @@ const googleAnalyticsConfigSchema = {
   properties: {
     includeInDevelopment: { type: 'boolean' },
     trackingId: { type: 'string' },
+
+    conversionId: { type: 'string' },
+    floodlightId: { type: 'string' },
+
     head: { type: 'boolean' },
     respectDNT: { type: 'boolean' },
-    anonymize: { type: 'boolean' },
     exclude: { type: 'array', items: { type: 'string' } },
-    optimizeId: { type: 'string' },
-    experimentId: { type: 'string' },
-    variationId: { type: 'string' },
-    enableWebVitalsTracking: { type: 'boolean' },
 
-    defer: { type: 'boolean' },
-    sampleRate: { type: 'number' },
-    name: { type: 'string' },
-    clientId: { type: 'string' },
-    siteSpeedSampleRate: { type: 'number' },
-    alwaysSendReferrer: { type: 'boolean' },
-    allowAnchor: { type: 'boolean' },
-    cookieName: { type: 'string' },
-    cookieFlags: { type: 'string' },
-    cookieDomain: { type: 'string' },
+    optimizeId: { type: 'string' },
+    anonymizeIp: { type: 'boolean' },
     cookieExpires: { type: 'number' },
-    storeGac: { type: 'boolean' },
-    legacyCookieDomain: { type: 'string' },
-    legacyHistoryImport: { type: 'boolean' },
-    allowLinker: { type: 'boolean' },
-    storage: { type: 'string' },
-
-    allowAdFeatures: { type: 'boolean' },
-    dataSource: { type: 'string' },
-    queueTime: { type: 'number' },
-    forceSSL: { type: 'boolean' },
-    transport: { type: 'string' },
   },
   additionalProperties: false,
   required: ['trackingId'],
@@ -513,6 +228,11 @@ const navItemSchema = {
     label: { type: 'string' },
     separator: { type: 'string' },
     separatorLine: { type: 'boolean' },
+    linePosition: {
+      type: 'string',
+      enum: ['top', 'bottom'],
+      default: 'top',
+    },
     version: { type: 'string' },
     menuStyle: { type: 'string', enum: ['drilldown'] },
     expanded: { type: 'string', const: 'always' },
@@ -579,6 +299,14 @@ const scorecardConfigSchema = {
   required: ['levels'],
   properties: {
     failBuildIfBelowMinimum: { type: 'boolean', default: false },
+    teamMetadataProperty: {
+      type: 'object',
+      properties: {
+        property: { type: 'string' },
+        label: { type: 'string' },
+        default: { type: 'string' },
+      },
+    },
     levels: {
       type: 'array',
       items: {
@@ -622,11 +350,15 @@ const scorecardConfigSchema = {
 const catalogSchema = {
   type: 'object',
   additionalProperties: true,
-  required: ['slug', 'filters', 'groupByFirstFilter', 'items'],
+  required: ['slug', 'items'],
   properties: {
     slug: { type: 'string' },
     filters: { type: 'array', items: catalogFilterSchema },
     groupByFirstFilter: { type: 'boolean' },
+    filterValuesCasing: {
+      type: 'string',
+      enum: ['sentence', 'original', 'lowercase', 'uppercase'],
+    },
     items: navItemsSchema,
     requiredPermission: { type: 'string' },
     separateVersions: { type: 'boolean' },
@@ -670,11 +402,30 @@ export const themeConfigSchema = {
       properties: {
         items: navItemsSchema,
         copyrightText: { type: 'string' },
+        logo: hideConfigSchema,
         ...hideConfigSchema.properties,
       },
       additionalProperties: false,
     },
-    sidebar: hideConfigSchema,
+    sidebar: {
+      type: 'object',
+      properties: {
+        separatorLine: { type: 'boolean' },
+        linePosition: {
+          type: 'string',
+          enum: ['top', 'bottom'],
+          default: 'bottom',
+        },
+        ...hideConfigSchema.properties,
+      },
+      additionalProperties: false,
+    },
+    seo: {
+      type: 'object',
+      properties: {
+        title: { type: 'string' },
+      },
+    },
     scripts: {
       type: 'object',
       properties: {
@@ -873,6 +624,15 @@ export const themeConfigSchema = {
       additionalProperties: false,
       default: {},
     },
+    versionPicker: {
+      type: 'object',
+      properties: {
+        hide: { type: 'boolean' },
+        showForUnversioned: {
+          type: 'boolean',
+        },
+      },
+    },
     breadcrumbs: {
       type: 'object',
       properties: {
@@ -915,3 +675,9 @@ export const productThemeOverrideSchema = {
   additionalProperties: true,
   default: {},
 } as const;
+
+export enum ScorecardStatus {
+  BelowMinimum = 'Below minimum',
+  Highest = 'Highest',
+  Minimum = 'Minimum',
+}


Test patch:
diff --git a/__tests__/lint/deprecated-apiDefinitions/snapshot.js b/__tests__/lint/deprecated-apiDefinitions/snapshot.js
index e09d2edd..efd0848a 100644
--- a/__tests__/lint/deprecated-apiDefinitions/snapshot.js
+++ b/__tests__/lint/deprecated-apiDefinitions/snapshot.js
@@ -18,6 +18,8 @@ Warning was generated by the configuration spec rule.
 
 Property \`lint\` is not expected here.
 
+Did you mean: env ?
+
 1 | apiDefinitions:
 2 |   main: ./openapi.yaml
 3 | lint:
diff --git a/__tests__/lint/deprecated-lint/snapshot.js b/__tests__/lint/deprecated-lint/snapshot.js
index 464b2942..d9924ab4 100644
--- a/__tests__/lint/deprecated-lint/snapshot.js
+++ b/__tests__/lint/deprecated-lint/snapshot.js
@@ -6,6 +6,8 @@ exports[`E2E lint deprecated-lint 1`] = `
 
 Property \`lint\` is not expected here.
 
+Did you mean: env ?
+
 5 |       rules:
 6 |         operation-4xx-response: error
 7 | lint:
diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts
index 8a885bab..fb3575bb 100644
--- a/packages/core/src/__tests__/lint.test.ts
+++ b/packages/core/src/__tests__/lint.test.ts
@@ -111,6 +111,7 @@ describe('lint', () => {
           "severity": "error",
           "suggest": Array [
             "theme",
+            "env",
             "seo",
             "sso",
           ],
@@ -178,6 +179,7 @@ describe('lint', () => {
             "apis",
             "seo",
             "sso",
+            "env",
           ],
         },
       ]