@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 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 with a repo-level ruleset and JSON artifacts.
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. Create a repo-level `spectral.yaml`.
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
- 4. Start the app.
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
- 5. Confirm the outcome.
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 Signale-style success and hype lines in the terminal
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 or tests
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 { Elysia } from 'elysia'
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
- ruleset: './spectral.yaml',
440
+ preset: 'strict',
414
441
  failOn: 'error',
415
442
  output: {
416
443
  console: true,
417
- jsonReportPath: './artifacts/openapi-lint.json',
418
- specSnapshotPath: true,
419
- artifactWriteFailures: 'error'
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
- await runtime.run(app)
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
- ) => void | Partial<OpenApiLintArtifacts> | Promise<void | Partial<OpenApiLintArtifacts>>
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,
@@ -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-BrFQCFDI.mjs";
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 };
@@ -1,2 +1,2 @@
1
- import { a as OpenApiLintThresholdError, c as shouldFail, d as defaultRulesetResolvers, f as loadResolvedRuleset, h as normalizeFindings, i as resolveStartupMode, m as lintOpenApi, n as createOpenApiLintRuntime, o as enforceThreshold, p as loadRuleset, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, u as RulesetLoadError } from "../core-CTQQBA_q.mjs";
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/rulesets/default-ruleset.ts
121
- const { schema: schema$1, truthy: truthy$1 } = spectralFunctions;
122
- const { oas: oas$1 } = spectralRulesets;
123
- const operationSelector = "$.paths[*][get,put,post,delete,options,head,patch,trace]";
124
- const defaultRuleset = {
125
- extends: [[oas$1, "recommended"]],
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$1
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$1,
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) reporter.ruleset(`OpenAPI lint autodiscovered ruleset ${loadedRuleset.source.path} and merged it with the package default ruleset.`);
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, defaultRulesetResolvers as d, loadResolvedRuleset as f, normalizeFindings as h, resolveStartupMode as i, resolveReporter as l, lintOpenApi as m, createOpenApiLintRuntime as n, enforceThreshold as o, loadRuleset as p, isEnabled as r, exceedsThreshold as s, OpenApiLintArtifactWriteError as t, RulesetLoadError as u };
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 { OpenApiLintSinkContext as A, LintRunResult as C, OpenApiLintRuntimeFailure as D, OpenApiLintRuntime as E, StartupLintMode as F, SpecProvider as M, SpectralLogger as N, OpenApiLintRuntimeStatus as O, SpectralPluginOptions as P, LintFinding as S, OpenApiLintArtifacts 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, SeverityThreshold as j, OpenApiLintSink 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, LintSeverity as w, ArtifactWriteFailureMode as x, loadRuleset as y };
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 OpenApiLintSinkContext, C as LintRunResult, D as OpenApiLintRuntimeFailure, E as OpenApiLintRuntime, F as StartupLintMode, M as SpecProvider, N as SpectralLogger, O as OpenApiLintRuntimeStatus, P as SpectralPluginOptions, S as LintFinding, T as OpenApiLintArtifacts, _ 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 SeverityThreshold, k as OpenApiLintSink, 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 LintSeverity, x as ArtifactWriteFailureMode, y as loadRuleset } from "./index-BrFQCFDI.mjs";
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
- export { ArtifactWriteFailureMode, LintFinding, LintRunResult, LintSeverity, LoadResolvedRulesetOptions, LoadedRuleset, OpenApiLintArtifactWriteError, OpenApiLintArtifacts, OpenApiLintRuntime, OpenApiLintRuntimeFailure, OpenApiLintRuntimeStatus, OpenApiLintSink, OpenApiLintSinkContext, OpenApiLintThresholdError, ResolvedRulesetCandidate, RulesetLoadError, RulesetResolver, RulesetResolverContext, RulesetResolverInput, SeverityThreshold, SpecProvider, SpectralLogger, SpectralPluginOptions, StartupLintMode, createOpenApiLintRuntime, defaultRulesetResolvers, enforceThreshold, exceedsThreshold, isEnabled, lintOpenApi, loadResolvedRuleset, loadRuleset, normalizeFindings, resolveStartupMode, shouldFail, spectralPlugin };
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 defaultRulesetResolvers, f as loadResolvedRuleset, h as normalizeFindings, i as resolveStartupMode, l as resolveReporter, m as lintOpenApi, n as createOpenApiLintRuntime, o as enforceThreshold, p as loadRuleset, r as isEnabled, s as exceedsThreshold, t as OpenApiLintArtifactWriteError, u as RulesetLoadError } from "./core-CTQQBA_q.mjs";
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsydyn/elysia-spectral",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
4
4
  "description": "Thin Elysia plugin that lints generated OpenAPI documents with Spectral.",
5
5
  "packageManager": "bun@1.2.9",
6
6
  "publishConfig": {