genoc 0.4.3 → 0.5.1

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.
Files changed (32) hide show
  1. package/dist/analyzer/analyze.js +69 -27
  2. package/dist/analyzer/path-analyzer.d.ts +3 -1
  3. package/dist/analyzer/path-analyzer.js +2 -1
  4. package/dist/analyzer/types.d.ts +25 -0
  5. package/dist/cli/impl.js +3 -1
  6. package/dist/generator/client-generator.js +64 -4
  7. package/dist/generator/contracts-generator.js +7 -0
  8. package/dist/generator/method-generator.d.ts +9 -1
  9. package/dist/generator/method-generator.js +26 -0
  10. package/dist/parser/spec-reader.js +6 -25
  11. package/dist/parser/version/spec-links.d.ts +55 -0
  12. package/dist/parser/version/spec-links.js +65 -0
  13. package/dist/parser/version/v3.0/validator.d.ts +3 -1
  14. package/dist/parser/version/v3.0/validator.js +23 -23
  15. package/dist/parser/version/v3.1/validator.d.ts +3 -1
  16. package/dist/parser/version/v3.1/validator.js +25 -23
  17. package/dist/parser/version/validate-info.d.ts +9 -0
  18. package/dist/parser/version/validate-info.js +22 -0
  19. package/dist/parser/version/validate-security.d.ts +17 -0
  20. package/dist/parser/version/validate-security.js +218 -0
  21. package/dist/parser/version/version-detector.js +27 -8
  22. package/dist/runtime/defined-errors.d.ts +3 -2
  23. package/dist/runtime/defined-errors.js +6 -3
  24. package/dist/runtime/errors.d.ts +1 -3
  25. package/dist/runtime/errors.js +2 -6
  26. package/dist/runtime/index.d.ts +1 -1
  27. package/dist/runtime/requester.d.ts +67 -0
  28. package/dist/types/openapi.d.ts +5 -4
  29. package/dist/utils/http-methods.d.ts +8 -0
  30. package/dist/utils/http-methods.js +17 -0
  31. package/dist/utils/operation-naming.js +1 -0
  32. package/package.json +1 -1
@@ -5,9 +5,11 @@ import type { ValidationResult } from '../validate.js';
5
5
  * 3.0-specific validation rules:
6
6
  * - `openapi` must start with "3.0"
7
7
  * - `paths` is required (unlike 3.1 where paths/components/webhooks is flexible)
8
- * - `info` with `title` and `version` required
8
+ * - `info` with `title` and `version` required (shared rule)
9
9
  * - Warns about 3.1-only features: `webhooks`, `$schema` in schemas, `type` as array
10
10
  * - Accepts `nullable` and `example` (valid in 3.0, not in 3.1)
11
11
  * - Errors on `items` as array (3.1-only tuple syntax)
12
+ * - Validates `components.securitySchemes` and `security` requirements (shared rules;
13
+ * non-oauth2/openIdConnect scopes must be empty, `mutualTLS` rejected as 3.1-only)
12
14
  */
13
15
  export declare function validateSpec30(spec: unknown): ValidationResult;
@@ -1,46 +1,45 @@
1
+ import { specViolation } from '../spec-links.js';
2
+ import { validateInfo } from '../validate-info.js';
3
+ import { validateSecurity } from '../validate-security.js';
4
+ /**
5
+ * Every violation in this file belongs to the 3.0 dialect;
6
+ * bind it once so call sites stay key-and-message only.
7
+ */
8
+ const violation = (key, message) => specViolation('3.0', key, message);
1
9
  /**
2
10
  * Validate a raw OpenAPI 3.0.x specification.
3
11
  *
4
12
  * 3.0-specific validation rules:
5
13
  * - `openapi` must start with "3.0"
6
14
  * - `paths` is required (unlike 3.1 where paths/components/webhooks is flexible)
7
- * - `info` with `title` and `version` required
15
+ * - `info` with `title` and `version` required (shared rule)
8
16
  * - Warns about 3.1-only features: `webhooks`, `$schema` in schemas, `type` as array
9
17
  * - Accepts `nullable` and `example` (valid in 3.0, not in 3.1)
10
18
  * - Errors on `items` as array (3.1-only tuple syntax)
19
+ * - Validates `components.securitySchemes` and `security` requirements (shared rules;
20
+ * non-oauth2/openIdConnect scopes must be empty, `mutualTLS` rejected as 3.1-only)
11
21
  */
12
22
  export function validateSpec30(spec) {
13
23
  const errors = [];
14
24
  if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
15
- errors.push('Document must be an object');
25
+ errors.push(violation('document-root', 'Document must be an object'));
16
26
  return { valid: false, errors };
17
27
  }
18
28
  const s = spec;
