diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 08b834df..67bb2aaf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,12 +9,18 @@ on:
+permissions:
+ contents: read
+ pull-requests: read
+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
lint_unit_tests_and_builds:
+ needs: changes
+ if: needs.changes.outputs.renderer == 'true'
runs-on: ubuntu-latest
steps:
# eslint-plugin-diff needs the base branch present to diff against.
@@ -43,8 +49,8 @@ jobs:
run: npm run build:standalone -w packages/bruno-api-docs
e2e:
- needs: lint_unit_tests_and_builds
- if: github.event_name == 'push' || github.event.pull_request.draft == false
+ needs: [changes, lint_unit_tests_and_builds]
+ if: needs.changes.outputs.renderer == 'true' && (github.event_name == 'push' || github.event.pull_request.draft == false)
runs-on: ubuntu-latest
defaults:
run:
@@ -70,3 +76,92 @@ jobs:
name: playwright-report
path: packages/bruno-api-docs/playwright-report/
retention-days: 30
+
+ changes:
+ runs-on: ubuntu-latest
+ outputs:
+ renderer: ${{ steps.filter.outputs.renderer }}
+ integrations: ${{ steps.filter.outputs.integrations }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@0e4a8c6effa4802afeda77dc8d303f8176d7dfad # v3
+ id: filter
+ with:
+ filters: |
+ renderer:
+ - '!integrations/**'
+ integrations:
+ - 'integrations/**'
+ - '.github/workflows/ci.yml'
+ - '.github/workflows/integrations-matrix.yml'
+
+ integrations:
+ needs: changes
+ # a job-level if, not a workflow paths filter: a skipped job still reports and satisfies the required check
+ if: needs.changes.outputs.integrations == 'true'
+ runs-on: ubuntu-latest
+ # checks: write is for the test reporter; job-scoped so the renderer jobs keep least privilege
+ permissions:
+ contents: read
+ checks: write
+ defaults:
+ run:
+ working-directory: integrations
+ env:
+ FORCE_COLOR: 1
+ JUNIT_DIR: results
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: jdx/mise-action@v2
+ with:
+ working_directory: integrations
+
+ - uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: npm-integrations-${{ hashFiles('integrations/package-lock.json') }}
+ restore-keys: npm-integrations-
+
+ - run: npm ci
+
+ - name: lint
+ run: npm run lint
+
+ - name: build
+ run: npm run build
+
+ - name: unit
+ run: bash nodejs/scripts/unit.sh
+
+ - name: contract, every framework byte for byte
+ run: bash nodejs/scripts/check-all.sh
+
+ - name: examples
+ run: bash contract-tests/examples.sh
+
+ - name: one matrix cell, from packed tarballs
+ run: bash nodejs/scripts/matrix.sh --cell express@5 --results results
+
+ # every assertion in the Checks tab, failures as annotations
+ - uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
+ if: always()
+ with:
+ name: integrations
+ path: integrations/results/**/*.xml
+ reporter: java-junit
+ fail-on-error: false
+
+ # both tiers depend on the live CDN bundle, which this repo does not control, so neither can
+ # block a merge. `contract` is ours to fix when red; `renderer` is theirs to know about.
+ - name: browser
+ continue-on-error: true
+ run: npx playwright install --with-deps chromium
+
+ - name: browser, our contract
+ continue-on-error: true
+ run: npx playwright test --config contract-tests/browser/playwright.config.mjs --project=contract
+
+ - name: browser, the renderer
+ continue-on-error: true
+ run: npx playwright test --config contract-tests/browser/playwright.config.mjs --project=renderer
diff --git a/.github/workflows/integrations-matrix.yml b/.github/workflows/integrations-matrix.yml
new file mode 100644
index 00000000..75aa221e
--- /dev/null
+++ b/.github/workflows/integrations-matrix.yml
@@ -0,0 +1,127 @@
+name: Integrations matrix
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: '0 6 * * 1'
+ pull_request:
+ branches: [main, 'release/**']
+ types: [labeled, synchronize]
+
+permissions:
+ contents: read
+ pull-requests: write
+ checks: write
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+env:
+ FORCE_COLOR: 1
+
+jobs:
+ cells:
+ if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-integrations-matrix-tests')
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [22, 24]
+ family: [express, fastify, nestjs]
+ name: node ${{ matrix.node }} / ${{ matrix.family }}
+ defaults:
+ run:
+ working-directory: integrations
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+
+ - run: npm ci
+
+ - run: npm run build
+
+ - run: bash nodejs/scripts/matrix.sh --family ${{ matrix.family }} --results results
+
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: matrix-${{ matrix.node }}-${{ matrix.family }}
+ path: integrations/results
+
+ node-20:
+ if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-integrations-matrix-tests')
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ name: node 20 (end of life, courtesy)
+ defaults:
+ run:
+ working-directory: integrations
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ - run: npm ci
+ - run: npm run build
+ - run: bash nodejs/scripts/matrix.sh --results results
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: matrix-20-all
+ path: integrations/results
+
+ report:
+ needs: [cells, node-20]
+ if: always() && needs.cells.result != 'skipped'
+ runs-on: ubuntu-latest
+ name: results table
+ defaults:
+ run:
+ working-directory: integrations
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: matrix-*
+ merge-multiple: true
+ path: integrations/results
+
+ - run: |
+ {
+ echo "## Integrations matrix"
+ echo
+ bash nodejs/scripts/matrix.sh --report results --md
+ echo
+ echo "_${{ github.sha }} · node 20 is informational_"
+ } > table.md
+ cat table.md >> "$GITHUB_STEP_SUMMARY"
+
+ # every assertion in the Checks tab, failures as annotations
+ - uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
+ with:
+ name: integrations matrix
+ path: integrations/results/**/*.xml
+ reporter: java-junit
+ fail-on-error: false
+
+ # one comment per PR, updated in place on every run
+ - if: github.event_name == 'pull_request'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR: ${{ github.event.pull_request.number }}
+ run: |
+ marker=''
+ printf '%s\n' "$marker" > body.md
+ cat table.md >> body.md
+ existing=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" --paginate \
+ --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -1)
+ if [ -n "$existing" ]; then
+ gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing" -F body=@body.md >/dev/null
+ else
+ gh pr comment "$PR" --body-file body.md
+ fi
diff --git a/.gitignore b/.gitignore
index 3a885038..724e39c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,7 @@ test.js
# Claude Code
.claude/
-CLAUDE.md
\ No newline at end of file
+CLAUDE.md
+test-results
+!integrations/contract-tests/fixtures/walk-safety/node_modules/
+integrations/dist-tarballs
diff --git a/integrations/.dockerignore b/integrations/.dockerignore
new file mode 100644
index 00000000..c78e9e83
--- /dev/null
+++ b/integrations/.dockerignore
@@ -0,0 +1,9 @@
+# the container installs its own: esbuild and playwright ship native binaries built for this host
+node_modules
+nodejs/*/node_modules
+nodejs/*/dist
+shell/shell.js
+nodejs/*/shell
+test-results
+nodejs/nestjs/examples/dist
+dist-tarballs
diff --git a/integrations/.gitignore b/integrations/.gitignore
new file mode 100644
index 00000000..1936eda1
--- /dev/null
+++ b/integrations/.gitignore
@@ -0,0 +1,3 @@
+# build output, and the copy of it that each language core publishes
+shell/shell.js
+*/*/shell/
diff --git a/integrations/README.md b/integrations/README.md
new file mode 100644
index 00000000..3da4da38
--- /dev/null
+++ b/integrations/README.md
@@ -0,0 +1,9 @@
+# Bruno API Docs — framework integrations
+
+Serve a Bruno collection as API docs from inside your own app, behind your own auth.
+
+```sh
+cd integrations
+npm ci
+npm run lint
+```
diff --git a/integrations/contract-tests/apps/express.js b/integrations/contract-tests/apps/express.js
new file mode 100644
index 00000000..7e4a84c5
--- /dev/null
+++ b/integrations/contract-tests/apps/express.js
@@ -0,0 +1,68 @@
+'use strict';
+
+const express = require('express');
+const helmet = require('helmet');
+const { apiDocs, embed } = require('@usebruno/api-docs-express');
+
+const PORT = Number(process.env.PORT || 5456);
+// relative on purpose: the core resolves it against this file, not the cwd, and check.sh boots
+// from a different directory to prove it
+const COLLECTION = '../fixtures/api-collection';
+const BUNDLED = '../fixtures/bundled.yml';
+
+const app = express();
+
+// the CSP the README documents: 'self' covers shell.js, the CDN serves the renderer, and the
+// renderer's wasm sandbox needs 'wasm-unsafe-eval' to instantiate and data: to be fetched
+app.use(
+ helmet({
+ contentSecurityPolicy: {
+ directives: {
+ scriptSrc: ["'self'", 'https://cdn.usebruno.com', 'https://cdn.jsdelivr.net', "'wasm-unsafe-eval'"],
+ styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.usebruno.com', 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
+ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
+ workerSrc: ["'self'", 'blob:', 'https://cdn.jsdelivr.net'],
+ connectSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net']
+ }
+ }
+ })
+);
+
+app.get('/control', (req, res) => res.json({ ok: true, from: 'the app itself' }));
+
+app.use('/docs', apiDocs({
+ collection: COLLECTION,
+ environments: { include: ['Local'] },
+ tags: { exclude: ['internal'] },
+ pageTitle: 'Acme API',
+ logo: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 10 10%22%3E%3Ccircle cx=%225%22 cy=%225%22 r=%225%22/%3E%3C/svg%3E',
+ gitCollectionUrl: 'https://token:secret@github.com/acme/api-collection'
+}));
+
+app.use('/api/v2/docs', apiDocs({ collection: COLLECTION, environments: { all: true, exclude: ['Prod'] } }));
+
+app.use('/internal/docs', apiDocs({ collection: COLLECTION }));
+
+app.use('/bundled/docs', apiDocs({ collection: BUNDLED }));
+
+app.use('/broken/docs', apiDocs({ collection: './there-is-no-collection-here' }));
+
+app.use('/oversize/docs', apiDocs({ collection: '../fixtures/walk-oversize' }));
+
+// theme is what the renderer will take next; until it does, passing it is a mistake we report
+app.use('/misconfigured/docs', apiDocs({ collection: COLLECTION, theme: 'dark' }));
+
+// the docs inside the host's own page: the mount serves, the block only points at it
+app.use('/portal/docs', apiDocs({ collection: COLLECTION, environments: { include: ['Local'] } }));
+app.get('/portal', (req, res) => {
+ res.type('html').send(`
+
Acme Developer Portal
+
+ Acme Developer Portal
+ ${embed({ base: '/portal/docs' })}
+`);
+});
+
+app.listen(PORT, () => {
+ console.log(`express rig on http://localhost:${PORT}`);
+});
diff --git a/integrations/contract-tests/apps/fastify.js b/integrations/contract-tests/apps/fastify.js
new file mode 100644
index 00000000..ffac87d2
--- /dev/null
+++ b/integrations/contract-tests/apps/fastify.js
@@ -0,0 +1,63 @@
+'use strict';
+
+const Fastify = require('fastify');
+const { apiDocs } = require('@usebruno/api-docs-fastify');
+
+const PORT = Number(process.env.PORT || 5457);
+// relative on purpose: the core resolves it against this file, not the cwd
+const COLLECTION = '../fixtures/api-collection';
+const BUNDLED = '../fixtures/bundled.yml';
+
+const app = Fastify({ logger: false });
+
+// the same CSP the Express rig sets with helmet, by hand so the rig stays free of a plugin the
+// contract does not depend on
+app.addHook('onSend', async (request, reply) => {
+ reply.header(
+ 'content-security-policy',
+ "default-src 'self'; "
+ + "script-src 'self' https://cdn.usebruno.com https://cdn.jsdelivr.net 'wasm-unsafe-eval'; "
+ + "style-src 'self' 'unsafe-inline' https://cdn.usebruno.com https://cdn.jsdelivr.net https://fonts.googleapis.com; "
+ + "font-src 'self' data: https://fonts.gstatic.com; "
+ + "worker-src 'self' blob: https://cdn.jsdelivr.net; "
+ + "connect-src 'self' data: https://cdn.jsdelivr.net"
+ );
+});
+
+async function main() {
+ // swagger first and awaited: it only records routes registered after it loads, so a host that
+ // registers it late publishes an empty spec
+ await app.register(require('@fastify/swagger'), {
+ openapi: { info: { title: 'Acme API', version: '1.0.0' } }
+ });
+
+ app.get('/control', async () => ({ ok: true, from: 'the app itself' }));
+ app.get('/openapi.json', { schema: { hide: true } }, async () => app.swagger());
+
+ await app.register(apiDocs, {
+ prefix: '/docs',
+ collection: COLLECTION,
+ environments: { include: ['Local'] },
+ tags: { exclude: ['internal'] },
+ pageTitle: 'Acme API',
+ logo: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 10 10%22%3E%3Ccircle cx=%225%22 cy=%225%22 r=%225%22/%3E%3C/svg%3E',
+ gitCollectionUrl: 'https://token:secret@github.com/acme/api-collection'
+ });
+
+ await app.register(apiDocs, {
+ prefix: '/api/v2/docs',
+ collection: COLLECTION,
+ environments: { all: true, exclude: ['Prod'] }
+ });
+
+ await app.register(apiDocs, { prefix: '/internal/docs', collection: COLLECTION });
+ await app.register(apiDocs, { prefix: '/bundled/docs', collection: BUNDLED });
+ await app.register(apiDocs, { prefix: '/broken/docs', collection: './there-is-no-collection-here' });
+ await app.register(apiDocs, { prefix: '/oversize/docs', collection: '../fixtures/walk-oversize' });
+ await app.register(apiDocs, { prefix: '/misconfigured/docs', collection: COLLECTION, theme: 'dark' });
+
+ await app.listen({ port: PORT, host: '127.0.0.1' });
+ console.log(`fastify rig on http://localhost:${PORT}`);
+}
+
+main();
diff --git a/integrations/contract-tests/apps/nestjs.js b/integrations/contract-tests/apps/nestjs.js
new file mode 100644
index 00000000..68067d7f
--- /dev/null
+++ b/integrations/contract-tests/apps/nestjs.js
@@ -0,0 +1,72 @@
+'use strict';
+
+require('reflect-metadata');
+const { Module } = require('@nestjs/common');
+const { NestFactory } = require('@nestjs/core');
+const helmet = require('helmet');
+const { ApiDocsModule } = require('@usebruno/api-docs-nestjs');
+
+const PORT = Number(process.env.PORT || 5458);
+// ADAPTER=fastify runs the same app on platform-fastify. The wrapper is unchanged between them;
+// what differs is that middie leaves req.url as '/', which is why the mount path is an option.
+const ADAPTER = process.env.ADAPTER || 'express';
+// relative on purpose: the core resolves it against this file, not the cwd
+const COLLECTION = '../fixtures/api-collection';
+const BUNDLED = '../fixtures/bundled.yml';
+
+// plain JS, so the decorator is applied as the function it is. The real Nest examples live in
+// nodejs/nestjs/examples and are TypeScript.
+class AppModule {}
+Module({
+ imports: [
+ ApiDocsModule.forRoot({
+ mountPath: '/docs',
+ collection: COLLECTION,
+ environments: { include: ['Local'] },
+ tags: { exclude: ['internal'] },
+ pageTitle: 'Acme API',
+ logo: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 10 10%22%3E%3Ccircle cx=%225%22 cy=%225%22 r=%225%22/%3E%3C/svg%3E',
+ gitCollectionUrl: 'https://token:secret@github.com/acme/api-collection'
+ }),
+ ApiDocsModule.forRoot({
+ mountPath: '/api/v2/docs',
+ collection: COLLECTION,
+ environments: { all: true, exclude: ['Prod'] }
+ }),
+ ApiDocsModule.forRoot({ mountPath: '/internal/docs', collection: COLLECTION }),
+ ApiDocsModule.forRoot({ mountPath: '/bundled/docs', collection: BUNDLED }),
+ ApiDocsModule.forRoot({ mountPath: '/broken/docs', collection: './there-is-no-collection-here' }),
+ ApiDocsModule.forRoot({ mountPath: '/oversize/docs', collection: '../fixtures/walk-oversize' }),
+ ApiDocsModule.forRoot({ mountPath: '/misconfigured/docs', collection: COLLECTION, theme: 'dark' })
+ ]
+})(AppModule);
+
+async function main() {
+ let app;
+ if (ADAPTER === 'fastify') {
+ const { FastifyAdapter } = require('@nestjs/platform-fastify');
+ app = await NestFactory.create(AppModule, new FastifyAdapter(), { logger: false });
+ } else {
+ app = await NestFactory.create(AppModule, { logger: false });
+ }
+
+ app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ scriptSrc: ["'self'", 'https://cdn.usebruno.com', 'https://cdn.jsdelivr.net', "'wasm-unsafe-eval'"],
+ styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.usebruno.com', 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
+ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
+ workerSrc: ["'self'", 'blob:', 'https://cdn.jsdelivr.net'],
+ connectSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net']
+ }
+ }
+ }));
+
+ // the host's own route, straight on the adapter: both adapters take (req, res) and both `.send`
+ app.getHttpAdapter().get('/control', (req, res) => res.send({ ok: true, from: 'the app itself' }));
+
+ await app.listen(PORT, '127.0.0.1');
+ console.log(`nestjs rig on http://localhost:${PORT} (platform-${ADAPTER})`);
+}
+
+main();
diff --git a/integrations/contract-tests/browser/contract.spec.mjs b/integrations/contract-tests/browser/contract.spec.mjs
new file mode 100644
index 00000000..39b86233
--- /dev/null
+++ b/integrations/contract-tests/browser/contract.spec.mjs
@@ -0,0 +1,124 @@
+import { test, expect } from '@playwright/test';
+
+// What our shell and server promise a reader, seen from a browser. check.sh proves the bytes; this
+// proves the page built from them. Everything here is ours to fix when it fails.
+
+const LOCAL_TOKEN = 'local-token-safe-to-publish';
+
+const consoleErrorsOn = (page) => {
+ const errors = [];
+ page.on('console', (m) => {
+ if (m.type() === 'error') {
+ errors.push(m.text());
+ }
+ });
+ page.on('pageerror', (e) => errors.push(String(e)));
+
+ return errors;
+};
+
+test('the docs render, and nothing in the console complains', async ({ page }) => {
+ const errors = consoleErrorsOn(page);
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+
+ await expect(page).toHaveTitle('Acme API');
+ await expect(page.getByText('Catalog', { exact: true }).first()).toBeVisible();
+ await expect(page.locator('#bruno-docs-error')).toBeHidden();
+ expect(errors, 'the adopter CSP we document must not break the renderer').toEqual([]);
+});
+
+test('the renderer booted, from the CDN, with our shell from our own origin', async ({ page }) => {
+ const sources = [];
+ page.on('request', (r) => sources.push(r.url()));
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+
+ expect(sources.some((u) => u.endsWith('/docs/shell.js')), 'shell.js is same-origin').toBe(true);
+ expect(sources.some((u) => u.startsWith('https://cdn.usebruno.com/')), 'the renderer is the CDN bundle').toBe(true);
+ expect(await page.evaluate(() => typeof window.OpenCollection)).toBe('function');
+
+ // the shell boots through window.Bruno first when it exists. The day the CDN bundle ships it,
+ // this fails on purpose: re-check the boot order and the options we pass, then update it.
+ expect(await page.evaluate(() => typeof window.Bruno), 'window.Bruno has appeared on the CDN bundle').toBe('undefined');
+});
+
+test('the filters hold all the way to the page', async ({ page }) => {
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+ const text = await page.evaluate(() => document.body.innerText);
+
+ await expect(page.getByText('Mixed', { exact: true }).first()).toBeVisible();
+ expect(text, 'a folder whose only request was tagged is pruned').not.toContain('Internal');
+
+ for (const hidden of ['Reindex catalog', 'Purge cache', 'Drain node']) {
+ expect(text, `${hidden} is tagged internal and must not reach a reader`).not.toContain(hidden);
+ }
+
+ expect(text, 'only the published environment is offered').toContain('Local');
+ expect(text, 'an unpublished environment is not named').not.toContain('Prod');
+ expect(text, 'and its values never left the server').not.toContain('prod-token-must-never-be-served');
+});
+
+test('the page is served at any depth, and the renderer routes by hash', async ({ page }) => {
+ const errors = consoleErrorsOn(page);
+
+ // any path under the mount is the page: a proxy or a hand-typed URL lands on the docs
+ await page.goto('/docs/catalog/list-products/', { waitUntil: 'networkidle' });
+ await expect(page.getByText('Catalog', { exact: true }).first()).toBeVisible();
+
+ // the link that opens a request is the renderer's hash route, and it survives a reload
+ await page.goto('/docs/#/catalog/list-products', { waitUntil: 'networkidle' });
+ await expect(page.locator('[data-page-slug="catalog/list-products"]')).toBeVisible();
+ await page.reload({ waitUntil: 'networkidle' });
+ await expect(page.locator('[data-page-slug="catalog/list-products"]')).toBeVisible();
+ expect(errors).toEqual([]);
+});
+
+test('try it sends the published environment, and only that', async ({ page }) => {
+ const errors = consoleErrorsOn(page);
+ let sent;
+ // the fixture's baseUrl points at a port nothing listens on; answer for it and keep the request
+ await page.route('**/products', async (route) => {
+ sent = route.request();
+ await route.fulfill({ status: 200, contentType: 'application/json', body: '[]', headers: { 'access-control-allow-origin': '*', 'access-control-allow-headers': '*' } });
+ });
+
+ await page.goto('/docs/#/catalog/list-products', { waitUntil: 'networkidle' });
+ await page.getByTestId('request-try-button').click();
+ await page.getByTestId('query-bar-send').click();
+ await expect.poll(() => sent, 'the request left the browser').toBeTruthy();
+
+ const headers = await sent.allHeaders();
+ expect(headers.authorization, 'the collection auth, resolved from the Local environment').toBe(`Bearer ${LOCAL_TOKEN}`);
+ expect(sent.url(), 'the Local baseUrl was interpolated').toContain('localhost:5456/products');
+
+ // the response viewer is Monaco, loaded from jsdelivr: the CSP we document has to let it in
+ await expect(page.locator('.monaco-editor').first()).toBeVisible();
+ expect(errors, 'nothing the playground loads is blocked by the documented CSP').toEqual([]);
+});
+
+test('a bundled single file renders the same way', async ({ page }) => {
+ await page.goto('/bundled/docs/', { waitUntil: 'networkidle' });
+ await expect(page).toHaveTitle('API Documentation');
+ await expect(page.getByText('Catalog', { exact: true }).first()).toBeVisible();
+});
+
+test('a broken mount explains itself in the page, and the app is still up', async ({ page, request }) => {
+ const response = await page.goto('/broken/docs/', { waitUntil: 'networkidle' });
+
+ // every route of a failed mount serves the reason directly, so there is no shell to fail inside
+ expect(response.status()).toBe(404);
+ await expect(page.getByText('Collection not found')).toBeVisible();
+ expect(await page.locator('#bruno-docs').count(), 'no half-built page is served').toBe(0);
+
+ expect((await request.get('/control')).status(), 'the host app is unaffected').toBe(200);
+});
+
+test('when the CDN is unreachable, the shell says so instead of a blank page', async ({ page }) => {
+ await page.route('https://cdn.usebruno.com/**', (route) => route.abort());
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+
+ const box = page.locator('#bruno-docs-error');
+ await expect(box).toBeVisible();
+ await expect(box).toContainText('renderer bundle did not load');
+ await expect(box, 'it names the origin the adopter has to allow').toContainText('cdn.usebruno.com');
+ await expect(page.locator('#bruno-docs')).toBeHidden();
+});
diff --git a/integrations/contract-tests/browser/playwright.config.mjs b/integrations/contract-tests/browser/playwright.config.mjs
new file mode 100644
index 00000000..3d669ee3
--- /dev/null
+++ b/integrations/contract-tests/browser/playwright.config.mjs
@@ -0,0 +1,42 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { defineConfig } from '@playwright/test';
+
+const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
+
+// APP and PORT are how another language core points this at its own server. The specs assert
+// only what the contract promises, so they do not care which one booted.
+const APP = process.env.APP || 'contract-tests/apps/express.js';
+// 5456 is the fixture's Local baseUrl: try it hits the same origin, as it does when the docs live in the API
+const PORT = process.env.PORT || '5456';
+
+// two tiers. `contract` is what our shell and server promise, and a failure there is ours.
+// `renderer` is the renderer's own behaviour reached through our mount, and a failure there is
+// something to tell them, so CI runs it without blocking: npx playwright test --project renderer
+export default defineConfig({
+ testDir: '.',
+ timeout: 60_000,
+ fullyParallel: false,
+ reporter: [
+ ['list'],
+ ...(process.env.JUNIT_DIR ? [['junit', { outputFile: path.join(ROOT, process.env.JUNIT_DIR, 'browser.xml') }]] : [])
+ ],
+ projects: [
+ { name: 'contract', testMatch: /contract\.spec\.mjs/ },
+ { name: 'renderer', testMatch: /renderer\.spec\.mjs/ }
+ ],
+ use: {
+ baseURL: `http://localhost:${PORT}`,
+ // the renderer comes from the CDN, and there is no `cdn` option to point elsewhere, so this
+ // layer needs the network. That is the trade for not shipping a self-hosted bundle yet.
+ trace: 'retain-on-failure'
+ },
+ webServer: {
+ command: `node ${APP}`,
+ cwd: ROOT,
+ env: { ...process.env, PORT },
+ port: Number(PORT),
+ reuseExistingServer: false,
+ timeout: 30_000
+ }
+});
diff --git a/integrations/contract-tests/browser/renderer.spec.mjs b/integrations/contract-tests/browser/renderer.spec.mjs
new file mode 100644
index 00000000..846cb968
--- /dev/null
+++ b/integrations/contract-tests/browser/renderer.spec.mjs
@@ -0,0 +1,68 @@
+import { test, expect } from '@playwright/test';
+
+// The renderer's own behaviour, reached through our mount. These are the things a reader does on
+// the page: pick an environment, open a request, search, use a phone. They lean on the renderer's
+// data-testids, so a failure here is most likely theirs to know about, not ours to fix. CI runs
+// this project without blocking.
+
+test('the environment switcher offers exactly what was published', async ({ page }) => {
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+ await page.getByLabel('Select environment').first().click();
+
+ const menu = page.getByRole('menu');
+ await expect(menu.getByText('Local', { exact: true })).toBeVisible();
+ await expect(menu.getByText('Prod', { exact: true })).toHaveCount(0);
+});
+
+test('a request page shows its method and the interpolated URL', async ({ page }) => {
+ await page.goto('/docs/#/catalog/list-products', { waitUntil: 'networkidle' });
+
+ await expect(page.getByTestId('request-method').first()).toHaveText(/GET/i);
+ await expect(page.getByText('/products').first()).toBeVisible();
+});
+
+test('the sidebar keeps the collection order', async ({ page }) => {
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+ const slugs = () => page.locator('[data-slug]').evaluateAll((els) => els.map((el) => el.dataset.slug));
+
+ // folders come collapsed; their seq decides the order
+ expect(await slugs()).toEqual(['catalog', 'mixed']);
+
+ await page.getByRole('button', { name: 'Expand folder' }).first().click();
+ expect(await slugs()).toEqual(['catalog', 'catalog/list-products', 'catalog/get-product', 'mixed']);
+});
+
+test('the logo we forwarded is what the renderer shows', async ({ page }) => {
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+ await expect(page.locator('img[src^="data:image/svg+xml"]').first()).toBeVisible();
+});
+
+test('search opens on the keyboard and respects the filters', async ({ page }) => {
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+ await page.keyboard.press('ControlOrMeta+k');
+
+ const input = page.getByLabel('Search requests and folders');
+ await expect(input).toBeFocused();
+ await input.fill('prod');
+
+ const results = page.getByTestId('search-results');
+ await expect(results).toContainText('List products');
+ await expect(results).toContainText('Get product');
+
+ // reindex is tagged internal: not on the page, and not in the index either
+ await input.fill('reindex');
+ await expect(page.getByText('No matches')).toBeVisible();
+});
+
+test('on a phone the sidebar is a drawer', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto('/docs/', { waitUntil: 'networkidle' });
+
+ const drawer = page.getByRole('dialog', { name: 'Navigation' });
+ await expect(page.getByTestId('app-sidebar'), 'no fixed sidebar on a phone').toHaveCount(0);
+ await expect(drawer).toBeHidden();
+
+ await page.getByLabel('Toggle sidebar').click();
+ await expect(drawer).toBeVisible();
+ await expect(drawer.getByText('Catalog', { exact: true })).toBeVisible();
+});
diff --git a/integrations/contract-tests/check.sh b/integrations/contract-tests/check.sh
new file mode 100755
index 00000000..da66737b
--- /dev/null
+++ b/integrations/contract-tests/check.sh
@@ -0,0 +1,186 @@
+#!/usr/bin/env bash
+# The HTTP contract every wrapper, in every framework, in every language must satisfy.
+# Boots one rig from contract-tests/apps, asserts from outside it, exits non-zero on any miss.
+#
+# bash contract-tests/check.sh --app contract-tests/apps/express.js --port 5456
+# bash contract-tests/check.sh --app ... --save /tmp/express keep the served bodies
+# bash contract-tests/check.sh --app ... --compare /tmp/express diff bodies against a run
+#
+# Every rig serves the same five mounts over the same fixture. That convention is what makes one
+# script cover the whole matrix: a cell is an invocation, not an edit.
+#
+# /docs environments include Local, tags exclude internal, pageTitle, gitCollectionUrl
+# /api/v2/docs environments all minus Prod
+# /internal/docs no options
+# /bundled/docs a single bundled yml
+# /broken/docs a collection that is not there
+# /oversize/docs a collection over the caps
+# /misconfigured/docs an option the core does not know
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+source "$ROOT/contract-tests/lib.sh"
+
+APP=""
+PORT=5456
+SAVE=""
+COMPARE=""
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --app) APP="$2"; shift 2 ;;
+ --port) PORT="$2"; shift 2 ;;
+ --save) SAVE="$2"; shift 2 ;;
+ --compare) COMPARE="$2"; shift 2 ;;
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
+ esac
+done
+[ -n "$APP" ] || { echo "--app is required" >&2; exit 2; }
+case "$APP" in /*) ;; *) APP="$ROOT/$APP" ;; esac
+[ -f "$APP" ] || { echo "no app at $APP" >&2; exit 2; }
+
+BASE="http://localhost:$PORT"
+JUNIT_NAME="${JUNIT_NAME:-check-$(basename "$APP" .js)${ADAPTER:+-$ADAPTER}}"
+TMP="$(mktemp -d)"
+trap 'stop; rm -rf "$TMP"' EXIT
+
+# ---- boot, from a different cwd on purpose ---------------------------------
+
+port_is_free "$PORT"
+boot "$APP" "$PORT"
+echo "${BOLD}$(basename "$APP")${RESET} on :$PORT ${DIM}(cwd $TMP)${RESET}"
+
+# ---- the routes, identical at every mount ----------------------------------
+
+for MOUNT in /docs /api/v2/docs /internal/docs; do
+ describe "$MOUNT"
+ url="$BASE$MOUNT"
+ last="${MOUNT##*/}"
+ saved="${MOUNT//\//_}"
+
+ check "$MOUNT -> 301 to relative '$last/'" redirects_to "$url" "$last/"
+ check "$MOUNT/ -> text/html" header_has Content-Type text/html "$url/"
+ check "$MOUNT/ -> no-cache, it carries the mount base" header_has Cache-Control no-cache "$url/"
+ check "$MOUNT/ no inline script" body_lacks "$url/" '',
+ logo: 'https://x.test/a"onerror="alert(1)'
+ }).html('/docs/').body;
+
+ assert.ok(!nasty.includes(''), 'a title cannot close the tag and inject');
+ assert.ok(nasty.includes('</title>'), 'it is escaped instead');
+ assert.ok(!/data-config="[^"]*"onerror=/.test(nasty), 'a config value cannot break out of the attribute');
+}
+
+// --- only renderer options reach the browser, and credentials never do
+{
+ const config = rendererConfig({
+ collection: './secret-path',
+ environments: { all: true },
+ tags: { exclude: ['internal'] },
+ pageTitle: 'Docs',
+ logo: 'https://x.test/l.svg',
+ gitCollectionUrl: 'https://user:tok@github.com/acme/api-collection'
+ });
+
+ assert.deepEqual(Object.keys(config).sort(), ['gitCollectionUrl', 'logo'],
+ 'only what the renderer reads is forwarded');
+ assert.equal(config.gitCollectionUrl, 'https://github.com/acme/api-collection', 'credentials stripped');
+ assert.deepEqual(rendererConfig({}), {}, 'nothing set means nothing forwarded');
+
+ const html = loadShell({
+ collection: './secret-path',
+ tags: { exclude: ['internal'] },
+ environments: { include: ['Prod'] },
+ pageTitle: 'Acme API'
+ }).html('/docs/').body;
+ for (const leak of ['secret-path', 'internal', 'Prod', 'tags', 'environments']) {
+ assert.ok(!html.includes(leak), `the page never mentions ${leak}`);
+ }
+
+ const sentToRenderer = html.match(/data-config="([^"]*)"/)[1];
+ assert.ok(!sentToRenderer.includes('pageTitle'), 'pageTitle titles the page, it is not sent to the renderer');
+ assert.ok(html.includes('Acme API'), 'it is used for the title tag');
+}
+
+// --- stripGitCredentials on its own
+{
+ assert.equal(stripGitCredentials('https://u:p@host/r.git'), 'https://host/r.git');
+ assert.equal(stripGitCredentials('https://u@host/r.git'), 'https://host/r.git');
+ assert.equal(stripGitCredentials('https://host/r.git'), 'https://host/r.git', 'untouched when there is nothing to strip');
+ assert.equal(stripGitCredentials('git@github.com:acme/r.git'), 'git@github.com:acme/r.git', 'scp-style is not a URL, left alone');
+ assert.equal(stripGitCredentials('not a url'), 'not a url');
+}
+
+// --- shell.js: static, cacheable, revalidates
+{
+ const shell = loadShell({ collection: './c' });
+ const res = shell.js({});
+ assert.equal(res.status, 200);
+ assert.equal(res.headers['Content-Type'], 'application/javascript; charset=utf-8');
+ assert.equal(res.headers['Cache-Control'], 'public, max-age=3600');
+ assert.equal(res.headers['X-Content-Type-Options'], 'nosniff');
+ assert.equal(shell.js({ ifNoneMatch: res.headers.ETag }).status, 304);
+ assert.equal(shell.js({ ifNoneMatch: '"stale"' }).status, 200);
+
+ assert.equal(loadShell({ collection: './c', pageTitle: 'Other' }).js({}).headers.ETag, res.headers.ETag,
+ 'shell.js is identical whatever the options, so it caches across mounts');
+}
+
+// --- the page itself must never be cached: it carries the per-mount base
+{
+ assert.equal(loadShell({ collection: './c' }).html('/docs/').headers['Cache-Control'], 'no-cache');
+}
+
+// --- embed(): the body block only, for an adopter's own page. No collection involved.
+{
+ const block = embed({ base: '/docs' });
+ assert.ok(!block.includes('') && block.endsWith(''));
+ assert.equal(embed({ base: '/docs/' }), block, 'with or without the slash, the same block');
+ assert.ok(embed({ base: '/docs', gitCollectionUrl: 'https://u:p@github.com/acme/api' }).includes('github.com/acme/api'), 'renderer options reach the block');
+ assert.ok(!embed({ base: '/docs', gitCollectionUrl: 'https://u:p@github.com/acme/api' }).includes('u:p@'), 'with credentials stripped, as on the page');
+ assert.throws(() => embed({ base: '' }), /`base`/);
+}
+
+// --- the built bundle the core ships
+{
+ const bundle = fs.readFileSync(path.join(here, '..', 'shell', 'shell.js'), 'utf8');
+ assert.ok(bundle.includes('opencollection-fragments'), 'the fragments format is understood in the browser');
+ assert.ok(bundle.length > 20000, 'js-yaml is bundled in, not fetched');
+ assert.deepEqual(bundle.match(/https?:\/\/[A-Za-z0-9./_-]+/g), null,
+ 'no origin is hardcoded in the bundle: the CDN arrives as data-cdn');
+ assert.ok(fs.existsSync(path.join(here, '..', 'shell', 'shell.html')), 'the template ships beside it');
+}
+
+console.log('shell-test: all assertions passed');
diff --git a/integrations/nodejs/core/test/walk.test.mjs b/integrations/nodejs/core/test/walk.test.mjs
new file mode 100644
index 00000000..ef3b0007
--- /dev/null
+++ b/integrations/nodejs/core/test/walk.test.mjs
@@ -0,0 +1,65 @@
+// The walk is the security surface. Every language port must produce these
+// exact outcomes on the two fixtures. Run: npm run test:walk
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const require = createRequire(import.meta.url);
+const { walkCollection, CapError } = require(path.join(here, '..', 'dist', 'collection', 'walk.js'));
+const fixture = (name) => path.join(here, '..', '..', '..', 'contract-tests', 'fixtures', name);
+
+// --- walk-safety: symlink to /etc/passwd, dotfiles, editor strays, non-yml, nested dirs
+{
+ const { files, skipped } = walkCollection(fixture('walk-safety'));
+ const served = files.map((f) => f.path);
+
+ assert.ok(served.includes('opencollection.yml'), 'manifest is served');
+ assert.ok(!served.some((p) => p.endsWith('secret.yml')), 'symlink target is never served');
+ assert.ok(skipped.some((s) => s.path === 'secret.yml' && /symlink/.test(s.rule)), 'symlink is skipped as a symlink, not as some other kind of file');
+ assert.ok(!served.some((p) => p.startsWith('.') || p.includes('/.')), 'no dotfiles');
+ assert.ok(!served.some((p) => /(~|\.swp|\.bak|\.orig)$/.test(p)), 'no editor strays');
+ assert.ok(served.every((p) => /\.ya?ml$/.test(p)), 'only yml served');
+ for (const f of files) {
+ assert.ok(!f.text.includes('root:x:0:0'), '/etc/passwd content never served');
+ }
+ assert.ok(skipped.some((s) => s.path === 'control/not-utf8.yml' && /UTF-8/.test(s.rule)), 'invalid UTF-8 file is skipped, not served');
+ assert.ok(skipped.some((s) => s.path === 'node_modules' && /stray/.test(s.rule)), 'node_modules is skipped whole, never entered');
+ assert.ok(skipped.some((s) => s.path === 'notes.md' && /not \.yml/.test(s.rule)), 'a non-yml file is skipped by extension');
+ assert.deepEqual(served, ['control/ping.yml', 'environments/Local.yml', 'opencollection.yml'], 'exactly these files, in this order');
+ console.log(`walk-safety: ${served.length} served, ${skipped.length} skipped`);
+ for (const s of skipped) {
+ console.log(` skipped ${s.path} (${s.rule})`);
+ }
+}
+
+// --- walk-oversize: a single file above the 1 MB cap -> CapError, nothing served
+{
+ assert.throws(() => walkCollection(fixture('walk-oversize')), (err) => {
+ assert.ok(err instanceof CapError, 'throws CapError');
+ assert.match(err.message, /per-file size cap/);
+ return true;
+ });
+ console.log('walk-oversize: CapError raised before reading the file');
+}
+
+// --- the other three caps, with the limits lowered instead of a 5 MB, 5000-file, 32-deep fixture
+{
+ const safety = fixture('walk-safety');
+ const generous = { fileBytes: 1e9, totalBytes: 1e9, fileCount: 1e9, depth: 1e9 };
+ assert.equal(walkCollection(safety, generous).files.length, 3, 'nothing is refused when the caps are wide');
+
+ assert.throws(() => walkCollection(safety, { ...generous, totalBytes: 100 }),
+ (err) => err instanceof CapError && /total size cap/.test(err.message), 'the total size cap');
+
+ assert.throws(() => walkCollection(safety, { ...generous, fileCount: 2 }),
+ (err) => err instanceof CapError && /file count cap/.test(err.message), 'the file count cap');
+
+ assert.throws(() => walkCollection(safety, { ...generous, depth: 0 }),
+ (err) => err instanceof CapError && /max directory depth/.test(err.message), 'the directory depth cap');
+
+ assert.equal(walkCollection(safety).files.length, 3, 'and the real caps are the default');
+}
+
+console.log('walk-test: all assertions passed');
diff --git a/integrations/nodejs/core/tsconfig.json b/integrations/nodejs/core/tsconfig.json
new file mode 100644
index 00000000..5285d28a
--- /dev/null
+++ b/integrations/nodejs/core/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist"
+ },
+ "include": ["src"]
+}
diff --git a/integrations/nodejs/express/examples/api-collection/catalog/folder.yml b/integrations/nodejs/express/examples/api-collection/catalog/folder.yml
new file mode 100644
index 00000000..11250ef1
--- /dev/null
+++ b/integrations/nodejs/express/examples/api-collection/catalog/folder.yml
@@ -0,0 +1,3 @@
+info:
+ name: Catalog
+ seq: 1
diff --git a/integrations/nodejs/express/examples/api-collection/catalog/get product.yml b/integrations/nodejs/express/examples/api-collection/catalog/get product.yml
new file mode 100644
index 00000000..dbd33356
--- /dev/null
+++ b/integrations/nodejs/express/examples/api-collection/catalog/get product.yml
@@ -0,0 +1,7 @@
+info:
+ name: Get product
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: https://api.acme.example/products/42
diff --git a/integrations/nodejs/express/examples/api-collection/catalog/list products.yml b/integrations/nodejs/express/examples/api-collection/catalog/list products.yml
new file mode 100644
index 00000000..653d6bcf
--- /dev/null
+++ b/integrations/nodejs/express/examples/api-collection/catalog/list products.yml
@@ -0,0 +1,7 @@
+info:
+ name: List products
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: https://api.acme.example/products
diff --git a/integrations/nodejs/express/examples/api-collection/opencollection.yml b/integrations/nodejs/express/examples/api-collection/opencollection.yml
new file mode 100644
index 00000000..36f29597
--- /dev/null
+++ b/integrations/nodejs/express/examples/api-collection/opencollection.yml
@@ -0,0 +1,5 @@
+opencollection: 1.0.0
+info:
+ name: Acme API
+ version: 1.0.0
+ summary: A small collection to see the docs with. Point the option at your own to replace it.
diff --git a/integrations/nodejs/express/examples/auth.js b/integrations/nodejs/express/examples/auth.js
new file mode 100644
index 00000000..d6cd60bd
--- /dev/null
+++ b/integrations/nodejs/express/examples/auth.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const express = require('express');
+const { apiDocs } = require('@usebruno/api-docs-express');
+
+const app = express();
+const port = process.env.PORT ?? 3000;
+
+// stand-in for your real auth: a session check, an SSO middleware, whatever the app already uses
+const requireToken = (req, res, next) => {
+ if (req.headers.authorization !== 'Bearer let-me-in') {
+ res.set('WWW-Authenticate', 'Bearer').sendStatus(401);
+ return;
+ }
+
+ next();
+};
+
+app.get('/health', (req, res) => res.json({ ok: true }));
+
+// the guard runs before the docs on this mount and nowhere else. It covers collection.yml too,
+// which is the file that carries the collection's own auth and variables.
+app.use('/docs', requireToken, apiDocs({ collection: './api-collection' }));
+
+app.listen(port, () => console.log(`docs at http://localhost:${port}/docs/ (Authorization: Bearer let-me-in)`));
diff --git a/integrations/nodejs/express/examples/csp.js b/integrations/nodejs/express/examples/csp.js
new file mode 100644
index 00000000..673aca1e
--- /dev/null
+++ b/integrations/nodejs/express/examples/csp.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const express = require('express');
+const helmet = require('helmet');
+const { apiDocs } = require('@usebruno/api-docs-express');
+
+const app = express();
+const port = process.env.PORT ?? 3000;
+
+// what the docs page needs from a strict CSP. 'self' covers shell.js; the renderer itself comes
+// from cdn.usebruno.com and runs a wasm sandbox, which is what 'wasm-unsafe-eval' and data: are for.
+// The response viewer is Monaco, loaded from cdn.jsdelivr.net when "try it" opens, with its workers.
+// Without these the page still loads and looks fine, and the errors are only in the console.
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ scriptSrc: ["'self'", 'https://cdn.usebruno.com', 'https://cdn.jsdelivr.net', "'wasm-unsafe-eval'"],
+ styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.usebruno.com', 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
+ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
+ workerSrc: ["'self'", 'blob:', 'https://cdn.jsdelivr.net'],
+ connectSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net']
+ }
+ }
+}));
+
+app.get('/health', (req, res) => res.json({ ok: true }));
+
+app.use('/docs', apiDocs({ collection: './api-collection' }));
+
+app.listen(port, () => console.log(`docs at http://localhost:${port}/docs/`));
diff --git a/integrations/nodejs/express/examples/embed.js b/integrations/nodejs/express/examples/embed.js
new file mode 100644
index 00000000..5de3c49e
--- /dev/null
+++ b/integrations/nodejs/express/examples/embed.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const express = require('express');
+const { apiDocs, embed } = require('@usebruno/api-docs-express');
+
+const app = express();
+const port = process.env.PORT ?? 3000;
+
+// the docs inside a page of your own. The mount serves the collection and shell.js; the block
+// only has to know where the mount is.
+app.use('/docs', apiDocs({ collection: './api-collection' }));
+
+app.get('/', (req, res) => {
+ res.type('html').send(`
+
+Acme Developer Portal
+
+ Acme Developer Portal
+ ${embed({ base: '/docs' })}
+
+`);
+});
+
+app.listen(port, () => console.log(`portal at http://localhost:${port}/`));
diff --git a/integrations/nodejs/express/examples/quickstart.js b/integrations/nodejs/express/examples/quickstart.js
new file mode 100644
index 00000000..9c02d824
--- /dev/null
+++ b/integrations/nodejs/express/examples/quickstart.js
@@ -0,0 +1,14 @@
+'use strict';
+
+const express = require('express');
+const { apiDocs } = require('@usebruno/api-docs-express');
+
+const app = express();
+const port = process.env.PORT ?? 3000;
+
+app.get('/health', (req, res) => res.json({ ok: true }));
+
+// the path is relative to this file, not to where node was started
+app.use('/docs', apiDocs({ collection: './api-collection' }));
+
+app.listen(port, () => console.log(`docs at http://localhost:${port}/docs/`));
diff --git a/integrations/nodejs/express/package.json b/integrations/nodejs/express/package.json
new file mode 100644
index 00000000..c9c6b36a
--- /dev/null
+++ b/integrations/nodejs/express/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@usebruno/api-docs-express",
+ "version": "0.1.0-beta.1",
+ "private": true,
+ "description": "Serve a Bruno collection as API docs from inside your own Express app, behind your own auth.",
+ "license": "MIT",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "express": "^4.0.0 || ^5.0.0"
+ },
+ "dependencies": {
+ "@usebruno/api-docs-core": "0.1.0-beta.1"
+ }
+}
diff --git a/integrations/nodejs/express/src/index.ts b/integrations/nodejs/express/src/index.ts
new file mode 100644
index 00000000..5f983147
--- /dev/null
+++ b/integrations/nodejs/express/src/index.ts
@@ -0,0 +1,14 @@
+import type { RequestHandler } from 'express';
+import { createDocs, embed, type ApiDocsOptions } from '@usebruno/api-docs-core';
+
+export { createDocs, embed };
+export type { ApiDocsOptions, CollectionOptions, RendererOptions, EmbedOptions, EnvironmentsOption, TagsOption }
+ from '@usebruno/api-docs-core';
+
+/**
+ * `app.use('/docs', apiDocs({ collection: './api-collection' }))`. Express decides the mount and
+ * strips it before the handler sees the request, so there is no `mountPath` here.
+ */
+export function apiDocs(options: ApiDocsOptions): RequestHandler {
+ return createDocs(options).handler();
+}
diff --git a/integrations/nodejs/express/tsconfig.json b/integrations/nodejs/express/tsconfig.json
new file mode 100644
index 00000000..3493e6ba
--- /dev/null
+++ b/integrations/nodejs/express/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist"
+ },
+ "include": [
+ "src"
+ ]
+}
diff --git a/integrations/nodejs/fastify/examples/api-collection/catalog/folder.yml b/integrations/nodejs/fastify/examples/api-collection/catalog/folder.yml
new file mode 100644
index 00000000..11250ef1
--- /dev/null
+++ b/integrations/nodejs/fastify/examples/api-collection/catalog/folder.yml
@@ -0,0 +1,3 @@
+info:
+ name: Catalog
+ seq: 1
diff --git a/integrations/nodejs/fastify/examples/api-collection/catalog/get product.yml b/integrations/nodejs/fastify/examples/api-collection/catalog/get product.yml
new file mode 100644
index 00000000..dbd33356
--- /dev/null
+++ b/integrations/nodejs/fastify/examples/api-collection/catalog/get product.yml
@@ -0,0 +1,7 @@
+info:
+ name: Get product
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: https://api.acme.example/products/42
diff --git a/integrations/nodejs/fastify/examples/api-collection/catalog/list products.yml b/integrations/nodejs/fastify/examples/api-collection/catalog/list products.yml
new file mode 100644
index 00000000..653d6bcf
--- /dev/null
+++ b/integrations/nodejs/fastify/examples/api-collection/catalog/list products.yml
@@ -0,0 +1,7 @@
+info:
+ name: List products
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: https://api.acme.example/products
diff --git a/integrations/nodejs/fastify/examples/api-collection/opencollection.yml b/integrations/nodejs/fastify/examples/api-collection/opencollection.yml
new file mode 100644
index 00000000..36f29597
--- /dev/null
+++ b/integrations/nodejs/fastify/examples/api-collection/opencollection.yml
@@ -0,0 +1,5 @@
+opencollection: 1.0.0
+info:
+ name: Acme API
+ version: 1.0.0
+ summary: A small collection to see the docs with. Point the option at your own to replace it.
diff --git a/integrations/nodejs/fastify/examples/auth.js b/integrations/nodejs/fastify/examples/auth.js
new file mode 100644
index 00000000..edaabfee
--- /dev/null
+++ b/integrations/nodejs/fastify/examples/auth.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const Fastify = require('fastify');
+const { apiDocs } = require('@usebruno/api-docs-fastify');
+
+const app = Fastify();
+const port = process.env.PORT ?? 3000;
+
+app.get('/health', async () => ({ ok: true }));
+
+// a hook is scoped to the plugin it is added in, so a wrapper plugin is the Fastify way to put a
+// guard in front of the docs and nothing else. It covers collection.yml too, which is the file
+// that carries the collection's own auth and variables.
+app.register(async (guarded) => {
+ guarded.addHook('onRequest', async (request, reply) => {
+ // stand-in for your real auth: a session check, a JWT plugin, whatever the app already uses
+ if (request.headers.authorization !== 'Bearer let-me-in') {
+ return reply.code(401).header('www-authenticate', 'Bearer').send();
+ }
+ });
+
+ await guarded.register(apiDocs, { prefix: '/docs', collection: './api-collection' });
+});
+
+app.listen({ port }).then(() => console.log(`docs at http://localhost:${port}/docs/ (Authorization: Bearer let-me-in)`));
diff --git a/integrations/nodejs/fastify/examples/csp.js b/integrations/nodejs/fastify/examples/csp.js
new file mode 100644
index 00000000..ad9b4f1d
--- /dev/null
+++ b/integrations/nodejs/fastify/examples/csp.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const Fastify = require('fastify');
+const { apiDocs } = require('@usebruno/api-docs-fastify');
+
+const app = Fastify();
+const port = process.env.PORT ?? 3000;
+
+// what the docs page needs from a strict CSP. 'self' covers shell.js; the renderer itself comes
+// from cdn.usebruno.com and runs a wasm sandbox, which is what 'wasm-unsafe-eval' and data: are for.
+// The response viewer is Monaco, loaded from cdn.jsdelivr.net when "try it" opens, with its workers.
+// Without these the page still loads and looks fine, and the errors are only in the console.
+app.addHook('onSend', async (request, reply) => {
+ reply.header(
+ 'content-security-policy',
+ "default-src 'self'; "
+ + "script-src 'self' https://cdn.usebruno.com https://cdn.jsdelivr.net 'wasm-unsafe-eval'; "
+ + "style-src 'self' 'unsafe-inline' https://cdn.usebruno.com https://cdn.jsdelivr.net https://fonts.googleapis.com; "
+ + "font-src 'self' data: https://fonts.gstatic.com; "
+ + "worker-src 'self' blob: https://cdn.jsdelivr.net; "
+ + "connect-src 'self' data: https://cdn.jsdelivr.net"
+ );
+});
+
+app.get('/health', async () => ({ ok: true }));
+
+app.register(apiDocs, { prefix: '/docs', collection: './api-collection' });
+
+app.listen({ port }).then(() => console.log(`docs at http://localhost:${port}/docs/`));
diff --git a/integrations/nodejs/fastify/examples/embed.js b/integrations/nodejs/fastify/examples/embed.js
new file mode 100644
index 00000000..4ebdd842
--- /dev/null
+++ b/integrations/nodejs/fastify/examples/embed.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const Fastify = require('fastify');
+const { apiDocs, embed } = require('@usebruno/api-docs-fastify');
+
+const app = Fastify();
+const port = process.env.PORT ?? 3000;
+
+// the docs inside a page of your own. The mount serves the collection and shell.js; the block
+// only has to know where the mount is.
+app.register(apiDocs, { prefix: '/docs', collection: './api-collection' });
+
+app.get('/', async (request, reply) => {
+ return reply.type('text/html').send(`
+
+Acme Developer Portal
+
+ Acme Developer Portal
+ ${embed({ base: '/docs' })}
+
+`);
+});
+
+app.listen({ port }).then(() => console.log(`portal at http://localhost:${port}/`));
diff --git a/integrations/nodejs/fastify/examples/quickstart.js b/integrations/nodejs/fastify/examples/quickstart.js
new file mode 100644
index 00000000..89bb5de7
--- /dev/null
+++ b/integrations/nodejs/fastify/examples/quickstart.js
@@ -0,0 +1,14 @@
+'use strict';
+
+const Fastify = require('fastify');
+const { apiDocs } = require('@usebruno/api-docs-fastify');
+
+const app = Fastify();
+const port = process.env.PORT ?? 3000;
+
+app.get('/health', async () => ({ ok: true }));
+
+// prefix is the mount. The collection path is relative to this file, not to where node was started.
+app.register(apiDocs, { prefix: '/docs', collection: './api-collection' });
+
+app.listen({ port }).then(() => console.log(`docs at http://localhost:${port}/docs/`));
diff --git a/integrations/nodejs/fastify/package.json b/integrations/nodejs/fastify/package.json
new file mode 100644
index 00000000..cb434d6f
--- /dev/null
+++ b/integrations/nodejs/fastify/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@usebruno/api-docs-fastify",
+ "version": "0.1.0-beta.1",
+ "private": true,
+ "description": "Serve a Bruno collection as API docs from inside your own Fastify app, behind your own auth.",
+ "license": "MIT",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "fastify": "^4.0.0 || ^5.0.0"
+ },
+ "dependencies": {
+ "@usebruno/api-docs-core": "0.1.0-beta.1"
+ }
+}
diff --git a/integrations/nodejs/fastify/src/index.ts b/integrations/nodejs/fastify/src/index.ts
new file mode 100644
index 00000000..debc6a36
--- /dev/null
+++ b/integrations/nodejs/fastify/src/index.ts
@@ -0,0 +1,51 @@
+import type { FastifyPluginAsync, FastifyReply, RegisterOptions } from 'fastify';
+import { createDocs, embed, type ApiDocsOptions, type HttpResponse } from '@usebruno/api-docs-core';
+
+export { createDocs, embed };
+export type { ApiDocsOptions, CollectionOptions, RendererOptions, EmbedOptions, EnvironmentsOption, TagsOption }
+ from '@usebruno/api-docs-core';
+
+/** `hide` is @fastify/swagger's, read at runtime, and Fastify's own FastifySchema omits it. */
+declare module 'fastify' {
+ interface FastifySchema {
+ hide?: boolean;
+ }
+}
+
+const send = (reply: FastifyReply, response: HttpResponse): FastifyReply => {
+ reply.code(response.status).headers(response.headers);
+
+ return response.body === null ? reply.send() : reply.send(response.body);
+};
+
+/**
+ * `app.register(apiDocs, { collection: './api-collection', prefix: '/docs' })`. The mount is
+ * Fastify's own `prefix`, so there is no `mountPath`. Deliberately not wrapped in `fastify-plugin`:
+ * staying encapsulated is what keeps the 405 off the host's routes and lets an adopter put auth in
+ * front with `child.addHook('onRequest', ...)`.
+ */
+export const apiDocs: FastifyPluginAsync = async (fastify, options) => {
+ // Fastify hands its own register options (prefix, logLevel, ...) to the plugin with ours
+ const { prefix: _prefix, logLevel: _logLevel, logSerializers: _logSerializers, ...docsOptions }
+ = options as ApiDocsOptions & RegisterOptions;
+ const docs = createDocs(docsOptions);
+ const base = `${fastify.prefix}/`;
+ const lastSegment = fastify.prefix.split('/').filter(Boolean).pop() ?? '';
+ const hide = { schema: { hide: true } };
+
+ // no-slash, or this route claims `/docs/` too and the page below never answers
+ fastify.get('/', { ...hide, prefixTrailingSlash: 'no-slash' }, (request, reply) =>
+ reply.code(301).header('location', `${lastSegment}/`).send());
+
+ fastify.get('/shell.js', hide, (request, reply) =>
+ send(reply, docs.shellJs({ ifNoneMatch: request.headers['if-none-match'] })));
+
+ fastify.get('/collection.yml', hide, (request, reply) =>
+ send(reply, docs.collection({ ifNoneMatch: request.headers['if-none-match'] })));
+
+ fastify.get('/*', hide, (request, reply) => send(reply, docs.shell(base)));
+
+ // a matched path with an unmatched method arrives here, which is where 405 belongs
+ fastify.setNotFoundHandler((request, reply) =>
+ reply.code(405).header('allow', 'GET, HEAD').send());
+};
diff --git a/integrations/nodejs/fastify/tsconfig.json b/integrations/nodejs/fastify/tsconfig.json
new file mode 100644
index 00000000..5285d28a
--- /dev/null
+++ b/integrations/nodejs/fastify/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist"
+ },
+ "include": ["src"]
+}
diff --git a/integrations/nodejs/nestjs/examples/01-quickstart/app.module.ts b/integrations/nodejs/nestjs/examples/01-quickstart/app.module.ts
new file mode 100644
index 00000000..2f2c978d
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/01-quickstart/app.module.ts
@@ -0,0 +1,11 @@
+import { Module } from '@nestjs/common';
+import { ApiDocsModule } from '@usebruno/api-docs-nestjs';
+
+@Module({
+ imports: [
+ // relative to the compiled entry file, dist/01-quickstart/main.js, not to this source file
+ // and not to the cwd. From there, examples/api-collection is two levels up.
+ ApiDocsModule.forRoot({ collection: '../../api-collection' })
+ ]
+})
+export class AppModule {}
diff --git a/integrations/nodejs/nestjs/examples/01-quickstart/main.ts b/integrations/nodejs/nestjs/examples/01-quickstart/main.ts
new file mode 100644
index 00000000..87d15bd4
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/01-quickstart/main.ts
@@ -0,0 +1,9 @@
+import { NestFactory } from '@nestjs/core';
+import { AppModule } from './app.module';
+
+async function bootstrap(): Promise {
+ const app = await NestFactory.create(AppModule);
+ await app.listen(process.env.PORT ?? 3000);
+}
+
+bootstrap();
diff --git a/integrations/nodejs/nestjs/examples/02-auth/app.module.ts b/integrations/nodejs/nestjs/examples/02-auth/app.module.ts
new file mode 100644
index 00000000..282045ea
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/02-auth/app.module.ts
@@ -0,0 +1,28 @@
+import { Module, type MiddlewareConsumer, type NestModule } from '@nestjs/common';
+import type { Request, Response, NextFunction } from 'express';
+import { ApiDocsModule } from '@usebruno/api-docs-nestjs';
+
+// stand-in for your real auth: a session check, a passport strategy, whatever the app already uses
+function requireApiKey(req: Request, res: Response, next: NextFunction): void {
+ if (req.headers['x-api-key'] !== 'let-me-in') {
+ res.status(401).end();
+ return;
+ }
+
+ next();
+}
+
+@Module({
+ imports: [
+ ApiDocsModule.forRoot({ collection: '../../api-collection' }),
+ ApiDocsModule.forRoot({ mountPath: '/internal/docs', collection: '../../api-collection' })
+ ]
+})
+export class AppModule implements NestModule {
+ configure(consumer: MiddlewareConsumer): void {
+ // the root module's middleware runs before an imported module's, so the docs never see an
+ // unauthenticated request. That includes collection.yml, the file that carries the
+ // collection's own auth and variables. /docs stays open.
+ consumer.apply(requireApiKey).forRoutes('internal/docs', 'internal/docs/*');
+ }
+}
diff --git a/integrations/nodejs/nestjs/examples/02-auth/main.ts b/integrations/nodejs/nestjs/examples/02-auth/main.ts
new file mode 100644
index 00000000..87d15bd4
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/02-auth/main.ts
@@ -0,0 +1,9 @@
+import { NestFactory } from '@nestjs/core';
+import { AppModule } from './app.module';
+
+async function bootstrap(): Promise {
+ const app = await NestFactory.create(AppModule);
+ await app.listen(process.env.PORT ?? 3000);
+}
+
+bootstrap();
diff --git a/integrations/nodejs/nestjs/examples/03-global-prefix/app.module.ts b/integrations/nodejs/nestjs/examples/03-global-prefix/app.module.ts
new file mode 100644
index 00000000..9abb96e6
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/03-global-prefix/app.module.ts
@@ -0,0 +1,7 @@
+import { Module } from '@nestjs/common';
+import { ApiDocsModule } from '@usebruno/api-docs-nestjs';
+
+@Module({
+ imports: [ApiDocsModule.forRoot({ collection: '../../api-collection' })]
+})
+export class AppModule {}
diff --git a/integrations/nodejs/nestjs/examples/03-global-prefix/main.ts b/integrations/nodejs/nestjs/examples/03-global-prefix/main.ts
new file mode 100644
index 00000000..26b012d2
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/03-global-prefix/main.ts
@@ -0,0 +1,12 @@
+import { NestFactory } from '@nestjs/core';
+import { AppModule } from './app.module';
+
+async function bootstrap(): Promise {
+ const app = await NestFactory.create(AppModule);
+ // the docs move with the prefix, like every other route: they answer at /api/docs/, and the
+ // module keeps its default mountPath of '/docs'
+ app.setGlobalPrefix('api');
+ await app.listen(process.env.PORT ?? 3000);
+}
+
+bootstrap();
diff --git a/integrations/nodejs/nestjs/examples/04-fastify-adapter/app.module.ts b/integrations/nodejs/nestjs/examples/04-fastify-adapter/app.module.ts
new file mode 100644
index 00000000..9abb96e6
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/04-fastify-adapter/app.module.ts
@@ -0,0 +1,7 @@
+import { Module } from '@nestjs/common';
+import { ApiDocsModule } from '@usebruno/api-docs-nestjs';
+
+@Module({
+ imports: [ApiDocsModule.forRoot({ collection: '../../api-collection' })]
+})
+export class AppModule {}
diff --git a/integrations/nodejs/nestjs/examples/04-fastify-adapter/main.ts b/integrations/nodejs/nestjs/examples/04-fastify-adapter/main.ts
new file mode 100644
index 00000000..85000fd6
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/04-fastify-adapter/main.ts
@@ -0,0 +1,11 @@
+import { NestFactory } from '@nestjs/core';
+import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
+import { AppModule } from './app.module';
+
+async function bootstrap(): Promise {
+ // nothing changes in the module: the docs are middleware, and Nest runs middleware on both adapters
+ const app = await NestFactory.create(AppModule, new FastifyAdapter());
+ await app.listen(process.env.PORT ?? 3000);
+}
+
+bootstrap();
diff --git a/integrations/nodejs/nestjs/examples/05-csp/app.module.ts b/integrations/nodejs/nestjs/examples/05-csp/app.module.ts
new file mode 100644
index 00000000..9abb96e6
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/05-csp/app.module.ts
@@ -0,0 +1,7 @@
+import { Module } from '@nestjs/common';
+import { ApiDocsModule } from '@usebruno/api-docs-nestjs';
+
+@Module({
+ imports: [ApiDocsModule.forRoot({ collection: '../../api-collection' })]
+})
+export class AppModule {}
diff --git a/integrations/nodejs/nestjs/examples/05-csp/main.ts b/integrations/nodejs/nestjs/examples/05-csp/main.ts
new file mode 100644
index 00000000..bef0be52
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/05-csp/main.ts
@@ -0,0 +1,27 @@
+import { NestFactory } from '@nestjs/core';
+import helmet from 'helmet';
+import { AppModule } from './app.module';
+
+async function bootstrap(): Promise {
+ const app = await NestFactory.create(AppModule);
+
+ // what the docs page needs from a strict CSP. 'self' covers shell.js; the renderer itself comes
+ // from cdn.usebruno.com and runs a wasm sandbox, which is what 'wasm-unsafe-eval' and data: are for.
+ // The response viewer is Monaco, loaded from cdn.jsdelivr.net when "try it" opens, with its workers.
+ // Without these the page still loads and looks fine, and the errors are only in the console.
+ app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ scriptSrc: ["'self'", 'https://cdn.usebruno.com', 'https://cdn.jsdelivr.net', "'wasm-unsafe-eval'"],
+ styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.usebruno.com', 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
+ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
+ workerSrc: ["'self'", 'blob:', 'https://cdn.jsdelivr.net'],
+ connectSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net']
+ }
+ }
+ }));
+
+ await app.listen(process.env.PORT ?? 3000);
+}
+
+bootstrap();
diff --git a/integrations/nodejs/nestjs/examples/06-embed/app.module.ts b/integrations/nodejs/nestjs/examples/06-embed/app.module.ts
new file mode 100644
index 00000000..e2290aa0
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/06-embed/app.module.ts
@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+import { ApiDocsModule } from '@usebruno/api-docs-nestjs';
+import { PortalController } from './portal.controller';
+
+@Module({
+ controllers: [PortalController],
+ imports: [ApiDocsModule.forRoot({ collection: '../../api-collection' })]
+})
+export class AppModule {}
diff --git a/integrations/nodejs/nestjs/examples/06-embed/main.ts b/integrations/nodejs/nestjs/examples/06-embed/main.ts
new file mode 100644
index 00000000..87d15bd4
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/06-embed/main.ts
@@ -0,0 +1,9 @@
+import { NestFactory } from '@nestjs/core';
+import { AppModule } from './app.module';
+
+async function bootstrap(): Promise {
+ const app = await NestFactory.create(AppModule);
+ await app.listen(process.env.PORT ?? 3000);
+}
+
+bootstrap();
diff --git a/integrations/nodejs/nestjs/examples/06-embed/portal.controller.ts b/integrations/nodejs/nestjs/examples/06-embed/portal.controller.ts
new file mode 100644
index 00000000..24d95524
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/06-embed/portal.controller.ts
@@ -0,0 +1,20 @@
+import { Controller, Get, Header } from '@nestjs/common';
+import { embed } from '@usebruno/api-docs-nestjs';
+
+@Controller()
+export class PortalController {
+ // the docs inside a page of your own. The module serves the collection and shell.js at /docs;
+ // the block only has to know where that mount is.
+ @Get()
+ @Header('Content-Type', 'text/html; charset=utf-8')
+ portal(): string {
+ return `
+
+Acme Developer Portal
+
+ Acme Developer Portal
+ ${embed({ base: '/docs' })}
+
+`;
+ }
+}
diff --git a/integrations/nodejs/nestjs/examples/api-collection/catalog/folder.yml b/integrations/nodejs/nestjs/examples/api-collection/catalog/folder.yml
new file mode 100644
index 00000000..11250ef1
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/api-collection/catalog/folder.yml
@@ -0,0 +1,3 @@
+info:
+ name: Catalog
+ seq: 1
diff --git a/integrations/nodejs/nestjs/examples/api-collection/catalog/get product.yml b/integrations/nodejs/nestjs/examples/api-collection/catalog/get product.yml
new file mode 100644
index 00000000..dbd33356
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/api-collection/catalog/get product.yml
@@ -0,0 +1,7 @@
+info:
+ name: Get product
+ type: http
+ seq: 2
+http:
+ method: GET
+ url: https://api.acme.example/products/42
diff --git a/integrations/nodejs/nestjs/examples/api-collection/catalog/list products.yml b/integrations/nodejs/nestjs/examples/api-collection/catalog/list products.yml
new file mode 100644
index 00000000..653d6bcf
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/api-collection/catalog/list products.yml
@@ -0,0 +1,7 @@
+info:
+ name: List products
+ type: http
+ seq: 1
+http:
+ method: GET
+ url: https://api.acme.example/products
diff --git a/integrations/nodejs/nestjs/examples/api-collection/opencollection.yml b/integrations/nodejs/nestjs/examples/api-collection/opencollection.yml
new file mode 100644
index 00000000..36f29597
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/api-collection/opencollection.yml
@@ -0,0 +1,5 @@
+opencollection: 1.0.0
+info:
+ name: Acme API
+ version: 1.0.0
+ summary: A small collection to see the docs with. Point the option at your own to replace it.
diff --git a/integrations/nodejs/nestjs/examples/tsconfig.json b/integrations/nodejs/nestjs/examples/tsconfig.json
new file mode 100644
index 00000000..5b45c3ad
--- /dev/null
+++ b/integrations/nodejs/nestjs/examples/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "dist",
+ "declaration": false,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true
+ },
+ "include": ["0*/*.ts"]
+}
diff --git a/integrations/nodejs/nestjs/package.json b/integrations/nodejs/nestjs/package.json
new file mode 100644
index 00000000..2d9a0a0f
--- /dev/null
+++ b/integrations/nodejs/nestjs/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@usebruno/api-docs-nestjs",
+ "version": "0.1.0-beta.1",
+ "private": true,
+ "description": "Serve a Bruno collection as API docs from inside your own NestJS app, behind your own auth.",
+ "license": "MIT",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0",
+ "@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0"
+ },
+ "dependencies": {
+ "@usebruno/api-docs-core": "0.1.0-beta.1"
+ }
+}
diff --git a/integrations/nodejs/nestjs/src/index.ts b/integrations/nodejs/nestjs/src/index.ts
new file mode 100644
index 00000000..9f4fc32e
--- /dev/null
+++ b/integrations/nodejs/nestjs/src/index.ts
@@ -0,0 +1,71 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import type { ServerResponse } from 'node:http';
+import { Module, type DynamicModule, type MiddlewareConsumer, type NestModule } from '@nestjs/common';
+import { createDocs, embed, type ApiDocsOptions, type Request } from '@usebruno/api-docs-core';
+
+export { createDocs, embed };
+export type { ApiDocsOptions, CollectionOptions, RendererOptions, EmbedOptions, EnvironmentsOption, TagsOption }
+ from '@usebruno/api-docs-core';
+
+export interface ApiDocsModuleOptions extends ApiDocsOptions {
+ /** Where the docs mount. Nest gives no way to mount a module from outside, so it is an option. */
+ mountPath?: string;
+}
+
+const DEFAULT_MOUNT_PATH = '/docs';
+
+/**
+ * Nest 10 routes `/*` and its fastify adapter throws on `/*splat`; Nest 11 and 12 route both but log
+ * a deprecation warning for `/*`. Read from the installed package, whose `exports` map on 12 no
+ * longer lets `require('@nestjs/core/package.json')` through.
+ */
+function wildcard(): string {
+ let dir = path.dirname(require.resolve('@nestjs/core'));
+ while (!fs.existsSync(path.join(dir, 'package.json'))) {
+ dir = path.dirname(dir);
+ }
+ const major = Number(JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version.split('.')[0]);
+
+ return major >= 11 ? '*splat' : '*';
+}
+
+/** Searched for, not stripped: `setGlobalPrefix('api')` moves a `/docs` mount to `/api/docs`. */
+export function subPathOf(fullUrl: string, mountPath: string): string {
+ const at = fullUrl.indexOf(mountPath);
+ if (at === -1) {
+ return fullUrl;
+ }
+
+ return fullUrl.slice(at + mountPath.length) || '/';
+}
+
+/**
+ * `ApiDocsModule.forRoot({ collection: '../api-collection', mountPath: '/docs' })`. The path counts
+ * from the built entry file, `dist/main.js`, so a collection beside `src/` is one level up.
+ */
+export class ApiDocsModule {
+ static forRoot(options: ApiDocsModuleOptions): DynamicModule {
+ const { mountPath: mountOption, ...docsOptions } = options;
+ const mountPath = (mountOption ?? DEFAULT_MOUNT_PATH).replace(/\/+$/, '');
+ const handler = createDocs(docsOptions).handler();
+
+ @Module({})
+ class ApiDocsHostModule implements NestModule {
+ configure(consumer: MiddlewareConsumer): void {
+ consumer
+ .apply((req: Request, res: ServerResponse) => {
+ const originalUrl = req.originalUrl ?? req.url ?? '/';
+ const url = subPathOf(originalUrl, mountPath);
+ handler({ method: req.method, url, originalUrl, headers: req.headers }, res);
+ })
+ // the mount alone is enough on platform-express, which matches by prefix. On
+ // platform-fastify it matches only the mount itself, so the wildcard is what serves
+ // shell.js and every deep link
+ .forRoutes(mountPath, `${mountPath}/${wildcard()}`);
+ }
+ }
+
+ return { module: ApiDocsHostModule };
+ }
+}
diff --git a/integrations/nodejs/nestjs/test/sub-path.test.mjs b/integrations/nodejs/nestjs/test/sub-path.test.mjs
new file mode 100644
index 00000000..935646ba
--- /dev/null
+++ b/integrations/nodejs/nestjs/test/sub-path.test.mjs
@@ -0,0 +1,31 @@
+// Nest is the only wrapper that has to work out the path itself: on platform-fastify the
+// middleware is handed `/` whatever was requested. Run: npm run test:nestjs
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const require = createRequire(import.meta.url);
+const { subPathOf } = require(path.join(here, '..', 'dist', 'index.js'));
+
+// --- the ordinary mount
+assert.equal(subPathOf('/docs', '/docs'), '/', 'the mount itself is the root of the docs');
+assert.equal(subPathOf('/docs/', '/docs'), '/');
+assert.equal(subPathOf('/docs/shell.js', '/docs'), '/shell.js');
+assert.equal(subPathOf('/docs/collection.yml', '/docs'), '/collection.yml');
+assert.equal(subPathOf('/docs/catalog/list/', '/docs'), '/catalog/list/', 'any depth, for deep links');
+assert.equal(subPathOf('/docs/shell.js?v=2', '/docs'), '/shell.js?v=2', 'the query survives, the handler strips it');
+
+// --- a mount several segments deep
+assert.equal(subPathOf('/api/v2/docs/shell.js', '/api/v2/docs'), '/shell.js');
+
+// --- setGlobalPrefix: Nest applies it to middleware routes, so a /docs mount answers at /api/docs
+assert.equal(subPathOf('/api/docs/shell.js', '/docs'), '/shell.js', 'the prefix in front of the mount is skipped');
+assert.equal(subPathOf('/api/docs/', '/docs'), '/');
+assert.equal(subPathOf('/api/docs', '/docs'), '/');
+
+// --- nothing to match
+assert.equal(subPathOf('/somewhere-else', '/docs'), '/somewhere-else', 'left alone rather than guessed at');
+
+console.log('sub-path-test: all assertions passed');
diff --git a/integrations/nodejs/nestjs/tsconfig.json b/integrations/nodejs/nestjs/tsconfig.json
new file mode 100644
index 00000000..0a787d37
--- /dev/null
+++ b/integrations/nodejs/nestjs/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist",
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true
+ },
+ "include": ["src"]
+}
diff --git a/integrations/nodejs/scripts/check-all.sh b/integrations/nodejs/scripts/check-all.sh
new file mode 100755
index 00000000..5bbd047d
--- /dev/null
+++ b/integrations/nodejs/scripts/check-all.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# Every Node framework against the one contract suite, and every one of them byte for byte against
+# the first. Different ports rather than one reused, so a server still shutting down cannot look
+# like a port already in use.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+source "$ROOT/contract-tests/lib.sh"
+
+covered "$0" $(cd "$ROOT/contract-tests/apps" && ls *.js | sed 's|^|apps/|')
+
+BODIES="$(mktemp -d)"
+trap 'rm -rf "$BODIES"' EXIT
+
+run() {
+ local name=$1 app=$2 port=$3 adapter=${4:-express}
+ echo
+ echo "${BOLD}▸ $name${RESET}"
+ ADAPTER="$adapter" JUNIT_NAME="check-${name//\//-}" bash "$ROOT/contract-tests/check.sh" --app "$app" --port "$port" "${@:5}"
+}
+
+# the first run is the baseline every other one is compared against
+run "express" contract-tests/apps/express.js 5456 express --save "$BODIES"
+run "fastify" contract-tests/apps/fastify.js 5457 express --compare "$BODIES"
+run "nestjs/express" contract-tests/apps/nestjs.js 5458 express --compare "$BODIES"
+run "nestjs/fastify" contract-tests/apps/nestjs.js 5459 fastify --compare "$BODIES"
+
+echo
+echo "${GREEN}every framework agrees, byte for byte: the page, the documents, the bundle${RESET}"
diff --git a/integrations/nodejs/scripts/matrix.Dockerfile b/integrations/nodejs/scripts/matrix.Dockerfile
new file mode 100644
index 00000000..45c0592c
--- /dev/null
+++ b/integrations/nodejs/scripts/matrix.Dockerfile
@@ -0,0 +1,23 @@
+# The matrix under a Node this machine does not have. Built and run by `matrix.sh --docker`.
+ARG NODE=22
+# slim: nothing we install compiles native code, esbuild ships a prebuilt binary
+FROM node:${NODE}-slim
+WORKDIR /integrations
+
+# what check.sh needs that slim leaves out: curl for every assertion, procps for the pkill in its trap
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends curl procps \
+ && rm -rf /var/lib/apt/lists/*
+
+# manifests first, so the install layer caches until a dependency actually changes
+COPY package.json package-lock.json ./
+COPY nodejs/core/package.json nodejs/core/
+COPY nodejs/express/package.json nodejs/express/
+COPY nodejs/fastify/package.json nodejs/fastify/
+COPY nodejs/nestjs/package.json nodejs/nestjs/
+RUN npm ci --no-audit --no-fund
+
+COPY . .
+RUN npm run build
+
+ENTRYPOINT ["bash", "nodejs/scripts/matrix.sh"]
diff --git a/integrations/nodejs/scripts/matrix.sh b/integrations/nodejs/scripts/matrix.sh
new file mode 100755
index 00000000..2e0a8143
--- /dev/null
+++ b/integrations/nodejs/scripts/matrix.sh
@@ -0,0 +1,218 @@
+#!/usr/bin/env bash
+# The framework matrix: every major we claim in peerDependencies, each installed from our tarballs
+# into its own clean tree next to a copy of its example, with the contract suite run against it.
+# This is the evidence for the peer ranges we publish.
+#
+# bash nodejs/scripts/matrix.sh every cell, under the node that is running
+# bash nodejs/scripts/matrix.sh --family fastify that family's majors (CI runs one job per family)
+# bash nodejs/scripts/matrix.sh --cell nestjs@10 one cell, to reproduce a failure
+# bash nodejs/scripts/matrix.sh --docker every cell under node 22 and 24, in containers
+# bash nodejs/scripts/matrix.sh --results DIR also leave one result file per suite in DIR
+# bash nodejs/scripts/matrix.sh --report DIR [--md] only render the table from those files
+#
+# A cell is family@major. The nestjs cells run both adapters, since the adapter is what broke.
+# Every run ends with a cell x node table in markdown; CI's jobs each leave their results as an
+# artifact and one job renders them all into the step summary and the PR comment.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+source "$ROOT/contract-tests/lib.sh"
+
+FAMILY=""
+CELL=""
+DOCKER=""
+RESULTS_DIR=""
+COLLECTED=""
+REPORT_DIR=""
+MD=""
+PASSTHROUGH=()
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --family) FAMILY="$2"; PASSTHROUGH+=("$1" "$2"); shift 2 ;;
+ --cell) CELL="$2"; PASSTHROUGH+=("$1" "$2"); shift 2 ;;
+ --docker) DOCKER=1; shift ;;
+ --results) RESULTS_DIR="$2"; COLLECTED=1; shift 2 ;;
+ --report) REPORT_DIR="$2"; shift 2 ;;
+ --md) MD="--md"; shift ;;
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
+ esac
+done
+
+# ---- the table --------------------------------------------------------------------------
+
+report() {
+ echo
+ node "$ROOT/contract-tests/report.mjs" "$1" $MD
+}
+
+if [ -n "$REPORT_DIR" ]; then
+ report "$REPORT_DIR"
+ exit 0
+fi
+
+# --docker is a wrapper, not a second implementation: each container runs this same script natively
+if [ -n "$DOCKER" ]; then
+ docker info >/dev/null 2>&1 || { echo "docker is installed but the daemon is not running" >&2; exit 2; }
+ # the container's stdout is a pipe, so it would go plain: pass our own colour decision in
+ docker_env=()
+ if [ -n "$GREEN" ]; then
+ docker_env=(-e FORCE_COLOR=1)
+ fi
+ # the containers write their results into one host directory, so the table covers both nodes
+ RESULTS_DIR="${RESULTS_DIR:-$(mktemp -d)}"
+ mkdir -p "$RESULTS_DIR"
+ status=0
+ for node in 22 24; do
+ echo
+ echo "${BOLD}############ node $node, in a container${RESET}"
+ docker build --quiet --build-arg "NODE=$node" -f "$ROOT/nodejs/scripts/matrix.Dockerfile" \
+ -t "api-docs-matrix:node$node" "$ROOT" >/dev/null
+ docker run --rm ${docker_env[@]+"${docker_env[@]}"} -v "$RESULTS_DIR:/results" \
+ "api-docs-matrix:node$node" --results /results ${PASSTHROUGH[@]+"${PASSTHROUGH[@]}"} || status=1
+ done
+ report "$RESULTS_DIR"
+ exit "$status"
+fi
+
+ALL_CELLS="express@4 express@5 fastify@4 fastify@5 nestjs@10 nestjs@11 nestjs@12"
+for app in "$ROOT"/contract-tests/apps/*.js; do
+ family="$(basename "$app" .js)"
+ [[ " $ALL_CELLS " == *" $family@"* ]] || { echo "$family has a rig but no cells in the matrix. Add its majors to ALL_CELLS and deps_for." >&2; exit 1; }
+done
+if [ -n "$CELL" ]; then
+ case " $ALL_CELLS " in *" $CELL "*) ;; *) echo "unknown cell '$CELL'. one of: $ALL_CELLS" >&2; exit 2 ;; esac
+ CELLS="$CELL"
+elif [ -n "$FAMILY" ]; then
+ # no case inside $(...): bash 3.2 reads the pattern's ')' as the end of the substitution
+ CELLS=""
+ for c in $ALL_CELLS; do
+ if [[ "$c" == "$FAMILY"@* ]]; then
+ CELLS="$CELLS $c"
+ fi
+ done
+ [ -n "$CELLS" ] || { echo "no cells for family '$FAMILY'" >&2; exit 2; }
+else
+ CELLS="$ALL_CELLS"
+fi
+
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+RESULTS_DIR="${RESULTS_DIR:-$TMP/results}"
+mkdir -p "$RESULTS_DIR"
+NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
+
+suites=0
+for c in $CELLS; do
+ case "$c" in nestjs@*) suites=$((suites + 2)) ;; *) suites=$((suites + 1)) ;; esac
+done
+echo
+echo "${BOLD}Integrations matrix${RESET} ${DIM}· node $(node -v) · $(echo $CELLS | wc -w | tr -d ' ') cells, $suites suites${RESET}"
+echo " ${DIM}$CELLS${RESET}"
+echo
+
+# ---- pack once, every cell installs the same tarballs ---------------------------------
+
+TARBALLS="$TMP/tarballs"
+mkdir -p "$TARBALLS"
+for pkg in core express fastify nestjs; do
+ ( cd "$ROOT/nodejs/$pkg" && npm pack --silent --pack-destination "$TARBALLS" >/dev/null )
+done
+# listed once into a variable: `tar | grep -q` under pipefail fails when grep quits before tar
+# finishes writing, which GNU tar's archive order made happen on every run
+core_files="$(tar -tzf "$TARBALLS"/*api-docs-core*.tgz)"
+grep -q 'package/shell/shell.js' <<<"$core_files" || { echo "shell.js is NOT in the core tarball" >&2; exit 1; }
+grep -q 'package/shell/shell.html' <<<"$core_files" || { echo "shell.html is NOT in the core tarball" >&2; exit 1; }
+echo "${DIM}packed $(ls "$TARBALLS" | wc -l | tr -d ' ') tarballs, the core ships its shell${RESET}"
+
+# ---- what each cell installs, on top of our tarballs -----------------------------------
+
+deps_for() {
+ case "$1" in
+ express@4) echo "express@^4 helmet" ;;
+ express@5) echo "express@^5 helmet" ;;
+ fastify@4) echo "fastify@^4 @fastify/swagger@^8" ;;
+ fastify@5) echo "fastify@^5 @fastify/swagger@^9" ;;
+ nestjs@10) echo "@nestjs/common@^10 @nestjs/core@^10 @nestjs/platform-express@^10 @nestjs/platform-fastify@^10 reflect-metadata rxjs helmet" ;;
+ nestjs@11) echo "@nestjs/common@^11 @nestjs/core@^11 @nestjs/platform-express@^11 @nestjs/platform-fastify@^11 reflect-metadata rxjs helmet" ;;
+ nestjs@12) echo "@nestjs/common@^12 @nestjs/core@^12 @nestjs/platform-express@^12 @nestjs/platform-fastify@^12 reflect-metadata rxjs helmet" ;;
+ *) echo "unknown cell: $1" >&2; exit 2 ;;
+ esac
+}
+
+# ---- one cell -------------------------------------------------------------------------
+
+run_cell() {
+ local cell=$1 port=$2
+ local family="${cell%@*}"
+ local app="$TMP/$cell"
+
+ mkdir -p "$app/apps"
+ printf '{ "name": "matrix-%s", "private": true, "version": "0.0.0" }\n' "${cell/@/-}" >"$app/package.json"
+ cp "$ROOT/contract-tests/apps/$family.js" "$app/apps/$family.js"
+ cp -R "$ROOT/contract-tests/fixtures" "$app/fixtures"
+
+ # shellcheck disable=SC2046
+ ( cd "$app" && npm install --silent --no-audit --no-fund \
+ "$TARBALLS"/*api-docs-core*.tgz "$TARBALLS"/*api-docs-"$family"*.tgz $(deps_for "$cell") >/dev/null )
+
+ local installed
+ case "$family" in
+ express) installed="express $(node -p "require('$app/node_modules/express/package.json').version")" ;;
+ fastify) installed="fastify $(node -p "require('$app/node_modules/fastify/package.json').version")" ;;
+ nestjs) installed="@nestjs/core $(node -p "require('$app/node_modules/@nestjs/core/package.json').version")" ;;
+ esac
+ echo " ${DIM}installed $installed${RESET}"
+
+ # suite PORT ADAPTER [VAR=value ...]: runs the contract suite, leaves its row for the table.
+ # A suite that never wrote its counts did not get as far as booting.
+ suite() {
+ local port=$1 adapter=$2 status=0
+ shift 2
+ local counts="$TMP/counts.tsv"
+ rm -f "$counts"
+ env RESULTS_FILE="$counts" JUNIT_DIR="$RESULTS_DIR" JUNIT_NAME="node$NODE_MAJOR-$cell-$adapter" "$@" \
+ bash "$ROOT/contract-tests/check.sh" --app "$app/apps/$family.js" --port "$port" || status=1
+ printf '%s\t%s\t%s\t%s\n' "$NODE_MAJOR" "$cell" "$adapter" "$(cat "$counts" 2>/dev/null || printf -- '-\t-')" \
+ >"$RESULTS_DIR/node$NODE_MAJOR-$cell-$adapter.tsv"
+
+ return "$status"
+ }
+ local status=0
+ if [ "$family" = "nestjs" ]; then
+ echo " ${BOLD}platform-express${RESET}"
+ suite "$port" express || status=1
+ echo
+ echo " ${BOLD}platform-fastify${RESET}"
+ suite "$((port + 1))" fastify ADAPTER=fastify || status=1
+ else
+ suite "$port" - || status=1
+ fi
+
+ return "$status"
+}
+
+# ---- the run ----------------------------------------------------------------------------
+
+total=$(echo $CELLS | wc -w | tr -d ' ')
+n=0
+failed=""
+for cell in $CELLS; do
+ n=$((n + 1))
+ echo
+ echo "${BOLD}▸ [$n/$total] $cell${RESET}"
+ if run_cell "$cell" $((5470 + n * 2)); then
+ :
+ else
+ failed="$failed $cell"
+ fi
+done
+
+# with --results, whoever collects the files renders the table: the --docker host, or CI's report job
+if [ -z "$COLLECTED" ]; then
+ report "$RESULTS_DIR"
+fi
+echo
+if [ -n "$failed" ]; then
+ echo " ${RED}failed cells:$failed${RESET}"
+ exit 1
+fi
diff --git a/integrations/nodejs/scripts/unit.sh b/integrations/nodejs/scripts/unit.sh
new file mode 100644
index 00000000..279e80c2
--- /dev/null
+++ b/integrations/nodejs/scripts/unit.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# The unit suites, reported like the other layers: one line each, a JUnit file when CI asks.
+# Each suite is a plain node script that exits non-zero on its first failed assertion.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+source "$ROOT/contract-tests/lib.sh"
+JUNIT_NAME="${JUNIT_NAME:-unit}"
+
+# runs CMD...: passes when the command exits clean; otherwise the tail of its output is what we saw
+runs() {
+ local out
+ out="$("$@" 2>&1)" && return 0
+ want="a clean exit"
+ got="$(tail -4 <<<"$out")"
+
+ return 1
+}
+
+describe "core"
+check "walk: the safety rules and the caps" runs node "$ROOT/nodejs/core/test/walk.test.mjs"
+check "filter: environments and tags by file" runs node "$ROOT/nodejs/core/test/filter.test.mjs"
+check "collection: sources and providers" runs node "$ROOT/nodejs/core/test/collection.test.mjs"
+check "shell: the page, the bundle, embed()" runs node "$ROOT/nodejs/core/test/shell.test.mjs"
+check "docs: createDocs end to end" runs node "$ROOT/nodejs/core/test/docs.test.mjs"
+
+describe "wrappers"
+check "nestjs: the sub-path on platform-fastify" runs node "$ROOT/nodejs/nestjs/test/sub-path.test.mjs"
+
+describe "shell"
+check "assemble: fragments into one document" runs node "$ROOT/shell/test/assemble.test.mjs"
+
+summary
diff --git a/integrations/package-lock.json b/integrations/package-lock.json
new file mode 100644
index 00000000..c2f2068b
--- /dev/null
+++ b/integrations/package-lock.json
@@ -0,0 +1,4520 @@
+{
+ "name": "bruno-api-docs-integrations",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "bruno-api-docs-integrations",
+ "license": "MIT",
+ "workspaces": [
+ "nodejs/*"
+ ],
+ "devDependencies": {
+ "@eslint/js": "^9.39.5",
+ "@fastify/swagger": "^9.8.1",
+ "@nestjs/common": "^12.0.3",
+ "@nestjs/core": "^12.0.3",
+ "@nestjs/platform-express": "^12.0.3",
+ "@nestjs/platform-fastify": "^12.0.3",
+ "@playwright/test": "^1.61.1",
+ "@stylistic/eslint-plugin": "^5.3.1",
+ "@types/express": "^5.0.0",
+ "@types/js-yaml": "^4.0.9",
+ "@types/node": "^22.10.0",
+ "@typescript-eslint/eslint-plugin": "^8.65.0",
+ "@typescript-eslint/parser": "^8.39.0",
+ "esbuild": "^0.28.2",
+ "eslint": "^9.21.0",
+ "express": "^5.0.0",
+ "fastify": "^5.12.5",
+ "globals": "^16.0.0",
+ "helmet": "^8.0.0",
+ "reflect-metadata": "^0.2.2",
+ "rxjs": "^7.8.2",
+ "typescript": "^5.7.0"
+ }
+ },
+ "node_modules/@borewit/text-codec": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
+ "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+ "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz",
+ "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.3.2",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+ "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz",
+ "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@fastify/ajv-compiler": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz",
+ "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.12.0",
+ "ajv-formats": "^3.0.1",
+ "fast-uri": "^4.0.0"
+ }
+ },
+ "node_modules/@fastify/ajv-compiler/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@fastify/ajv-compiler/node_modules/ajv/node_modules/fast-uri": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/@fastify/cors": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.3.0.tgz",
+ "integrity": "sha512-ggQGua+xHv1MvePbPr0v//xLYEsCXbWspquXCJS9Ot5YoRXq8J8ZWzHnxDBVnbtXosvistXo6LtNzOJswf64Fw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "fastify-plugin": "^6.0.0",
+ "toad-cache": "^3.7.0"
+ }
+ },
+ "node_modules/@fastify/error": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
+ "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@fastify/fast-json-stringify-compiler": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz",
+ "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "fast-json-stringify": "^7.0.0"
+ }
+ },
+ "node_modules/@fastify/formbody": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-9.0.0.tgz",
+ "integrity": "sha512-T/af26CSrUARBCvsEmv+DJLPfZlrRKESzqironxP1j7qzuLyKcoZtj6MuTGShuKx1THXugoie2oFbUJxXfGFzA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "fast-querystring": "^1.1.2",
+ "fastify-plugin": "^6.0.0"
+ }
+ },
+ "node_modules/@fastify/forwarded": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz",
+ "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@fastify/merge-json-schemas": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
+ "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/@fastify/middie": {
+ "version": "9.3.4",
+ "resolved": "https://registry.npmjs.org/@fastify/middie/-/middie-9.3.4.tgz",
+ "integrity": "sha512-dd8phTnVWzx3iKPBKSvtRUT8+Igvy/MP+P5HcY2tjejEyeVbyjkD4HgzKessUObue5RRM5J6PZdxtAqgbn84yw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/error": "^4.0.0",
+ "fastify-plugin": "^6.0.0",
+ "find-my-way": "^9.5.0",
+ "path-to-regexp": "^8.1.0",
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/@fastify/proxy-addr": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.1.tgz",
+ "integrity": "sha512-zv07Y9GEuDsJPegZoDFd4SDWaZOW8N2pa0GSrYmKpId/tjt1Hgo3BjZBVjdVpfVrHaA+Qv5jawtS2O50J5xM9g==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/forwarded": "^3.0.0",
+ "ipaddr.js": "^2.1.0"
+ }
+ },
+ "node_modules/@fastify/proxy-addr/node_modules/ipaddr.js": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz",
+ "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@fastify/swagger": {
+ "version": "9.8.1",
+ "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.8.1.tgz",
+ "integrity": "sha512-VpHMnqZTY8iBZYJE8WWkbKPrXIYWy2rDfIf5qLr6DzZSpQYZ+KxQVcJFiq/AMlvNwI4gCBd66++iUlxXXGT0IQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "fastify-plugin": "^6.0.0",
+ "json-schema-resolver": "^3.0.0",
+ "openapi-types": "^12.1.3",
+ "rfdc": "^1.3.1",
+ "yaml": "^2.4.2"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@lukeed/csprng": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
+ "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@nestjs/common": {
+ "version": "12.0.3",
+ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-12.0.3.tgz",
+ "integrity": "sha512-Fosz6lHc9OagZNcF0LTrVHyJS8W7mtuNFBOo9RRccGwyeICBd0hiutjAi9zxnzBcP45RkYXeNB+idA/52jZhBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "1.1.0",
+ "file-type": "22.1.0",
+ "iterare": "1.2.1",
+ "load-esm": "1.0.3",
+ "tslib": "2.8.1",
+ "uid": "2.0.2"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nest"
+ },
+ "peerDependencies": {
+ "class-transformer": ">=0.4.1",
+ "class-validator": ">=0.13.2",
+ "reflect-metadata": "^0.1.12 || ^0.2.0",
+ "rxjs": "^7.1.0"
+ },
+ "peerDependenciesMeta": {
+ "class-transformer": {
+ "optional": true
+ },
+ "class-validator": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@nestjs/core": {
+ "version": "12.0.3",
+ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-12.0.3.tgz",
+ "integrity": "sha512-Ouv++gAXOL50S8K8n6AN19QGmw51rnIo6v7y9AqUYoaf0NcZlufTu5XTDeC2RtpMz8oXRr9nykBngRjflcoQKA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-safe-stringify": "2.1.1",
+ "iterare": "1.2.1",
+ "path-to-regexp": "8.4.2",
+ "tslib": "2.8.1",
+ "uid": "2.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nest"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^12.0.0",
+ "@nestjs/microservices": "^12.0.0",
+ "@nestjs/platform-express": "^12.0.0",
+ "@nestjs/websockets": "^12.0.0",
+ "reflect-metadata": "^0.1.12 || ^0.2.0",
+ "rxjs": "^7.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@nestjs/microservices": {
+ "optional": true
+ },
+ "@nestjs/platform-express": {
+ "optional": true
+ },
+ "@nestjs/websockets": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@nestjs/platform-express": {
+ "version": "12.0.3",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-12.0.3.tgz",
+ "integrity": "sha512-q9ImjCMjbYYgqvRHHro6sAypNwhjspIAHad0RTL2ww5wYd9gwUGWMss7Q+ACCw5Sksu7Q8S/3rjSLhB+W6B6bg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "cors": "2.8.6",
+ "express": "5.2.1",
+ "multer": "2.4.0",
+ "path-to-regexp": "8.4.2",
+ "tslib": "2.8.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nest"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^12.0.0",
+ "@nestjs/core": "^12.0.0"
+ }
+ },
+ "node_modules/@nestjs/platform-fastify": {
+ "version": "12.0.3",
+ "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-12.0.3.tgz",
+ "integrity": "sha512-Cn8jR8jwPJBk7ftkzunplz0LHDahnq7urAO9VjOV0OWkk1YCwaFMm4CElSk3BwBiDwP177anJjJUbVxTlASk4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/cors": "11.3.0",
+ "@fastify/formbody": "9.0.0",
+ "@fastify/middie": "9.3.4",
+ "fast-querystring": "1.1.2",
+ "fastify": "5.12.4",
+ "find-my-way": "9.9.0",
+ "light-my-request": "6.6.0",
+ "path-to-regexp": "8.4.2",
+ "tslib": "2.8.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nest"
+ },
+ "peerDependencies": {
+ "@fastify/static": "^10.1.2",
+ "@fastify/view": "^10.0.0 || ^11.0.0 || ^12.0.0",
+ "@nestjs/common": "^12.0.0",
+ "@nestjs/core": "^12.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@fastify/static": {
+ "optional": true
+ },
+ "@fastify/view": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@nestjs/platform-fastify/node_modules/fastify": {
+ "version": "5.12.4",
+ "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.4.tgz",
+ "integrity": "sha512-zATD676fP6tBMsn+jQ7Q5/cO/Qo+149TXH0dUqUKel+1/hcZxwIbnflqtPa9EjlrvpY8eCZcL3M1UYrMc31Y7g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/ajv-compiler": "^4.0.5",
+ "@fastify/error": "^4.0.0",
+ "@fastify/fast-json-stringify-compiler": "^5.0.0",
+ "@fastify/proxy-addr": "^5.0.0",
+ "abstract-logging": "^2.0.1",
+ "avvio": "^9.0.0",
+ "fast-json-stringify": "^7.0.0",
+ "find-my-way": "^9.6.0",
+ "light-my-request": "^6.0.0",
+ "pino": "^9.14.0 || ^10.1.0",
+ "process-warning": "^5.1.0",
+ "rfdc": "^1.3.1",
+ "secure-json-parse": "^4.0.0",
+ "semver": "^7.6.0",
+ "toad-cache": "^3.7.0"
+ }
+ },
+ "node_modules/@pinojs/redact": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
+ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
+ "license": "MIT"
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
+ "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@stylistic/eslint-plugin": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
+ "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/types": "^8.56.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "estraverse": "^5.3.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": "^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/@tokenizer/inflate": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
+ "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "token-types": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/@tokenizer/token": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz",
+ "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/js-yaml": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
+ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz",
+ "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz",
+ "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.70.0",
+ "@typescript-eslint/type-utils": "8.70.0",
+ "@typescript-eslint/utils": "8.70.0",
+ "@typescript-eslint/visitor-keys": "8.70.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.70.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz",
+ "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.70.0",
+ "@typescript-eslint/types": "8.70.0",
+ "@typescript-eslint/typescript-estree": "8.70.0",
+ "@typescript-eslint/visitor-keys": "8.70.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz",
+ "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.70.0",
+ "@typescript-eslint/types": "^8.70.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz",
+ "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.70.0",
+ "@typescript-eslint/visitor-keys": "8.70.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz",
+ "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz",
+ "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.70.0",
+ "@typescript-eslint/typescript-estree": "8.70.0",
+ "@typescript-eslint/utils": "8.70.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz",
+ "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz",
+ "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.70.0",
+ "@typescript-eslint/tsconfig-utils": "8.70.0",
+ "@typescript-eslint/types": "8.70.0",
+ "@typescript-eslint/visitor-keys": "8.70.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz",
+ "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.70.0",
+ "@typescript-eslint/types": "8.70.0",
+ "@typescript-eslint/typescript-estree": "8.70.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.70.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz",
+ "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.70.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@usebruno/api-docs-core": {
+ "resolved": "nodejs/core",
+ "link": true
+ },
+ "node_modules/@usebruno/api-docs-express": {
+ "resolved": "nodejs/express",
+ "link": true
+ },
+ "node_modules/@usebruno/api-docs-fastify": {
+ "resolved": "nodejs/fastify",
+ "link": true
+ },
+ "node_modules/@usebruno/api-docs-nestjs": {
+ "resolved": "nodejs/nestjs",
+ "link": true
+ },
+ "node_modules/abstract-logging": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
+ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/fast-uri": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/atomic-sleep": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
+ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/avvio": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz",
+ "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/error": "^4.0.0",
+ "fastq": "^1.17.1"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^2.0.0",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
+ "on-finished": "^2.4.1",
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/body-parser/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "devOptional": true,
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.5",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz",
+ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.6",
+ "@eslint/js": "9.39.5",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+ "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fast-decode-uri-component": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
+ "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stringify": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz",
+ "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/merge-json-schemas": "^0.2.0",
+ "ajv": "^8.12.0",
+ "ajv-formats": "^3.0.1",
+ "fast-uri": "^4.0.0",
+ "json-schema-ref-resolver": "^3.0.0",
+ "rfdc": "^1.2.0"
+ }
+ },
+ "node_modules/fast-json-stringify/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fast-json-stringify/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-querystring": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
+ "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-decode-uri-component": "^1.0.1"
+ }
+ },
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.5.tgz",
+ "integrity": "sha512-vZeoMRB4epNr7QfdHxel7te/RcX16CxyXI07JCCTFWZA2s4v1azGNESRj+2EoaHSaWFL/Z3GmKT2jF6A202jLg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastify": {
+ "version": "5.12.5",
+ "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.5.tgz",
+ "integrity": "sha512-OB2k1dlxs5/NAABqeKV2FUHkSD2BbENsCak8yULVcymn3fHIPDVa9TI3SDnJSWYSllZmSYuZXy2gTnsT+Sut1A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/ajv-compiler": "^4.0.5",
+ "@fastify/error": "^4.0.0",
+ "@fastify/fast-json-stringify-compiler": "^5.0.0",
+ "@fastify/proxy-addr": "^5.0.0",
+ "abstract-logging": "^2.0.1",
+ "avvio": "^9.0.0",
+ "fast-json-stringify": "^7.0.0",
+ "find-my-way": "^9.6.0",
+ "light-my-request": "^6.0.0",
+ "pino": "^9.14.0 || ^10.1.0",
+ "process-warning": "^5.1.0",
+ "rfdc": "^1.3.1",
+ "secure-json-parse": "^4.0.0",
+ "semver": "^7.6.0",
+ "toad-cache": "^3.7.0"
+ }
+ },
+ "node_modules/fastify-plugin": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz",
+ "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
+ "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/file-type": {
+ "version": "22.1.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.1.0.tgz",
+ "integrity": "sha512-fbvf9u6jnHWr1EP6V8R99E9iLMhNLXgZkUzWBiPpJ5/89UJ/Atuxjn5YXR+XaZWo2hc2/8mGfTez+NDAgOcpLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/inflate": "^0.4.1",
+ "strtok3": "^10.3.5",
+ "token-types": "^6.1.2",
+ "uint8array-extras": "^1.5.0"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/find-my-way": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.9.0.tgz",
+ "integrity": "sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-querystring": "^1.0.0",
+ "safe-regex2": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/helmet": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
+ "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/EvanHahn"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "7.0.9",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz",
+ "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/iterare": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz",
+ "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-ref-resolver": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz",
+ "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/json-schema-resolver": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz",
+ "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fast-uri": "^3.0.5",
+ "rfdc": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1"
+ }
+ },
+ "node_modules/json-schema-resolver/node_modules/fast-uri": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/light-my-request": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
+ "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "process-warning": "^4.0.0",
+ "set-cookie-parser": "^2.6.0"
+ }
+ },
+ "node_modules/light-my-request/node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/light-my-request/node_modules/process-warning": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
+ "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/load-esm": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz",
+ "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://buymeacoffee.com/borewit"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=13.2.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
+ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/multer": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz",
+ "integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.6.0",
+ "type-is": "^1.6.18"
+ },
+ "engines": {
+ "node": ">= 10.16.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/multer/node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
+ "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/negotiator/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-exit-leak-free": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
+ "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/openapi-types": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
+ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pino": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
+ "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
+ "license": "MIT",
+ "dependencies": {
+ "@pinojs/redact": "^0.4.0",
+ "atomic-sleep": "^1.0.0",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^3.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^5.0.0",
+ "quick-format-unescaped": "^4.0.3",
+ "real-require": "^0.2.0",
+ "safe-stable-stringify": "^2.3.1",
+ "sonic-boom": "^4.0.1",
+ "thread-stream": "^4.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
+ }
+ },
+ "node_modules/pino-abstract-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
+ "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.0.0"
+ }
+ },
+ "node_modules/pino-std-serializers": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
+ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
+ "license": "MIT"
+ },
+ "node_modules/playwright": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
+ "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
+ "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/process-warning": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz",
+ "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/quick-format-unescaped": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
+ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
+ "license": "MIT"
+ },
+ "node_modules/range-parser": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
+ "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/real-require": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
+ "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/reflect-metadata": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ret": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
+ "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "license": "MIT"
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-regex2": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz",
+ "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "ret": "~0.5.0"
+ },
+ "bin": {
+ "safe-regex2": "bin/safe-regex2.js"
+ }
+ },
+ "node_modules/safe-stable-stringify": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/secure-json-parse": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
+ "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sonic-boom": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
+ "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "atomic-sleep": "^1.0.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strtok3": {
+ "version": "10.3.5",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
+ "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/thread-stream": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
+ "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "real-require": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/thread-stream/node_modules/real-require": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz",
+ "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/toad-cache": {
+ "version": "3.7.4",
+ "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz",
+ "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/token-types": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
+ "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@borewit/text-codec": "^0.2.1",
+ "@tokenizer/token": "^0.3.0",
+ "ieee754": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/uid": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz",
+ "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@lukeed/csprng": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/uint8array-extras": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
+ "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
+ "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "nodejs/core": {
+ "name": "@usebruno/api-docs-core",
+ "version": "0.1.0-beta.1",
+ "license": "MIT",
+ "dependencies": {
+ "js-yaml": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "nodejs/express": {
+ "name": "@usebruno/api-docs-express",
+ "version": "0.1.0-beta.1",
+ "license": "MIT",
+ "dependencies": {
+ "@usebruno/api-docs-core": "0.1.0-beta.1"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "express": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "nodejs/fastify": {
+ "name": "@usebruno/api-docs-fastify",
+ "version": "0.1.0-beta.1",
+ "license": "MIT",
+ "dependencies": {
+ "@usebruno/api-docs-core": "0.1.0-beta.1"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "fastify": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "nodejs/nestjs": {
+ "name": "@usebruno/api-docs-nestjs",
+ "version": "0.1.0-beta.1",
+ "license": "MIT",
+ "dependencies": {
+ "@usebruno/api-docs-core": "0.1.0-beta.1"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0",
+ "@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0"
+ }
+ }
+ }
+}
diff --git a/integrations/package.json b/integrations/package.json
new file mode 100644
index 00000000..4c64ab95
--- /dev/null
+++ b/integrations/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "bruno-api-docs-integrations",
+ "private": true,
+ "description": "Serve a Bruno collection as API docs from inside your own app, behind your own auth.",
+ "license": "MIT",
+ "workspaces": [
+ "nodejs/*"
+ ],
+ "scripts": {
+ "build": "npm run build:shell && npm run build:core && npm run build:express && npm run build:fastify && npm run build:nestjs && npm run build:examples",
+ "build:core": "tsc -p nodejs/core/tsconfig.json",
+ "build:examples": "tsc -p nodejs/nestjs/examples/tsconfig.json",
+ "build:express": "tsc -p nodejs/express/tsconfig.json",
+ "build:fastify": "tsc -p nodejs/fastify/tsconfig.json",
+ "build:nestjs": "tsc -p nodejs/nestjs/tsconfig.json",
+ "build:shell": "esbuild shell/shell.src.mjs --bundle --minify --format=iife --platform=browser --target=es2020 --outfile=shell/shell.js --log-level=warning && mkdir -p nodejs/core/shell && cp shell/shell.html shell/shell.js nodejs/core/shell/",
+ "check:all": "npm run build && bash nodejs/scripts/check-all.sh",
+ "check:express": "npm run build && bash contract-tests/check.sh --app contract-tests/apps/express.js --port 5456",
+ "check:fastify": "npm run build && bash contract-tests/check.sh --app contract-tests/apps/fastify.js --port 5457",
+ "check:nestjs": "npm run build && bash contract-tests/check.sh --app contract-tests/apps/nestjs.js --port 5458",
+ "check:nestjs-fastify": "npm run build && ADAPTER=fastify bash contract-tests/check.sh --app contract-tests/apps/nestjs.js --port 5459",
+ "lint": "eslint",
+ "lint:fix": "eslint --fix",
+ "matrix": "npm run build && bash nodejs/scripts/matrix.sh",
+ "matrix:docker": "npm run build && bash nodejs/scripts/matrix.sh --docker",
+ "pack-check": "npm run build && bash nodejs/scripts/matrix.sh --cell express@5",
+ "setup": "bash scripts/setup.sh",
+ "start:express": "node contract-tests/apps/express.js",
+ "start:fastify": "node contract-tests/apps/fastify.js",
+ "start:nestjs": "node contract-tests/apps/nestjs.js",
+ "test": "npm run test:ci && npm run test:browser",
+ "test:assemble": "node shell/test/assemble.test.mjs",
+ "test:browser": "npm run build && playwright test --config contract-tests/browser/playwright.config.mjs",
+ "test:ci": "npm run lint && npm run test:unit && bash nodejs/scripts/check-all.sh && bash contract-tests/examples.sh && bash nodejs/scripts/matrix.sh --cell express@5",
+ "test:collection": "npm run build:core && node nodejs/core/test/collection.test.mjs",
+ "test:docs": "npm run build && node nodejs/core/test/docs.test.mjs",
+ "test:examples": "npm run build && bash contract-tests/examples.sh",
+ "test:filter": "npm run build:core && node nodejs/core/test/filter.test.mjs",
+ "test:nestjs": "npm run build && node nodejs/nestjs/test/sub-path.test.mjs",
+ "test:shell": "npm run build && node nodejs/core/test/shell.test.mjs",
+ "test:unit": "npm run build && bash nodejs/scripts/unit.sh",
+ "test:walk": "npm run build:core && node nodejs/core/test/walk.test.mjs",
+ "try": "bash scripts/try.sh"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.5",
+ "@fastify/swagger": "^9.8.1",
+ "@nestjs/common": "^12.0.3",
+ "@nestjs/core": "^12.0.3",
+ "@nestjs/platform-express": "^12.0.3",
+ "@nestjs/platform-fastify": "^12.0.3",
+ "@playwright/test": "^1.61.1",
+ "@stylistic/eslint-plugin": "^5.3.1",
+ "@types/express": "^5.0.0",
+ "@types/js-yaml": "^4.0.9",
+ "@types/node": "^22.10.0",
+ "@typescript-eslint/eslint-plugin": "^8.65.0",
+ "@typescript-eslint/parser": "^8.39.0",
+ "esbuild": "^0.28.2",
+ "eslint": "^9.21.0",
+ "express": "^5.0.0",
+ "fastify": "^5.12.5",
+ "globals": "^16.0.0",
+ "helmet": "^8.0.0",
+ "reflect-metadata": "^0.2.2",
+ "rxjs": "^7.8.2",
+ "typescript": "^5.7.0"
+ }
+}
diff --git a/integrations/scripts/setup.sh b/integrations/scripts/setup.sh
new file mode 100755
index 00000000..43242063
--- /dev/null
+++ b/integrations/scripts/setup.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# First-time setup for integrations/, safe to rerun: the pinned toolchain and a clean build.
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+if ! command -v mise >/dev/null 2>&1; then
+ echo "mise is not installed. It reads mise.toml and gives you the pinned Node."
+ if command -v brew >/dev/null 2>&1 && [ -t 0 ]; then
+ read -r -p "Install it with Homebrew now? [y/N] " answer
+ if [ "$answer" = "y" ]; then
+ brew install mise
+ fi
+ fi
+ command -v mise >/dev/null 2>&1 || { echo "Install it first: https://mise.jdx.dev/getting-started.html"; exit 1; }
+fi
+
+mise trust --quiet 2>/dev/null || mise trust
+mise install
+
+pinned="$(mise exec -- node -v)"
+current="$(node -v 2>/dev/null || echo none)"
+if [ "$pinned" != "$current" ]; then
+ echo
+ echo "mise.toml pins node $pinned; your shell runs $current."
+ echo "Activate mise for your shell and cd into this directory switches automatically:"
+ echo " https://mise.jdx.dev/getting-started.html#activate-mise"
+ echo "Until then, prefix commands with: mise exec --"
+ echo
+fi
+
+# stale build output from another node or an older checkout is the usual source of a mystery failure
+rm -rf nodejs/*/dist nodejs/*/shell shell/shell.js
+mise exec -- npm ci
+mise exec -- npm run build
+
+echo
+echo "Ready. The whole gate: npm test"
diff --git a/integrations/scripts/try.sh b/integrations/scripts/try.sh
new file mode 100644
index 00000000..258ab53a
--- /dev/null
+++ b/integrations/scripts/try.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+# Try the packages in a real app before they are on npm. Builds, packs the tarballs, installs the
+# core and one wrapper into the app exactly as npm would, and prints the mount to add.
+#
+# npm run try -- ~/code/my-api express install into that app (express | fastify | nestjs)
+# npm run try -- ~/code/my-api --remove uninstall again, leaving the app as it was
+#
+# Nothing is linked: the packages are copied into the app's node_modules with file: entries in
+# its package.json, so --remove is a plain npm uninstall.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+APP="${1:-}"
+WHAT="${2:-}"
+PACKAGES="@usebruno/api-docs-core @usebruno/api-docs-express @usebruno/api-docs-fastify @usebruno/api-docs-nestjs"
+
+usage() { sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'; exit 2; }
+[ -n "$APP" ] && [ -n "$WHAT" ] || usage
+[ -f "$APP/package.json" ] || { echo "no package.json in $APP" >&2; exit 2; }
+
+if [ "$WHAT" = "--remove" ]; then
+ installed=""
+ for p in $PACKAGES; do
+ grep -q "\"$p\"" "$APP/package.json" && installed="$installed $p"
+ done
+ [ -n "$installed" ] || { echo "nothing of ours is installed in $APP"; exit 0; }
+ # shellcheck disable=SC2086
+ ( cd "$APP" && npm uninstall $installed )
+ echo "removed:$installed"
+ exit 0
+fi
+
+case "$WHAT" in express | fastify | nestjs) ;; *) usage ;; esac
+
+# the app's package.json will point at these by path, so they stay in the checkout, not in a temp dir
+( cd "$ROOT" && npm run build --silent )
+TARBALLS="$ROOT/dist-tarballs"
+mkdir -p "$TARBALLS"
+for p in core "$WHAT"; do
+ ( cd "$ROOT/nodejs/$p" && npm pack --silent --pack-destination "$TARBALLS" >/dev/null )
+done
+
+# one install for both, so the wrapper's exact pin on the core resolves from the tarball beside it
+( cd "$APP" && npm install --no-audit --no-fund "$TARBALLS"/*api-docs-core*.tgz "$TARBALLS"/*api-docs-"$WHAT"*.tgz )
+echo
+echo "installed @usebruno/api-docs-core and @usebruno/api-docs-$WHAT into $APP"
+echo
+echo "add the mount. The collection path is relative to your entry file, not the cwd;"
+echo "for a built Nest app that is dist/main.js, so count from dist/."
+echo
+case "$WHAT" in
+ express)
+ echo " const { apiDocs } = require('@usebruno/api-docs-express');"
+ echo " app.use('/docs', apiDocs({ collection: './api-collection' }));"
+ ;;
+ fastify)
+ echo " const { apiDocs } = require('@usebruno/api-docs-fastify');"
+ echo " app.register(apiDocs, { prefix: '/docs', collection: './api-collection' });"
+ ;;
+ nestjs)
+ echo " import { ApiDocsModule } from '@usebruno/api-docs-nestjs';"
+ echo " @Module({ imports: [ApiDocsModule.forRoot({ collection: '../api-collection' })] })"
+ ;;
+esac
+echo
+echo "then open http://localhost:/docs/. If the app sets a CSP, script-src needs"
+echo "https://cdn.usebruno.com and 'wasm-unsafe-eval', and connect-src needs data:."
+echo "to undo: npm run try -- $APP --remove"
diff --git a/integrations/shell/assemble.mjs b/integrations/shell/assemble.mjs
new file mode 100644
index 00000000..dbb79f84
--- /dev/null
+++ b/integrations/shell/assemble.mjs
@@ -0,0 +1,113 @@
+// One flat map of path -> raw text becomes one OpenCollection document. Pure, so it can be
+// tested without a browser, and so it can move into the renderer bundle later without moving
+// anything else with it.
+
+import { load as yamlLoad } from 'js-yaml';
+
+const MANIFEST_FILE = /^opencollection\.ya?ml$/;
+const FOLDER_FILE = /^folder\.ya?ml$/;
+
+export function toOpenCollection(text) {
+ const json = parseJsonOrNull(text);
+ if (json === null) {
+ return yamlLoad(text);
+ }
+ if (json['opencollection-fragments'] && json.files) {
+ return assembleFragments(json.files);
+ }
+
+ return json;
+}
+
+function parseJsonOrNull(text) {
+ try {
+ const value = JSON.parse(text);
+ return value && typeof value === 'object' ? value : null;
+ }
+ catch {
+ return null;
+ }
+}
+
+export function assembleFragments(files) {
+ const manifestPath = Object.keys(files).find((p) => MANIFEST_FILE.test(p));
+ if (!manifestPath) {
+ throw new Error('fragments payload has no opencollection.yml manifest');
+ }
+
+ const parse = (filePath) => {
+ try {
+ return yamlLoad(files[filePath]);
+ }
+ catch {
+ throw new Error('invalid YAML in ' + filePath);
+ }
+ };
+ const parentOf = (dir) => (dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : '');
+
+ const doc = parse(manifestPath) || {};
+ const environments = [];
+ const dirs = new Map([['', newDir()]]);
+
+ // set before recursing, so ensuring a parent cannot recreate the child it was reached from
+ const ensureDir = (dir) => {
+ if (!dirs.has(dir)) {
+ dirs.set(dir, newDir());
+ ensureDir(parentOf(dir)).children.push(dir);
+ }
+
+ return dirs.get(dir);
+ };
+
+ for (const filePath of Object.keys(files)) {
+ if (filePath === manifestPath) continue;
+ if (filePath.startsWith('environments/')) {
+ environments.push(parse(filePath));
+ continue;
+ }
+
+ const slash = filePath.lastIndexOf('/');
+ const node = ensureDir(slash === -1 ? '' : filePath.slice(0, slash));
+ if (FOLDER_FILE.test(filePath.slice(slash + 1))) {
+ node.meta = parse(filePath);
+ }
+ else {
+ node.requests.push(parse(filePath));
+ }
+ }
+
+ doc.items = materialize('', dirs);
+ if (environments.length) {
+ doc.config = doc.config || {};
+ doc.config.environments = environments;
+ }
+ doc.bundled = true;
+
+ return doc;
+}
+
+const newDir = () => ({ meta: null, requests: [], children: [] });
+
+const seqOf = (item) => {
+ const seq = item && item.info && item.info.seq;
+ return typeof seq === 'number' ? seq : Number.MAX_SAFE_INTEGER;
+};
+const nameOf = (item) => (item && item.info && item.info.name) || '';
+const bySeqThenName = (a, b) => seqOf(a) - seqOf(b) || nameOf(a).localeCompare(nameOf(b));
+
+function materialize(dir, dirs) {
+ const node = dirs.get(dir);
+ const folders = node.children.map((childDir) => {
+ const child = dirs.get(childDir);
+ const name = childDir.slice(childDir.lastIndexOf('/') + 1);
+ const folder = child.meta || { info: { name } };
+ // a directory is a folder whatever its folder.yml says or leaves out; the renderer decides
+ // by this field alone, and without it a folder renders as an empty request
+ folder.info = { ...folder.info, type: 'folder' };
+ folder.items = materialize(childDir, dirs);
+
+ return folder;
+ });
+
+ return [...folders.sort(bySeqThenName), ...node.requests.sort(bySeqThenName)];
+}
diff --git a/integrations/shell/shell.html b/integrations/shell/shell.html
new file mode 100644
index 00000000..06a64392
--- /dev/null
+++ b/integrations/shell/shell.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ {{TITLE}}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/integrations/shell/shell.src.mjs b/integrations/shell/shell.src.mjs
new file mode 100644
index 00000000..3307365a
--- /dev/null
+++ b/integrations/shell/shell.src.mjs
@@ -0,0 +1,83 @@
+// The browser half, identical for every language core: fetch collection.yml, assemble, boot.
+
+import { toOpenCollection } from './assemble.mjs';
+
+(async function main() {
+ const mount = document.getElementById('bruno-docs');
+ const errorBox = document.getElementById('bruno-docs-error');
+ const base = mount.dataset.base;
+ const cdn = mount.dataset.cdn;
+ const config = JSON.parse(mount.dataset.config || '{}');
+ const version = (window.Bruno && window.Bruno.version) || 'version unknown';
+
+ // via CSSOM: an inline