@scalar/openapi-validator 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @scalar/openapi-validator
2
2
 
3
+ ## 0.1.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [#10489](https://github.com/scalar/scalar/pull/10489): Report invalid non-exploded structured cookies in OpenAPI 3.2 documents and prevent invalid request and code sample generation. Recommend `style: cookie` with `explode: true`.
8
+ - [#10485](https://github.com/scalar/scalar/pull/10485): Validate missing and mismatched path parameters for QUERY and methods under additionalOperations, including inherited path-level parameters.
9
+
3
10
  ## 0.1.10
4
11
 
5
12
  ## 0.1.9
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { OpenApiVersions as supportedVersions } from './specifications.js';
2
2
  export type { ErrorObject, OpenApiVersion, ThrowOnErrorOption, ValidationOutcome } from './types.js';
3
3
  export { type ValidateOptions, validate } from './validate.js';
4
+ export { validateCookieParameters } from './validate-cookie-parameters.js';
4
5
  export { validatePathParameters } from './validate-path-parameters.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,IAAI,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACvE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AACjG,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,IAAI,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACvE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AACjG,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAA;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { OpenApiVersions as supportedVersions } from './specifications.js';
2
2
  export { validate } from './validate.js';
3
+ export { validateCookieParameters } from './validate-cookie-parameters.js';
3
4
  export { validatePathParameters } from './validate-path-parameters.js';
@@ -0,0 +1,9 @@
1
+ import type { AnyObject } from '@scalar/types/utils';
2
+ import type { ErrorObject } from './types.js';
3
+ /**
4
+ * Checks cookie serialization declarations in OpenAPI 3.2 documents.
5
+ * Resolve references before calling to also check referenced parameters and schemas.
6
+ * Only OpenAPI parameter locations are visited; examples and extensions are data.
7
+ */
8
+ export declare const validateCookieParameters: (specification: AnyObject) => ErrorObject[];
9
+ //# sourceMappingURL=validate-cookie-parameters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-cookie-parameters.d.ts","sourceRoot":"","sources":["../src/validate-cookie-parameters.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,eAAe,SAAS,KAAG,WAAW,EAoG9E,CAAA"}
@@ -0,0 +1,103 @@
1
+ import { getCookieSerializationError } from '@scalar/helpers/http/get-cookie-serialization-error';
2
+ import { isHttpMethod } from '@scalar/helpers/http/is-http-method';
3
+ import { isObject } from '@scalar/helpers/object/is-object';
4
+ /**
5
+ * Checks cookie serialization declarations in OpenAPI 3.2 documents.
6
+ * Resolve references before calling to also check referenced parameters and schemas.
7
+ * Only OpenAPI parameter locations are visited; examples and extensions are data.
8
+ */
9
+ export const validateCookieParameters = (specification) => {
10
+ const [major, minor] = String(specification.openapi).split('.');
11
+ if (major !== '3' || minor !== '2') {
12
+ return [];
13
+ }
14
+ const errors = [];
15
+ const ancestors = new WeakSet();
16
+ const checkParameter = (parameter, path) => {
17
+ if (!isObject(parameter) || 'content' in parameter || typeof parameter.name !== 'string') {
18
+ return;
19
+ }
20
+ const message = getCookieSerializationError({
21
+ name: parameter.name,
22
+ in: String(parameter.in),
23
+ style: typeof parameter.style === 'string' ? parameter.style : undefined,
24
+ explode: typeof parameter.explode === 'boolean' ? parameter.explode : undefined,
25
+ schema: parameter.schema,
26
+ });
27
+ if (message) {
28
+ errors.push({ path: [...path, 'explode'], message });
29
+ }
30
+ };
31
+ const checkParameters = (parameters, path) => {
32
+ if (Array.isArray(parameters)) {
33
+ parameters.forEach((parameter, index) => checkParameter(parameter, [...path, String(index)]));
34
+ }
35
+ };
36
+ const visitCallback = (callback, path) => {
37
+ if (!isObject(callback) || ancestors.has(callback)) {
38
+ return;
39
+ }
40
+ ancestors.add(callback);
41
+ for (const [expression, pathItem] of Object.entries(callback)) {
42
+ if (!expression.startsWith('x-') && expression !== '$ref') {
43
+ visitPathItem(pathItem, [...path, expression]);
44
+ }
45
+ }
46
+ ancestors.delete(callback);
47
+ };
48
+ const visitOperation = (operation, path) => {
49
+ if (!isObject(operation)) {
50
+ return;
51
+ }
52
+ checkParameters(operation.parameters, [...path, 'parameters']);
53
+ if (isObject(operation.callbacks)) {
54
+ for (const [name, callback] of Object.entries(operation.callbacks)) {
55
+ visitCallback(callback, [...path, 'callbacks', name]);
56
+ }
57
+ }
58
+ };
59
+ const visitPathItem = (pathItem, path) => {
60
+ if (!isObject(pathItem) || ancestors.has(pathItem)) {
61
+ return;
62
+ }
63
+ ancestors.add(pathItem);
64
+ checkParameters(pathItem.parameters, [...path, 'parameters']);
65
+ for (const [method, operation] of Object.entries(pathItem)) {
66
+ if (isHttpMethod(method)) {
67
+ visitOperation(operation, [...path, method]);
68
+ }
69
+ }
70
+ if (isObject(pathItem.additionalOperations)) {
71
+ for (const [method, operation] of Object.entries(pathItem.additionalOperations)) {
72
+ visitOperation(operation, [...path, 'additionalOperations', method]);
73
+ }
74
+ }
75
+ ancestors.delete(pathItem);
76
+ };
77
+ for (const key of ['paths', 'webhooks']) {
78
+ const items = specification[key];
79
+ if (isObject(items)) {
80
+ for (const [name, pathItem] of Object.entries(items)) {
81
+ if (!name.startsWith('x-')) {
82
+ visitPathItem(pathItem, [key, name]);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ const components = specification.components;
88
+ if (isObject(components)) {
89
+ for (const [key, visit] of [
90
+ ['parameters', checkParameter],
91
+ ['pathItems', visitPathItem],
92
+ ['callbacks', visitCallback],
93
+ ]) {
94
+ const items = components[key];
95
+ if (isObject(items)) {
96
+ for (const [name, item] of Object.entries(items)) {
97
+ visit(item, ['components', key, name]);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ return errors;
103
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"validate-path-parameters.d.ts","sourceRoot":"","sources":["../src/validate-path-parameters.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAmB1C;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,SAAS,GAAG,WAAW,EAAE,CAoE9E"}
1
+ {"version":3,"file":"validate-path-parameters.d.ts","sourceRoot":"","sources":["../src/validate-path-parameters.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAmB1C;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,SAAS,GAAG,WAAW,EAAE,CAyE9E"}
@@ -1,3 +1,4 @@
1
+ import { HTTP_METHODS } from '@scalar/helpers/http/http-methods';
1
2
  import { isObjectLike } from '@scalar/helpers/object/is-object';
2
3
  import { deduplicateErrors } from '@scalar/json-schema-validator';
3
4
  /**
@@ -8,7 +9,7 @@ import { deduplicateErrors } from '@scalar/json-schema-validator';
8
9
  * hand-constructed document rather than validating them.
9
10
  */
10
11
  const isRecord = (value) => isObjectLike(value) && !Array.isArray(value);
11
- const OPERATION_KEYS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
12
+ const OPERATION_KEYS = new Set(HTTP_METHODS);
12
13
  const PATH_PARAMETER_PATTERN = /{([^}]+)}/g;
13
14
  /**
14
15
  * Validates path-template semantics that are not enforced by the JSON schema.
@@ -23,7 +24,15 @@ export function validatePathParameters(specification) {
23
24
  if (!isRecord(pathItem)) {
24
25
  continue;
25
26
  }
26
- const operations = Object.entries(pathItem).filter(([key, value]) => OPERATION_KEYS.has(key) && isRecord(value));
27
+ const operations = Object.entries(pathItem).flatMap(([key, value]) => {
28
+ if (OPERATION_KEYS.has(key) && isRecord(value)) {
29
+ return [{ operation: value, path: ['paths', pathName, key] }];
30
+ }
31
+ if (key === 'additionalOperations' && isRecord(value)) {
32
+ return Object.entries(value).flatMap(([method, operation]) => isRecord(operation) ? [{ operation, path: ['paths', pathName, 'additionalOperations', method] }] : []);
33
+ }
34
+ return [];
35
+ });
27
36
  // Preserve the repo's current behaviour for empty path items.
28
37
  if (operations.length === 0) {
29
38
  continue;
@@ -38,13 +47,8 @@ export function validatePathParameters(specification) {
38
47
  });
39
48
  }
40
49
  }
41
- for (const [operationKey, operation] of operations) {
42
- const operationParameters = getPathParameters(operation.parameters, [
43
- 'paths',
44
- pathName,
45
- operationKey,
46
- 'parameters',
47
- ]);
50
+ for (const { operation, path } of operations) {
51
+ const operationParameters = getPathParameters(operation.parameters, [...path, 'parameters']);
48
52
  for (const parameter of operationParameters) {
49
53
  if (!templateParameters.has(parameter.name)) {
50
54
  errors.push({
@@ -57,7 +61,7 @@ export function validatePathParameters(specification) {
57
61
  for (const templateParameter of templateParameters) {
58
62
  if (!effectiveParameters.has(templateParameter)) {
59
63
  errors.push({
60
- path: ['paths', pathName, operationKey],
64
+ path,
61
65
  message: `Declared path parameter "${templateParameter}" needs to be defined as a path parameter at either the path or operation level`,
62
66
  });
63
67
  }
@@ -15,12 +15,18 @@ export type ValidateOptions = ThrowOnErrorOption & {
15
15
  * @default false
16
16
  */
17
17
  checkPathParameters?: boolean;
18
+ /**
19
+ * Check OpenAPI 3.2 cookie serialization declarations.
20
+ * Resolve references first for complete coverage.
21
+ * @default true
22
+ */
23
+ checkCookieParameters?: boolean;
18
24
  };
19
25
  /**
20
26
  * Validates a single OpenAPI document against the OpenAPI Specification.
21
27
  *
22
28
  * Schema validation is delegated to `@scalar/json-schema-validator`; version
23
- * detection and the path-parameter semantic checks are OpenAPI-specific.
29
+ * detection and parameter semantic checks are OpenAPI-specific.
24
30
  *
25
31
  * This validator is strict about the specification: every required field,
26
32
  * including `info.version`, must be present. Callers that want to be lenient
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAMpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAGpE,MAAM,MAAM,eAAe,GAAG,kBAAkB,GAAG;IACjD;;;;;;;;;;;;OAYG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B,CAAA;AAgBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAEnG"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAMpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAIpE,MAAM,MAAM,eAAe,GAAG,kBAAkB,GAAG;IACjD;;;;;;;;;;;;OAYG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,CAAA;AAiBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAEnG"}
package/dist/validate.js CHANGED
@@ -3,6 +3,7 @@ import { detectVersion } from './detect-version.js';
3
3
  import { ERRORS } from './errors.js';
4
4
  import { explainComponentReferences } from './explain-component-references.js';
5
5
  import { OpenApiSpecifications } from './specifications.js';
6
+ import { validateCookieParameters } from './validate-cookie-parameters.js';
6
7
  import { validatePathParameters } from './validate-path-parameters.js';
7
8
  const validateDocument = createSpecificationValidator({
8
9
  schemas: OpenApiSpecifications,
@@ -11,16 +12,18 @@ const validateDocument = createSpecificationValidator({
11
12
  formats: (version) => (version === '3.1' || version === '3.2' ? { 'media-range': true } : undefined),
12
13
  errors: { emptyOrInvalid: ERRORS.EMPTY_OR_INVALID, versionNotSupported: ERRORS.OPENAPI_VERSION_NOT_SUPPORTED },
13
14
  transformErrors: explainComponentReferences,
14
- // Path-template semantics that the JSON schema cannot express. These need a
15
- // fully resolved document (a path parameter can be declared via `$ref`), so
16
- // they are opt-in: callers that resolve references first can enable them.
17
- postValidate: (specification, _version, options) => options?.checkPathParameters ? validatePathParameters(specification) : [],
15
+ // Cookie declarations can be checked without resolving references. Path-template
16
+ // checks require a resolved document to avoid reporting referenced parameters as missing.
17
+ postValidate: (specification, _version, options) => [
18
+ ...(options?.checkPathParameters ? validatePathParameters(specification) : []),
19
+ ...(options?.checkCookieParameters === false ? [] : validateCookieParameters(specification)),
20
+ ],
18
21
  });
19
22
  /**
20
23
  * Validates a single OpenAPI document against the OpenAPI Specification.
21
24
  *
22
25
  * Schema validation is delegated to `@scalar/json-schema-validator`; version
23
- * detection and the path-parameter semantic checks are OpenAPI-specific.
26
+ * detection and parameter semantic checks are OpenAPI-specific.
24
27
  *
25
28
  * This validator is strict about the specification: every required field,
26
29
  * including `info.version`, must be present. Callers that want to be lenient
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "validator",
18
18
  "typescript"
19
19
  ],
20
- "version": "0.1.10",
20
+ "version": "0.1.11",
21
21
  "engines": {
22
22
  "node": ">=22"
23
23
  },
@@ -38,13 +38,13 @@
38
38
  ],
39
39
  "sideEffects": false,
40
40
  "dependencies": {
41
- "@scalar/helpers": "0.16.1",
42
- "@scalar/json-schema-validator": "0.1.10",
43
- "@scalar/types": "0.23.0",
41
+ "@scalar/helpers": "0.16.2",
42
+ "@scalar/json-schema-validator": "0.1.11",
43
+ "@scalar/types": "0.23.1",
44
44
  "yaml": "^2.9.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@scalar/themes": "0.18.1",
47
+ "@scalar/themes": "0.19.0",
48
48
  "@scalar/use-codemirror": "0.14.15",
49
49
  "@tailwindcss/vite": "^4.3.3",
50
50
  "@types/node": "^24.1.0",