19
29
  if (!s.openapi || typeof s.openapi !== 'string') {
20
- errors.push("OpenAPI specification must have an 'openapi' field with string value");
30
+ errors.push(violation('openapi-field', "OpenAPI specification must have an 'openapi' field with string value"));
21
31
  }
22
32
  else if (!s.openapi.startsWith('3.0')) {
23
- errors.push(`OpenAPI version must start with '3.0', got: ${s.openapi}`);
24
- }
25
- if (!s.info || typeof s.info !== 'object' || Array.isArray(s.info)) {
26
- errors.push("OpenAPI specification must have an 'info' field with object value");
27
- }
28
- else {
29
- const info = s.info;
30
- if (!info.title || typeof info.title !== 'string') {
31
- errors.push("Info object must have a 'title' field with string value");
32
- }
33
- if (!info.version || typeof info.version !== 'string') {
34
- errors.push("Info object must have a 'version' field with string value");
35
- }
33
+ errors.push(violation('openapi-field', `OpenAPI version must start with '3.0', got: ${s.openapi}`));
36
34
  }
35
+ validateInfo(s, '3.0', errors);
37
36
  if (!s.paths || typeof s.paths !== 'object' || Array.isArray(s.paths)) {
38
- errors.push("OpenAPI 3.0 specification must have a 'paths' field with object value");
37
+ errors.push(violation('paths-object', "OpenAPI 3.0 specification must have a 'paths' field with object value"));
39
38
  }
40
39
  if (s.paths && typeof s.paths === 'object' && !Array.isArray(s.paths)) {
41
40
  for (const [pathKey, pathItem] of Object.entries(s.paths)) {
42
41
  if (!pathItem || typeof pathItem !== 'object' || Array.isArray(pathItem)) {
43
- errors.push(`Path item '${pathKey}' must be an object`);
42
+ errors.push(violation('path-item-object', `Path item '${pathKey}' must be an object`));
44
43
  }
45
44
  }
46
45
  }
