@opsydyn/elysia-spectral 0.2.5 → 0.4.0
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 +14 -0
- package/README.md +179 -49
- package/dist/core/index.d.mts +1 -1
- package/dist/core/index.mjs +1 -1
- package/dist/{core-CTQQBA_q.mjs → core-D_ro1XEW.mjs} +185 -36
- package/dist/{index-BrFQCFDI.d.mts → index-CMyl_MsI.d.mts} +8 -3
- package/dist/index.d.mts +41 -2
- package/dist/index.mjs +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/opsydyn/elysia-spectral/compare/v0.3.0...v0.4.0) (2026-04-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add source metadata to LintRunResult ([bdd5a81](https://github.com/opsydyn/elysia-spectral/commit/bdd5a817c00b5fdccf4e1d27c8dbf4c6d4c36d2c))
|
|
9
|
+
|
|
10
|
+
## [0.3.0](https://github.com/opsydyn/elysia-spectral/compare/v0.2.5...v0.3.0) (2026-04-14)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add recommended, server, and strict governance presets ([692061e](https://github.com/opsydyn/elysia-spectral/commit/692061e23ba5735fc0ad1a8d1a9110ced1f869fe))
|
|
16
|
+
|
|
3
17
|
## [0.2.5](https://github.com/opsydyn/elysia-spectral/compare/v0.2.4...v0.2.5) (2026-04-14)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
Thin Elysia plugin that lints the OpenAPI document generated by `@elysiajs/openapi` with Spectral.
|
|
8
8
|
|
|
9
|
+
## What Is Elysia?
|
|
10
|
+
|
|
11
|
+
Elysia is a fast, ergonomic TypeScript web framework for Bun. It uses a plugin model for composing functionality and integrates with `@elysiajs/openapi` to generate OpenAPI documentation directly from route schemas — no separate spec file or annotation layer required.
|
|
12
|
+
|
|
13
|
+
- Official site: <https://elysiajs.com/>
|
|
14
|
+
- `@elysiajs/openapi`: <https://elysiajs.com/plugins/openapi>
|
|
15
|
+
|
|
9
16
|
## What Is Spectral?
|
|
10
17
|
|
|
11
18
|
Spectral is an open-source linter and style guide engine for API descriptions. It was built with OpenAPI, AsyncAPI, and JSON Schema in mind, and is commonly used to enforce API style guides, catch weak or inconsistent contract definitions, and improve the usefulness of generated API descriptions.
|
|
@@ -25,6 +32,12 @@ Official Spectral references:
|
|
|
25
32
|
- Stoplight overview: <https://stoplight.io/open-source/spectral>
|
|
26
33
|
- GitHub repository: <https://github.com/stoplightio/spectral>
|
|
27
34
|
|
|
35
|
+
## Standards
|
|
36
|
+
|
|
37
|
+
- RFC 9457 — Problem Details for HTTP APIs: <https://datatracker.ietf.org/doc/html/rfc9457>
|
|
38
|
+
|
|
39
|
+
The `strict` preset enforces RFC 9457 by requiring that all 4xx and 5xx responses declare `application/problem+json` as their content type.
|
|
40
|
+
|
|
28
41
|
This README is organized using the Diataxis documentation model:
|
|
29
42
|
|
|
30
43
|
- Tutorial: learn by building a working setup
|
|
@@ -51,7 +64,7 @@ Current package scope:
|
|
|
51
64
|
|
|
52
65
|
### Add OpenAPI linting to an Elysia app
|
|
53
66
|
|
|
54
|
-
This tutorial takes a minimal Elysia app and adds startup OpenAPI linting
|
|
67
|
+
This tutorial takes a minimal Elysia app and adds startup OpenAPI linting using the `strict` preset. The `strict` preset is the recommended starting point for teams shipping production APIs — it enforces summaries, descriptions, operation IDs, tags, server declarations, and RFC 9457 Problem Details on error responses.
|
|
55
68
|
|
|
56
69
|
1. Install the dependencies.
|
|
57
70
|
|
|
@@ -63,24 +76,7 @@ bun add elysia @elysiajs/openapi @opsydyn/elysia-spectral
|
|
|
63
76
|
npm install elysia @elysiajs/openapi @opsydyn/elysia-spectral
|
|
64
77
|
```
|
|
65
78
|
|
|
66
|
-
2.
|
|
67
|
-
|
|
68
|
-
```yaml
|
|
69
|
-
extends:
|
|
70
|
-
- spectral:oas
|
|
71
|
-
|
|
72
|
-
rules:
|
|
73
|
-
operation-description:
|
|
74
|
-
severity: error
|
|
75
|
-
|
|
76
|
-
operation-tags:
|
|
77
|
-
severity: warn
|
|
78
|
-
|
|
79
|
-
elysia-operation-summary:
|
|
80
|
-
severity: error
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
3. Add `@elysiajs/openapi` and `spectralPlugin` to your app.
|
|
79
|
+
2. Add `@elysiajs/openapi` and `spectralPlugin` to your app with the `strict` preset.
|
|
84
80
|
|
|
85
81
|
```ts
|
|
86
82
|
import { Elysia, t } from 'elysia'
|
|
@@ -93,18 +89,19 @@ new Elysia()
|
|
|
93
89
|
documentation: {
|
|
94
90
|
info: {
|
|
95
91
|
title: 'Example API',
|
|
96
|
-
version: '1.0.0'
|
|
92
|
+
version: '1.0.0',
|
|
93
|
+
contact: { name: 'API Team', url: 'https://example.com/support' }
|
|
97
94
|
},
|
|
95
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
98
96
|
tags: [{ name: 'Users', description: 'User operations' }]
|
|
99
97
|
}
|
|
100
98
|
})
|
|
101
99
|
)
|
|
102
100
|
.use(
|
|
103
101
|
spectralPlugin({
|
|
102
|
+
preset: 'strict',
|
|
104
103
|
failOn: 'error',
|
|
105
|
-
startup: {
|
|
106
|
-
mode: 'enforce'
|
|
107
|
-
},
|
|
104
|
+
startup: { mode: 'enforce' },
|
|
108
105
|
output: {
|
|
109
106
|
console: true,
|
|
110
107
|
jsonReportPath: './artifacts/openapi-lint.json',
|
|
@@ -113,36 +110,44 @@ new Elysia()
|
|
|
113
110
|
})
|
|
114
111
|
)
|
|
115
112
|
.get('/users', () => [{ id: '1', name: 'Ada Lovelace' }], {
|
|
116
|
-
response: {
|
|
117
|
-
200: t.Array(
|
|
118
|
-
t.Object({
|
|
119
|
-
id: t.String(),
|
|
120
|
-
name: t.String()
|
|
121
|
-
})
|
|
122
|
-
)
|
|
123
|
-
},
|
|
124
113
|
detail: {
|
|
125
114
|
summary: 'List users',
|
|
126
|
-
description: 'Return all users.',
|
|
115
|
+
description: 'Return all registered users.',
|
|
127
116
|
operationId: 'listUsers',
|
|
128
117
|
tags: ['Users']
|
|
118
|
+
},
|
|
119
|
+
response: {
|
|
120
|
+
200: t.Array(
|
|
121
|
+
t.Object({ id: t.String(), name: t.String() })
|
|
122
|
+
),
|
|
123
|
+
500: t.Object(
|
|
124
|
+
{
|
|
125
|
+
type: t.String(),
|
|
126
|
+
title: t.String(),
|
|
127
|
+
status: t.Number(),
|
|
128
|
+
detail: t.String()
|
|
129
|
+
},
|
|
130
|
+
{ description: 'Internal server error (RFC 9457 Problem Details)' }
|
|
131
|
+
)
|
|
129
132
|
}
|
|
130
133
|
})
|
|
131
134
|
.listen(3000)
|
|
132
135
|
```
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
The `strict` preset requires error responses to use `application/problem+json` (RFC 9457). The `500` response above satisfies that when `@elysiajs/openapi` generates the schema with the correct content type.
|
|
138
|
+
|
|
139
|
+
3. Start the app.
|
|
135
140
|
|
|
136
141
|
```bash
|
|
137
142
|
bun run src/index.ts
|
|
138
143
|
```
|
|
139
144
|
|
|
140
|
-
|
|
145
|
+
4. Confirm the outcome.
|
|
141
146
|
|
|
142
147
|
- the app serves OpenAPI JSON at `/openapi/json`
|
|
143
|
-
- the plugin lints that generated document during startup
|
|
144
|
-
- a clean run prints
|
|
145
|
-
- `./artifacts/openapi-lint.json` contains the lint result
|
|
148
|
+
- the plugin lints that generated document during startup using the `strict` preset
|
|
149
|
+
- a clean run prints a success banner in the terminal
|
|
150
|
+
- `./artifacts/openapi-lint.json` contains the full lint result
|
|
146
151
|
- `./<package-name>.open-api.json` contains the generated OpenAPI snapshot
|
|
147
152
|
|
|
148
153
|
If startup fails, the terminal output includes:
|
|
@@ -152,6 +157,27 @@ If startup fails, the terminal output includes:
|
|
|
152
157
|
- a fix hint when one is known
|
|
153
158
|
- a spec reference in `open-api.json#/json/pointer` form
|
|
154
159
|
|
|
160
|
+
### Choose a preset
|
|
161
|
+
|
|
162
|
+
Three first-party presets are available. Import them directly or set `preset` in the plugin options.
|
|
163
|
+
|
|
164
|
+
| Preset | Use case | elysia rules | description / operationId | servers | RFC 9457 |
|
|
165
|
+
|---|---|---|---|---|---|
|
|
166
|
+
| `recommended` | local dev, low friction | warn | — | off | — |
|
|
167
|
+
| `server` | production API gate | error | warn | warn | — |
|
|
168
|
+
| `strict` | full API governance | error | error | error | warn |
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// simplest — preset string
|
|
172
|
+
spectralPlugin({ preset: 'strict' })
|
|
173
|
+
|
|
174
|
+
// or import the preset object and pass as ruleset
|
|
175
|
+
import { strict } from '@opsydyn/elysia-spectral'
|
|
176
|
+
spectralPlugin({ ruleset: strict })
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
When `preset` is set, autodiscovered `spectral.yaml` overrides merge on top of the preset rather than the package default. This lets you tighten or loosen individual rules without losing the preset baseline.
|
|
180
|
+
|
|
155
181
|
## How-to Guides
|
|
156
182
|
|
|
157
183
|
### Use a repo-level ruleset
|
|
@@ -400,29 +426,101 @@ spectralPlugin({
|
|
|
400
426
|
|
|
401
427
|
Use `'warn'` for local development and `'error'` when artifact generation is required.
|
|
402
428
|
|
|
403
|
-
### Run the runtime in CI
|
|
429
|
+
### Run the runtime in CI
|
|
430
|
+
|
|
431
|
+
Use `createOpenApiLintRuntime` to run a standalone lint check in CI without starting an HTTP server. Import your Elysia app instance directly — the runtime uses `app.handle()` in-process to retrieve the generated OpenAPI document. No port binding required.
|
|
432
|
+
|
|
433
|
+
Create a script at `scripts/lint-openapi.ts`:
|
|
404
434
|
|
|
405
435
|
```ts
|
|
406
|
-
import {
|
|
407
|
-
import { openapi } from '@elysiajs/openapi'
|
|
436
|
+
import { app } from '../src/app'
|
|
408
437
|
import { createOpenApiLintRuntime } from '@opsydyn/elysia-spectral'
|
|
409
438
|
|
|
410
|
-
const app = new Elysia().use(openapi())
|
|
411
|
-
|
|
412
439
|
const runtime = createOpenApiLintRuntime({
|
|
413
|
-
|
|
440
|
+
preset: 'strict',
|
|
414
441
|
failOn: 'error',
|
|
415
442
|
output: {
|
|
416
443
|
console: true,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
444
|
+
sarifReportPath: './reports/openapi.sarif',
|
|
445
|
+
junitReportPath: './reports/openapi.junit.xml',
|
|
446
|
+
specSnapshotPath: './reports/openapi-snapshot.json',
|
|
447
|
+
artifactWriteFailures: 'error',
|
|
448
|
+
},
|
|
421
449
|
})
|
|
422
450
|
|
|
423
|
-
|
|
451
|
+
try {
|
|
452
|
+
await runtime.run(app)
|
|
453
|
+
process.exit(0)
|
|
454
|
+
} catch {
|
|
455
|
+
process.exit(1)
|
|
456
|
+
}
|
|
424
457
|
```
|
|
425
458
|
|
|
459
|
+
Run it as a CI step:
|
|
460
|
+
|
|
461
|
+
```yaml
|
|
462
|
+
- name: Lint OpenAPI spec
|
|
463
|
+
run: bun scripts/lint-openapi.ts
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Integrate SARIF with GitHub code scanning
|
|
467
|
+
|
|
468
|
+
SARIF output maps lint findings to code locations and surfaces them as GitHub code scanning alerts on pull requests.
|
|
469
|
+
|
|
470
|
+
```yaml
|
|
471
|
+
- name: Lint OpenAPI spec
|
|
472
|
+
run: bun scripts/lint-openapi.ts
|
|
473
|
+
|
|
474
|
+
- name: Upload SARIF to GitHub code scanning
|
|
475
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
476
|
+
with:
|
|
477
|
+
sarif_file: reports/openapi.sarif
|
|
478
|
+
if: always()
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
`if: always()` ensures the SARIF upload runs even when the lint step fails, so findings are visible on failing PRs.
|
|
482
|
+
|
|
483
|
+
### Report lint findings as JUnit test results
|
|
484
|
+
|
|
485
|
+
JUnit output lets CI systems that consume test XML (Buildkite, CircleCI, GitLab, GitHub via third-party actions) show lint findings alongside test results.
|
|
486
|
+
|
|
487
|
+
```yaml
|
|
488
|
+
- name: Lint OpenAPI spec
|
|
489
|
+
run: bun scripts/lint-openapi.ts
|
|
490
|
+
|
|
491
|
+
- name: Publish lint results
|
|
492
|
+
uses: dorny/test-reporter@v1
|
|
493
|
+
with:
|
|
494
|
+
name: OpenAPI Lint
|
|
495
|
+
path: reports/openapi.junit.xml
|
|
496
|
+
reporter: java-junit
|
|
497
|
+
if: always()
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Track OpenAPI snapshot drift
|
|
501
|
+
|
|
502
|
+
Commit the generated OpenAPI snapshot and use `git diff --exit-code` to detect when the API surface changes unexpectedly in a PR.
|
|
503
|
+
|
|
504
|
+
1. Generate and commit the initial snapshot:
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
bun scripts/lint-openapi.ts
|
|
508
|
+
git add reports/openapi-snapshot.json
|
|
509
|
+
git commit -m "chore: add openapi snapshot"
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
1. In CI, regenerate the snapshot and check for drift:
|
|
513
|
+
|
|
514
|
+
```yaml
|
|
515
|
+
- name: Lint OpenAPI spec
|
|
516
|
+
run: bun scripts/lint-openapi.ts
|
|
517
|
+
|
|
518
|
+
- name: Check for snapshot drift
|
|
519
|
+
run: git diff --exit-code reports/openapi-snapshot.json
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
If the snapshot has changed, the CI step fails and the diff is visible in the logs. Deliberate API changes are acknowledged by updating the committed snapshot — accidental ones are caught before they ship.
|
|
523
|
+
|
|
426
524
|
### Work on this repository locally
|
|
427
525
|
|
|
428
526
|
From the monorepo root:
|
|
@@ -453,6 +551,8 @@ That example uses `startup.mode: 'report'`, so the app still boots while the pac
|
|
|
453
551
|
### Package API
|
|
454
552
|
|
|
455
553
|
```ts
|
|
554
|
+
type PresetName = 'recommended' | 'server' | 'strict'
|
|
555
|
+
|
|
456
556
|
type SeverityThreshold = 'error' | 'warn' | 'info' | 'hint' | 'never'
|
|
457
557
|
|
|
458
558
|
type StartupLintMode = 'enforce' | 'report' | 'off'
|
|
@@ -474,7 +574,7 @@ type OpenApiLintSink = {
|
|
|
474
574
|
spec: Record<string, unknown>
|
|
475
575
|
logger: SpectralLogger
|
|
476
576
|
}
|
|
477
|
-
) =>
|
|
577
|
+
) => undefined | Partial<OpenApiLintArtifacts> | Promise<undefined | Partial<OpenApiLintArtifacts>>
|
|
478
578
|
}
|
|
479
579
|
|
|
480
580
|
type RulesetResolver = (
|
|
@@ -490,9 +590,14 @@ type LoadResolvedRulesetOptions = {
|
|
|
490
590
|
baseDir?: string
|
|
491
591
|
resolvers?: RulesetResolver[]
|
|
492
592
|
mergeAutodiscoveredWithDefault?: boolean
|
|
593
|
+
/** Override the base ruleset used for autodiscovery merging and the fallback when no ruleset is configured. */
|
|
594
|
+
defaultRuleset?: RulesetDefinition
|
|
493
595
|
}
|
|
494
596
|
|
|
495
597
|
type SpectralPluginOptions = {
|
|
598
|
+
/** First-party governance preset. Sets the base ruleset and autodiscovery merge target. */
|
|
599
|
+
preset?: PresetName
|
|
600
|
+
/** Custom ruleset path, object, or inline definition. Merged on top of preset when both are set. */
|
|
496
601
|
ruleset?: string | RulesetDefinition | Record<string, unknown>
|
|
497
602
|
failOn?: SeverityThreshold
|
|
498
603
|
healthcheck?: false | { path?: string }
|
|
@@ -522,6 +627,30 @@ type SpectralPluginOptions = {
|
|
|
522
627
|
}
|
|
523
628
|
```
|
|
524
629
|
|
|
630
|
+
### Presets
|
|
631
|
+
|
|
632
|
+
| Preset | Extends | elysia-operation-summary | elysia-operation-tags | operation-description | operation-operationId | oas3-api-servers | info-contact | rfc9457-problem-details |
|
|
633
|
+
|---|---|---|---|---|---|---|---|---|
|
|
634
|
+
| `recommended` | spectral:oas/recommended | warn | warn | — | — | off | off | — |
|
|
635
|
+
| `server` | spectral:oas/recommended | error | error | warn | warn | warn | off | — |
|
|
636
|
+
| `strict` | spectral:oas/recommended | error | error | error | error | error | warn | warn |
|
|
637
|
+
|
|
638
|
+
All three presets are exported as `RulesetDefinition` objects and can be passed directly to `ruleset` if you need to compose them:
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
import { strict } from '@opsydyn/elysia-spectral'
|
|
642
|
+
|
|
643
|
+
spectralPlugin({
|
|
644
|
+
ruleset: {
|
|
645
|
+
...strict,
|
|
646
|
+
rules: {
|
|
647
|
+
...(strict.rules as object),
|
|
648
|
+
'rfc9457-problem-details': 'error' // escalate to error for this service
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
```
|
|
653
|
+
|
|
525
654
|
### Runtime state
|
|
526
655
|
|
|
527
656
|
The runtime object exposes:
|
|
@@ -549,6 +678,7 @@ Example successful response:
|
|
|
549
678
|
"result": {
|
|
550
679
|
"ok": true,
|
|
551
680
|
"generatedAt": "2026-04-06T12:00:00.000Z",
|
|
681
|
+
"source": "startup",
|
|
552
682
|
"summary": {
|
|
553
683
|
"error": 0,
|
|
554
684
|
"warn": 0,
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, y as loadRuleset } from "../index-
|
|
1
|
+
import { _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, y as loadRuleset } from "../index-CMyl_MsI.mjs";
|
|
2
2
|
export { LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintThresholdError, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as OpenApiLintThresholdError, c as shouldFail,
|
|
1
|
+
import { a as OpenApiLintThresholdError, c as shouldFail, g as loadRuleset, h as loadResolvedRuleset, i as resolveStartupMode, m as defaultRulesetResolvers, n as createOpenApiLintRuntime, o as enforceThreshold, p as RulesetLoadError, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, v as lintOpenApi, y as normalizeFindings } from "../core-D_ro1XEW.mjs";
|
|
2
2
|
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail };
|
|
@@ -12,7 +12,12 @@ const guidanceByCode = {
|
|
|
12
12
|
"elysia-operation-summary": "Add detail.summary to the Elysia route options so generated docs and clients have a short operation label.",
|
|
13
13
|
"elysia-operation-tags": "Add detail.tags with at least one stable tag, for example ['Users'] or ['Dev'].",
|
|
14
14
|
"operation-description": "Add detail.description with a short user-facing explanation of what the route does.",
|
|
15
|
-
"operation-tags": "Add a non-empty detail.tags array on the route so the OpenAPI operation is grouped consistently."
|
|
15
|
+
"operation-tags": "Add a non-empty detail.tags array on the route so the OpenAPI operation is grouped consistently.",
|
|
16
|
+
"operation-operationId": "Add detail.operationId with a unique camelCase identifier so generated clients and SDKs have stable method names.",
|
|
17
|
+
"operation-success-response": "Add at least one 2xx response schema to the route, for example response: { 200: t.Object(...) }.",
|
|
18
|
+
"oas3-api-servers": "Add a servers array to the OpenAPI documentation config with at least one base URL.",
|
|
19
|
+
"info-contact": "Add an info.contact object to the OpenAPI documentation config with a name and url or email.",
|
|
20
|
+
"rfc9457-problem-details": "Add an \"application/problem+json\" content entry to the error response. See RFC 9457 for the Problem Details schema."
|
|
16
21
|
};
|
|
17
22
|
const getFindingRecommendation = (code, message) => {
|
|
18
23
|
const direct = guidanceByCode[code];
|
|
@@ -48,6 +53,7 @@ const normalizeFindings = (diagnostics, spec) => {
|
|
|
48
53
|
return {
|
|
49
54
|
ok: summary.error === 0,
|
|
50
55
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
56
|
+
source: "manual",
|
|
51
57
|
summary,
|
|
52
58
|
findings
|
|
53
59
|
};
|
|
@@ -117,31 +123,38 @@ const lintOpenApi = async (spec, ruleset) => {
|
|
|
117
123
|
return normalizeFindings(await spectral.run(spec, { ignoreUnknownFormat: false }), spec);
|
|
118
124
|
};
|
|
119
125
|
//#endregion
|
|
120
|
-
//#region src/
|
|
121
|
-
const { schema: schema$
|
|
122
|
-
const { oas: oas$
|
|
123
|
-
const operationSelector = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
//#region src/presets/recommended.ts
|
|
127
|
+
const { schema: schema$3, truthy: truthy$3 } = spectralFunctions;
|
|
128
|
+
const { oas: oas$3 } = spectralRulesets;
|
|
129
|
+
const operationSelector$2 = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
|
|
130
|
+
/**
|
|
131
|
+
* Baseline quality preset. Equivalent to the package default ruleset.
|
|
132
|
+
*
|
|
133
|
+
* - Extends spectral:oas/recommended
|
|
134
|
+
* - elysia-operation-summary and elysia-operation-tags at warn
|
|
135
|
+
* - oas3-api-servers and info-contact disabled (local-dev friendly)
|
|
136
|
+
*/
|
|
137
|
+
const recommended = {
|
|
138
|
+
extends: [[oas$3, "recommended"]],
|
|
126
139
|
rules: {
|
|
127
140
|
"oas3-api-servers": "off",
|
|
128
141
|
"info-contact": "off",
|
|
129
142
|
"elysia-operation-summary": {
|
|
130
143
|
description: "Operations should define a summary for generated docs and clients.",
|
|
131
144
|
severity: "warn",
|
|
132
|
-
given: operationSelector,
|
|
145
|
+
given: operationSelector$2,
|
|
133
146
|
then: {
|
|
134
147
|
field: "summary",
|
|
135
|
-
function: truthy$
|
|
148
|
+
function: truthy$3
|
|
136
149
|
}
|
|
137
150
|
},
|
|
138
151
|
"elysia-operation-tags": {
|
|
139
152
|
description: "Operations should declare at least one tag for grouping and downstream tooling.",
|
|
140
153
|
severity: "warn",
|
|
141
|
-
given: operationSelector,
|
|
154
|
+
given: operationSelector$2,
|
|
142
155
|
then: {
|
|
143
156
|
field: "tags",
|
|
144
|
-
function: schema$
|
|
157
|
+
function: schema$3,
|
|
145
158
|
functionOptions: { schema: {
|
|
146
159
|
type: "array",
|
|
147
160
|
minItems: 1
|
|
@@ -151,9 +164,12 @@ const defaultRuleset = {
|
|
|
151
164
|
}
|
|
152
165
|
};
|
|
153
166
|
//#endregion
|
|
167
|
+
//#region src/rulesets/default-ruleset.ts
|
|
168
|
+
const defaultRuleset = recommended;
|
|
169
|
+
//#endregion
|
|
154
170
|
//#region src/core/load-ruleset.ts
|
|
155
|
-
const { alphabetical, casing, defined, enumeration, falsy, length, or, pattern, schema, truthy, undefined: undefinedFunction, unreferencedReusableObject, xor } = spectralFunctions;
|
|
156
|
-
const { oas } = spectralRulesets;
|
|
171
|
+
const { alphabetical, casing, defined, enumeration, falsy, length, or, pattern, schema: schema$2, truthy: truthy$2, undefined: undefinedFunction, unreferencedReusableObject, xor } = spectralFunctions;
|
|
172
|
+
const { oas: oas$2 } = spectralRulesets;
|
|
157
173
|
const functionMap = {
|
|
158
174
|
alphabetical,
|
|
159
175
|
casing,
|
|
@@ -163,13 +179,13 @@ const functionMap = {
|
|
|
163
179
|
length,
|
|
164
180
|
or,
|
|
165
181
|
pattern,
|
|
166
|
-
schema,
|
|
167
|
-
truthy,
|
|
182
|
+
schema: schema$2,
|
|
183
|
+
truthy: truthy$2,
|
|
168
184
|
undefined: undefinedFunction,
|
|
169
185
|
unreferencedReusableObject,
|
|
170
186
|
xor
|
|
171
187
|
};
|
|
172
|
-
const extendsMap = { "spectral:oas": oas };
|
|
188
|
+
const extendsMap = { "spectral:oas": oas$2 };
|
|
173
189
|
const autodiscoverRulesetFilenames = [
|
|
174
190
|
"spectral.yaml",
|
|
175
191
|
"spectral.yml",
|
|
@@ -202,7 +218,7 @@ const loadResolvedRuleset = async (input, baseDirOrOptions = process.cwd()) => {
|
|
|
202
218
|
const options = normalizeLoadResolvedRulesetOptions(baseDirOrOptions);
|
|
203
219
|
const context = {
|
|
204
220
|
baseDir: options.baseDir,
|
|
205
|
-
defaultRuleset,
|
|
221
|
+
defaultRuleset: options.defaultRuleset,
|
|
206
222
|
mergeAutodiscoveredWithDefault: options.mergeAutodiscoveredWithDefault
|
|
207
223
|
};
|
|
208
224
|
for (const resolver of options.resolvers) {
|
|
@@ -213,19 +229,21 @@ const loadResolvedRuleset = async (input, baseDirOrOptions = process.cwd()) => {
|
|
|
213
229
|
return normalized;
|
|
214
230
|
}
|
|
215
231
|
}
|
|
216
|
-
if (input === void 0) return { ruleset: defaultRuleset };
|
|
232
|
+
if (input === void 0) return { ruleset: options.defaultRuleset };
|
|
217
233
|
throw new RulesetLoadError("Ruleset input could not be resolved.");
|
|
218
234
|
};
|
|
219
235
|
const normalizeLoadResolvedRulesetOptions = (value) => {
|
|
220
236
|
if (typeof value === "string") return {
|
|
221
237
|
baseDir: value,
|
|
222
238
|
resolvers: defaultRulesetResolvers,
|
|
223
|
-
mergeAutodiscoveredWithDefault: true
|
|
239
|
+
mergeAutodiscoveredWithDefault: true,
|
|
240
|
+
defaultRuleset
|
|
224
241
|
};
|
|
225
242
|
return {
|
|
226
243
|
baseDir: value.baseDir ?? process.cwd(),
|
|
227
244
|
resolvers: value.resolvers ?? defaultRulesetResolvers,
|
|
228
|
-
mergeAutodiscoveredWithDefault: value.mergeAutodiscoveredWithDefault ?? true
|
|
245
|
+
mergeAutodiscoveredWithDefault: value.mergeAutodiscoveredWithDefault ?? true,
|
|
246
|
+
defaultRuleset: value.defaultRuleset ?? defaultRuleset
|
|
229
247
|
};
|
|
230
248
|
};
|
|
231
249
|
const resolveAutodiscoveredRuleset = async (input, context) => {
|
|
@@ -324,30 +342,30 @@ const loadModuleRuleset = async (resolvedPath) => {
|
|
|
324
342
|
});
|
|
325
343
|
};
|
|
326
344
|
const resolveModuleRulesetValue = (imported) => {
|
|
327
|
-
if (!isRecord(imported)) return;
|
|
345
|
+
if (!isRecord$1(imported)) return;
|
|
328
346
|
if ("default" in imported) return imported.default;
|
|
329
347
|
if ("ruleset" in imported) return imported.ruleset;
|
|
330
348
|
};
|
|
331
349
|
const resolveModuleFunctions = (imported) => {
|
|
332
|
-
if (!isRecord(imported) || !("functions" in imported)) return {};
|
|
350
|
+
if (!isRecord$1(imported) || !("functions" in imported)) return {};
|
|
333
351
|
const { functions } = imported;
|
|
334
|
-
if (!isRecord(functions)) throw new RulesetLoadError("Module ruleset \"functions\" export must be an object map of function names to Spectral functions.");
|
|
352
|
+
if (!isRecord$1(functions)) throw new RulesetLoadError("Module ruleset \"functions\" export must be an object map of function names to Spectral functions.");
|
|
335
353
|
const entries = Object.entries(functions).filter(([, value]) => typeof value === "function");
|
|
336
354
|
return Object.fromEntries(entries);
|
|
337
355
|
};
|
|
338
356
|
const isYamlRulesetPath = (value) => value.endsWith(".yaml") || value.endsWith(".yml");
|
|
339
357
|
const isModuleRulesetPath = (value) => value.endsWith(".js") || value.endsWith(".mjs") || value.endsWith(".cjs") || value.endsWith(".ts") || value.endsWith(".mts") || value.endsWith(".cts");
|
|
340
358
|
const normalizeRulesetDefinition = (input, availableFunctions = functionMap) => {
|
|
341
|
-
if (!isRecord(input)) throw new RulesetLoadError("Ruleset must be an object.");
|
|
359
|
+
if (!isRecord$1(input)) throw new RulesetLoadError("Ruleset must be an object.");
|
|
342
360
|
const normalized = { ...input };
|
|
343
361
|
if ("extends" in normalized) normalized.extends = normalizeExtends(normalized.extends);
|
|
344
362
|
if ("rules" in normalized) normalized.rules = normalizeRules(normalized.rules, availableFunctions);
|
|
345
363
|
return normalized;
|
|
346
364
|
};
|
|
347
365
|
const mergeRuleEntry = (base, override) => {
|
|
348
|
-
if (!isRecord(override)) return override;
|
|
366
|
+
if (!isRecord$1(override)) return override;
|
|
349
367
|
if ("given" in override || "then" in override) return override;
|
|
350
|
-
if (isRecord(base) && ("given" in base || "then" in base)) return {
|
|
368
|
+
if (isRecord$1(base) && ("given" in base || "then" in base)) return {
|
|
351
369
|
...base,
|
|
352
370
|
...override
|
|
353
371
|
};
|
|
@@ -358,8 +376,8 @@ const mergeRuleEntry = (base, override) => {
|
|
|
358
376
|
const mergeRulesets = (baseRuleset, overrideRuleset) => {
|
|
359
377
|
const mergedBase = baseRuleset;
|
|
360
378
|
const mergedOverride = overrideRuleset;
|
|
361
|
-
const baseRules = isRecord(mergedBase.rules) ? mergedBase.rules : {};
|
|
362
|
-
const overrideRules = isRecord(mergedOverride.rules) ? mergedOverride.rules : {};
|
|
379
|
+
const baseRules = isRecord$1(mergedBase.rules) ? mergedBase.rules : {};
|
|
380
|
+
const overrideRules = isRecord$1(mergedOverride.rules) ? mergedOverride.rules : {};
|
|
363
381
|
const mergedRules = { ...baseRules };
|
|
364
382
|
for (const [name, overrideRule] of Object.entries(overrideRules)) mergedRules[name] = mergeRuleEntry(baseRules[name], overrideRule);
|
|
365
383
|
const baseExtends = toExtendsArray(mergedBase.extends);
|
|
@@ -394,12 +412,12 @@ const resolveExtendsEntry = (value) => {
|
|
|
394
412
|
return resolved;
|
|
395
413
|
};
|
|
396
414
|
const normalizeRules = (value, availableFunctions) => {
|
|
397
|
-
if (!isRecord(value)) return value;
|
|
415
|
+
if (!isRecord$1(value)) return value;
|
|
398
416
|
const entries = Object.entries(value).map(([ruleName, ruleValue]) => [ruleName, normalizeRule(ruleValue, availableFunctions)]);
|
|
399
417
|
return Object.fromEntries(entries);
|
|
400
418
|
};
|
|
401
419
|
const normalizeRule = (value, availableFunctions) => {
|
|
402
|
-
if (!isRecord(value)) return value;
|
|
420
|
+
if (!isRecord$1(value)) return value;
|
|
403
421
|
const normalized = { ...value };
|
|
404
422
|
if ("then" in normalized) normalized.then = normalizeThen(normalized.then, availableFunctions);
|
|
405
423
|
return normalized;
|
|
@@ -409,7 +427,7 @@ const normalizeThen = (value, availableFunctions) => {
|
|
|
409
427
|
return normalizeThenEntry(value, availableFunctions);
|
|
410
428
|
};
|
|
411
429
|
const normalizeThenEntry = (value, availableFunctions) => {
|
|
412
|
-
if (!isRecord(value)) return value;
|
|
430
|
+
if (!isRecord$1(value)) return value;
|
|
413
431
|
const normalized = { ...value };
|
|
414
432
|
if (typeof normalized.function === "string") {
|
|
415
433
|
const resolved = availableFunctions[normalized.function];
|
|
@@ -418,7 +436,7 @@ const normalizeThenEntry = (value, availableFunctions) => {
|
|
|
418
436
|
}
|
|
419
437
|
return normalized;
|
|
420
438
|
};
|
|
421
|
-
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
439
|
+
const isRecord$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
422
440
|
//#endregion
|
|
423
441
|
//#region src/output/terminal-format.ts
|
|
424
442
|
const severityStyles = {
|
|
@@ -873,6 +891,133 @@ const createOutputSinks = (options) => {
|
|
|
873
891
|
return sinks;
|
|
874
892
|
};
|
|
875
893
|
//#endregion
|
|
894
|
+
//#region src/presets/server.ts
|
|
895
|
+
const { schema: schema$1, truthy: truthy$1 } = spectralFunctions;
|
|
896
|
+
const { oas: oas$1 } = spectralRulesets;
|
|
897
|
+
const operationSelector$1 = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
|
|
898
|
+
/**
|
|
899
|
+
* Production API quality preset. Suitable as a CI gate for teams shipping
|
|
900
|
+
* public or internal APIs where contract quality matters.
|
|
901
|
+
*
|
|
902
|
+
* Tightens recommended:
|
|
903
|
+
* - elysia-operation-summary and elysia-operation-tags escalated to error
|
|
904
|
+
* - operation-description, operation-operationId, operation-success-response at warn
|
|
905
|
+
* - oas3-api-servers at warn (servers should be declared in production specs)
|
|
906
|
+
*/
|
|
907
|
+
const server = {
|
|
908
|
+
extends: [[oas$1, "recommended"]],
|
|
909
|
+
rules: {
|
|
910
|
+
"oas3-api-servers": "warn",
|
|
911
|
+
"info-contact": "off",
|
|
912
|
+
"elysia-operation-summary": {
|
|
913
|
+
description: "Operations should define a summary for generated docs and clients.",
|
|
914
|
+
severity: "error",
|
|
915
|
+
given: operationSelector$1,
|
|
916
|
+
then: {
|
|
917
|
+
field: "summary",
|
|
918
|
+
function: truthy$1
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
"elysia-operation-tags": {
|
|
922
|
+
description: "Operations should declare at least one tag for grouping and downstream tooling.",
|
|
923
|
+
severity: "error",
|
|
924
|
+
given: operationSelector$1,
|
|
925
|
+
then: {
|
|
926
|
+
field: "tags",
|
|
927
|
+
function: schema$1,
|
|
928
|
+
functionOptions: { schema: {
|
|
929
|
+
type: "array",
|
|
930
|
+
minItems: 1
|
|
931
|
+
} }
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
"operation-description": "warn",
|
|
935
|
+
"operation-operationId": "warn",
|
|
936
|
+
"operation-success-response": "warn"
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region src/presets/strict.ts
|
|
941
|
+
const { schema, truthy } = spectralFunctions;
|
|
942
|
+
const { oas } = spectralRulesets;
|
|
943
|
+
const operationSelector = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
|
|
944
|
+
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
945
|
+
/**
|
|
946
|
+
* Checks that all 4xx/5xx responses declare application/problem+json as
|
|
947
|
+
* their content type, conforming to RFC 9457 Problem Details for HTTP APIs.
|
|
948
|
+
*/
|
|
949
|
+
const checkProblemDetails = (operation) => {
|
|
950
|
+
if (!isRecord(operation) || !isRecord(operation.responses)) return;
|
|
951
|
+
const results = [];
|
|
952
|
+
for (const [statusCode, response] of Object.entries(operation.responses)) {
|
|
953
|
+
const code = Number(statusCode);
|
|
954
|
+
if (!Number.isFinite(code) || code < 400) continue;
|
|
955
|
+
if (!isRecord(response)) continue;
|
|
956
|
+
const content = response.content;
|
|
957
|
+
if (!isRecord(content) || !("application/problem+json" in content)) results.push({
|
|
958
|
+
message: `${statusCode} error response should use "application/problem+json" content type (RFC 9457 Problem Details).`,
|
|
959
|
+
path: ["responses", statusCode]
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
return results.length > 0 ? results : void 0;
|
|
963
|
+
};
|
|
964
|
+
/**
|
|
965
|
+
* Full API governance preset. Suitable for teams with formal API governance
|
|
966
|
+
* requirements, public API programs, or downstream client generation pipelines.
|
|
967
|
+
*
|
|
968
|
+
* Tightens server:
|
|
969
|
+
* - All elysia rules and operation metadata rules escalated to error
|
|
970
|
+
* - info-contact at warn (API ownership should be declared)
|
|
971
|
+
* - oas3-api-servers at error (server declaration is required)
|
|
972
|
+
* - rfc9457-problem-details at warn (error responses should use Problem Details)
|
|
973
|
+
*/
|
|
974
|
+
const strict = {
|
|
975
|
+
extends: [[oas, "recommended"]],
|
|
976
|
+
rules: {
|
|
977
|
+
"oas3-api-servers": "error",
|
|
978
|
+
"info-contact": "warn",
|
|
979
|
+
"elysia-operation-summary": {
|
|
980
|
+
description: "Operations should define a summary for generated docs and clients.",
|
|
981
|
+
severity: "error",
|
|
982
|
+
given: operationSelector,
|
|
983
|
+
then: {
|
|
984
|
+
field: "summary",
|
|
985
|
+
function: truthy
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
"elysia-operation-tags": {
|
|
989
|
+
description: "Operations should declare at least one tag for grouping and downstream tooling.",
|
|
990
|
+
severity: "error",
|
|
991
|
+
given: operationSelector,
|
|
992
|
+
then: {
|
|
993
|
+
field: "tags",
|
|
994
|
+
function: schema,
|
|
995
|
+
functionOptions: { schema: {
|
|
996
|
+
type: "array",
|
|
997
|
+
minItems: 1
|
|
998
|
+
} }
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
"operation-description": "error",
|
|
1002
|
+
"operation-operationId": "error",
|
|
1003
|
+
"operation-success-response": "error",
|
|
1004
|
+
"rfc9457-problem-details": {
|
|
1005
|
+
description: "Error responses (4xx, 5xx) should use RFC 9457 Problem Details (application/problem+json).",
|
|
1006
|
+
message: "{{error}}",
|
|
1007
|
+
severity: "warn",
|
|
1008
|
+
given: operationSelector,
|
|
1009
|
+
then: { function: checkProblemDetails }
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
//#endregion
|
|
1014
|
+
//#region src/presets/index.ts
|
|
1015
|
+
const presets = {
|
|
1016
|
+
recommended,
|
|
1017
|
+
server,
|
|
1018
|
+
strict
|
|
1019
|
+
};
|
|
1020
|
+
//#endregion
|
|
876
1021
|
//#region src/providers/spec-provider.ts
|
|
877
1022
|
var BaseSpecProvider = class {
|
|
878
1023
|
constructor(app, options = {}) {
|
|
@@ -1011,7 +1156,7 @@ const createOpenApiLintRuntime = (options = {}) => {
|
|
|
1011
1156
|
lastSuccess: null,
|
|
1012
1157
|
lastFailure: null,
|
|
1013
1158
|
running: false,
|
|
1014
|
-
async run(app) {
|
|
1159
|
+
async run(app, source = "manual") {
|
|
1015
1160
|
if (inFlight) return await inFlight;
|
|
1016
1161
|
inFlight = (async () => {
|
|
1017
1162
|
const startedAt = /* @__PURE__ */ new Date();
|
|
@@ -1023,10 +1168,14 @@ const createOpenApiLintRuntime = (options = {}) => {
|
|
|
1023
1168
|
reporter.start("OpenAPI lint started.");
|
|
1024
1169
|
try {
|
|
1025
1170
|
const spec = await new PublicSpecProvider(app, options.source).getSpec();
|
|
1026
|
-
const loadedRuleset = await loadResolvedRuleset(options.ruleset);
|
|
1027
|
-
if (loadedRuleset.source?.autodiscovered)
|
|
1171
|
+
const loadedRuleset = await loadResolvedRuleset(options.ruleset, { ...options.preset ? { defaultRuleset: presets[options.preset] } : {} });
|
|
1172
|
+
if (loadedRuleset.source?.autodiscovered) {
|
|
1173
|
+
const base = options.preset ? `"${options.preset}" preset` : "package default ruleset";
|
|
1174
|
+
reporter.ruleset(`OpenAPI lint autodiscovered ruleset ${loadedRuleset.source.path} and merged it with the ${base}.`);
|
|
1175
|
+
} else if (options.preset && !loadedRuleset.source?.path) reporter.ruleset(`OpenAPI lint using "${options.preset}" preset.`);
|
|
1028
1176
|
else if (loadedRuleset.source?.path) reporter.ruleset(`OpenAPI lint loaded ruleset ${loadedRuleset.source.path}.`);
|
|
1029
1177
|
const result = await lintOpenApi(spec, loadedRuleset.ruleset);
|
|
1178
|
+
result.source = source;
|
|
1030
1179
|
await writeOutputSinks(result, spec, options, artifactWriteFailureMode);
|
|
1031
1180
|
runtime.latest = result;
|
|
1032
1181
|
reporter.complete("OpenAPI lint completed.");
|
|
@@ -1105,4 +1254,4 @@ const resolveStartupMode = (options = {}) => {
|
|
|
1105
1254
|
return options.enabled === false ? "off" : "enforce";
|
|
1106
1255
|
};
|
|
1107
1256
|
//#endregion
|
|
1108
|
-
export { OpenApiLintThresholdError as a, shouldFail as c,
|
|
1257
|
+
export { recommended as _, OpenApiLintThresholdError as a, shouldFail as c, server as d, resolveReporter as f, loadRuleset as g, loadResolvedRuleset as h, resolveStartupMode as i, presets as l, defaultRulesetResolvers as m, createOpenApiLintRuntime as n, enforceThreshold as o, RulesetLoadError as p, isEnabled as r, exceedsThreshold as s, OpenApiLintArtifactWriteError as t, strict as u, lintOpenApi as v, normalizeFindings as y };
|
|
@@ -2,10 +2,12 @@ import { ISpectralDiagnostic, RulesetDefinition } from "@stoplight/spectral-core
|
|
|
2
2
|
import { AnyElysia } from "elysia";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
5
|
+
type PresetName = 'recommended' | 'server' | 'strict';
|
|
5
6
|
type SeverityThreshold = 'error' | 'warn' | 'info' | 'hint' | 'never';
|
|
6
7
|
type LintSeverity = 'error' | 'warn' | 'info' | 'hint';
|
|
7
8
|
type StartupLintMode = 'enforce' | 'report' | 'off';
|
|
8
9
|
type OpenApiLintRuntimeStatus = 'idle' | 'running' | 'passed' | 'failed';
|
|
10
|
+
type LintRunSource = 'startup' | 'healthcheck' | 'manual';
|
|
9
11
|
type ArtifactWriteFailureMode = 'warn' | 'error';
|
|
10
12
|
type SpectralLogger = {
|
|
11
13
|
info: (message: string) => void;
|
|
@@ -27,6 +29,7 @@ type OpenApiLintSink = {
|
|
|
27
29
|
write: (result: LintRunResult, context: OpenApiLintSinkContext) => undefined | Partial<OpenApiLintArtifacts> | Promise<undefined | Partial<OpenApiLintArtifacts>>;
|
|
28
30
|
};
|
|
29
31
|
type SpectralPluginOptions = {
|
|
32
|
+
preset?: PresetName;
|
|
30
33
|
ruleset?: string | RulesetDefinition | Record<string, unknown>;
|
|
31
34
|
failOn?: SeverityThreshold;
|
|
32
35
|
healthcheck?: false | {
|
|
@@ -79,6 +82,7 @@ type LintFinding = {
|
|
|
79
82
|
type LintRunResult = {
|
|
80
83
|
ok: boolean;
|
|
81
84
|
generatedAt: string;
|
|
85
|
+
source: LintRunSource;
|
|
82
86
|
summary: {
|
|
83
87
|
error: number;
|
|
84
88
|
warn: number;
|
|
@@ -106,7 +110,7 @@ type OpenApiLintRuntime = {
|
|
|
106
110
|
lastSuccess: LintRunResult | null;
|
|
107
111
|
lastFailure: OpenApiLintRuntimeFailure | null;
|
|
108
112
|
running: boolean;
|
|
109
|
-
run: (app: AnyElysia) => Promise<LintRunResult>;
|
|
113
|
+
run: (app: AnyElysia, source?: LintRunSource) => Promise<LintRunResult>;
|
|
110
114
|
};
|
|
111
115
|
//#endregion
|
|
112
116
|
//#region src/core/lint-openapi.d.ts
|
|
@@ -135,7 +139,8 @@ type RulesetResolver = (input: RulesetResolverInput, context: RulesetResolverCon
|
|
|
135
139
|
type LoadResolvedRulesetOptions = {
|
|
136
140
|
baseDir?: string;
|
|
137
141
|
resolvers?: RulesetResolver[];
|
|
138
|
-
mergeAutodiscoveredWithDefault?: boolean;
|
|
142
|
+
mergeAutodiscoveredWithDefault?: boolean; /** Override the ruleset used as the merge base for autodiscovery and the fallback when no ruleset is configured. Defaults to the package default (recommended preset). */
|
|
143
|
+
defaultRuleset?: RulesetDefinition;
|
|
139
144
|
};
|
|
140
145
|
declare class RulesetLoadError extends Error {
|
|
141
146
|
constructor(message: string, options?: {
|
|
@@ -169,4 +174,4 @@ declare const exceedsThreshold: (severity: LintSeverity, threshold: SeverityThre
|
|
|
169
174
|
declare const shouldFail: (result: LintRunResult, threshold: SeverityThreshold) => boolean;
|
|
170
175
|
declare const enforceThreshold: (result: LintRunResult, threshold: SeverityThreshold) => void;
|
|
171
176
|
//#endregion
|
|
172
|
-
export {
|
|
177
|
+
export { OpenApiLintSink as A, LintRunResult as C, OpenApiLintRuntime as D, OpenApiLintArtifacts as E, SpectralLogger as F, SpectralPluginOptions as I, StartupLintMode as L, PresetName as M, SeverityThreshold as N, OpenApiLintRuntimeFailure as O, SpecProvider as P, LintFinding as S, LintSeverity as T, defaultRulesetResolvers as _, OpenApiLintArtifactWriteError as a, lintOpenApi as b, resolveStartupMode as c, LoadedRuleset as d, ResolvedRulesetCandidate as f, RulesetResolverInput as g, RulesetResolverContext as h, shouldFail as i, OpenApiLintSinkContext as j, OpenApiLintRuntimeStatus as k, normalizeFindings as l, RulesetResolver as m, enforceThreshold as n, createOpenApiLintRuntime as o, RulesetLoadError as p, exceedsThreshold as r, isEnabled as s, OpenApiLintThresholdError as t, LoadResolvedRulesetOptions as u, loadResolvedRuleset as v, LintRunSource as w, ArtifactWriteFailureMode as x, loadRuleset as y };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as OpenApiLintSink, C as LintRunResult, D as OpenApiLintRuntime, E as OpenApiLintArtifacts, F as SpectralLogger, I as SpectralPluginOptions, L as StartupLintMode, M as PresetName, N as SeverityThreshold, O as OpenApiLintRuntimeFailure, P as SpecProvider, S as LintFinding, T as LintSeverity, _ as defaultRulesetResolvers, a as OpenApiLintArtifactWriteError, b as lintOpenApi, c as resolveStartupMode, d as LoadedRuleset, f as ResolvedRulesetCandidate, g as RulesetResolverInput, h as RulesetResolverContext, i as shouldFail, j as OpenApiLintSinkContext, k as OpenApiLintRuntimeStatus, l as normalizeFindings, m as RulesetResolver, n as enforceThreshold, o as createOpenApiLintRuntime, p as RulesetLoadError, r as exceedsThreshold, s as isEnabled, t as OpenApiLintThresholdError, u as LoadResolvedRulesetOptions, v as loadResolvedRuleset, w as LintRunSource, x as ArtifactWriteFailureMode, y as loadRuleset } from "./index-CMyl_MsI.mjs";
|
|
2
|
+
import { RulesetDefinition } from "@stoplight/spectral-core";
|
|
2
3
|
import { Elysia } from "elysia";
|
|
3
4
|
|
|
4
5
|
//#region src/plugin.d.ts
|
|
@@ -33,4 +34,42 @@ declare const spectralPlugin: (options?: SpectralPluginOptions) => Elysia<"", {
|
|
|
33
34
|
response: {};
|
|
34
35
|
}>;
|
|
35
36
|
//#endregion
|
|
36
|
-
|
|
37
|
+
//#region src/presets/recommended.d.ts
|
|
38
|
+
/**
|
|
39
|
+
* Baseline quality preset. Equivalent to the package default ruleset.
|
|
40
|
+
*
|
|
41
|
+
* - Extends spectral:oas/recommended
|
|
42
|
+
* - elysia-operation-summary and elysia-operation-tags at warn
|
|
43
|
+
* - oas3-api-servers and info-contact disabled (local-dev friendly)
|
|
44
|
+
*/
|
|
45
|
+
declare const recommended: RulesetDefinition;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/presets/server.d.ts
|
|
48
|
+
/**
|
|
49
|
+
* Production API quality preset. Suitable as a CI gate for teams shipping
|
|
50
|
+
* public or internal APIs where contract quality matters.
|
|
51
|
+
*
|
|
52
|
+
* Tightens recommended:
|
|
53
|
+
* - elysia-operation-summary and elysia-operation-tags escalated to error
|
|
54
|
+
* - operation-description, operation-operationId, operation-success-response at warn
|
|
55
|
+
* - oas3-api-servers at warn (servers should be declared in production specs)
|
|
56
|
+
*/
|
|
57
|
+
declare const server: RulesetDefinition;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/presets/strict.d.ts
|
|
60
|
+
/**
|
|
61
|
+
* Full API governance preset. Suitable for teams with formal API governance
|
|
62
|
+
* requirements, public API programs, or downstream client generation pipelines.
|
|
63
|
+
*
|
|
64
|
+
* Tightens server:
|
|
65
|
+
* - All elysia rules and operation metadata rules escalated to error
|
|
66
|
+
* - info-contact at warn (API ownership should be declared)
|
|
67
|
+
* - oas3-api-servers at error (server declaration is required)
|
|
68
|
+
* - rfc9457-problem-details at warn (error responses should use Problem Details)
|
|
69
|
+
*/
|
|
70
|
+
declare const strict: RulesetDefinition;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/presets/index.d.ts
|
|
73
|
+
declare const presets: Record<PresetName, RulesetDefinition>;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintRunSource, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, PresetName, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold, SpecProvider, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, presets, recommended, resolveStartupMode, server, shouldFail, spectralPlugin, strict };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as OpenApiLintThresholdError, c as shouldFail, d as
|
|
1
|
+
import { _ as recommended, a as OpenApiLintThresholdError, c as shouldFail, d as server, f as resolveReporter, g as loadRuleset, h as loadResolvedRuleset, i as resolveStartupMode, l as presets, m as defaultRulesetResolvers, n as createOpenApiLintRuntime, o as enforceThreshold, p as RulesetLoadError, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, u as strict, v as lintOpenApi, y as normalizeFindings } from "./core-D_ro1XEW.mjs";
|
|
2
2
|
import { Elysia } from "elysia";
|
|
3
3
|
//#region src/plugin.ts
|
|
4
4
|
const spectralPlugin = (options = {}) => {
|
|
@@ -11,7 +11,7 @@ const spectralPlugin = (options = {}) => {
|
|
|
11
11
|
const startupMode = resolveStartupMode(options);
|
|
12
12
|
if (startupMode === "off") return;
|
|
13
13
|
try {
|
|
14
|
-
await runtime.run(app);
|
|
14
|
+
await runtime.run(app, "startup");
|
|
15
15
|
} catch (error) {
|
|
16
16
|
if (startupMode === "report" && error instanceof OpenApiLintThresholdError) {
|
|
17
17
|
reporter.report(`OpenAPI lint exceeded the "${options.failOn ?? "error"}" threshold, but startup is continuing because startup.mode is "report".`);
|
|
@@ -37,7 +37,7 @@ const spectralPlugin = (options = {}) => {
|
|
|
37
37
|
}
|
|
38
38
|
try {
|
|
39
39
|
const usedCache = !fresh && (runtime.latest !== null || runtime.running);
|
|
40
|
-
const result = usedCache ? runtime.latest ?? await runtime.run(currentApp) : await runtime.run(currentApp);
|
|
40
|
+
const result = usedCache ? runtime.latest ?? await runtime.run(currentApp, "healthcheck") : await runtime.run(currentApp, "healthcheck");
|
|
41
41
|
if (result === null) {
|
|
42
42
|
set.status = 500;
|
|
43
43
|
return {
|
|
@@ -82,4 +82,4 @@ const spectralPlugin = (options = {}) => {
|
|
|
82
82
|
return plugin;
|
|
83
83
|
};
|
|
84
84
|
//#endregion
|
|
85
|
-
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail, spectralPlugin };
|
|
85
|
+
export { OpenApiLintArtifactWriteError, OpenApiLintThresholdError, RulesetLoadError, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, presets, recommended, resolveStartupMode, server, shouldFail, spectralPlugin, strict };
|