From 364e7839e02b93fa298c01ede65eef98142ae43b Mon Sep 17 00:00:00 2001 From: pasibun Date: Thu, 12 Mar 2026 14:38:53 +0100 Subject: [PATCH 1/6] feat: add HarvestService for API harvesting and OAS conversion - Introduced HarvestService to manage API harvesting from a specified endpoint. - Implemented methods for fetching index URLs, posting API data, and obtaining access tokens. - Added utility functions for URL building, error handling, and data normalization. refactor: update OasConversionService to utilize new conversion libraries - Replaced custom schema conversion logic with @apiture/openapi-down-convert and @scalar/openapi-upgrader. - Simplified conversion logic for OpenAPI specifications between versions 3.0 and 3.1. - Enhanced error handling for conversion processes. test: add unit tests for OasConversionService - Created tests for converting OpenAPI specifications between versions 3.0 and 3.1. - Verified preservation of key OpenAPI features during conversion. - Ensured correct handling of YAML and JSON formats in conversion responses. chore: update package dependencies - Updated @redocly/cli to version 2.20.5. - Added @apiture/openapi-down-convert and @scalar/openapi-upgrader as dependencies. - Removed unused dependencies from package.json. --- .github/dependabot.yml | 12 + .github/workflows/json-ci.yml | 25 + expressServer.js | 2 - index.js | 21 + jobs/HarvestJob.js | 162 +++++ package-lock.json | 1103 +++++++++-------------------- package.json | 12 +- services/HarvestService.js | 423 +++++++++++ services/OasConversionService.js | 187 +---- test/OasConversionService.test.js | 134 ++++ 10 files changed, 1134 insertions(+), 947 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/json-ci.yml create mode 100644 jobs/HarvestJob.js create mode 100644 services/HarvestService.js create mode 100644 test/OasConversionService.test.js diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..16d702c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + open-pull-requests-limit: 10 + schedule: + interval: weekly + - package-ecosystem: github-actions + directory: /.github/workflows + open-pull-requests-limit: 10 + schedule: + interval: weekly diff --git a/.github/workflows/json-ci.yml b/.github/workflows/json-ci.yml new file mode 100644 index 0000000..2b0aa43 --- /dev/null +++ b/.github/workflows/json-ci.yml @@ -0,0 +1,25 @@ +name: JSON CI + +on: pull_request + +jobs: + lint-and-validate: + name: JSON Lint & OpenAPI Validate + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v6.0.2 + with: + fetch-depth: 0 + - name: Run Super-Linter + uses: super-linter/super-linter@v8.5.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VALIDATE_ALL_CODEBASE: true + VALIDATE_JSON: true + VALIDATE_OPENAPI: true + ENABLE_GITHUB_PULL_REQUEST_SUMMARY_COMMENT: true diff --git a/expressServer.js b/expressServer.js index 92aea43..9500c7b 100644 --- a/expressServer.js +++ b/expressServer.js @@ -3,7 +3,6 @@ const fs = require("node:fs"); const path = require("node:path"); const express = require("express"); const cors = require("cors"); -const cookieParser = require("cookie-parser"); const bodyParser = require("body-parser"); const OpenApiValidator = require("express-openapi-validator"); const logger = require("./logger"); @@ -134,7 +133,6 @@ class ExpressServer { this.app.use(bodyParser.json({ limit: "14MB" })); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: false })); - this.app.use(cookieParser()); this.app.use((_req, res, next) => { res.set("API-Version", this.schema.info.version); next(); diff --git a/index.js b/index.js index b8a28ff..99bef17 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,35 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const loadLocalEnvFile = () => { + if (typeof process.loadEnvFile !== "function") { + return; + } + const envPath = path.join(__dirname, ".env"); + if (!fs.existsSync(envPath)) { + return; + } + process.loadEnvFile(envPath); +}; + +loadLocalEnvFile(); + const config = require("./config"); const logger = require("./logger"); const ExpressServer = require("./expressServer"); +const { schedulePdokHarvestFromEnv } = require("./jobs/HarvestJob"); const launchServer = async () => { try { this.expressServer = new ExpressServer(config.URL_PORT, config.OPENAPI_JSON); this.expressServer.launch(); + this.harvestScheduler = schedulePdokHarvestFromEnv(); logger.info("Express server running"); } catch (error) { logger.error("Express Server failure", error.message); + if (this.harvestScheduler && typeof this.harvestScheduler.stop === "function") { + this.harvestScheduler.stop(); + } await this.close(); } }; diff --git a/jobs/HarvestJob.js b/jobs/HarvestJob.js new file mode 100644 index 0000000..568f69c --- /dev/null +++ b/jobs/HarvestJob.js @@ -0,0 +1,162 @@ +const logger = require("../logger"); +const { HarvestService } = require("../services/HarvestService"); + +const DEFAULT_DAILY_HOUR = 15; +const DEFAULT_DAILY_MINUTE = 30; +const DEFAULT_RUN_TIMEOUT_MS = 5 * 60 * 1000; + +const trimString = (value) => (typeof value === "string" ? value.trim() : ""); + +const parsePositiveInteger = (value, fallback) => { + const parsed = Number(value); + if (Number.isFinite(parsed) && parsed > 0) { + return Math.floor(parsed); + } + return fallback; +}; + +const getNextRunAt = (hour, minute, now = new Date()) => { + const next = new Date(now); + next.setHours(hour, minute, 0, 0); + if (next.getTime() <= now.getTime()) { + next.setDate(next.getDate() + 1); + } + return next; +}; + +const sourceLabel = (source) => trimString(source?.name) || trimString(source?.indexUrl) || "unknown-source"; + +const createAbortControllerWithTimeout = (timeoutMs) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + }, timeoutMs); + const cleanup = () => clearTimeout(timeoutId); + return { controller, cleanup }; +}; + +const scheduleHarvest = (service, sources, options = {}) => { + const sourceList = Array.isArray(sources) + ? sources.filter((source) => source && typeof source === "object" && !Array.isArray(source)) + : []; + if (sourceList.length === 0) { + logger.warn("[HarvestJob] Geen geldige harvest sources geconfigureerd, scheduler wordt niet gestart."); + return null; + } + + const runTimeoutMs = parsePositiveInteger(options.runTimeoutMs, DEFAULT_RUN_TIMEOUT_MS); + const hour = Number.isInteger(options.hour) ? options.hour : DEFAULT_DAILY_HOUR; + const minute = Number.isInteger(options.minute) ? options.minute : DEFAULT_DAILY_MINUTE; + + let timer = null; + let stopped = false; + let running = false; + let activeController = null; + + const runSources = async (reason) => { + if (stopped) { + return; + } + if (running) { + logger.warn(`[HarvestJob] Run '${reason}' overgeslagen: vorige run draait nog.`); + return; + } + running = true; + const { controller, cleanup } = createAbortControllerWithTimeout(runTimeoutMs); + activeController = controller; + try { + for (const source of sourceList) { + const label = sourceLabel(source); + try { + const summary = await service.runOnce(source, { signal: controller.signal }); + logger.info( + `[HarvestJob] Bron '${label}' verwerkt: scanned=${summary.scanned}, posted=${summary.posted}, badRequest=${summary.badRequest}, failed=${summary.failed}`, + ); + } catch (error) { + logger.error(`[HarvestJob] Bron '${label}' mislukt: ${error?.message || "onbekende fout"}`); + } + } + } finally { + cleanup(); + activeController = null; + running = false; + } + }; + + const scheduleNext = () => { + if (stopped) { + return; + } + const nextRunAt = getNextRunAt(hour, minute); + const delay = Math.max(0, nextRunAt.getTime() - Date.now()); + timer = setTimeout(async () => { + await runSources("daily"); + scheduleNext(); + }, delay); + logger.info(`[HarvestJob] Volgende harvest run gepland op ${nextRunAt.toISOString()}.`); + }; + + scheduleNext(); + void runSources("startup"); + + return { + stop: () => { + if (stopped) { + return; + } + stopped = true; + if (timer) { + clearTimeout(timer); + } + if (activeController) { + activeController.abort(); + } + logger.info("[HarvestJob] Scheduler gestopt."); + }, + runNow: () => runSources("manual"), + }; +}; + +const buildPdokSource = () => ({ + name: "pdok", + indexUrl: "https://api.pdok.nl/index.json", + organisationUri: "https://www.pdok.nl", + contact: { + name: "PDOK Support", + url: "https://www.pdok.nl/support1", + email: "support@pdok.nl", + }, +}); + +const schedulePdokHarvestFromEnv = () => { + const service = HarvestService.fromEnv(); + if (!service.isConfigured()) { + logger.info("[HarvestJob] PDOK harvest scheduler niet gestart: PDOK_REGISTER_ENDPOINT ontbreekt."); + return null; + } + if (!service.hasAuthConfig()) { + logger.warn( + "[HarvestJob] PDOK harvest scheduler niet gestart: auth mist (AUTH_TOKEN_URL of KEYCLOAK_BASE_URL+KEYCLOAK_REALM, AUTH_CLIENT_ID, AUTH_CLIENT_SECRET).", + ); + return null; + } + + const scheduleTime = { hour: DEFAULT_DAILY_HOUR, minute: DEFAULT_DAILY_MINUTE }; + const runTimeoutMs = DEFAULT_RUN_TIMEOUT_MS; + logger.info( + `[HarvestJob] PDOK harvest scheduler gestart (dagelijks ${String(scheduleTime.hour).padStart(2, "0")}:${String( + scheduleTime.minute, + ).padStart(2, "0")}, directe startup-run).`, + ); + return scheduleHarvest(service, [buildPdokSource()], { + hour: scheduleTime.hour, + minute: scheduleTime.minute, + runTimeoutMs, + }); +}; + +module.exports = { + scheduleHarvest, + schedulePdokHarvestFromEnv, + buildPdokSource, +}; diff --git a/package-lock.json b/package-lock.json index 0d2c24d..a7da454 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,25 +9,24 @@ "version": "1.0.0", "license": "Unlicense", "dependencies": { - "@redocly/cli": "^2.11.0", + "@apiture/openapi-down-convert": "^0.14.2", + "@redocly/cli": "^2.20.5", + "@scalar/openapi-upgrader": "^0.1.8", "@stoplight/spectral-parsers": "^1.0.5", "@stoplight/spectral-rulesets": "^1.22.0", "@stoplight/spectral-runtime": "^1.1.4", "body-parser": "^2.2.0", "case-anything": "^3.1.2", - "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^5.1.0", - "express-openapi-validator": "^5.6.0", + "express-openapi-validator": "^5.6.2", "js-yaml": "^4.1.0", "jszip": "^3.10.1", "openapi-to-postmanv2": "^5.3.4", "winston": "^3.18.3" }, "devDependencies": { - "@biomejs/biome": "^2.3.4", - "chai": "^6.2.0", - "chai-as-promised": "^8.0.2" + "@biomejs/biome": "^2.3.4" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -48,6 +47,29 @@ "@types/json-schema": "^7.0.15" } }, + "node_modules/@apiture/openapi-down-convert": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@apiture/openapi-down-convert/-/openapi-down-convert-0.14.2.tgz", + "integrity": "sha512-vT3eqOEmJ4KZ2qojycEV0zzFBPDrkwntyfnWi8/WMaag8jvGN4osIdVRuwxWcK6AvQr32ohrpcLGqEZ+ZHVNcw==", + "license": "ISC", + "dependencies": { + "commander": "^9.4.1", + "js-yaml": "^4.1.0", + "typescript": "^4.8.4" + }, + "bin": { + "openapi-down-convert": "lib/src/cli.js" + } + }, + "node_modules/@apiture/openapi-down-convert/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/@asyncapi/specs": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.10.0.tgz", @@ -58,12 +80,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -273,24 +295,24 @@ } }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", "dependencies": { - "@emotion/memoize": "^0.8.1" + "@emotion/memoize": "^0.9.0" } }, "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", "license": "MIT" }, "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", "license": "MIT" }, "node_modules/@exodus/schemasafe": { @@ -315,123 +337,6 @@ "node": ">=10.10.0" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", @@ -748,15 +653,15 @@ "license": "BSD-3-Clause" }, "node_modules/@redocly/ajv": { - "version": "8.11.4", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.4.tgz", - "integrity": "sha512-77MhyFgZ1zGMwtCpqsk532SJEc3IJmSOXKTCeWoMTAvPnQOkuOgxEip1n5pG5YX1IzCTJ4kCvPKr8xYyzWFdhg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js-replace": "^1.0.1" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -764,36 +669,38 @@ } }, "node_modules/@redocly/cli": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.11.0.tgz", - "integrity": "sha512-Wr8me9M5tQ4pZT7Z0Llxojlo8L0GBBt45zceQ8iKyBmJUHWDbKYYdKubZBCH0XktQLEA8HitYBGN1unsxwx20g==", + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.21.1.tgz", + "integrity": "sha512-tqMvNzXB2dD9ThfNo3O69ZwNPfs1O41q04o1Fgc4iSNn1jpKUcE371u79qF4q/axRpbvQblZs7I6i7XwRN/zmg==", "license": "MIT", "dependencies": { "@opentelemetry/exporter-trace-otlp-http": "0.202.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-node": "2.0.1", "@opentelemetry/semantic-conventions": "1.34.0", - "@redocly/openapi-core": "2.11.0", - "@redocly/respect-core": "2.11.0", + "@redocly/openapi-core": "2.21.1", + "@redocly/respect-core": "2.21.1", "abort-controller": "^3.0.0", - "chokidar": "^3.5.1", + "ajv": "npm:@redocly/ajv@8.18.0", + "ajv-formats": "^3.0.1", "colorette": "^1.2.0", "cookie": "^0.7.2", "dotenv": "16.4.7", - "form-data": "^4.0.4", - "glob": "^11.0.1", + "glob": "^13.0.5", "handlebars": "^4.7.6", "https-proxy-agent": "^7.0.5", "mobx": "^6.0.4", + "picomatch": "^4.0.3", "pluralize": "^8.0.0", - "react": "^17.0.0 || ^18.2.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.2.0 || ^19.0.0", + "react": "^17.0.0 || ^18.2.0 || ^19.2.1", + "react-dom": "^17.0.0 || ^18.2.0 || ^19.2.1", "redoc": "2.5.1", "semver": "^7.5.2", "set-cookie-parser": "^2.3.5", "simple-websocket": "^9.0.0", - "styled-components": "^6.0.7", - "undici": "^6.21.3", + "styled-components": "6.3.9", + "ulid": "^3.0.1", + "undici": "^6.23.0", "yargs": "17.0.1" }, "bin": { @@ -805,6 +712,23 @@ "npm": ">=10" } }, + "node_modules/@redocly/cli/node_modules/ajv": { + "name": "@redocly/ajv", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==", + "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/@redocly/cli/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -856,23 +780,24 @@ } }, "node_modules/@redocly/config": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.37.0.tgz", - "integrity": "sha512-cYN+rTTCQIp5mVt1xumJsNqpZcaPVUf1x0ryD0QKXpVKsxKc+lHaMF2P1CqMgdQNY9B7i84z/kvxD0EhxzlxbQ==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.44.1.tgz", + "integrity": "sha512-l6/ZE+/RBfNDdhzltau6cbW8+k5PgJbJBMqaBrlQlZQlmGBHMxqGyDaon4dPLj0jdi37gsMQ3yf95JBY/vaDSg==", "license": "MIT", "dependencies": { "json-schema-to-ts": "2.7.2" } }, "node_modules/@redocly/openapi-core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-2.11.0.tgz", - "integrity": "sha512-CF4QpCoxxHIB7Dib1XnhdL0WuW4dO4zvNfaEWpN7TASlitOX2mhrc6sD3dYG9knW1iG16e3Oauv2O+tVJx1E9Q==", + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-2.21.1.tgz", + "integrity": "sha512-xqO0avM42DOnninr3NqCPGgD61L1EunmDy+hQNZhuCM2/a6X0g19ZYNioQxeGw3/OlKNVeplSO26lEq52R12VQ==", "license": "MIT", "dependencies": { - "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.37.0", - "ajv-formats": "^2.1.1", + "@redocly/ajv": "^8.18.0", + "@redocly/config": "^0.44.1", + "ajv": "npm:@redocly/ajv@8.18.0", + "ajv-formats": "^3.0.1", "colorette": "^1.2.0", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", @@ -885,51 +810,41 @@ "npm": ">=10" } }, - "node_modules/@redocly/openapi-core/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "node_modules/@redocly/openapi-core/node_modules/ajv": { + "name": "@redocly/ajv", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==", "license": "MIT", "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@redocly/openapi-core/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "engines": { - "node": ">=12" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/@redocly/respect-core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-2.11.0.tgz", - "integrity": "sha512-lAvDILvq82IIei2gVyapGyfuWEamJgCiGO++yQriVk4Wr0hE3lF7ZWusUM3aGZrxEWCVGeeLwbMBpv1BQOnmEg==", + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-2.21.1.tgz", + "integrity": "sha512-Y5betL4vL8UEElOkDoSul5X30QQj8lw7NKrE0ncvA9viN8BQqnqxhC3PBTVvpKAkCXsEDdw98C09xQ7weygT+g==", "license": "MIT", "dependencies": { "@faker-js/faker": "^7.6.0", "@noble/hashes": "^1.8.0", - "@redocly/ajv": "8.11.2", - "@redocly/openapi-core": "2.11.0", + "@redocly/ajv": "^8.18.0", + "@redocly/openapi-core": "2.21.1", + "ajv": "npm:@redocly/ajv@8.18.0", "better-ajv-errors": "^1.2.0", "colorette": "^2.0.20", "json-pointer": "^0.6.2", "jsonpath-rfc9535": "1.3.0", - "openapi-sampler": "^1.6.1", - "outdent": "^0.8.0" + "openapi-sampler": "^1.7.1", + "outdent": "^0.8.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=22.12.0 || >=20.19.0 <21.0.0", @@ -946,16 +861,17 @@ "npm": ">=6.0.0" } }, - "node_modules/@redocly/respect-core/node_modules/@redocly/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "node_modules/@redocly/respect-core/node_modules/ajv": { + "name": "@redocly/ajv", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js-replace": "^1.0.1" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -968,6 +884,30 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, + "node_modules/@scalar/openapi-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.5.3.tgz", + "integrity": "sha512-m4n/Su3K01d15dmdWO1LlqecdSPKuNjuokrJLdiQ485kW/hRHbXW1QP6tJL75myhw/XhX5YhYAR+jrwnGjXiMw==", + "license": "MIT", + "dependencies": { + "zod": "^4.1.11" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@scalar/openapi-upgrader": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.1.8.tgz", + "integrity": "sha512-2xuYLLs0fBadLIk4I1ObjMiCnOyLPEMPf24A1HtHQvhKGDnGlvT63F2rU2Xw8lxCjgHnzveMPnOJEbwIy64RCg==", + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.5.3" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@so-ric/colorspace": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", @@ -1521,20 +1461,20 @@ } }, "node_modules/@types/express": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", - "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^1" + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", - "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -1556,16 +1496,10 @@ "license": "MIT", "peer": true }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "license": "MIT" - }, "node_modules/@types/multer": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", - "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", "license": "MIT", "dependencies": { "@types/express": "*" @@ -1581,9 +1515,9 @@ } }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", "license": "MIT" }, "node_modules/@types/range-parser": { @@ -1602,30 +1536,19 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "license": "MIT", - "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/stylis": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", - "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", "license": "MIT" }, "node_modules/@types/triple-beam": { @@ -1769,19 +1692,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -1855,12 +1765,6 @@ "node": ">= 0.4" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1901,18 +1805,6 @@ "ajv": "4.11.8 - 8" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -1943,18 +1835,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2055,30 +1935,6 @@ "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/chai": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", - "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/chai-as-promised": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.2.tgz", - "integrity": "sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "check-error": "^2.1.1" - }, - "peerDependencies": { - "chai": ">= 2.1.2 < 7" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2104,40 +1960,6 @@ "node": ">=4.0.0" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -2246,18 +2068,6 @@ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2336,25 +2146,6 @@ "node": ">= 0.6" } }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, "node_modules/core-js": { "version": "3.46.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz", @@ -2386,20 +2177,6 @@ "node": ">= 0.10" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -2421,9 +2198,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/data-view-buffer": { @@ -2533,15 +2310,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2583,12 +2351,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2856,13 +2618,13 @@ } }, "node_modules/express-openapi-validator": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-5.6.0.tgz", - "integrity": "sha512-gNaMgDb1cAT8QKcuh9WrED9p3mqi/V7yocNrvnE1fOz7e8p8JkbYaTUcOB4VsZKerz/X+Sey7ptTGF5FwsXh8Q==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-5.6.2.tgz", + "integrity": "sha512-fkDn4+ImUC4HTJ1g0cek/ItqYhmEO19AglJd2Iw2OJco0jLIbxIlDGVazmXbvvYeziU4Bnah2h+S2tb6NtWg8w==", "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^14.0.3", - "@types/multer": "^1.4.13", + "@apidevtools/json-schema-ref-parser": "^14.2.1", + "@types/multer": "^2.0.0", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", @@ -2873,8 +2635,8 @@ "media-typer": "^1.1.0", "multer": "^2.0.2", "ono": "^7.1.3", - "path-to-regexp": "^8.2.0", - "qs": "^6.14.0" + "path-to-regexp": "^8.3.0", + "qs": "^6.14.1" }, "peerDependencies": { "express": "*" @@ -2939,10 +2701,25 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz", + "integrity": "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.3.tgz", + "integrity": "sha512-Ymnuefk6VzAhT3SxLzVUw+nMio/wB1NGypHkgetwtXcK1JfryaHk4DWQFGVwQ9XgzyS5iRZ7C2ZGI4AMsdMZ6A==", "funding": [ { "type": "github", @@ -2951,7 +2728,9 @@ ], "license": "MIT", "dependencies": { - "strnum": "^1.1.1" + "fast-xml-builder": "^1.1.2", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -2972,18 +2751,6 @@ "node": ">=0.10.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -3028,59 +2795,6 @@ "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", "license": "MIT" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/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==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3099,20 +2813,6 @@ "node": ">= 0.8" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "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", @@ -3224,50 +2924,53 @@ } }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "license": "ISC", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", + "node_modules/glob/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==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">= 6" + "node": "18 || 20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3576,18 +3279,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -3649,15 +3340,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-finalizationregistry": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", @@ -3701,18 +3383,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -3737,15 +3407,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -3922,27 +3583,6 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -4215,6 +3855,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -4333,10 +3982,10 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -4583,15 +4232,6 @@ "es6-promise": "^3.2.1" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/oas-kit-common": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", @@ -4781,13 +4421,13 @@ } }, "node_modules/openapi-sampler": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.6.2.tgz", - "integrity": "sha512-NyKGiFKfSWAZr4srD/5WDhInOWDhfml32h/FKUqLpEwKJt0kG0LGUU0MdyNkKrVGuJnw6DuPWq/sHCwAMpiRxg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.7.2.tgz", + "integrity": "sha512-OKytvqB5XIaTgA9xtw8W8UTar+uymW2xPVpFN0NihMtuHPdPTGxBEhGnfFnJW5g/gOSIvkP+H0Xh3XhVI9/n7g==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.7", - "fast-xml-parser": "^4.5.0", + "fast-xml-parser": "^5.5.1", "json-pointer": "0.6.2" } }, @@ -4862,12 +4502,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -4889,40 +4523,37 @@ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "license": "MIT" }, - "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==", + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -4946,12 +4577,12 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -5137,9 +4768,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -5221,9 +4852,9 @@ } }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "peer": true, "engines": { @@ -5231,16 +4862,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.4" } }, "node_modules/react-is": { @@ -5276,18 +4907,6 @@ "node": ">= 6" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/redoc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.5.1.tgz", @@ -5673,27 +5292,6 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/should": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", @@ -5820,18 +5418,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/simple-eval": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", @@ -5965,21 +5551,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -6048,23 +5619,10 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", "funding": [ { "type": "github", @@ -6074,21 +5632,21 @@ "license": "MIT" }, "node_modules/styled-components": { - "version": "6.1.19", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", - "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.9.tgz", + "integrity": "sha512-J72R4ltw0UBVUlEjTzI0gg2STOqlI9JBhQOL4Dxt7aJOnnSesy0qJDn4PYfMCafk9cWOaVg129Pesl5o+DIh0Q==", "license": "MIT", "peer": true, "dependencies": { - "@emotion/is-prop-valid": "1.2.2", - "@emotion/unitless": "0.8.1", - "@types/stylis": "4.2.5", + "@emotion/is-prop-valid": "1.4.0", + "@emotion/unitless": "0.10.0", + "@types/stylis": "4.2.7", "css-to-react-native": "3.2.0", - "csstype": "3.1.3", + "csstype": "3.2.3", "postcss": "8.4.49", "shallowequal": "1.1.0", - "stylis": "4.3.2", - "tslib": "2.6.2" + "stylis": "4.3.6", + "tslib": "2.8.1" }, "engines": { "node": ">= 16" @@ -6100,18 +5658,23 @@ "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/styled-components/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "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/stylis": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", - "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT" }, "node_modules/supports-color": { @@ -6159,18 +5722,6 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -6301,6 +5852,19 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -6314,6 +5878,15 @@ "node": ">=0.8.0" } }, + "node_modules/ulid": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-3.0.2.tgz", + "integrity": "sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==", + "license": "MIT", + "bin": { + "ulid": "dist/cli.js" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -6333,9 +5906,9 @@ } }, "node_modules/undici": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", - "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "license": "MIT", "engines": { "node": ">=18.17" @@ -6365,12 +5938,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uri-js-replace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", - "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", - "license": "MIT" - }, "node_modules/urijs": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", @@ -6474,21 +6041,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/which-boxed-primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", @@ -6633,24 +6185,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6737,6 +6271,15 @@ "engines": { "node": ">=12" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 95dabf2..2f544f4 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "prestart": "npm install", "start": "node index.js", "start-mock": "USE_MOCKS=true node index.js", + "test": "node --test", "lint": "biome lint .", "format": "biome format --write ." }, @@ -17,24 +18,23 @@ "license": "Unlicense", "private": true, "dependencies": { - "@redocly/cli": "^2.11.0", + "@apiture/openapi-down-convert": "^0.14.2", + "@redocly/cli": "^2.20.5", + "@scalar/openapi-upgrader": "^0.1.8", "@stoplight/spectral-parsers": "^1.0.5", "@stoplight/spectral-rulesets": "^1.22.0", "@stoplight/spectral-runtime": "^1.1.4", "body-parser": "^2.2.0", "case-anything": "^3.1.2", - "cookie-parser": "^1.4.7", "cors": "^2.8.5", "express": "^5.1.0", - "express-openapi-validator": "^5.6.0", + "express-openapi-validator": "^5.6.2", "js-yaml": "^4.1.0", "jszip": "^3.10.1", "openapi-to-postmanv2": "^5.3.4", "winston": "^3.18.3" }, "devDependencies": { - "@biomejs/biome": "^2.3.4", - "chai": "^6.2.0", - "chai-as-promised": "^8.0.2" + "@biomejs/biome": "^2.3.4" } } diff --git a/services/HarvestService.js b/services/HarvestService.js new file mode 100644 index 0000000..c0027b6 --- /dev/null +++ b/services/HarvestService.js @@ -0,0 +1,423 @@ +const logger = require("../logger"); + +const PDOK_OAS_PATH = "openapi.json"; +const DEFAULT_TIMEOUT_MS = 30_000; +const DEFAULT_RATE_LIMIT_DELAY_MS = 500; +const MAX_ERROR_BODY_LENGTH = 8192; + +const trimString = (value) => (typeof value === "string" ? value.trim() : ""); + +const truncate = (value, limit = MAX_ERROR_BODY_LENGTH) => { + if (typeof value !== "string") { + return ""; + } + if (value.length <= limit) { + return value; + } + return `${value.slice(0, limit)}…`; +}; + +const resolveFetch = (fetchImpl) => { + if (typeof fetchImpl === "function") { + return fetchImpl; + } + if (typeof fetch === "function") { + return fetch; + } + throw new Error("Fetch API is niet beschikbaar in de huidige runtime."); +}; + +const buildUrlFromEnv = (baseUrl, realm, prefix, suffix = "") => { + const baseTrimmed = trimString(baseUrl).replace(/\/+$/, ""); + const realmTrimmed = trimString(realm); + if (!baseTrimmed || !realmTrimmed) { + return ""; + } + return `${baseTrimmed}${prefix}${encodeURIComponent(realmTrimmed)}${suffix}`; +}; + +const buildRequestSignal = (externalSignal, timeoutMs) => { + if (externalSignal) { + if ( + typeof AbortSignal !== "undefined" && + typeof AbortSignal.any === "function" && + typeof AbortSignal.timeout === "function" + ) { + return AbortSignal.any([externalSignal, AbortSignal.timeout(timeoutMs)]); + } + return externalSignal; + } + if ( + typeof AbortSignal !== "undefined" && + typeof AbortSignal.timeout === "function" + ) { + return AbortSignal.timeout(timeoutMs); + } + return undefined; +}; + +const isAbortError = (error) => + error?.name === "AbortError" || error?.name === "TimeoutError"; + +const delay = (ms, signal) => + new Promise((resolve, reject) => { + if (ms <= 0) { + resolve(); + return; + } + if (signal?.aborted) { + reject(new Error("Request geannuleerd")); + return; + } + + let settled = false; + let timeoutId; + let abortHandler; + + const cleanup = () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (signal && abortHandler) { + signal.removeEventListener("abort", abortHandler); + } + }; + + const resolveOnce = () => { + if (settled) { + return; + } + settled = true; + cleanup(); + resolve(); + }; + + const rejectOnce = (error) => { + if (settled) { + return; + } + settled = true; + cleanup(); + reject(error); + }; + + abortHandler = () => rejectOnce(new Error("Request geannuleerd")); + timeoutId = setTimeout(resolveOnce, ms); + if (signal) { + signal.addEventListener("abort", abortHandler, { once: true }); + } + }); + +const normalizeContact = (contact) => { + if (!contact || typeof contact !== "object" || Array.isArray(contact)) { + return undefined; + } + const normalized = {}; + const name = trimString(contact.name); + const url = trimString(contact.url); + const email = trimString(contact.email); + if (name) { + normalized.name = name; + } + if (url) { + normalized.url = url; + } + if (email) { + normalized.email = email; + } + if (Object.keys(normalized).length === 0) { + return undefined; + } + return normalized; +}; + +const deriveOASURLWith = (href) => { + const trimmedHref = trimString(href).replace(/\/+$/, ""); + if (!trimmedHref) { + throw new Error("lege href voor OAS-afleiding"); + } + return `${trimmedHref}/${PDOK_OAS_PATH}`; +}; + +const extractIndexHrefs = (data) => { + let parsed; + try { + parsed = JSON.parse(typeof data === "string" ? data : String(data || "")); + } catch (error) { + throw new Error(`parse index.json: ${error.message}`); + } + + const apis = Array.isArray(parsed?.apis) ? parsed.apis : []; + const out = []; + for (const apiEntry of apis) { + const links = apiEntry?.links; + if (Array.isArray(links)) { + for (const link of links) { + const href = trimString(link?.href); + if (href) { + out.push(href); + } + } + continue; + } + if (links && typeof links === "object") { + const href = trimString(links.href); + if (href) { + out.push(href); + } + } + } + return out; +}; + +class HarvestService { + constructor({ + registerEndpoint = "", + tokenURL = "", + clientID = "", + clientSecret = "", + fetchImpl, + } = {}) { + this.registerEndpoint = trimString(registerEndpoint); + this.tokenURL = trimString(tokenURL); + this.clientID = trimString(clientID); + this.clientSecret = trimString(clientSecret); + this.timeoutMs = DEFAULT_TIMEOUT_MS; + this.rateLimitDelayMs = DEFAULT_RATE_LIMIT_DELAY_MS; + this.fetch = resolveFetch(fetchImpl); + } + + static fromEnv() { + const tokenFromEnv = trimString(process.env.AUTH_TOKEN_URL); + const tokenURL = + tokenFromEnv || + buildUrlFromEnv( + process.env.KEYCLOAK_BASE_URL, + process.env.KEYCLOAK_REALM, + "/realms/", + "/protocol/openid-connect/token", + ); + return new HarvestService({ + registerEndpoint: process.env.PDOK_REGISTER_ENDPOINT, + tokenURL, + clientID: process.env.AUTH_CLIENT_ID, + clientSecret: process.env.AUTH_CLIENT_SECRET, + }); + } + + isConfigured() { + return Boolean(this.registerEndpoint); + } + + hasAuthConfig() { + return ( + Boolean(this.tokenURL) && + Boolean(this.clientID) && + Boolean(this.clientSecret) + ); + } + + async runOnce(source, { signal } = {}) { + if (!this.isConfigured()) { + throw new Error( + "register endpoint is not configured (PDOK_REGISTER_ENDPOINT)", + ); + } + if (!source || typeof source !== "object" || Array.isArray(source)) { + throw new Error("harvest source is ongeldig"); + } + const indexUrl = trimString(source.indexUrl); + if (!indexUrl) { + throw new Error("source indexUrl is empty"); + } + + const hrefs = await this.fetchIndexHrefs(indexUrl, signal); + const sourceName = trimString(source.name) || indexUrl; + if (hrefs.length === 0) { + logger.info(`[HarvestService] geen API-links gevonden in ${indexUrl}`); + return { + source: sourceName, + scanned: 0, + posted: 0, + badRequest: 0, + failed: 0, + }; + } + + const token = await this.getAccessToken(signal); + const organisationUri = trimString(source.organisationUri); + const contact = normalizeContact(source.contact); + + let posted = 0; + let badRequest = 0; + const failures = []; + + for (let i = 0; i < hrefs.length; i += 1) { + if (i > 0) { + await delay(this.rateLimitDelayMs, signal); + } + const href = hrefs[i]; + const oasUrl = deriveOASURLWith(href); + const payload = { + oasUrl, + }; + if (organisationUri) { + payload.organisationUri = organisationUri; + } + if (contact) { + payload.contact = contact; + } + + try { + await this.postAPI(payload, token, signal); + posted += 1; + } catch (error) { + const status = typeof error?.status === "number" ? error.status : 0; + if (status === 400) { + badRequest += 1; + logger.warn( + `[HarvestService] bad request op ${oasUrl}: ${error.message}`, + ); + continue; + } + failures.push(`${oasUrl}: ${error.message}`); + } + } + + const summary = { + source: sourceName, + scanned: hrefs.length, + posted, + badRequest, + failed: failures.length, + }; + if (failures.length > 0) { + const error = new Error( + `${failures.length} failures; first: ${failures[0]}`, + ); + error.summary = summary; + error.failures = failures; + throw error; + } + return summary; + } + + async fetchIndexHrefs(indexUrl, signal) { + const requestSignal = buildRequestSignal(signal, this.timeoutMs); + let response; + try { + response = await this.fetch(indexUrl, { + method: "GET", + signal: requestSignal, + }); + } catch (error) { + if (isAbortError(error)) { + throw new Error(`timeout tijdens ophalen van index: ${indexUrl}`); + } + throw new Error(`netwerkfout bij ophalen van index: ${error.message}`); + } + + const body = await response.text(); + if (response.status < 200 || response.status >= 300) { + throw new Error( + `unexpected status ${response.status} from index: ${truncate(body, 4096)}`, + ); + } + return extractIndexHrefs(body); + } + + async postAPI(payload, bearer, signal) { + const requestSignal = buildRequestSignal(signal, this.timeoutMs); + let response; + try { + response = await this.fetch(this.registerEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(trimString(bearer) ? { Authorization: `Bearer ${bearer}` } : {}), + }, + body: JSON.stringify(payload), + signal: requestSignal, + }); + } catch (error) { + if (isAbortError(error)) { + const timeoutError = new Error( + "timeout tijdens post naar register endpoint", + ); + timeoutError.status = 0; + throw timeoutError; + } + const networkError = new Error( + `netwerkfout richting register endpoint: ${error.message}`, + ); + networkError.status = 0; + throw networkError; + } + + const body = truncate(await response.text()); + if (response.status < 200 || response.status >= 300) { + const requestError = new Error( + `unexpected status ${response.status} from register endpoint ${this.registerEndpoint}: ${body}`, + ); + requestError.status = response.status; + requestError.responseBody = body; + throw requestError; + } + return { status: response.status, body }; + } + + async getAccessToken(signal) { + if (!this.hasAuthConfig()) { + throw new Error( + "auth not configured (AUTH_TOKEN_URL, AUTH_CLIENT_ID, AUTH_CLIENT_SECRET)", + ); + } + const body = new URLSearchParams({ + grant_type: "client_credentials", + client_id: this.clientID, + client_secret: this.clientSecret, + }); + + const requestSignal = buildRequestSignal(signal, this.timeoutMs); + let response; + try { + response = await this.fetch(this.tokenURL, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body, + signal: requestSignal, + }); + } catch (error) { + if (isAbortError(error)) { + throw new Error("timeout tijdens ophalen van access token"); + } + throw new Error(`netwerkfout richting token endpoint: ${error.message}`); + } + + const text = truncate(await response.text()); + if (response.status < 200 || response.status >= 300) { + throw new Error(`token endpoint status ${response.status}: ${text}`); + } + + let parsed; + try { + parsed = JSON.parse(text || "{}"); + } catch (error) { + throw new Error(`token response is geen geldige JSON: ${error.message}`); + } + const accessToken = trimString(parsed.access_token); + if (!accessToken) { + throw new Error("empty access_token in response"); + } + return accessToken; + } +} + +module.exports = { + HarvestService, + deriveOASURLWith, + extractIndexHrefs, +}; diff --git a/services/OasConversionService.js b/services/OasConversionService.js index 4152608..660ca85 100644 --- a/services/OasConversionService.js +++ b/services/OasConversionService.js @@ -1,9 +1,10 @@ +const { Converter } = require("@apiture/openapi-down-convert"); +const { upgrade: scalarUpgrade } = require("@scalar/openapi-upgrader"); const jsYaml = require("js-yaml"); const Service = require("./Service"); const { resolveOasInput } = require("./OasInputService"); const logger = require("../logger"); -const JSON_SCHEMA_DIALECT_BASE = "https://spec.openapis.org/oas/3.1/dialect/base"; const DEFAULT_TARGET_VERSION = "3.1.0"; const EMPTY_BODY_ERROR = "Body ontbreekt of ongeldig: gebruik oasUrl of oasBody"; @@ -35,119 +36,6 @@ const parseSpecification = (contents) => { } }; -const mergeTypeWithNull = (target) => { - const current = target.type; - if (Array.isArray(current)) { - const hasNull = current.some((value) => value === "null"); - target.type = hasNull ? current : [...current, "null"]; - return; - } - if (typeof current === "string" && current.length > 0) { - target.type = [current, "null"]; - return; - } - target.type = ["null"]; -}; - -const convertSchemas30To31 = (node) => { - if (Array.isArray(node)) { - for (const item of node) { - convertSchemas30To31(item); - } - return; - } - if (!node || typeof node !== "object") { - return; - } - Object.entries(node).forEach(([key, value]) => { - convertSchemas30To31(value); - node[key] = value; - }); - if (node.nullable === true) { - mergeTypeWithNull(node); - delete node.nullable; - } -}; - -const normalizeTypeArray = (target) => { - const currentType = target.type; - if (!Array.isArray(currentType)) { - return; - } - const filtered = []; - let hasNull = false; - currentType.forEach((item) => { - if (item === null || item === undefined) { - return; - } - if (typeof item === "string" && item === "null") { - hasNull = true; - return; - } - filtered.push(item); - }); - if (hasNull) { - target.nullable = true; - } - if (filtered.length === 0) { - delete target.type; - } else if (filtered.length === 1) { - [target.type] = filtered; - } else { - target.type = filtered; - } -}; - -const normalizeEnumNull = (target) => { - const { enum: enumValues } = target; - if (!Array.isArray(enumValues)) { - return; - } - const filtered = []; - let hasNull = false; - enumValues.forEach((value) => { - if (value === null) { - hasNull = true; - return; - } - filtered.push(value); - }); - if (hasNull) { - target.nullable = true; - } - if (filtered.length === 0) { - delete target.enum; - return; - } - if (filtered.length !== enumValues.length) { - target.enum = filtered; - } -}; - -const convertSchemas31To30 = (node) => { - if (Array.isArray(node)) { - for (const item of node) { - convertSchemas31To30(item); - } - return; - } - if (!node || typeof node !== "object") { - return; - } - Object.entries(node).forEach(([key, value]) => { - convertSchemas31To30(value); - node[key] = value; - }); - if (Object.hasOwn(node, "const")) { - if (!Object.hasOwn(node, "enum")) { - node.enum = [node.const]; - } - delete node.const; - } - normalizeTypeArray(node); - normalizeEnumNull(node); -}; - const normalizeVersionInput = (value) => { if (typeof value === "number" && Number.isFinite(value)) { return value.toString(); @@ -191,7 +79,14 @@ const normalizeTargetVersion = (value) => { return descriptor.canonical; }; -const convertSpec = (spec, targetVersion) => { +const ensureObjectSpec = (value, errorMessage) => { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(errorMessage); + } + return value; +}; + +const convertSpec = async (spec, targetVersion) => { const sourceDescriptor = resolveVersionDescriptor(spec.openapi); const openapiValue = spec.openapi; const rawVersion = openapiValue === undefined || openapiValue === null ? "" : String(openapiValue).trim(); @@ -206,54 +101,28 @@ const convertSpec = (spec, targetVersion) => { if (sourceDescriptor.major === targetDescriptor.major) { spec.openapi = targetDescriptor.canonical; - if (targetDescriptor.major === "3.1") { - if (!Object.hasOwn(spec, "jsonSchemaDialect")) { - spec.jsonSchemaDialect = JSON_SCHEMA_DIALECT_BASE; - } - if (Object.hasOwn(spec, "x-webhooks")) { - if (!Object.hasOwn(spec, "webhooks")) { - spec.webhooks = spec["x-webhooks"]; - } - delete spec["x-webhooks"]; - } - } else { - delete spec.jsonSchemaDialect; - if (Object.hasOwn(spec, "webhooks")) { - if (!Object.hasOwn(spec, "x-webhooks")) { - spec["x-webhooks"] = spec.webhooks; - } - delete spec.webhooks; - } - } - return targetDescriptor.canonical; + return { spec, resolvedVersion: targetDescriptor.canonical }; } if (sourceDescriptor.major === "3.0" && targetDescriptor.major === "3.1") { - convertSchemas30To31(spec); - if (!Object.hasOwn(spec, "jsonSchemaDialect")) { - spec.jsonSchemaDialect = JSON_SCHEMA_DIALECT_BASE; - } - if (Object.hasOwn(spec, "x-webhooks")) { - if (!Object.hasOwn(spec, "webhooks")) { - spec.webhooks = spec["x-webhooks"]; - } - delete spec["x-webhooks"]; - } - spec.openapi = targetDescriptor.canonical; - return targetDescriptor.canonical; + const upgraded = ensureObjectSpec( + scalarUpgrade(spec, "3.1"), + "Scalar OpenAPI upgrader retourneerde een ongeldig document.", + ); + upgraded.openapi = targetDescriptor.canonical; + return { spec: upgraded, resolvedVersion: targetDescriptor.canonical }; } + if (sourceDescriptor.major === "3.1" && targetDescriptor.major === "3.0") { - convertSchemas31To30(spec); - delete spec.jsonSchemaDialect; - if (Object.hasOwn(spec, "webhooks")) { - if (!Object.hasOwn(spec, "x-webhooks")) { - spec["x-webhooks"] = spec.webhooks; - } - delete spec.webhooks; - } - spec.openapi = targetDescriptor.canonical; - return targetDescriptor.canonical; + const downConverter = new Converter(spec); + const downgraded = ensureObjectSpec( + downConverter.convert(), + "OpenAPI down converter retourneerde een ongeldig document.", + ); + downgraded.openapi = targetDescriptor.canonical; + return { spec: downgraded, resolvedVersion: targetDescriptor.canonical }; } + throw Service.rejectResponse({ message: UNSUPPORTED_VERSION_ERROR }, 400); }; @@ -306,8 +175,8 @@ const convert = async (input) => { } const { spec, format } = parsed; try { - const resolvedVersion = convertSpec(spec, targetVersion); - const { buffer, contentType, filename } = serializeSpecification(spec, format, resolvedVersion); + const { spec: convertedSpec, resolvedVersion } = await convertSpec(spec, targetVersion); + const { buffer, contentType, filename } = serializeSpecification(convertedSpec, format, resolvedVersion); return { headers: { "Content-Type": contentType, diff --git a/test/OasConversionService.test.js b/test/OasConversionService.test.js new file mode 100644 index 0000000..c904633 --- /dev/null +++ b/test/OasConversionService.test.js @@ -0,0 +1,134 @@ +const assert = require("node:assert/strict"); +const test = require("node:test"); +const jsYaml = require("js-yaml"); +const OasConversionService = require("../services/OasConversionService"); + +const toJson = (buffer) => JSON.parse(buffer.toString("utf8")); +const toYaml = (buffer) => jsYaml.load(buffer.toString("utf8")); + +test("convert 3.0 -> 3.1 (JSON) upgrades key OpenAPI features", async () => { + const sourceSpec = { + openapi: "3.0.3", + info: { + title: "Test API", + version: "1.0.0", + }, + paths: {}, + "x-webhooks": { + onEvent: { + post: { + responses: { + 200: { + description: "OK", + }, + }, + }, + }, + }, + components: { + schemas: { + Pet: { + type: "object", + properties: { + nickname: { + type: "string", + nullable: true, + }, + }, + }, + }, + }, + }; + + const result = await OasConversionService.convert({ + oasBody: JSON.stringify(sourceSpec), + targetVersion: "3.1", + }); + + const converted = toJson(result.rawBody); + + assert.equal(result.headers["Content-Type"], "application/json"); + assert.equal(result.headers["Content-Disposition"], 'attachment; filename="openapi-3-1-0.json"'); + assert.equal(converted.openapi, "3.1.0"); + assert.ok(Object.hasOwn(converted, "webhooks")); + assert.ok(!Object.hasOwn(converted, "x-webhooks")); + assert.deepEqual(converted.components.schemas.Pet.properties.nickname.type, ["string", "null"]); +}); + +test("convert 3.1 -> 3.0 (JSON) downgrades key OpenAPI features", async () => { + const sourceSpec = { + openapi: "3.1.0", + info: { + title: "Test API", + version: "1.0.0", + }, + paths: {}, + webhooks: { + onEvent: { + post: { + responses: { + 200: { + description: "OK", + }, + }, + }, + }, + }, + components: { + schemas: { + Pet: { + type: "object", + properties: { + nickname: { + type: ["string", "null"], + }, + }, + }, + }, + }, + }; + + const result = await OasConversionService.convert({ + oasBody: JSON.stringify(sourceSpec), + targetVersion: "3.0", + }); + + const converted = toJson(result.rawBody); + + assert.equal(result.headers["Content-Type"], "application/json"); + assert.equal(result.headers["Content-Disposition"], 'attachment; filename="openapi-3-0-3.json"'); + assert.equal(converted.openapi, "3.0.3"); + assert.ok(!Object.hasOwn(converted, "webhooks")); + assert.equal(converted.components.schemas.Pet.properties.nickname.type, "string"); + assert.equal(converted.components.schemas.Pet.properties.nickname.nullable, true); +}); + +test("convert preserves YAML format in response", async () => { + const sourceSpecYaml = ` +openapi: 3.0.3 +info: + title: Test API + version: 1.0.0 +paths: {} +components: + schemas: + Item: + type: object + properties: + maybeText: + type: string + nullable: true +`; + + const result = await OasConversionService.convert({ + oasBody: sourceSpecYaml, + targetVersion: "3.1", + }); + + const converted = toYaml(result.rawBody); + + assert.equal(result.headers["Content-Type"], "application/yaml"); + assert.equal(result.headers["Content-Disposition"], 'attachment; filename="openapi-3-1-0.yaml"'); + assert.equal(converted.openapi, "3.1.0"); + assert.deepEqual(converted.components.schemas.Item.properties.maybeText.type, ["string", "null"]); +}); From 2c69bec4d9a543291b8017a0ab3c45c1c3050d80 Mon Sep 17 00:00:00 2001 From: pasibun Date: Thu, 12 Mar 2026 16:32:25 +0100 Subject: [PATCH 2/6] feat: enhance convertSpec to preserve source version for OpenAPI 3.1 --- services/OasConversionService.js | 15 ++++++++++++--- test/OasConversionService.test.js | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/services/OasConversionService.js b/services/OasConversionService.js index 660ca85..5276c51 100644 --- a/services/OasConversionService.js +++ b/services/OasConversionService.js @@ -86,7 +86,7 @@ const ensureObjectSpec = (value, errorMessage) => { return value; }; -const convertSpec = async (spec, targetVersion) => { +const convertSpec = async (spec, targetVersion, options = {}) => { const sourceDescriptor = resolveVersionDescriptor(spec.openapi); const openapiValue = spec.openapi; const rawVersion = openapiValue === undefined || openapiValue === null ? "" : String(openapiValue).trim(); @@ -100,6 +100,10 @@ const convertSpec = async (spec, targetVersion) => { } if (sourceDescriptor.major === targetDescriptor.major) { + if (options.preserveSourceVersion && sourceDescriptor.major === "3.1") { + spec.openapi = rawVersion; + return { spec, resolvedVersion: rawVersion }; + } spec.openapi = targetDescriptor.canonical; return { spec, resolvedVersion: targetDescriptor.canonical }; } @@ -152,7 +156,10 @@ const extractTargetVersion = (input) => { }; const convert = async (input) => { - const targetVersion = normalizeTargetVersion(extractTargetVersion(input)); + const requestedTargetVersion = extractTargetVersion(input); + const targetVersion = normalizeTargetVersion(requestedTargetVersion); + const hasExplicitTargetVersion = + typeof requestedTargetVersion === "string" && requestedTargetVersion.trim().length > 0; const { contents } = await resolveOasInput(input); let parsed; try { @@ -175,7 +182,9 @@ const convert = async (input) => { } const { spec, format } = parsed; try { - const { spec: convertedSpec, resolvedVersion } = await convertSpec(spec, targetVersion); + const { spec: convertedSpec, resolvedVersion } = await convertSpec(spec, targetVersion, { + preserveSourceVersion: !hasExplicitTargetVersion, + }); const { buffer, contentType, filename } = serializeSpecification(convertedSpec, format, resolvedVersion); return { headers: { diff --git a/test/OasConversionService.test.js b/test/OasConversionService.test.js index c904633..fa8f1b5 100644 --- a/test/OasConversionService.test.js +++ b/test/OasConversionService.test.js @@ -132,3 +132,24 @@ components: assert.equal(converted.openapi, "3.1.0"); assert.deepEqual(converted.components.schemas.Item.properties.maybeText.type, ["string", "null"]); }); + +test("convert without targetVersion keeps existing 3.1 patch version", async () => { + const sourceSpec = { + openapi: "3.1.2", + info: { + title: "Test API", + version: "1.0.0", + }, + paths: {}, + }; + + const result = await OasConversionService.convert({ + oasBody: JSON.stringify(sourceSpec), + }); + + const converted = toJson(result.rawBody); + + assert.equal(result.headers["Content-Type"], "application/json"); + assert.equal(result.headers["Content-Disposition"], 'attachment; filename="openapi-3-1-2.json"'); + assert.equal(converted.openapi, "3.1.2"); +}); From 583a6e9564847c5ad983c3b2698407f555f89e12 Mon Sep 17 00:00:00 2001 From: Matthijs Hovestad Date: Thu, 12 Mar 2026 20:10:29 +0100 Subject: [PATCH 3/6] Update index.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- index.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 99bef17..40477f9 100644 --- a/index.js +++ b/index.js @@ -19,18 +19,21 @@ const logger = require("./logger"); const ExpressServer = require("./expressServer"); const { schedulePdokHarvestFromEnv } = require("./jobs/HarvestJob"); +let expressServer; +let harvestScheduler; + const launchServer = async () => { try { - this.expressServer = new ExpressServer(config.URL_PORT, config.OPENAPI_JSON); - this.expressServer.launch(); - this.harvestScheduler = schedulePdokHarvestFromEnv(); + expressServer = new ExpressServer(config.URL_PORT, config.OPENAPI_JSON); + expressServer.launch(); + harvestScheduler = schedulePdokHarvestFromEnv(); logger.info("Express server running"); } catch (error) { logger.error("Express Server failure", error.message); - if (this.harvestScheduler && typeof this.harvestScheduler.stop === "function") { - this.harvestScheduler.stop(); + if (harvestScheduler && typeof harvestScheduler.stop === "function") { + harvestScheduler.stop(); } - await this.close(); + await expressServer?.close?.(); } }; From 43294a1e285fc29bd89a11ae5010d1246b0d2231 Mon Sep 17 00:00:00 2001 From: pasibun Date: Wed, 18 Mar 2026 07:14:27 +0100 Subject: [PATCH 4/6] refactor: simplify HarvestJob and HarvestService logic by removing unused functions and improving timeout handling --- jobs/HarvestJob.js | 26 +++------- services/HarvestService.js | 101 ++++--------------------------------- 2 files changed, 17 insertions(+), 110 deletions(-) diff --git a/jobs/HarvestJob.js b/jobs/HarvestJob.js index 568f69c..fb58563 100644 --- a/jobs/HarvestJob.js +++ b/jobs/HarvestJob.js @@ -7,14 +7,6 @@ const DEFAULT_RUN_TIMEOUT_MS = 5 * 60 * 1000; const trimString = (value) => (typeof value === "string" ? value.trim() : ""); -const parsePositiveInteger = (value, fallback) => { - const parsed = Number(value); - if (Number.isFinite(parsed) && parsed > 0) { - return Math.floor(parsed); - } - return fallback; -}; - const getNextRunAt = (hour, minute, now = new Date()) => { const next = new Date(now); next.setHours(hour, minute, 0, 0); @@ -44,7 +36,7 @@ const scheduleHarvest = (service, sources, options = {}) => { return null; } - const runTimeoutMs = parsePositiveInteger(options.runTimeoutMs, DEFAULT_RUN_TIMEOUT_MS); + const runTimeoutMs = options.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS; const hour = Number.isInteger(options.hour) ? options.hour : DEFAULT_DAILY_HOUR; const minute = Number.isInteger(options.minute) ? options.minute : DEFAULT_DAILY_MINUTE; @@ -123,7 +115,7 @@ const buildPdokSource = () => ({ organisationUri: "https://www.pdok.nl", contact: { name: "PDOK Support", - url: "https://www.pdok.nl/support1", + url: "https://www.pdok.nl/support", email: "support@pdok.nl", }, }); @@ -136,22 +128,18 @@ const schedulePdokHarvestFromEnv = () => { } if (!service.hasAuthConfig()) { logger.warn( - "[HarvestJob] PDOK harvest scheduler niet gestart: auth mist (AUTH_TOKEN_URL of KEYCLOAK_BASE_URL+KEYCLOAK_REALM, AUTH_CLIENT_ID, AUTH_CLIENT_SECRET).", + "[HarvestJob] PDOK harvest scheduler niet gestart: auth mist (AUTH_TOKEN_URL, AUTH_CLIENT_ID, AUTH_CLIENT_SECRET).", ); return null; } - const scheduleTime = { hour: DEFAULT_DAILY_HOUR, minute: DEFAULT_DAILY_MINUTE }; - const runTimeoutMs = DEFAULT_RUN_TIMEOUT_MS; logger.info( - `[HarvestJob] PDOK harvest scheduler gestart (dagelijks ${String(scheduleTime.hour).padStart(2, "0")}:${String( - scheduleTime.minute, - ).padStart(2, "0")}, directe startup-run).`, + `[HarvestJob] PDOK harvest scheduler gestart (dagelijks ${String(DEFAULT_DAILY_HOUR).padStart(2, "0")}:${String(DEFAULT_DAILY_MINUTE).padStart(2, "0")}, directe startup-run).`, ); return scheduleHarvest(service, [buildPdokSource()], { - hour: scheduleTime.hour, - minute: scheduleTime.minute, - runTimeoutMs, + hour: DEFAULT_DAILY_HOUR, + minute: DEFAULT_DAILY_MINUTE, + runTimeoutMs: DEFAULT_RUN_TIMEOUT_MS, }); }; diff --git a/services/HarvestService.js b/services/HarvestService.js index c0027b6..4136aec 100644 --- a/services/HarvestService.js +++ b/services/HarvestService.js @@ -18,95 +18,23 @@ const truncate = (value, limit = MAX_ERROR_BODY_LENGTH) => { }; const resolveFetch = (fetchImpl) => { - if (typeof fetchImpl === "function") { - return fetchImpl; - } - if (typeof fetch === "function") { - return fetch; - } + if (typeof fetchImpl === "function") return fetchImpl; + if (typeof fetch === "function") return fetch; throw new Error("Fetch API is niet beschikbaar in de huidige runtime."); }; -const buildUrlFromEnv = (baseUrl, realm, prefix, suffix = "") => { - const baseTrimmed = trimString(baseUrl).replace(/\/+$/, ""); - const realmTrimmed = trimString(realm); - if (!baseTrimmed || !realmTrimmed) { - return ""; - } - return `${baseTrimmed}${prefix}${encodeURIComponent(realmTrimmed)}${suffix}`; -}; - const buildRequestSignal = (externalSignal, timeoutMs) => { - if (externalSignal) { - if ( - typeof AbortSignal !== "undefined" && - typeof AbortSignal.any === "function" && - typeof AbortSignal.timeout === "function" - ) { - return AbortSignal.any([externalSignal, AbortSignal.timeout(timeoutMs)]); - } - return externalSignal; - } - if ( - typeof AbortSignal !== "undefined" && - typeof AbortSignal.timeout === "function" - ) { - return AbortSignal.timeout(timeoutMs); - } - return undefined; + const timeout = AbortSignal.timeout(timeoutMs); + if (!externalSignal) return timeout; + return typeof AbortSignal.any === "function" + ? AbortSignal.any([externalSignal, timeout]) + : timeout; }; const isAbortError = (error) => error?.name === "AbortError" || error?.name === "TimeoutError"; -const delay = (ms, signal) => - new Promise((resolve, reject) => { - if (ms <= 0) { - resolve(); - return; - } - if (signal?.aborted) { - reject(new Error("Request geannuleerd")); - return; - } - - let settled = false; - let timeoutId; - let abortHandler; - - const cleanup = () => { - if (timeoutId) { - clearTimeout(timeoutId); - } - if (signal && abortHandler) { - signal.removeEventListener("abort", abortHandler); - } - }; - - const resolveOnce = () => { - if (settled) { - return; - } - settled = true; - cleanup(); - resolve(); - }; - - const rejectOnce = (error) => { - if (settled) { - return; - } - settled = true; - cleanup(); - reject(error); - }; - - abortHandler = () => rejectOnce(new Error("Request geannuleerd")); - timeoutId = setTimeout(resolveOnce, ms); - if (signal) { - signal.addEventListener("abort", abortHandler, { once: true }); - } - }); +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const normalizeContact = (contact) => { if (!contact || typeof contact !== "object" || Array.isArray(contact)) { @@ -188,18 +116,9 @@ class HarvestService { } static fromEnv() { - const tokenFromEnv = trimString(process.env.AUTH_TOKEN_URL); - const tokenURL = - tokenFromEnv || - buildUrlFromEnv( - process.env.KEYCLOAK_BASE_URL, - process.env.KEYCLOAK_REALM, - "/realms/", - "/protocol/openid-connect/token", - ); return new HarvestService({ registerEndpoint: process.env.PDOK_REGISTER_ENDPOINT, - tokenURL, + tokenURL: process.env.AUTH_TOKEN_URL, clientID: process.env.AUTH_CLIENT_ID, clientSecret: process.env.AUTH_CLIENT_SECRET, }); @@ -254,7 +173,7 @@ class HarvestService { for (let i = 0; i < hrefs.length; i += 1) { if (i > 0) { - await delay(this.rateLimitDelayMs, signal); + await delay(this.rateLimitDelayMs); } const href = hrefs[i]; const oasUrl = deriveOASURLWith(href); From 84e665281a991388caf44fdf8a833a379a88b8ed Mon Sep 17 00:00:00 2001 From: pasibun Date: Wed, 18 Mar 2026 07:17:39 +0100 Subject: [PATCH 5/6] refactor: streamline version handling and error logging in OasConversionService --- services/OasConversionService.js | 83 +++++++++++--------------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/services/OasConversionService.js b/services/OasConversionService.js index 5276c51..d851085 100644 --- a/services/OasConversionService.js +++ b/services/OasConversionService.js @@ -36,18 +36,13 @@ const parseSpecification = (contents) => { } }; -const normalizeVersionInput = (value) => { - if (typeof value === "number" && Number.isFinite(value)) { - return value.toString(); - } - if (typeof value === "string") { - return value.trim(); - } - return ""; -}; - const resolveVersionDescriptor = (value) => { - const raw = normalizeVersionInput(value); + const raw = + typeof value === "number" && Number.isFinite(value) + ? value.toString() + : typeof value === "string" + ? value.trim() + : ""; if (!raw) { return null; } @@ -88,8 +83,7 @@ const ensureObjectSpec = (value, errorMessage) => { const convertSpec = async (spec, targetVersion, options = {}) => { const sourceDescriptor = resolveVersionDescriptor(spec.openapi); - const openapiValue = spec.openapi; - const rawVersion = openapiValue === undefined || openapiValue === null ? "" : String(openapiValue).trim(); + const rawVersion = spec.openapi == null ? "" : String(spec.openapi).trim(); if (rawVersion.length === 0 || !sourceDescriptor) { throw Service.rejectResponse({ message: VERSION_MISSING_ERROR }, 400); } @@ -148,15 +142,8 @@ const serializeSpecification = (spec, format, targetVersion) => { }; }; -const extractTargetVersion = (input) => { - if (input && typeof input === "object" && !Array.isArray(input) && typeof input.targetVersion === "string") { - return input.targetVersion; - } - return undefined; -}; - const convert = async (input) => { - const requestedTargetVersion = extractTargetVersion(input); + const requestedTargetVersion = typeof input?.targetVersion === "string" ? input.targetVersion : undefined; const targetVersion = normalizeTargetVersion(requestedTargetVersion); const hasExplicitTargetVersion = typeof requestedTargetVersion === "string" && requestedTargetVersion.trim().length > 0; @@ -165,50 +152,34 @@ const convert = async (input) => { try { parsed = parseSpecification(contents); } catch (error) { - logger.error( - `[OasConversionService] parseSpecification failed: ${error?.message || "unknown"}${ - error?.stack ? ` stack=${error.stack}` : "" - }`, - ); - if (Service.isErrorResponse(error)) { - throw error; - } - throw Service.rejectResponse( - { - message: error.message, - }, - 500, - ); + if (Service.isErrorResponse(error)) throw error; + logger.error(`[OasConversionService] parseSpecification failed: ${error?.message}`); + throw Service.rejectResponse({ message: error.message }, 500); } + const { spec, format } = parsed; + let convertedSpec, resolvedVersion; try { - const { spec: convertedSpec, resolvedVersion } = await convertSpec(spec, targetVersion, { + ({ spec: convertedSpec, resolvedVersion } = await convertSpec(spec, targetVersion, { preserveSourceVersion: !hasExplicitTargetVersion, - }); - const { buffer, contentType, filename } = serializeSpecification(convertedSpec, format, resolvedVersion); - return { - headers: { - "Content-Type": contentType, - "Content-Disposition": `attachment; filename="${filename}"`, - }, - rawBody: buffer, - }; + })); } catch (error) { - logger.error( - `[OasConversionService] convertSpec failed: ${error?.message || "unknown"}${ - error?.stack ? ` stack=${error.stack}` : "" - }`, - ); - if (Service.isErrorResponse(error)) { - throw error; - } + if (Service.isErrorResponse(error)) throw error; + logger.error(`[OasConversionService] convertSpec failed: ${error?.message}`); throw Service.rejectResponse( - { - message: error.message || "Er is een fout opgetreden tijdens het converteren.", - }, + { message: error.message || "Er is een fout opgetreden tijdens het converteren." }, 500, ); } + + const { buffer, contentType, filename } = serializeSpecification(convertedSpec, format, resolvedVersion); + return { + headers: { + "Content-Type": contentType, + "Content-Disposition": `attachment; filename="${filename}"`, + }, + rawBody: buffer, + }; }; module.exports = { From ad1036d9b7704ba3e66ee78a64728f3cb46cc60c Mon Sep 17 00:00:00 2001 From: pasibun Date: Thu, 19 Mar 2026 10:21:55 +0100 Subject: [PATCH 6/6] refactor: remove HarvestJob and HarvestService to simplify server launch process --- index.js | 6 - jobs/HarvestJob.js | 150 ---------------- services/HarvestService.js | 342 ------------------------------------- 3 files changed, 498 deletions(-) delete mode 100644 jobs/HarvestJob.js delete mode 100644 services/HarvestService.js diff --git a/index.js b/index.js index 40477f9..4f1f314 100644 --- a/index.js +++ b/index.js @@ -17,22 +17,16 @@ loadLocalEnvFile(); const config = require("./config"); const logger = require("./logger"); const ExpressServer = require("./expressServer"); -const { schedulePdokHarvestFromEnv } = require("./jobs/HarvestJob"); let expressServer; -let harvestScheduler; const launchServer = async () => { try { expressServer = new ExpressServer(config.URL_PORT, config.OPENAPI_JSON); expressServer.launch(); - harvestScheduler = schedulePdokHarvestFromEnv(); logger.info("Express server running"); } catch (error) { logger.error("Express Server failure", error.message); - if (harvestScheduler && typeof harvestScheduler.stop === "function") { - harvestScheduler.stop(); - } await expressServer?.close?.(); } }; diff --git a/jobs/HarvestJob.js b/jobs/HarvestJob.js deleted file mode 100644 index fb58563..0000000 --- a/jobs/HarvestJob.js +++ /dev/null @@ -1,150 +0,0 @@ -const logger = require("../logger"); -const { HarvestService } = require("../services/HarvestService"); - -const DEFAULT_DAILY_HOUR = 15; -const DEFAULT_DAILY_MINUTE = 30; -const DEFAULT_RUN_TIMEOUT_MS = 5 * 60 * 1000; - -const trimString = (value) => (typeof value === "string" ? value.trim() : ""); - -const getNextRunAt = (hour, minute, now = new Date()) => { - const next = new Date(now); - next.setHours(hour, minute, 0, 0); - if (next.getTime() <= now.getTime()) { - next.setDate(next.getDate() + 1); - } - return next; -}; - -const sourceLabel = (source) => trimString(source?.name) || trimString(source?.indexUrl) || "unknown-source"; - -const createAbortControllerWithTimeout = (timeoutMs) => { - const controller = new AbortController(); - const timeoutId = setTimeout(() => { - controller.abort(); - }, timeoutMs); - const cleanup = () => clearTimeout(timeoutId); - return { controller, cleanup }; -}; - -const scheduleHarvest = (service, sources, options = {}) => { - const sourceList = Array.isArray(sources) - ? sources.filter((source) => source && typeof source === "object" && !Array.isArray(source)) - : []; - if (sourceList.length === 0) { - logger.warn("[HarvestJob] Geen geldige harvest sources geconfigureerd, scheduler wordt niet gestart."); - return null; - } - - const runTimeoutMs = options.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS; - const hour = Number.isInteger(options.hour) ? options.hour : DEFAULT_DAILY_HOUR; - const minute = Number.isInteger(options.minute) ? options.minute : DEFAULT_DAILY_MINUTE; - - let timer = null; - let stopped = false; - let running = false; - let activeController = null; - - const runSources = async (reason) => { - if (stopped) { - return; - } - if (running) { - logger.warn(`[HarvestJob] Run '${reason}' overgeslagen: vorige run draait nog.`); - return; - } - running = true; - const { controller, cleanup } = createAbortControllerWithTimeout(runTimeoutMs); - activeController = controller; - try { - for (const source of sourceList) { - const label = sourceLabel(source); - try { - const summary = await service.runOnce(source, { signal: controller.signal }); - logger.info( - `[HarvestJob] Bron '${label}' verwerkt: scanned=${summary.scanned}, posted=${summary.posted}, badRequest=${summary.badRequest}, failed=${summary.failed}`, - ); - } catch (error) { - logger.error(`[HarvestJob] Bron '${label}' mislukt: ${error?.message || "onbekende fout"}`); - } - } - } finally { - cleanup(); - activeController = null; - running = false; - } - }; - - const scheduleNext = () => { - if (stopped) { - return; - } - const nextRunAt = getNextRunAt(hour, minute); - const delay = Math.max(0, nextRunAt.getTime() - Date.now()); - timer = setTimeout(async () => { - await runSources("daily"); - scheduleNext(); - }, delay); - logger.info(`[HarvestJob] Volgende harvest run gepland op ${nextRunAt.toISOString()}.`); - }; - - scheduleNext(); - void runSources("startup"); - - return { - stop: () => { - if (stopped) { - return; - } - stopped = true; - if (timer) { - clearTimeout(timer); - } - if (activeController) { - activeController.abort(); - } - logger.info("[HarvestJob] Scheduler gestopt."); - }, - runNow: () => runSources("manual"), - }; -}; - -const buildPdokSource = () => ({ - name: "pdok", - indexUrl: "https://api.pdok.nl/index.json", - organisationUri: "https://www.pdok.nl", - contact: { - name: "PDOK Support", - url: "https://www.pdok.nl/support", - email: "support@pdok.nl", - }, -}); - -const schedulePdokHarvestFromEnv = () => { - const service = HarvestService.fromEnv(); - if (!service.isConfigured()) { - logger.info("[HarvestJob] PDOK harvest scheduler niet gestart: PDOK_REGISTER_ENDPOINT ontbreekt."); - return null; - } - if (!service.hasAuthConfig()) { - logger.warn( - "[HarvestJob] PDOK harvest scheduler niet gestart: auth mist (AUTH_TOKEN_URL, AUTH_CLIENT_ID, AUTH_CLIENT_SECRET).", - ); - return null; - } - - logger.info( - `[HarvestJob] PDOK harvest scheduler gestart (dagelijks ${String(DEFAULT_DAILY_HOUR).padStart(2, "0")}:${String(DEFAULT_DAILY_MINUTE).padStart(2, "0")}, directe startup-run).`, - ); - return scheduleHarvest(service, [buildPdokSource()], { - hour: DEFAULT_DAILY_HOUR, - minute: DEFAULT_DAILY_MINUTE, - runTimeoutMs: DEFAULT_RUN_TIMEOUT_MS, - }); -}; - -module.exports = { - scheduleHarvest, - schedulePdokHarvestFromEnv, - buildPdokSource, -}; diff --git a/services/HarvestService.js b/services/HarvestService.js deleted file mode 100644 index 4136aec..0000000 --- a/services/HarvestService.js +++ /dev/null @@ -1,342 +0,0 @@ -const logger = require("../logger"); - -const PDOK_OAS_PATH = "openapi.json"; -const DEFAULT_TIMEOUT_MS = 30_000; -const DEFAULT_RATE_LIMIT_DELAY_MS = 500; -const MAX_ERROR_BODY_LENGTH = 8192; - -const trimString = (value) => (typeof value === "string" ? value.trim() : ""); - -const truncate = (value, limit = MAX_ERROR_BODY_LENGTH) => { - if (typeof value !== "string") { - return ""; - } - if (value.length <= limit) { - return value; - } - return `${value.slice(0, limit)}…`; -}; - -const resolveFetch = (fetchImpl) => { - if (typeof fetchImpl === "function") return fetchImpl; - if (typeof fetch === "function") return fetch; - throw new Error("Fetch API is niet beschikbaar in de huidige runtime."); -}; - -const buildRequestSignal = (externalSignal, timeoutMs) => { - const timeout = AbortSignal.timeout(timeoutMs); - if (!externalSignal) return timeout; - return typeof AbortSignal.any === "function" - ? AbortSignal.any([externalSignal, timeout]) - : timeout; -}; - -const isAbortError = (error) => - error?.name === "AbortError" || error?.name === "TimeoutError"; - -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -const normalizeContact = (contact) => { - if (!contact || typeof contact !== "object" || Array.isArray(contact)) { - return undefined; - } - const normalized = {}; - const name = trimString(contact.name); - const url = trimString(contact.url); - const email = trimString(contact.email); - if (name) { - normalized.name = name; - } - if (url) { - normalized.url = url; - } - if (email) { - normalized.email = email; - } - if (Object.keys(normalized).length === 0) { - return undefined; - } - return normalized; -}; - -const deriveOASURLWith = (href) => { - const trimmedHref = trimString(href).replace(/\/+$/, ""); - if (!trimmedHref) { - throw new Error("lege href voor OAS-afleiding"); - } - return `${trimmedHref}/${PDOK_OAS_PATH}`; -}; - -const extractIndexHrefs = (data) => { - let parsed; - try { - parsed = JSON.parse(typeof data === "string" ? data : String(data || "")); - } catch (error) { - throw new Error(`parse index.json: ${error.message}`); - } - - const apis = Array.isArray(parsed?.apis) ? parsed.apis : []; - const out = []; - for (const apiEntry of apis) { - const links = apiEntry?.links; - if (Array.isArray(links)) { - for (const link of links) { - const href = trimString(link?.href); - if (href) { - out.push(href); - } - } - continue; - } - if (links && typeof links === "object") { - const href = trimString(links.href); - if (href) { - out.push(href); - } - } - } - return out; -}; - -class HarvestService { - constructor({ - registerEndpoint = "", - tokenURL = "", - clientID = "", - clientSecret = "", - fetchImpl, - } = {}) { - this.registerEndpoint = trimString(registerEndpoint); - this.tokenURL = trimString(tokenURL); - this.clientID = trimString(clientID); - this.clientSecret = trimString(clientSecret); - this.timeoutMs = DEFAULT_TIMEOUT_MS; - this.rateLimitDelayMs = DEFAULT_RATE_LIMIT_DELAY_MS; - this.fetch = resolveFetch(fetchImpl); - } - - static fromEnv() { - return new HarvestService({ - registerEndpoint: process.env.PDOK_REGISTER_ENDPOINT, - tokenURL: process.env.AUTH_TOKEN_URL, - clientID: process.env.AUTH_CLIENT_ID, - clientSecret: process.env.AUTH_CLIENT_SECRET, - }); - } - - isConfigured() { - return Boolean(this.registerEndpoint); - } - - hasAuthConfig() { - return ( - Boolean(this.tokenURL) && - Boolean(this.clientID) && - Boolean(this.clientSecret) - ); - } - - async runOnce(source, { signal } = {}) { - if (!this.isConfigured()) { - throw new Error( - "register endpoint is not configured (PDOK_REGISTER_ENDPOINT)", - ); - } - if (!source || typeof source !== "object" || Array.isArray(source)) { - throw new Error("harvest source is ongeldig"); - } - const indexUrl = trimString(source.indexUrl); - if (!indexUrl) { - throw new Error("source indexUrl is empty"); - } - - const hrefs = await this.fetchIndexHrefs(indexUrl, signal); - const sourceName = trimString(source.name) || indexUrl; - if (hrefs.length === 0) { - logger.info(`[HarvestService] geen API-links gevonden in ${indexUrl}`); - return { - source: sourceName, - scanned: 0, - posted: 0, - badRequest: 0, - failed: 0, - }; - } - - const token = await this.getAccessToken(signal); - const organisationUri = trimString(source.organisationUri); - const contact = normalizeContact(source.contact); - - let posted = 0; - let badRequest = 0; - const failures = []; - - for (let i = 0; i < hrefs.length; i += 1) { - if (i > 0) { - await delay(this.rateLimitDelayMs); - } - const href = hrefs[i]; - const oasUrl = deriveOASURLWith(href); - const payload = { - oasUrl, - }; - if (organisationUri) { - payload.organisationUri = organisationUri; - } - if (contact) { - payload.contact = contact; - } - - try { - await this.postAPI(payload, token, signal); - posted += 1; - } catch (error) { - const status = typeof error?.status === "number" ? error.status : 0; - if (status === 400) { - badRequest += 1; - logger.warn( - `[HarvestService] bad request op ${oasUrl}: ${error.message}`, - ); - continue; - } - failures.push(`${oasUrl}: ${error.message}`); - } - } - - const summary = { - source: sourceName, - scanned: hrefs.length, - posted, - badRequest, - failed: failures.length, - }; - if (failures.length > 0) { - const error = new Error( - `${failures.length} failures; first: ${failures[0]}`, - ); - error.summary = summary; - error.failures = failures; - throw error; - } - return summary; - } - - async fetchIndexHrefs(indexUrl, signal) { - const requestSignal = buildRequestSignal(signal, this.timeoutMs); - let response; - try { - response = await this.fetch(indexUrl, { - method: "GET", - signal: requestSignal, - }); - } catch (error) { - if (isAbortError(error)) { - throw new Error(`timeout tijdens ophalen van index: ${indexUrl}`); - } - throw new Error(`netwerkfout bij ophalen van index: ${error.message}`); - } - - const body = await response.text(); - if (response.status < 200 || response.status >= 300) { - throw new Error( - `unexpected status ${response.status} from index: ${truncate(body, 4096)}`, - ); - } - return extractIndexHrefs(body); - } - - async postAPI(payload, bearer, signal) { - const requestSignal = buildRequestSignal(signal, this.timeoutMs); - let response; - try { - response = await this.fetch(this.registerEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(trimString(bearer) ? { Authorization: `Bearer ${bearer}` } : {}), - }, - body: JSON.stringify(payload), - signal: requestSignal, - }); - } catch (error) { - if (isAbortError(error)) { - const timeoutError = new Error( - "timeout tijdens post naar register endpoint", - ); - timeoutError.status = 0; - throw timeoutError; - } - const networkError = new Error( - `netwerkfout richting register endpoint: ${error.message}`, - ); - networkError.status = 0; - throw networkError; - } - - const body = truncate(await response.text()); - if (response.status < 200 || response.status >= 300) { - const requestError = new Error( - `unexpected status ${response.status} from register endpoint ${this.registerEndpoint}: ${body}`, - ); - requestError.status = response.status; - requestError.responseBody = body; - throw requestError; - } - return { status: response.status, body }; - } - - async getAccessToken(signal) { - if (!this.hasAuthConfig()) { - throw new Error( - "auth not configured (AUTH_TOKEN_URL, AUTH_CLIENT_ID, AUTH_CLIENT_SECRET)", - ); - } - const body = new URLSearchParams({ - grant_type: "client_credentials", - client_id: this.clientID, - client_secret: this.clientSecret, - }); - - const requestSignal = buildRequestSignal(signal, this.timeoutMs); - let response; - try { - response = await this.fetch(this.tokenURL, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body, - signal: requestSignal, - }); - } catch (error) { - if (isAbortError(error)) { - throw new Error("timeout tijdens ophalen van access token"); - } - throw new Error(`netwerkfout richting token endpoint: ${error.message}`); - } - - const text = truncate(await response.text()); - if (response.status < 200 || response.status >= 300) { - throw new Error(`token endpoint status ${response.status}: ${text}`); - } - - let parsed; - try { - parsed = JSON.parse(text || "{}"); - } catch (error) { - throw new Error(`token response is geen geldige JSON: ${error.message}`); - } - const accessToken = trimString(parsed.access_token); - if (!accessToken) { - throw new Error("empty access_token in response"); - } - return accessToken; - } -} - -module.exports = { - HarvestService, - deriveOASURLWith, - extractIndexHrefs, -};