@@ -48,13 +47,13 @@ export function validateSpec30(spec) {
48
47
  const components = s.components;
49
48
  if (components.schemas) {
50
49
  if (Array.isArray(components.schemas) || typeof components.schemas !== 'object') {
51
- errors.push("'components.schemas' must be an object");
50
+ errors.push(violation('components-schemas', "'components.schemas' must be an object"));
52
51
  }
53
52
  else {
54
53
  const schemas = components.schemas;
55
54
  for (const [key, schema] of Object.entries(schemas)) {
56
55
  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
57
- errors.push(`Schema '${key}' must be an object`);
56
+ errors.push(violation('schema-object', `Schema '${key}' must be an object`));
58
57
  }
59
58
  else {
60
59
  validateSchema30(schema, `components.schemas.${key}`, errors);
@@ -63,6 +62,7 @@ export function validateSpec30(spec) {
63
62
  }
64
63
  }
65
64
  }
65
+ validateSecurity(s, '3.0', errors);
66
66
  return {
67
67
  valid: errors.length === 0,
68
68
  errors,
@@ -78,13 +78,13 @@ function validateSchema30(schema, path, errors) {
78
78
  }
79
79
  const s = schema;
80
80
  if (Array.isArray(s.type)) {
81
- errors.push(`Schema '${path}' uses 'type' as an array which is a 3.1-only feature`);
81
+ errors.push(violation('schema-object', `Schema '${path}' uses 'type' as an array which is a 3.1-only feature`));
82
82
  }
83
83
  if ('$schema' in s) {
84
- errors.push(`Schema '${path}' uses '$schema' which is a 3.1-only feature`);
84
+ errors.push(violation('schema-object', `Schema '${path}' uses '$schema' which is a 3.1-only feature`));
85
85
  }
86
86
  if (s.items !== undefined && Array.isArray(s.items)) {
87
- errors.push(`Schema '${path}' uses 'items' as an array (tuple syntax) which is a 3.1-only feature`);
87
+ errors.push(violation('schema-object', `Schema '${path}' uses 'items' as an array (tuple syntax) which is a 3.1-only feature`));
88
88
  }
89
89
  if (s.properties && typeof s.properties === 'object' && !Array.isArray(s.properties)) {
90
90
  for (const [key, prop] of Object.entries(s.properties)) {
@@ -5,7 +5,7 @@ import type { ValidationResult } from '../validate.js';
5
5
  * 3.1-specific validation rules:
6
6
  * - `openapi` must start with "3.1"
7
7
  * - at least one of `paths`, `components`, or `webhooks` (unlike 3.0 where `paths` is required)
8
- * - `info` with `title` and `version` required
8
+ * - `info` with `title` and `version` required (shared rule)
9
9
  * - `paths` must be an object when present
10
10
  * - `components.schemas` entries must be objects, validated recursively for 3.0-only features
11
11
  * - Warns about 3.0-only features in schemas: `nullable`, boolean `exclusiveMinimum`/`exclusiveMaximum`,
@@ -13,5 +13,7 @@ import type { ValidationResult } from '../validate.js';
13
13
  * - Accepts `type` as array, `$schema`, `$id`, `$comment`, `const`, `examples`, and `$ref` siblings
14
14
  * (all legal in 3.1 via JSON Schema 2020-12)
15
15
  * - Accepts schema-level `example` (deprecated in 3.1 but still legal)
16
+ * - Validates `components.securitySchemes` and `security` requirements (shared rules;
17
+ * `mutualTLS` accepted, non-empty scopes for non-oauth2/openIdConnect schemes allowed as role names)
16
18
  */
17
19
  export declare function validateSpec31(spec: unknown): ValidationResult;
@@ -1,10 +1,18 @@
1
+ import { specViolation } from '../spec-links.js';
2
+ import { validateInfo } from '../validate-info.js';
3
+ import { validateSecurity } from '../validate-security.js';
4
+ /**
5
+ * Every violation in this file belongs to the 3.1 dialect;
6
+ * bind it once so call sites stay key-and-message only.
7
+ */
8
+ const violation = (key, message) => specViolation('3.1', key, message);
1
9
  /**
2
10
  * Validate a raw OpenAPI 3.1.x specification.
3
11
  *
4
12
  * 3.1-specific validation rules:
5
13
  * - `openapi` must start with "3.1"
6
14
  * - at least one of `paths`, `components`, or `webhooks` (unlike 3.0 where `paths` is required)
7
- * - `info` with `title` and `version` required
15
+ * - `info` with `title` and `version` required (shared rule)
8
16
  * - `paths` must be an object when present
9
17
  * - `components.schemas` entries must be objects, validated recursively for 3.0-only features
10
18
  * - Warns about 3.0-only features in schemas: `nullable`, boolean `exclusiveMinimum`/`exclusiveMaximum`,
@@ -12,50 +20,43 @@
12
20
  * - Accepts `type` as array, `$schema`, `$id`, `$comment`, `const`, `examples`, and `$ref` siblings
13
21
  * (all legal in 3.1 via JSON Schema 2020-12)
14
22
  * - Accepts schema-level `example` (deprecated in 3.1 but still legal)
23
+ * - Validates `components.securitySchemes` and `security` requirements (shared rules;
24
+ * `mutualTLS` accepted, non-empty scopes for non-oauth2/openIdConnect schemes allowed as role names)
15
25
  */
16
26
  export function validateSpec31(spec) {
17
27
  const errors = [];
18
28
  if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
19
- errors.push('Document must be an object');
29
+ errors.push(violation('document-root', 'Document must be an object'));
20
30
  return { valid: false, errors };
21
31
  }
22
32
  const s = spec;
23
33
  if (!s.openapi || typeof s.openapi !== 'string') {
24
- errors.push("OpenAPI specification must have an 'openapi' field with string value");
34
+ errors.push(violation('openapi-field', "OpenAPI specification must have an 'openapi' field with string value"));
25
35
  }
26
36
  else if (!s.openapi.startsWith('3.1')) {
27
- errors.push(`OpenAPI version must start with '3.1', got: ${s.openapi}`);
28
- }
29
- if (!s.info || typeof s.info !== 'object' || Array.isArray(s.info)) {
30
- errors.push("OpenAPI specification must have an 'info' field with object value");
31
- }
32
- else {
33
- const info = s.info;
34
- if (!info.title || typeof info.title !== 'string') {
35
- errors.push("Info object must have a 'title' field with string value");
36
- }
37
- if (!info.version || typeof info.version !== 'string') {
38
- errors.push("Info object must have a 'version' field with string value");
39
- }
37
+ errors.push(violation('openapi-field', `OpenAPI version must start with '3.1', got: ${s.openapi}`));
40
38
  }
39
+ validateInfo(s, '3.1', errors);
41
40
  if (!s.paths && !s.components && !s.webhooks) {
41
+ // Not a spec violation: all three fields are optional in 3.1; the check
42
+ // exists because an empty document gives genoc nothing to generate.
42
43
  errors.push("OpenAPI specification must have at least one of 'paths', 'components', or 'webhooks'");
43
44
  }
44
45
  if (s.paths) {
45
46
  if (typeof s.paths !== 'object' || Array.isArray(s.paths)) {
46
- errors.push("'paths' field must be an object");
47
+ errors.push(violation('paths-object', "'paths' field must be an object"));
47
48
  }
48
49
  }
49
50
  if (s.components && typeof s.components === 'object' && !Array.isArray(s.components)) {
50
51
  const components = s.components;
51
52
  if (components.schemas) {
52
53
  if (typeof components.schemas !== 'object' || Array.isArray(components.schemas)) {
53
- errors.push("'components.schemas' must be an object");
54
+ errors.push(violation('components-schemas', "'components.schemas' must be an object"));
54
55
  }
55
56
  else {
56
57
  for (const [key, schema] of Object.entries(components.schemas)) {
57
58
  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
58
- errors.push(`Schema '${key}' must be an object`);
59
+ errors.push(violation('schema-object', `Schema '${key}' must be an object`));
59
60
  }
60
61
  else {
61
62
  validateSchema31(schema, `components.schemas.${key}`, errors);
@@ -64,6 +65,7 @@ export function validateSpec31(spec) {
64
65
  }
65
66
  }
66
67
  }
68
+ validateSecurity(s, '3.1', errors);
67
69
  return {
68
70
  valid: errors.length === 0,
69
71
  errors,
@@ -79,16 +81,16 @@ function validateSchema31(schema, path, errors) {
79
81
  }
80
82
  const s = schema;
81
83
  if ('nullable' in s) {
82
- errors.push(`Schema '${path}' uses 'nullable' which is a 3.0-only feature`);
84
+ errors.push(violation('schema-object', `Schema '${path}' uses 'nullable' which is a 3.0-only feature`));
83
85
  }
84
86
  if (typeof s.exclusiveMinimum === 'boolean') {
85
- errors.push(`Schema '${path}' uses 'exclusiveMinimum' as a boolean which is a 3.0-only feature`);
87
+ errors.push(violation('schema-object', `Schema '${path}' uses 'exclusiveMinimum' as a boolean which is a 3.0-only feature`));
86
88
  }
87
89
  if (typeof s.exclusiveMaximum === 'boolean') {
88
- errors.push(`Schema '${path}' uses 'exclusiveMaximum' as a boolean which is a 3.0-only feature`);
90
+ errors.push(violation('schema-object', `Schema '${path}' uses 'exclusiveMaximum' as a boolean which is a 3.0-only feature`));
89
91
  }
90
92
  if (s.items !== undefined && Array.isArray(s.items)) {
91
- errors.push(`Schema '${path}' uses 'items' as an array which is not allowed in 3.1 (use 'prefixItems')`);
93
+ errors.push(violation('schema-object', `Schema '${path}' uses 'items' as an array which is not allowed in 3.1 (use 'prefixItems')`));
92
94
  }
93
95
  if (s.properties && typeof s.properties === 'object' && !Array.isArray(s.properties)) {
94
96
  for (const [key, prop] of Object.entries(s.properties)) {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared Info Object validation for the 3.0 and 3.1 validators.
3
+ *
4
+ * Rules are identical across dialects — every published dialect (3.0, 3.1,
5
+ * 3.2 alike) requires `info` to be an object carrying `title` and `version`
6
+ * strings. Presence and type only: the spec puts no constraint on the
7
+ * values, so `title: ""` is a valid title.
8
+ */
9
+ export declare function validateInfo(spec: Record<string, unknown>, version: '3.0' | '3.1', errors: string[]): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared Info Object validation for the 3.0 and 3.1 validators.
3
+ *
4
+ * Rules are identical across dialects — every published dialect (3.0, 3.1,
5
+ * 3.2 alike) requires `info` to be an object carrying `title` and `version`
6
+ * strings. Presence and type only: the spec puts no constraint on the
7
+ * values, so `title: ""` is a valid title.
8
+ */
9
+ import { specViolation } from './spec-links.js';
10
+ export function validateInfo(spec, version, errors) {
11
+ if (!spec.info || typeof spec.info !== 'object' || Array.isArray(spec.info)) {
12
+ errors.push(specViolation(version, 'info-object', "OpenAPI specification must have an 'info' field with object value"));
13
+ return;
14
+ }
15
+ const info = spec.info;
16
+ if (typeof info.title !== 'string') {
17
+ errors.push(specViolation(version, 'info-title', "Info object must have a 'title' field with string value"));
18
+ }
19
+ if (typeof info.version !== 'string') {
20
+ errors.push(specViolation(version, 'info-version', "Info object must have a 'version' field with string value"));
21
+ }
22
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared security validation for 3.0 and 3.1 validators.
3
+ *
4
+ * Rules are identical across dialects except:
5
+ * - `mutualTLS` scheme type is 3.1-only
6
+ * - non-empty scope arrays for non-oauth2/openIdConnect schemes are legal in
7
+ * 3.1 (role names) but MUST be empty in 3.0
8
+ *
9
+ * `$ref` scheme entries are resolved here (internal `#/` pointers, cycle
10
+ * guarded) so the resolved object is validated like an inline scheme — the
11
+ * soundness guarantee "validation ⇒ generated scheme payloads are well-formed"
12
+ * holds for referenced schemes too. External-document refs are left to the
13
+ * ref-resolver, which rejects them at its own boundary. `security` on a Path
14
+ * Item is rejected: OpenAPI allows security at document and operation level
15
+ * only, and silently dropping it would lose auth metadata.
16
+ */
17
+ export declare function validateSecurity(spec: Record<string, unknown>, version: '3.0' | '3.1', errors: string[]): void;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Shared security validation for 3.0 and 3.1 validators.
3
+ *
4
+ * Rules are identical across dialects except:
5
+ * - `mutualTLS` scheme type is 3.1-only
6
+ * - non-empty scope arrays for non-oauth2/openIdConnect schemes are legal in
7
+ * 3.1 (role names) but MUST be empty in 3.0
8
+ *
9
+ * `$ref` scheme entries are resolved here (internal `#/` pointers, cycle
10
+ * guarded) so the resolved object is validated like an inline scheme — the
11
+ * soundness guarantee "validation ⇒ generated scheme payloads are well-formed"
12
+ * holds for referenced schemes too. External-document refs are left to the
13
+ * ref-resolver, which rejects them at its own boundary. `security` on a Path
14
+ * Item is rejected: OpenAPI allows security at document and operation level
15
+ * only, and silently dropping it would lose auth metadata.
16
+ */
17
+ import { HTTP_METHODS } from '../../utils/http-methods.js';
18
+ import { specViolation } from './spec-links.js';
19
+ const SCHEME_TYPES = {
20
+ '3.0': new Set(['apiKey', 'http', 'oauth2', 'openIdConnect']),
21
+ '3.1': new Set(['apiKey', 'http', 'oauth2', 'openIdConnect', 'mutualTLS']),
22
+ };
23
+ export function validateSecurity(spec, version, errors) {
24
+ /** Raw entries keyed by declared name — presence check for requirement refs. */
25
+ let declaredNames;
26
+ /** `name` → resolved (un-ref'd) scheme object used for scope-rule checks. */
27
+ let resolvedByName;
28
+ const components = spec.components;
29
+ const rawSchemes = components?.securitySchemes;
30
+ if (rawSchemes === undefined) {
31
+ declaredNames = undefined;
32
+ }
33
+ else if (typeof rawSchemes !== 'object' || rawSchemes === null || Array.isArray(rawSchemes)) {
34
+ errors.push(specViolation(version, 'components-security-schemes', "'components.securitySchemes' must be an object"));
35
+ declaredNames = new Set();
36
+ resolvedByName = new Map();
37
+ }
38
+ else {
39
+ const schemes = rawSchemes;
40
+ declaredNames = new Set(Object.keys(schemes));
41
+ resolvedByName = new Map();
42
+ for (const [name, scheme] of Object.entries(schemes)) {
43
+ const resolved = resolveSchemeTarget(spec, name, scheme, version, new Set(), errors);
44
+ if (resolved !== undefined) {
45
+ validateScheme(name, resolved, version, errors);
46
+ resolvedByName.set(name, resolved);
47
+ }
48
+ }
49
+ }
50
+ validateSecurityList(spec.security, 'security', declaredNames, resolvedByName, version, errors);
51
+ const paths = spec.paths;
52
+ if (typeof paths !== 'object' || paths === null || Array.isArray(paths)) {
53
+ return;
54
+ }
55
+ for (const [pathKey, pathItem] of Object.entries(paths)) {
56
+ if (typeof pathItem !== 'object' || pathItem === null || Array.isArray(pathItem)) {
57
+ continue;
58
+ }
59
+ if ('security' in pathItem) {
60
+ errors.push(specViolation(version, 'path-item-object', `'paths.${pathKey}.security' is not allowed: security requirements may be declared at the document and operation levels only`));
61
+ }
62
+ for (const method of HTTP_METHODS) {
63
+ const op = pathItem[method];
64
+ if (typeof op === 'object' && op !== null && !Array.isArray(op)) {
65
+ validateSecurityList(op.security, `paths.${pathKey}.${method}.security`, declaredNames, resolvedByName, version, errors);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Follow a scheme entry to its validation target: a non-`$ref` entry is its
72
+ * own target; internal pointers (`#/…` and the root pointer `#`) are walked
73
+ * (JSON Pointer escapes honored, cycles guarded) and chained refs are
74
+ * followed. External refs are returned unresolved (the ref-resolver rejects
75
+ * them later). Returns `undefined` — after pushing an error — when the entry
76
+ * is not an object, the pointer dangles or carries malformed percent-
77
+ * encoding, or a ref cycle is detected.
78
+ */
79
+ function resolveSchemeTarget(spec, name, entry, version, seenRefs, errors) {
80
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
81
+ errors.push(specViolation(version, 'security-scheme-object', `Security scheme '${name}' must be an object`));
82
+ return undefined;
83
+ }
84
+ const record = entry;
85
+ const ref = record.$ref;
86
+ if (typeof ref !== 'string') {
87
+ return record;
88
+ }
89
+ if (ref !== '#' && !ref.startsWith('#/')) {
90
+ return record; // external document — the ref-resolver's boundary
91
+ }
92
+ if (seenRefs.has(ref)) {
93
+ errors.push(specViolation(version, 'reference-object', `Security scheme '${name}' has a circular $ref chain ('${ref}')`));
94
+ return undefined;
95
+ }
96
+ seenRefs.add(ref);
97
+ let segments;
98
+ try {
99
+ const pointerBody = ref === '#' ? '' : ref.slice(2);
100
+ segments =
101
+ pointerBody === ''
102
+ ? []
103
+ : pointerBody
104
+ .split('/')
105
+ .map((s) => decodeURIComponent(s.replace(/~1/g, '/').replace(/~0/g, '~')));
106
+ }
107
+ catch {
108
+ // malformed percent-encoding (e.g. a bare '%') — same story as a dangling pointer
109
+ errors.push(specViolation(version, 'reference-object', `Security scheme '${name}' $ref '${ref}' does not resolve`));
110
+ return undefined;
111
+ }
112
+ let target = spec;
113
+ for (const segment of segments) {
114
+ if (typeof target !== 'object' ||
115
+ target === null ||
116
+ Array.isArray(target) ||
117
+ !(segment in target)) {
118
+ errors.push(specViolation(version, 'reference-object', `Security scheme '${name}' $ref '${ref}' does not resolve`));
119
+ return undefined;
120
+ }
121
+ target = target[segment];
122
+ }
123
+ return resolveSchemeTarget(spec, name, target, version, seenRefs, errors);
124
+ }
125
+ function validateScheme(name, scheme, version, errors) {
126
+ if (typeof scheme !== 'object' || scheme === null || Array.isArray(scheme)) {
127
+ errors.push(specViolation(version, 'security-scheme-object', `Security scheme '${name}' must be an object`));
128
+ return;
129
+ }
130
+ const s = scheme;
131
+ if (typeof s.$ref === 'string') {
132
+ return; // external ref — shape belongs to the target document's boundary
133
+ }
134
+ const type = s.type;
135
+ if (typeof type !== 'string' || !SCHEME_TYPES[version].has(type)) {
136
+ const allowed = [...SCHEME_TYPES[version]].join(', ');
137
+ errors.push(specViolation(version, 'security-scheme-type', `Security scheme '${name}' must have a 'type' field with one of: ${allowed}, got: ${JSON.stringify(type ?? null)}`));
138
+ return;
139
+ }
140
+ switch (type) {
141
+ case 'apiKey':
142
+ if (typeof s.name !== 'string' || s.name === '') {
143
+ errors.push(specViolation(version, 'security-scheme-name', `apiKey security scheme '${name}' must have a 'name' field with string value`));
144
+ }
145
+ if (s.in !== 'query' && s.in !== 'header' && s.in !== 'cookie') {
146
+ errors.push(specViolation(version, 'security-scheme-in', `apiKey security scheme '${name}' must have an 'in' field with one of: query, header, cookie`));
147
+ }
148
+ break;
149
+ case 'http':
150
+ if (typeof s.scheme !== 'string' || s.scheme === '') {
151
+ errors.push(specViolation(version, 'security-scheme-scheme', `http security scheme '${name}' must have a 'scheme' field with string value`));
152
+ }
153
+ break;
154
+ case 'oauth2':
155
+ validateOauth2Flows(name, s.flows, version, errors);
156
+ break;
157
+ case 'openIdConnect':
158
+ if (typeof s.openIdConnectUrl !== 'string' || s.openIdConnectUrl === '') {
159
+ errors.push(specViolation(version, 'security-scheme-open-id-connect-url', `openIdConnect security scheme '${name}' must have an 'openIdConnectUrl' field with string value`));
160
+ }
161
+ break;
162
+ case 'mutualTLS':
163
+ break; // no scheme-specific fields
164
+ }
165
+ }
166
+ function validateOauth2Flows(name, flows, version, errors) {
167
+ if (typeof flows !== 'object' || flows === null || Array.isArray(flows)) {
168
+ errors.push(specViolation(version, 'security-scheme-flows', `oauth2 security scheme '${name}' must have a 'flows' field with object value`));
169
+ return;
170
+ }
171
+ for (const [flowKind, flow] of Object.entries(flows)) {
172
+ if (flow === undefined) {
173
+ continue;
174
+ }
175
+ if (typeof flow !== 'object' || flow === null || Array.isArray(flow)) {
176
+ errors.push(specViolation(version, 'oauth-flow-object', `oauth2 flow '${flowKind}' of security scheme '${name}' must be an object`));
177
+ continue;
178
+ }
179
+ const scopes = flow.scopes;
180
+ if (typeof scopes !== 'object' || scopes === null || Array.isArray(scopes)) {
181
+ errors.push(specViolation(version, 'oauth-flow-scopes', `oauth2 flow '${flowKind}' of security scheme '${name}' must have a 'scopes' field with object value`));
182
+ }
183
+ }
184
+ }
185
+ function validateSecurityList(list, context, declaredNames, resolvedByName, version, errors) {
186
+ if (list === undefined) {
187
+ return;
188
+ }
189
+ if (!Array.isArray(list)) {
190
+ errors.push(specViolation(version, 'security-requirement-object', `'${context}' must be an array`));
191
+ return;
192
+ }
193
+ list.forEach((requirement, index) => {
194
+ const reqContext = `${context}[${index}]`;
195
+ if (typeof requirement !== 'object' || requirement === null || Array.isArray(requirement)) {
196
+ errors.push(specViolation(version, 'security-requirement-object', `'${reqContext}' must be an object`));
197
+ return;
198
+ }
199
+ for (const [schemeName, scopes] of Object.entries(requirement)) {
200
+ if (!declaredNames?.has(schemeName)) {
201
+ errors.push(specViolation(version, 'security-requirement-object', `'${reqContext}' references unknown security scheme '${schemeName}' (not declared in components.securitySchemes)`));
202
+ continue;
203
+ }
204
+ const schemeType = resolvedByName?.get(schemeName)?.type;
205
+ if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== 'string')) {
206
+ errors.push(specViolation(version, 'security-requirement-object', `'${reqContext}.${schemeName}' must be an array of strings`));
207
+ continue;
208
+ }
209
+ if (version === '3.0' &&
210
+ scopes.length > 0 &&
211
+ schemeType !== undefined &&
212
+ schemeType !== 'oauth2' &&
213
+ schemeType !== 'openIdConnect') {
214
+ errors.push(specViolation(version, 'security-requirement-object', `'${reqContext}.${schemeName}' must have an empty scopes array for '${String(schemeType)}' schemes in OpenAPI 3.0`));
215
+ }
216
+ }
217
+ });
218
+ }
@@ -1,32 +1,35 @@
1
1
  /**
2
2
  * Version detector for multi-version OpenAPI support
3
3
  */
4
+ import { specLinkUrl, SpecViolationError } from './spec-links.js';
4
5
  export function detectSpecVersion(rawSpec) {
5
6
  if (typeof rawSpec !== 'object' || rawSpec === null || Array.isArray(rawSpec)) {
6
- throw new Error('Invalid spec: must be a non-null object');
7
+ throw new SpecViolationError('Invalid spec: must be a non-null object', 'document-root');
7
8
  }
8
9
  const spec = rawSpec;
9
10
  // Check for Swagger 2.0
10
11
  if ('swagger' in spec && typeof spec.swagger === 'string') {
11
12
  if (spec.swagger === '2.0') {
13
+ // Unsupported feature, not a spec violation — the document is valid.
12
14
  throw new Error('Swagger 2.0 is not supported. Convert to OpenAPI using swagger2openapi first.');
13
15
  }
14
16
  throw new Error(`Unsupported Swagger version: ${spec.swagger}`);
15
17
  }
16
18
  // Check for OpenAPI 3.0+
17
19
  if (!('openapi' in spec) || typeof spec.openapi !== 'string') {
18
- throw new Error("Invalid spec: missing or invalid 'openapi' field");
20
+ throw new SpecViolationError("Invalid spec: missing or invalid 'openapi' field", 'openapi-field');
19
21
  }
20
22
  const openapiVersion = spec.openapi;
21
- // Extract major.minor version
22
- const versionMatch = openapiVersion.match(/^(\d+\.\d+)/);
23
- if (!versionMatch) {
24
- throw new Error(`Invalid OpenAPI version format: ${openapiVersion}`);
23
+ // The version must be major.minor, optionally with a patch segment.
24
+ if (!openapiVersion.match(/^\d+\.\d+(\.\d+)?$/)) {
25
+ throw new SpecViolationError(`Invalid OpenAPI version format: ${openapiVersion}`, 'openapi-field');
25
26
  }
26
- const majorMinorVersion = versionMatch[1];
27
+ // The format check above guarantees two leading numeric segments.
28
+ const majorMinorVersion = openapiVersion.split('.').slice(0, 2).join('.');
27
29
  // Validate supported versions
28
30
  const supportedVersions = ['3.0', '3.1', '3.2'];
29
31
  if (!supportedVersions.includes(majorMinorVersion)) {
32
+ // Unsupported feature, not a spec violation — the document is valid.
30
33
  throw new Error(`Unsupported OpenAPI version: ${openapiVersion}. ` +
31
34
  `Supported versions: ${supportedVersions.join(', ')}`);
32
35
  }
@@ -40,8 +43,24 @@ export function detectSpecVersion(rawSpec) {
40
43
  * dialect; `preserveRefSiblings` follows the effective dialect.
41
44
  */
42
45
  export function resolveVersion(doc, override) {
43
- const detected = detectSpecVersion(doc);
46
+ let detected;
47
+ try {
48
+ detected = detectSpecVersion(doc);
49
+ }
50
+ catch (error) {
51
+ if (error instanceof SpecViolationError) {
52
+ // The dialect is unknown (or the document is not a document at all), so
53
+ // the link targets the dialect being applied; the override decides when
54
+ // given, otherwise the latest dialect. Two-space indent: this error is
55
+ // thrown standalone, not as a ` - ` bullet of validateSpec's message.
56
+ throw new Error(`${error.message}\n ${specLinkUrl(override ?? '3.1', error.linkKey)}`, {
57
+ cause: error,
58
+ });
59
+ }
60
+ throw error;
61
+ }
44
62
  if (detected !== '3.0' && detected !== '3.1') {
63
+ // Unsupported feature, not a spec violation — the document is valid.
45
64
  throw new Error('OpenAPI 3.2 is not yet supported. Supported versions: 3.0, 3.1');
46
65
  }
47
66
  const effective = override ?? detected;
@@ -9,8 +9,9 @@ export declare function decorateWithErrors<T, E>(item: T, runtimeErrors: unknown
9
9
  };
10
10
  /**
11
11
  * Type guard that narrows a caught error to a method's defined error union.
12
- * Returns false for `UnspecifiedApiError` (status not declared in the spec)
13
- * and for anything that is not an `ApiError`.
12
+ * Returns false for `UnspecifiedApiError` (status not declared in the spec),
13
+ * for `DefaultApiError` (declared `default` response - not part of the
14
+ * per-status error union), and for anything that is not an `ApiError`.
14
15
  */
15
16
  export declare function isDefinedError<E extends ApiError<number, unknown>>(err: unknown, _fn: {
16
17
  __definedErrors: E;
@@ -1,4 +1,4 @@
1
- import { ApiError, UnspecifiedApiError } from './errors.js';
1
+ import { ApiError, DefaultApiError, UnspecifiedApiError } from './errors.js';
2
2
  /**
3
3
  * Attaches the `__definedErrors` metadata property to a client method.
4
4
  * The property carries the method's error codes and is used by
@@ -15,12 +15,15 @@ export function decorateWithErrors(item, runtimeErrors) {
15
15
  }
16
16
  /**
17
17
  * Type guard that narrows a caught error to a method's defined error union.
18
- * Returns false for `UnspecifiedApiError` (status not declared in the spec)
19
- * and for anything that is not an `ApiError`.
18
+ * Returns false for `UnspecifiedApiError` (status not declared in the spec),
19
+ * for `DefaultApiError` (declared `default` response - not part of the
20
+ * per-status error union), and for anything that is not an `ApiError`.
20
21
  */
21
22
  export function isDefinedError(err, _fn) {
22
23
  if (err instanceof UnspecifiedApiError)
23
24
  return false;
25
+ if (err instanceof DefaultApiError)
26
+ return false;
24
27
  if (!(err instanceof ApiError))
25
28
  return false;
26
29
  return true;
@@ -16,9 +16,7 @@ export declare class UnspecifiedApiError extends ApiError<number, unknown> {
16
16
  constructor(status: number, data: unknown, message: string);
17
17
  }
18
18
  /** Error thrown for operations that declare a `default` response. */
19
- export declare class DefaultApiError<TData> extends Error {
20
- readonly status: number;
21
- readonly data: TData;
19
+ export declare class DefaultApiError<TData> extends ApiError<number, TData> {
22
20
  constructor(status: number, data: TData, message: string);
23
21
  }
24
22
  /** Wraps unexpected failures inside a `Requester` implementation. */
@@ -24,13 +24,9 @@ export class UnspecifiedApiError extends ApiError {
24
24
  }
25
25
  }
26
26
  /** Error thrown for operations that declare a `default` response. */
27
- export class DefaultApiError extends Error {
28
- status;
29
- data;
27
+ export class DefaultApiError extends ApiError {
30
28
  constructor(status, data, message) {
31
- super(message);
32
- this.status = status;
33
- this.data = data;
29
+ super(status, data, message);
34
30
  this.name = 'DefaultApiError';
35
31
  }
36
32
  }