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