diff --git a/.github/workflows/qa-client.yml b/.github/workflows/qa-client.yml deleted file mode 100644 index b1f719f9..00000000 --- a/.github/workflows/qa-client.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: qa-client - -on: [push] - -jobs: - lint: - name: lint - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - lfs: true - - name: setup node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: ".nvmrc" - cache: npm - cache-dependency-path: | - project/client/package-lock.json - schema/package-lock.json - - name: install dependencies (schema / npm) - working-directory: schema - run: npm ci - - name: build schema - working-directory: schema - run: make build - - name: install dependencies (client / npm) - working-directory: project/client - run: npm ci - - name: lint - working-directory: project/client - run: make lint - - test: - name: test - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - lfs: true - - name: setup node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: ".nvmrc" - cache: npm - cache-dependency-path: | - project/client/package-lock.json - schema/package-lock.json - - name: install dependencies (schema / npm) - working-directory: schema - run: npm ci - - name: build schema - working-directory: schema - run: make build - - name: install dependencies (client / npm) - working-directory: project/client - run: npm ci - - name: test - working-directory: project/client - run: make test diff --git a/.github/workflows/qa-server.yml b/.github/workflows/qa-server.yml index 71380b56..68c5f41c 100644 --- a/.github/workflows/qa-server.yml +++ b/.github/workflows/qa-server.yml @@ -29,9 +29,6 @@ jobs: - name: install dependencies (server / npm) working-directory: project/server run: npm ci - - name: install dependencies (client / npm) - working-directory: project/client - run: npm ci - name: install dependencies (server / sqlc) run: make install-dependency-sqlc - name: lint @@ -64,9 +61,6 @@ jobs: - name: install dependencies (server / npm) working-directory: project/server run: npm ci - - name: install dependencies (client / npm) - working-directory: project/client - run: npm ci - name: install dependencies (server / sqlc) run: make install-dependency-sqlc - name: test diff --git a/project/client/.dockerignore b/project/client/.dockerignore deleted file mode 100644 index c2658d7d..00000000 --- a/project/client/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/project/client/.prettierrc b/project/client/.prettierrc deleted file mode 100644 index 05733786..00000000 --- a/project/client/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "printWidth": 80, - "tabWidth": 2, - "useTabs": true -} diff --git a/project/client/.zed/settings.json b/project/client/.zed/settings.json deleted file mode 100644 index 50b1d7d7..00000000 --- a/project/client/.zed/settings.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "languages": { - "TypeScript": { - "formatter": [ - { - "code_action": "source.fixAll.eslint" - } - ] - }, - "CSS": { - "formatter": [ - { - "code_action": "source.fixAll.eslint" - } - ] - }, - "JavaScript": { - "formatter": [ - { - "code_action": "source.fixAll.eslint" - } - ] - } - } -} diff --git a/project/client/Makefile b/project/client/Makefile deleted file mode 100644 index a8426d0f..00000000 --- a/project/client/Makefile +++ /dev/null @@ -1 +0,0 @@ -include make/dev.mk diff --git a/project/client/build.config.ts b/project/client/build.config.ts deleted file mode 100644 index 043de9ff..00000000 --- a/project/client/build.config.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { createHash } from "node:crypto"; -import { readFile } from "node:fs/promises"; -import { dirname, join, relative } from "node:path"; -import { fileURLToPath } from "node:url"; - -import { Transformer } from "@napi-rs/image"; -import sizeOf from "image-size"; -import { optimize as optimizeSvg } from "svgo"; -import type { BuildOptions, Plugin } from "esbuild"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const synTokenRegex = /\$X_SYN_LOCATION_TOKEN/g; -const synToken: Plugin = { - name: "syn-token", - setup: (build) => { - build.onLoad({ filter: /.*.tsx?$/ }, async (args) => { - const contents = await readFile(args.path); - const hashed = createHash("sha256") - .update(relative(__dirname, args.path)) - .digest("base64"); - - const key = (occurrence: number) => `${hashed}:${occurrence}`; - - let occurrence = 0; - const substituted = contents.toString().replaceAll(synTokenRegex, (_) => { - occurrence += 1; - return `"${key(occurrence)}"`; - }); - - return { - contents: substituted, - loader: args.path.endsWith("ts") ? "ts" : "tsx", - }; - }); - }, -}; - -const convertPngName = "convert-png"; -const convertPngNamespace = `${convertPngName}-namespace`; -const convertPngSizedNamespace = `${convertPngName}-sized-namespace`; -const convertPngRegex = /^(sized:)?(.+)\.png$/; - -type Operations = { - resize?: ResizeOperation; - lossy?: boolean; -}; -type ResizeOperation = { - width: number; - height?: number; -}; -const resizeOperationRegex = /^w=(?\d+)(?:&h=(?\d+))?$/; - -const transform = (file: Buffer, operations: Operations): Transformer => { - let transformer = new Transformer(file); - - if (typeof operations.resize !== "undefined") { - transformer = transformer.resize( - operations.resize.width, - operations.resize.height - ); - } - - return transformer; -}; - -const sortedReplacer = (key: string, value: unknown) => - value instanceof Object && !(value instanceof Array) - ? Object.keys(value) - .sort() - .reduce( - (sorted, key) => { - sorted[key] = (value as Record)[key]; - return sorted; - }, - {} as Record - ) - : value; - -const convertPng: Plugin = { - name: convertPngName, - setup: (build) => { - const cache: Map = new Map(); - build.onResolve({ filter: convertPngRegex }, async (args) => { - const match = args.path.match(convertPngRegex)!; - - const sized = typeof match[1] !== "undefined"; - - const path = match[2]; - - let resizeOperation: ResizeOperation | undefined = undefined; - resize: { - const unmatched = args.with["resize"]; - if (typeof unmatched === "undefined") { - break resize; - } - const matched = unmatched.match(resizeOperationRegex); - - if (!matched) { - console.error("malformed resize instruction"); - break resize; - } - - const width = matched?.groups?.["width"]; - const height = matched?.groups?.["height"]; - resizeOperation = { - width: parseInt(width!), - height: typeof height !== "undefined" ? parseInt(height) : undefined, - }; - } - let lossyOperation: boolean | undefined; - lossy: { - const value = args.with["lossy"]; - if (typeof value === "undefined") { - break lossy; - } - - switch (value) { - case "true": - lossyOperation = true; - break lossy; - case "false": - lossyOperation = false; - break lossy; - default: - console.error("malformed lossy instruction"); - break lossy; - } - } - - const source = join(__dirname, "public", `${path}.png`); - const file = await readFile(source); - - const destination = join(__dirname, "public", `${path}.webp`); - - const operations: Operations = { - resize: resizeOperation, - lossy: lossyOperation, - }; - - const cacheKey = path + "|" + JSON.stringify(operations, sortedReplacer); - - return { - path: destination, - namespace: sized ? convertPngSizedNamespace : convertPngNamespace, - pluginData: { - source: `${path}.png`, - file, - operations, - cacheKey, - }, - }; - }); - - build.onLoad( - { filter: /.*/, namespace: convertPngNamespace }, - async (args) => { - const file = args.pluginData.file; - const cacheKey = args.pluginData.cacheKey; - const operations: Operations = args.pluginData.operations; - - let transformed: Buffer; - { - const hit = cache.get(cacheKey); - if (typeof hit !== "undefined") { - transformed = hit; - } else { - transformed = operations.lossy - ? await transform(file, operations).webp() - : await transform(file, operations).webpLossless(); - cache.set(cacheKey, transformed); - } - } - - return { - contents: transformed, - loader: "file", - pluginName: convertPngName, - }; - } - ); - - build.onLoad( - { filter: /.*$/, namespace: convertPngSizedNamespace }, - async (args) => { - const file = args.pluginData.file; - const cacheKey = args.pluginData.cacheKey; - const operations: Operations = args.pluginData.operations; - - const transformed = operations.lossy - ? await transform(file, operations).webp() - : await transform(file, operations).webpLossless(); - cache.set(cacheKey, transformed); - - const size = sizeOf(transformed); - - const withStmt = - Object.keys(args.with).length !== 0 - ? ` with ${JSON.stringify(args.with)}` - : ""; - const contents = `import Image from "${args.pluginData.source}"${withStmt}; - -export default { - src: Image, - width: ${size.width}, - height: ${size.height}, -} - `; - - return { - contents, - loader: "js", - pluginName: convertPngName, - }; - } - ); - }, -}; - -// https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 -const encodeSvg = (svgString: string) => - svgString - .replace( - "/g, "%3E") - .replace(/\s+/g, " "); - -const inlineSvgNamespace = "inline-svg-plugin-namespace"; -const inlineSvgRegex = /^inline:(.+\.svg)$/; -const inlineSvg: Plugin = { - name: "inline-svg", - setup: (build) => { - build.onResolve({ filter: inlineSvgRegex }, (args) => { - const realPath = args.path.match(inlineSvgRegex)![1]; - - return { - path: join(__dirname, "public", realPath), - namespace: inlineSvgNamespace, - }; - }); - - build.onLoad( - { filter: /.*/, namespace: inlineSvgNamespace }, - async (args) => { - const contents = await readFile(args.path, "utf8"); - - const optimized = optimizeSvg(contents, { - path: args.path, - multipass: true, - }); - - return { - contents: `data:image/svg+xml;utf8,${encodeSvg(optimized.data)}`, - loader: "text", - }; - } - ); - }, -}; - -export const buildOptions = { - entryPoints: { - entrypoint: "src/entrypoint.ts", - style: "src/style.css", - }, - nodePaths: ["public"], - bundle: true, - format: "esm", - target: "es6", - sourcemap: true, - treeShaking: true, - logOverride: { - "import-is-undefined": "error", - }, - splitting: true, - loader: { - ".json": "copy", - ".txt": "copy", - ".mov": "file", - ".mp4": "file", - ".png": "file", - ".svg": "file", - ".webm": "file", - ".webp": "file", - ".woff2": "file", - }, - absWorkingDir: __dirname, - plugins: [synToken, inlineSvg, convertPng], -} as const satisfies BuildOptions; diff --git a/project/client/eslint.config.mjs b/project/client/eslint.config.mjs deleted file mode 100644 index 778f0970..00000000 --- a/project/client/eslint.config.mjs +++ /dev/null @@ -1,47 +0,0 @@ -// @ts-check - -import eslint from "@eslint/js"; -import { configs as configLit } from "eslint-plugin-lit"; -import { configs as configTs } from "typescript-eslint"; -import { configs as configWc } from "eslint-plugin-wc"; -import configPrettier from "eslint-plugin-prettier/recommended"; -import { defineConfig, globalIgnores } from "eslint/config"; -import configComments from "@eslint-community/eslint-plugin-eslint-comments/configs"; - -export default defineConfig( - [ - { - files: ["src/**/*.ts"], - }, - globalIgnores([ - "src/schema.ts", - "build.config.ts", - "watch.ts", - "eslint.config.mjs", - ]), - ], - configPrettier, - eslint.configs.recommended, - configTs.strictTypeChecked, - { - languageOptions: { - parserOptions: { - projectService: true, - }, - }, - rules: { "@typescript-eslint/consistent-type-imports": "error" }, - }, - configLit["flat/all"], - configWc["flat/recommended"], - // `eslint-plugin-eslint-comments` has no type definitions - // @ts-expect-error - configComments.recommended, - { - rules: { - "@eslint-community/eslint-comments/require-description": [ - "error", - { ignore: ["eslint-enable"] }, - ], - }, - } -); diff --git a/project/client/make/dev.mk b/project/client/make/dev.mk deleted file mode 100644 index 3f376b9f..00000000 --- a/project/client/make/dev.mk +++ /dev/null @@ -1,18 +0,0 @@ -.PHONY: test lint test - -watch: - @node --experimental-strip-types ./watch.ts - -lint: - npm exec -- eslint - npm exec -- prettier --check . - -test: - @node --test \ - --import tsx \ - --enable-source-maps \ - --experimental-test-module-mocks \ - --experimental-test-coverage \ - --experimental-test-snapshots \ - --no-warnings=ExperimentalWarning \ - $(if $(TEST_SNAPSHOT),--test-update-snapshots,) $(UNIT) diff --git a/project/client/package-lock.json b/project/client/package-lock.json deleted file mode 100644 index 891a6e22..00000000 --- a/project/client/package-lock.json +++ /dev/null @@ -1,3142 +0,0 @@ -{ - "name": "client", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "client", - "dependencies": { - "@lit-labs/ssr-client": "^1.1.7", - "@lit/context": "^1.1.6", - "@lit/task": "^1.0.3", - "effect": "^3.18.4", - "lit": "^3.3.1", - "tsx": "^4.20.6" - }, - "devDependencies": { - "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", - "@eslint/js": "^9.37.0", - "@jgoz/esbuild-plugin-typecheck": "^4.0.3", - "@napi-rs/image": "^1.11.2", - "@types/mime-types": "^3.0.1", - "@types/node": "^24.7.2", - "esbuild": "^0.25.9", - "eslint": "^9.37.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-lit": "^2.1.1", - "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-wc": "^3.0.2", - "image-size": "^2.0.2", - "mime-types": "^3.0.1", - "prettier": "3.6.2", - "svgo": "^4.0.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.46.1" - } - }, - "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-plugin-eslint-comments": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.5.0.tgz", - "integrity": "sha512-MAhuTKlr4y/CE3WYX26raZjy+I/kS2PLKSzvfmDCGrBLTFHOYwqROZdr4XwPgXwX3K9rjzMr4pSmUWGnzsUyMg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^4.0.0", - "ignore": "^5.2.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.16.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jgoz/esbuild-plugin-typecheck": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@jgoz/esbuild-plugin-typecheck/-/esbuild-plugin-typecheck-4.0.3.tgz", - "integrity": "sha512-tJzjV3pALNuEQ3+w18jt58Y5MogN+Hm2vmEUR2EcLr9+5PR/X0JLAAg0+6AOw168GRZgKzWKK4zdzY9uMT708Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@jgoz/esbuild-plugin-livereload": ">=2.1.3", - "esbuild": "0.17.x || 0.18.x || 0.19.x || 0.20.x || 0.21.x || 0.22.x || 0.23.x || 0.24.x || 0.25.x", - "typescript": ">= 3.5" - }, - "peerDependenciesMeta": { - "@jgoz/esbuild-plugin-livereload": { - "optional": true - } - } - }, - "node_modules/@lit-labs/ssr-client": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.7.tgz", - "integrity": "sha512-VvqhY/iif3FHrlhkzEPsuX/7h/NqnfxLwVf0p8ghNIlKegRyRqgeaJevZ57s/u/LiFyKgqksRP5n+LmNvpxN+A==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.0.4", - "lit": "^3.1.2", - "lit-html": "^3.1.2" - } - }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", - "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/context": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.6.tgz", - "integrity": "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^1.6.2 || ^2.1.0" - } - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz", - "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.4.0" - } - }, - "node_modules/@lit/task": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lit/task/-/task-1.0.3.tgz", - "integrity": "sha512-1gJGJl8WON+2j0y9xfcD+XsS1rvcy3XDgsIhcdUW++yTR8ESjZW6o7dn8M8a4SZM8NnJe6ynS2cKWwsbfLOurg==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^1.0.0 || ^2.0.0" - } - }, - "node_modules/@napi-rs/image": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image/-/image-1.11.2.tgz", - "integrity": "sha512-i5zlU1EgNBlgRjxMC1CgClZlHyGGnR1upLV64s8t5N+w9/lB7plHcb+rBJ5YmSP9Mho5RvLQpZ/ScaoJNNcnNg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/image-android-arm64": "1.11.2", - "@napi-rs/image-darwin-arm64": "1.11.2", - "@napi-rs/image-darwin-x64": "1.11.2", - "@napi-rs/image-freebsd-x64": "1.11.2", - "@napi-rs/image-linux-arm-gnueabihf": "1.11.2", - "@napi-rs/image-linux-arm64-gnu": "1.11.2", - "@napi-rs/image-linux-arm64-musl": "1.11.2", - "@napi-rs/image-linux-x64-gnu": "1.11.2", - "@napi-rs/image-linux-x64-musl": "1.11.2", - "@napi-rs/image-wasm32-wasi": "1.11.2", - "@napi-rs/image-win32-arm64-msvc": "1.11.2", - "@napi-rs/image-win32-ia32-msvc": "1.11.2", - "@napi-rs/image-win32-x64-msvc": "1.11.2" - } - }, - "node_modules/@napi-rs/image-android-arm64": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-android-arm64/-/image-android-arm64-1.11.2.tgz", - "integrity": "sha512-EUkTeYEayZn9IyzXcn8m5t0MCsiN08+SsPJBhWQR05pQiiuonBRAYuZWB3hJDVHKfKKXogzMRShcTAE70NXGzA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-darwin-arm64": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-darwin-arm64/-/image-darwin-arm64-1.11.2.tgz", - "integrity": "sha512-RLnv2bbvkDwaROZHqUEozSto5nE3mhlIS9U2WGHJwepYUneq0gMumibtzp7YjEl6coEgqKnUDoATzRnUovkHqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-darwin-x64": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-darwin-x64/-/image-darwin-x64-1.11.2.tgz", - "integrity": "sha512-uptDhysXHSB3OTetz15CVeqf0LyeXpUnzqmVFhQimdzTEDkG7CyasUWNM29DOn2XTlZESdwAvg+2YUiQqt0wqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-freebsd-x64": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-freebsd-x64/-/image-freebsd-x64-1.11.2.tgz", - "integrity": "sha512-W6Sk8MKjS95OVZ0TWurUttF0Kt/chRuZkFKF4iPAbwcW/tk92sIJlBi+7VNMvpjVr8xLTbPkzaPIs2jes19Tbg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-linux-arm-gnueabihf": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm-gnueabihf/-/image-linux-arm-gnueabihf-1.11.2.tgz", - "integrity": "sha512-Oj2l9DWig3d3wEmkX4j6Ecg3H2ElT+n5u5TZ47xm0vEZT0QBb4dcqhVvWx6RTn4rLLU4sxAlwEtb3SR2UmpZwQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-linux-arm64-gnu": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm64-gnu/-/image-linux-arm64-gnu-1.11.2.tgz", - "integrity": "sha512-/ntkbFvrP4ERrGFJ32PupmYuZxhCoqfSN9y9Nao86kGdxCASjS2zecufDw2IjgUhD8CBigD1o9aoA74lKQbuOw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-linux-arm64-musl": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm64-musl/-/image-linux-arm64-musl-1.11.2.tgz", - "integrity": "sha512-8N+PlYpTVMEAyaOHqpx3wtur34TxBvF0YI4i9Cv0zJttzlNgsUOZjFCxVOeH+QVzvz+XOqTb4BR1vYpethFqVQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-linux-x64-gnu": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-x64-gnu/-/image-linux-x64-gnu-1.11.2.tgz", - "integrity": "sha512-0F/nDFW2UcidE9qh6+M8Ew4a8GIraX2C71xuvYb+dIEAT4lFoqfNWatxKBOmnZFWpsrO/taRhWUBFXJlG7uOyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-linux-x64-musl": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-x64-musl/-/image-linux-x64-musl-1.11.2.tgz", - "integrity": "sha512-AZEXCQUfmrZXiPQHfTgQE0xTsz4Ox4p15Q5IUM02sCZTvNLKflbE+WmJ2dEIh78q6V2QomUKDVTx7Q6pCZylIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-wasm32-wasi": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-wasm32-wasi/-/image-wasm32-wasi-1.11.2.tgz", - "integrity": "sha512-JM/ZFveVEGIBFyIberr1RTp7FHZHAbZFwCLZ7eNbUE5ujpufExkvICAB2uW4a02ZqTwdXTuI2CxmdrUhhN/XkQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@napi-rs/image-win32-arm64-msvc": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-arm64-msvc/-/image-win32-arm64-msvc-1.11.2.tgz", - "integrity": "sha512-JTWYu5m+a+Pi8nN3jI6c5NJV5gJvpSYoN0kBml2QMQWVktwcDQ2pC1yiTzmDSa+FnFsT57BcSm5S6yhj+42wjQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-win32-ia32-msvc": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-ia32-msvc/-/image-win32-ia32-msvc-1.11.2.tgz", - "integrity": "sha512-tjTiyeoGD+vk6nt2fagT00+SVV6uvkAsC6CK72up0YQcKTNYBbnpM6K/yUmSDtB708brjo+xtMJ5rwYEpsyeRQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/image-win32-x64-msvc": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-x64-msvc/-/image-win32-x64-msvc-1.11.2.tgz", - "integrity": "sha512-HhGEXPHyuf7EeWgSct1/0B4vy7KckALgI4HCwv2PHisvKcp/v/hcPTMHPCkfwtEe7uBDAugcEuEaMaKZnTP9jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", - "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.14.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", - "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/type-utils": "8.46.1", - "@typescript-eslint/utils": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.46.1", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", - "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", - "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.1", - "@typescript-eslint/types": "^8.46.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", - "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", - "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", - "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/utils": "8.46.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", - "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.46.1", - "@typescript-eslint/tsconfig-utils": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", - "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", - "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", - "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.46.1", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/effect": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "fast-check": "^3.23.1" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", - "@eslint/core": "^0.16.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", - "@eslint/plugin-kit": "^0.4.0", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-lit": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-2.1.1.tgz", - "integrity": "sha512-qmyAOnnTCdS+vDnNxtCoF0icSKIio4GUv6ZLnaCtTX6G/YezRa6Ag6tOQ+MfV5Elvtw9CIXeliRX4mIBSwrPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "eslint": ">= 8" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", - "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.7" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-wc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-3.0.2.tgz", - "integrity": "sha512-siwTrxPTw6GU2JmP3faInw8nhi0ZCnKsiSRM3j7EAkZmBTGYdDAToeseLYsvPrc5Urp/vPz+g7Ewh7XcICLxww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-valid-element-name": "^1.0.0", - "js-levenshtein-esm": "^2.0.0" - }, - "peerDependencies": { - "eslint": ">=8.40.0" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/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==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "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/get-tsconfig": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", - "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/image-size": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", - "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", - "dev": true, - "license": "MIT", - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-valid-element-name": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-element-name/-/is-valid-element-name-1.0.0.tgz", - "integrity": "sha512-GZITEJY2LkSjQfaIPBha7eyZv+ge0PhBR7KITeCCWvy7VBQrCUdFkvpI+HrAPQjVtVjy1LvlEkqQTHckoszruw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "is-potential-custom-element-name": "^1.0.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-levenshtein-esm": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-2.0.0.tgz", - "integrity": "sha512-1n4LEPOL4wRXY8rOQcuA7Iuaphe5xCMayvufCzlLAi+hRsnBRDbSS6XPuV58CBVJxj5D9ApFLyjQ7KzFToyHBw==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lit": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz", - "integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz", - "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.4.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz", - "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, - "license": "ISC" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/svgo": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", - "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^11.1.0", - "css-select": "^5.1.0", - "css-tree": "^3.0.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.1.1", - "sax": "^1.4.1" - }, - "bin": { - "svgo": "bin/svgo.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz", - "integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.1", - "@typescript-eslint/parser": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/utils": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/project/client/package.json b/project/client/package.json deleted file mode 100644 index 7daf93e5..00000000 --- a/project/client/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "client", - "type": "module", - "devDependencies": { - "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", - "@eslint/js": "^9.37.0", - "@jgoz/esbuild-plugin-typecheck": "^4.0.3", - "@napi-rs/image": "^1.11.2", - "@types/mime-types": "^3.0.1", - "@types/node": "^24.7.2", - "esbuild": "^0.25.9", - "eslint": "^9.37.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-lit": "^2.1.1", - "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-wc": "^3.0.2", - "image-size": "^2.0.2", - "mime-types": "^3.0.1", - "prettier": "3.6.2", - "svgo": "^4.0.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.46.1" - }, - "dependencies": { - "@lit-labs/ssr-client": "^1.1.7", - "@lit/context": "^1.1.6", - "@lit/task": "^1.0.3", - "effect": "^3.18.4", - "lit": "^3.3.1", - "tsx": "^4.20.6" - } -} diff --git a/project/client/public/discord.svg b/project/client/public/discord.svg deleted file mode 100644 index b636d150..00000000 --- a/project/client/public/discord.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/project/client/public/logo.png b/project/client/public/logo.png deleted file mode 100644 index 1ceff347..00000000 --- a/project/client/public/logo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cedb4c6ae90f76deeed6aab2e4ab170e8c71daa2e59031155b3acd116acf8d95 -size 19771 diff --git a/project/client/public/open-home-foundation.svg b/project/client/public/open-home-foundation.svg deleted file mode 100644 index 922df46a..00000000 --- a/project/client/public/open-home-foundation.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/project/client/src/api/base.ts b/project/client/src/api/base.ts deleted file mode 100644 index 03243e9a..00000000 --- a/project/client/src/api/base.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { Schema } from "effect"; -import { isLeft, isRight } from "effect/Either"; - -import type { operations, paths } from "../schema"; -import type { ParseError } from "effect/ParseResult"; - -type IdempotentHttpMethod = "get" | "head"; -type EffectfulHttpMethod = "put" | "patch" | "post" | "delete"; - -type IdempotentOperation = { - [Operation in keyof operations as keyof EnclosingPath[keyof EnclosingPath] extends IdempotentHttpMethod - ? Operation - : never]: operations[Operation]; -}; - -type EffectfulOperation = { - [Operation in keyof operations as keyof EnclosingPath[keyof EnclosingPath] extends EffectfulHttpMethod - ? Operation - : never]: operations[Operation]; -}; - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- closest match -type WithoutEmpty = { [K in keyof T as {} extends T[K] ? never : K]: T[K] }; - -// path in which operation is nested -type EnclosingPath = WithoutEmpty<{ - [Path in keyof paths]: { - [Operation in keyof paths[Path] as paths[Path][Operation] extends operations[O] - ? Operation - : never]: paths[Path][Operation] extends operations[O] - ? operations[O] - : never; - }; -}>; - -type EndpointRequestParameters< - Path extends keyof paths, - Method extends keyof paths[Path], -> = "parameters" extends keyof paths[Path][Method] - ? paths[Path][Method]["parameters"] extends Record - ? { parameters?: paths[Path][Method]["parameters"] } - : // only support parameter types that extend string - // additionally enforced by schema linter enforces - { - parameters: { - [Type in keyof paths[Path][Method]["parameters"]]: { - [Parameter in keyof paths[Path][Method]["parameters"][Type]]: paths[Path][Method]["parameters"][Type][Parameter] extends string - ? paths[Path][Method]["parameters"][Type][Parameter] - : never; - }; - }; - } - : never; -type EndpointRequestRequestBody< - Path extends keyof paths, - Method extends keyof paths[Path], -> = "requestBody" extends keyof paths[Path][Method] - ? // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -- false positive - paths[Path][Method]["requestBody"] extends never | undefined - ? { requestBody?: paths[Path][Method]["requestBody"] } - : "content" extends keyof paths[Path][Method]["requestBody"] - ? { - requestBody: { - [CT in keyof paths[Path][Method]["requestBody"]["content"]]: { - kind: CT; - body: paths[Path][Method]["requestBody"]["content"][CT]; - }; - }[keyof paths[Path][Method]["requestBody"]["content"]]; - } - : never - : never; -type EndpointRequest< - Path extends keyof paths, - Method extends keyof paths[Path], -> = EndpointRequestParameters & - EndpointRequestRequestBody; - -type EndpointResponses< - Path extends keyof paths, - Method extends keyof paths[Path], -> = "responses" extends keyof paths[Path][Method] - ? paths[Path][Method]["responses"] - : never; - -type RequestBody = { - contentType: C; - body: B; -}; - -type RequestContentType = "text/plain" | "application/json"; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -- phantom type -export type BuiltOperation = { - name: string; - path: string; - method: string; - parameters: { - query: Record; - path: Record; - header: Record; - }; - body?: RequestBody; -}; - -export function idempotentOperation< - Operation extends keyof IdempotentOperation, - EnclosedWithin extends EnclosingPath, - Path extends keyof EnclosedWithin & keyof paths, - Method extends keyof EnclosedWithin[Path] & keyof paths[Path], - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- false positive - Parameters extends EndpointRequest["parameters"], - Responses extends EndpointResponses, ->( - operation: Operation, - path: Path, - method: Method & string, - parameters: Parameters -): BuiltOperation { - return { - name: operation, - path, - method, - parameters: { - query: {}, - path: {}, - header: {}, - ...parameters, - }, - }; -} - -export function effectfulOperation< - Operation extends keyof EffectfulOperation, - EnclosedWithin extends EnclosingPath, - Path extends keyof EnclosedWithin & keyof paths, - // effectful operations _without_ request body - Method extends keyof { - [M in keyof EnclosedWithin[Path] as "requestBody" extends keyof EnclosedWithin[Path][M] - ? never - : M]: never; - } & - keyof paths[Path], - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- false positive - Parameters extends EndpointRequest["parameters"], - Responses extends EndpointResponses, ->( - operation: Operation, - path: Path, - method: Method, - parameters: Parameters -): BuiltOperation; -export function effectfulOperation< - Operation extends keyof EffectfulOperation, - EnclosedWithin extends EnclosingPath, - Path extends keyof EnclosedWithin & keyof paths, - // effectful operations _with_ request body - Method extends keyof { - [_Method in keyof EnclosedWithin[Path] as "requestBody" extends keyof EnclosedWithin[Path][_Method] - ? _Method - : never]: never; - } & - keyof paths[Path], - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- false positive - Parameters extends EndpointRequest["parameters"], - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- false positive - RequestBody extends EndpointRequest["requestBody"], - Responses extends EnclosedWithin[Path][Method]["responses"], ->( - operation: Operation, - path: Path, - method: Method, - parameters: Parameters, - body: RequestBody -): BuiltOperation; -/* eslint-disable @typescript-eslint/no-explicit-any -- typing enforced in overloads */ -export function effectfulOperation( - operation: any, - path: any, - method: any, - parameters: any, - body?: any -) { - return { - /* eslint-disable @typescript-eslint/no-unsafe-assignment -- intentional */ - name: operation, - path: path, - method, - parameters: { - query: {}, - path: {}, - header: {}, - ...parameters, - }, - body, - }; - /* eslint-enable @typescript-eslint/no-unsafe-assignment */ -} -/* eslint-enable @typescript-eslint/no-explicit-any */ - -/* eslint-disable @typescript-eslint/no-explicit-any -- defines shape, `any` is intentional */ -export type ResponsesShape = Record< - number, - | { - content: Record; - } - | { - content: Record; - headers: Record; - } ->; -/* eslint-enable @typescript-eslint/no-explicit-any */ - -// https://fetch.spec.whatwg.org/#forbidden-response-header-name -type PrunedHeaders = Omit; - -// lowers status code into value to distribute -export type DistributeResponses = { - [Code in keyof T]: Record extends PrunedHeaders< - Code extends keyof T - ? "headers" extends keyof T[Code] - ? T[Code]["headers"] - : never - : never - > - ? { - code: Code; - body: "content" extends keyof T[Code] - ? T[Code]["content"][keyof T[Code]["content"]] - : never; - } - : { - code: Code; - body: "content" extends keyof T[Code] - ? T[Code]["content"][keyof T[Code]["content"]] - : never; - headers: PrunedHeaders< - Code extends keyof T - ? "headers" extends keyof T[Code] - ? T[Code]["headers"] - : never - : never - >; - }; -}[keyof T]; - -export type Io = ( - built: BuiltOperation, - signal?: AbortSignal -) => Promise>; - -export class ResponseError extends Error { - constructor(public message: string) { - super(message); - Object.setPrototypeOf(this, Error.prototype); - } -} - -export class UnexpectedResponseError extends ResponseError { - constructor(public error: ParseError) { - super(`decoding error occurred: ${error.message}`); - Object.setPrototypeOf(this, UnexpectedResponseError.prototype); - } -} - -export class NotFoundError extends ResponseError { - constructor() { - super("not found"); - Object.setPrototypeOf(this, NotFoundError.prototype); - } -} - -export class DescribedError extends ResponseError { - constructor(public message: string) { - super(message); - Object.setPrototypeOf(this, DescribedError.prototype); - } -} - -const ErrorNotFound = Schema.Struct({ - code: Schema.Literal(404), -}); -const errorNotFoundDecoder = Schema.decodeUnknownEither(ErrorNotFound); - -const ErrorDescribed = Schema.Struct({ - body: Schema.Struct({ - message: Schema.String, - }), -}); -const errorDescribedDecoder = Schema.decodeUnknownEither(ErrorDescribed); - -const fetch = async ( - built: BuiltOperation, - responses: M, - io: Io -): Promise => { - const response = await io(built); - - const decoder = Schema.decodeUnknownEither(responses); - const decoded = decoder(response); - if (isLeft(decoded)) { - { - const decoded = errorNotFoundDecoder(response); - if (isRight(decoded)) { - throw new NotFoundError(); - } - } - - { - const decoded = errorDescribedDecoder(response); - if (isRight(decoded)) { - throw new DescribedError(decoded.right.body.message); - } - } - - throw new UnexpectedResponseError(decoded.left); - } - - return response; -}; - -type Response = Extract< - DistributeResponses, - { code: C } ->; - -export const bindFetch = - (io: Io) => - async ( - built: BuiltOperation, - responses: M["Encoded"] extends Response< - R, - Extract["code"]> - > - ? Response< - R, - Extract["code"]> - > extends M["Encoded"] - ? M - : never - : never - ) => - fetch(built, responses, io); - -export type Fetch = ReturnType; diff --git a/project/client/src/api/index.ts b/project/client/src/api/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/project/client/src/context/fetch.ts b/project/client/src/context/fetch.ts deleted file mode 100644 index 26fee491..00000000 --- a/project/client/src/context/fetch.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from "@lit/context"; -import type { Fetch } from "../api/base"; - -export const ContextFetch = createContext(Symbol("fetch")); diff --git a/project/client/src/context/ssr/location.ts b/project/client/src/context/ssr/location.ts deleted file mode 100644 index 37287487..00000000 --- a/project/client/src/context/ssr/location.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createContext } from "@lit/context"; - -export type SsrLocation = { - origin: string; - pathname: string; - status: (code: number) => void; -}; - -export const ContextSsrLocation = createContext( - Symbol("location") -); diff --git a/project/client/src/context/ssr/resolve.ts b/project/client/src/context/ssr/resolve.ts deleted file mode 100644 index 015128c7..00000000 --- a/project/client/src/context/ssr/resolve.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext } from "@lit/context"; - -export type SsrResolveResolved = [readonly unknown[], unknown]; -export type SsrResolve = ( - locationToken: LocationToken, - task: () => Promise -) => void; - -export const ContextSsrResolve = createContext(Symbol("resolve")); diff --git a/project/client/src/context/ssr/resolved.ts b/project/client/src/context/ssr/resolved.ts deleted file mode 100644 index 14adbd3e..00000000 --- a/project/client/src/context/ssr/resolved.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext } from "@lit/context"; - -import type { SsrResolveResolved } from "./resolve"; - -export type SsrResolved = Record; - -export const ContextSsrResolved = createContext( - Symbol("resolved") -); diff --git a/project/client/src/csr.ts b/project/client/src/csr.ts deleted file mode 100644 index c89881e9..00000000 --- a/project/client/src/csr.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { - BuiltOperation, - DistributeResponses, - Io, - ResponsesShape, -} from "./api/base"; - -const pathSubstitutionRegex = /({\w+})/g; - -export class MissingContentTypeError extends Error { - constructor(public url: string) { - super(`encountered missing content type (${url})`); - Object.setPrototypeOf(this, MissingContentTypeError.prototype); - } -} - -export class UnsupportedContentTypeError extends Error { - constructor( - public url: string, - public contentType: string - ) { - super(`encountered unsupported content type (${contentType}) (${url})`); - Object.setPrototypeOf(this, UnsupportedContentTypeError.prototype); - } -} - -export const csrIo: Io = async ( - built: BuiltOperation, - signal?: AbortSignal -): Promise> => { - const requestHeaders: HeadersInit = new Headers(); - for (const [key, value] of Object.entries(built.parameters.header)) { - if (typeof value !== "string") { - continue; - } - - requestHeaders.set(key, value); - } - - const path = built.path.replaceAll(pathSubstitutionRegex, (match) => { - // slice off surrounding brackets - const variable = match.slice(1, -1); - return encodeURIComponent(built.parameters.path[variable] ?? ""); - }); - - const query = - Object.keys(built.parameters.query).length > 0 - ? `?${new URLSearchParams(built.parameters.query)}` - : ""; - - const body = built.body; - - let serialized: string | null = null; - if (body) { - switch (body.contentType) { - case "application/json": - serialized = JSON.stringify(body.body); - break; - case "text/plain": - serialized = body.body as string; - break; - } - - requestHeaders.set("content-type", body.contentType); - } - - const url = `${API_BASE_URL}${path}${query}`; - - const fetched = await fetch(url, { - method: built.method.toUpperCase(), - headers: requestHeaders, - body: serialized, - credentials: "same-origin", - signal: signal ?? null, - }); - - const contentType = fetched.headers.get("content-type"); - - const responseHeaders: Record = {}; - for (const [key, value] of fetched.headers) { - responseHeaders[key] = value; - } - - // remove encoding instructions - const strippedContentType = contentType?.split(";")[0]; - - switch (strippedContentType) { - case undefined: - throw new MissingContentTypeError(url); - case "application/json": - return { - code: fetched.status, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- concrete runtime type not known here - body: await fetched.json(), - headers: responseHeaders, - } as DistributeResponses; - case "text/plain": - return { - code: fetched.status, - body: await fetched.text(), - headers: responseHeaders, - } as DistributeResponses; - default: - throw new UnsupportedContentTypeError(url, contentType ?? ""); - } -}; diff --git a/project/client/src/element/sized-image.ts b/project/client/src/element/sized-image.ts deleted file mode 100644 index feebd5e2..00000000 --- a/project/client/src/element/sized-image.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; - -type SizedImage = { - src: string; - width: number; - height: number; -}; - -@customElement("element-sized-image") -export class ElementSizedImage extends LitElement { - @property({ type: Object }) - sized?: SizedImage; - @property({ type: String }) - alt?: string; - - render() { - return html`${this.alt}`; - } -} diff --git a/project/client/src/entrypoint.ts b/project/client/src/entrypoint.ts deleted file mode 100644 index 6827810c..00000000 --- a/project/client/src/entrypoint.ts +++ /dev/null @@ -1,126 +0,0 @@ -// https://lit.dev/docs/ssr/client-usage/#loading-@lit-labsssr-clientlit-element-hydrate-support.js -// if not imported before _anything_ else, stuff is rendered twice during hydration 🫣 -import "@lit-labs/ssr-client/lit-element-hydrate-support.js"; - -import { html, LitElement } from "lit"; -import { customElement } from "lit/decorators.js"; -import { hydrate } from "@lit-labs/ssr-client"; -import { consume, ContextProvider } from "@lit/context"; - -import { bindFetch, type Io } from "./api/base"; -import { csrIo } from "./csr"; -import { ContextFetch } from "./context/fetch"; -import type { SsrResolve } from "./context/ssr/resolve"; -import { ContextSsrResolve } from "./context/ssr/resolve"; -import { ContextSsrResolved, type SsrResolved } from "./context/ssr/resolved"; -import { Router } from "./vendor/@lit-labs/router/router"; -import { - RouterPathNotFoundError, - type RouteConfig, -} from "./vendor/@lit-labs/router/routes"; -import { ContextSsrLocation, type SsrLocation } from "./context/ssr/location"; - -import "./page/home"; - -const routes = [ - { - path: "/", - render: () => html``, - }, -] as const satisfies RouteConfig[]; - -@customElement("element-entrypoint") -export class Entrypoint extends LitElement { - private _router: Router | undefined; - - @consume({ context: ContextSsrLocation }) - private ssrLocation?: SsrLocation | undefined; - - override connectedCallback(): void { - super.connectedCallback(); - - if (typeof this._router !== "undefined") { - return; - } - - const location = { - origin: - this.ssrLocation?.origin ?? - (window.location.origin || - window.location.protocol + "//" + window.location.host), - pathname: this.ssrLocation?.pathname ?? window.location.pathname, - status: this.ssrLocation?.status, - }; - - const router = new Router(this, routes, { - origin: location.origin, - status: location.status, - }); - this._router = router; - - router.goto(location.pathname).catch((e: unknown) => { - if (e instanceof RouterPathNotFoundError) { - location.status?.(404); - } - }); - } - - render() { - return this._router?.outlet(); - } -} - -const host: HTMLElement = SSR - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -- injected during SSR - ((globalThis as any).litServerRoot as HTMLElement) - : document.body; - -const provider = { - fetch: new ContextProvider(host, { - context: ContextFetch, - initialValue: bindFetch(csrIo), - }), - resolve: new ContextProvider(host, { - context: ContextSsrResolve, - }), - resolved: new ContextProvider(host, { - context: ContextSsrResolved, - }), - location: new ContextProvider(host, { - context: ContextSsrLocation, - }), -} as const; - -type EntrypointTemplateContext = { - io: Io; - resolve?: SsrResolve; - resolved?: SsrResolved; - location?: SsrLocation; -}; - -export const entrypointTemplate = ({ - io, - resolve, - resolved, - location, -}: EntrypointTemplateContext) => { - provider.fetch.setValue(bindFetch(io)); - if (typeof resolve !== "undefined") { - provider.resolve.setValue(resolve); - } - if (typeof RESOLVED !== "undefined") { - provider.resolved.setValue(RESOLVED); - } else if (typeof resolved !== "undefined") { - provider.resolved.setValue(resolved); - } - - if (typeof location !== "undefined") { - provider.location.setValue(location); - } - - return html``; -}; - -export const csr = () => { - hydrate(entrypointTemplate({ io: csrIo }), document.body); -}; diff --git a/project/client/src/global.d.ts b/project/client/src/global.d.ts deleted file mode 100644 index 8669ed23..00000000 --- a/project/client/src/global.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -declare module "*.png" { - const value: string; - export = value; -} - -declare module "sized:*.png" { - const value: { - src: string; - width: number; - height: number; - }; - export = value; -} - -declare module "*.svg" { - const value: string; - export = value; -} - -/** injected during build */ -declare const SSR: boolean; -/** injected during build */ -declare const API_BASE_URL: string; -type LocationToken = string & { _brand: unique symbol }; -/** unique for every location it is referenced at */ -declare const $X_SYN_LOCATION_TOKEN: LocationToken; - -/** results of resolvees */ -declare const RESOLVED: - | Record - | undefined; diff --git a/project/client/src/mixin/isomorph/index.ts b/project/client/src/mixin/isomorph/index.ts deleted file mode 100644 index b52ebd5e..00000000 --- a/project/client/src/mixin/isomorph/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { LitElement } from "lit"; -import { ContextConsumer } from "@lit/context"; - -import type { Constructor } from "../../type/constructor"; - -import { ContextFetch } from "../../context/fetch"; -import type { - MixinIsomorphTaskConfiguration, - MixinIsomorphTaskContext, -} from "./task"; -import { MixinIsomorphTask } from "./task"; -import type { SsrResolve } from "../../context/ssr/resolve"; -import { ContextSsrResolve } from "../../context/ssr/resolve"; -import { ContextSsrResolved } from "../../context/ssr/resolved"; - -export const MixinIsomorph = >( - superClass: T -) => - class MixinIsomorph extends superClass { - // context only becomes available once `.connectedCallback` is called - // requires `resolve` and `io` context - private _resolving: (( - resolve: SsrResolve, - context: MixinIsomorphTaskContext - ) => unknown)[] = []; - // requires `resolved` context - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown is too specific in this context - private _completing: [LocationToken, MixinIsomorphTask][] = []; - - private _consumer = { - fetch: new ContextConsumer(this, { context: ContextFetch }), - resolve: new ContextConsumer(this, { - context: ContextSsrResolve, - }), - resolved: new ContextConsumer(this, { - context: ContextSsrResolved, - }), - } as const; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any -- required by typescript - constructor(...args: any[]) { - super(); - } - - override connectedCallback(): void { - super.connectedCallback(); - - // first ssr render pass "discovers" tasks that need to be resolved - const resolve = this._consumer.resolve.value; - if (typeof resolve !== "undefined") { - for (const task of this._resolving) { - const context: MixinIsomorphTaskContext = { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- value is resolved upon callback connect - fetch: this._consumer.fetch.value!, - }; - - task(resolve, context); - } - } - - // second ssr pass picks up the resolved dispatches from the first pass - // script-defined variables are unavailable during ssr, so they are instead injected through a context - const resolved = this._consumer.resolved.value; - if (typeof resolved !== "undefined") { - for (const [locationToken, task] of this._completing) { - const pair = resolved[locationToken]; - if (typeof pair === "undefined") { - continue; - } - - task.complete(pair[0], pair[1]); - } - } - } - - protected task, TaskResult>( - locationToken: LocationToken, - configuration: MixinIsomorphTaskConfiguration - ): MixinIsomorphTask { - const task = new MixinIsomorphTask(this, configuration); - - if (SSR) { - this._resolving.push((resolve, context) => { - resolve(locationToken, async () => - MixinIsomorphTask.run(configuration, context) - ); - }); - this._completing.push([locationToken, task] as const); - } else if (typeof RESOLVED !== "undefined") { - // uses script-defined global variable to complete task, because context only becomes - // available *after* the first render, leading to an unnecessary interim state - // before transitioning to complete - const pair = RESOLVED[locationToken]; - if (typeof pair !== "undefined") { - task.complete(pair[0] as ArgumentsResult, pair[1] as TaskResult); - } - } - - return task; - } - }; diff --git a/project/client/src/mixin/isomorph/task.ts b/project/client/src/mixin/isomorph/task.ts deleted file mode 100644 index 5a00addb..00000000 --- a/project/client/src/mixin/isomorph/task.ts +++ /dev/null @@ -1,223 +0,0 @@ -// adapted from https://github.com/lit/lit/blob/main/packages/task/src/task.ts -// allows setting the inner value - -import { shallowArrayEquals } from "@lit/task"; - -import type { ReactiveControllerHost } from "@lit/reactive-element/reactive-controller.js"; -import type { Fetch } from "../../api/base"; -import { ContextConsumer } from "@lit/context"; -import { ContextFetch } from "../../context/fetch"; - -export type MixinIsomorphTaskContext = { - fetch: Fetch; -}; - -type TaskFunctionContext = MixinIsomorphTaskContext & { - signal: AbortSignal | undefined; -}; - -export type MixinIsomorphTaskConfiguration< - ArgumentsResult extends ReadonlyArray, - TaskResult, -> = { - taskFn: ( - args: ArgumentsResult, - context: TaskFunctionContext - ) => Promise; - argsFn: () => ArgumentsResult; - argsEqualFn?: ( - previous: ArgumentsResult, - current: ArgumentsResult - ) => boolean; -}; - -type TaskStatusInitial = { kind: "initial" }; -type TaskStatusPending = { kind: "pending" }; -type TaskStatusComplete = { kind: "complete"; value: T }; -type TaskStatusError = { kind: "error"; error: unknown }; -type TaskStatus = - | TaskStatusInitial - | TaskStatusPending - | TaskStatusComplete - | TaskStatusError; - -type MaybeReturnType = F extends (...args: never[]) => infer R - ? R - : undefined; - -type StatusRenderer = { - initial?: () => unknown; - pending?: () => unknown; - complete?: (value: R) => unknown; - error?: (error: unknown) => unknown; -}; - -export class MixinIsomorphTask< - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -- false positive - ArgumentsResult extends ReadonlyArray | never, - TaskResult, -> { - private _host: ReactiveControllerHost; - private _configuration: MixinIsomorphTaskConfiguration< - ArgumentsResult, - TaskResult - >; - - private _previousArgs: ArgumentsResult | undefined; - private _callId = 0; - private _status: TaskStatus; - private _abortController?: AbortController; - - private _consumeFetch: ContextConsumer< - typeof ContextFetch, - ReactiveControllerHost & HTMLElement - >; - - constructor( - host: ReactiveControllerHost & HTMLElement, - configuration: MixinIsomorphTaskConfiguration, - completion?: [ArgumentsResult, TaskResult] - ) { - (this._host = host).addController(this); - this._configuration = configuration; - - this._consumeFetch = new ContextConsumer(host, { context: ContextFetch }); - - if (typeof completion !== "undefined") { - this._previousArgs = completion[0]; - this._status = { kind: "complete", value: completion[1] }; - } else { - this._status = { kind: "initial" }; - } - } - - public static async run< - ArgumentsResult extends ReadonlyArray, - TaskResult, - >( - configuration: MixinIsomorphTaskConfiguration, - context: MixinIsomorphTaskContext - ): Promise<[ArgumentsResult, TaskResult]> { - const taskFnContext: TaskFunctionContext = { - ...context, - signal: undefined, - }; - - const args = configuration.argsFn(); - return [args, await configuration.taskFn(args, taskFnContext)] as const; - } - - hostUpdate() { - void this._performTask(); - } - - private argsEqual( - previous: ArgumentsResult | undefined, - current: ArgumentsResult - ): boolean { - if (typeof previous === "undefined") { - return false; - } - - if ( - "argsEqualFn" in this._configuration && - typeof this._configuration.argsEqualFn !== "undefined" - ) { - return this._configuration.argsEqualFn(previous, current); - } - - return shallowArrayEquals(previous, current); - } - - private async _performTask() { - let args: ArgumentsResult | undefined; - guard: { - const previous = this._previousArgs; - const current = this._configuration.argsFn(); - this._previousArgs = current; - - if (!this.argsEqual(previous, current)) { - args = current; - break guard; - } else { - return; - } - } - - await this.run(args); - } - - async run(args?: ArgumentsResult) { - if (this._status.kind === "pending") { - this._abortController?.abort(); - } - - this._status = { kind: "pending" }; - let result!: TaskResult; - let error: unknown; - - this._host.requestUpdate(); - - const key = ++this._callId; - this._abortController = new AbortController(); - let errored = false; - try { - const context: TaskFunctionContext = { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed to be set by this point - fetch: this._consumeFetch.value!, - signal: this._abortController.signal, - }; - - const narrowedArgs = args ?? this._configuration.argsFn(); - this._previousArgs = narrowedArgs; - result = await this._configuration.taskFn(narrowedArgs, context); - } catch (e) { - errored = true; - error = e; - } - // if this is the most recent task call, process this value - if (this._callId === key) { - if (!errored) { - this._status = { kind: "complete", value: result }; - } else { - this._status = { kind: "error", error }; - } - - // request an update with the final value - this._host.requestUpdate(); - } - } - - abort(reason?: unknown) { - if (this._status.kind === "pending") { - this._abortController?.abort(reason); - } - } - - complete(args: ArgumentsResult, value: TaskResult) { - this._previousArgs = args; - this._status = { kind: "complete", value }; - this._host.requestUpdate(); - } - - get status(): TaskStatus { - return this._status; - } - - render>(renderer: T) { - switch (this._status.kind) { - case "initial": - return renderer.initial?.() as MaybeReturnType; - case "pending": - return renderer.pending?.() as MaybeReturnType; - case "complete": - return renderer.complete?.(this._status.value) as MaybeReturnType< - T["complete"] - >; - case "error": - return renderer.error?.(this._status.error) as MaybeReturnType< - T["error"] - >; - } - } -} diff --git a/project/client/src/page/home.ts b/project/client/src/page/home.ts deleted file mode 100644 index 14411da8..00000000 --- a/project/client/src/page/home.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Schema } from "effect"; -import { LitElement, css, html } from "lit"; -import { customElement } from "lit/decorators.js"; - -import { idempotentOperation } from "../api/base"; - -import "../element/sized-image"; - -import ImageOpenHomeFoundation from "inline:open-home-foundation.svg"; -import ImageDiscord from "inline:discord.svg"; -import { MixinIsomorph } from "../mixin/isomorph"; - -@customElement("element-page-home") -export class PageHome extends MixinIsomorph(LitElement) { - private _healthTask = (() => { - const operation = idempotentOperation( - "getHealth", - "/api/v1/health", - "get", - {} - ); - const expected = Schema.Union( - Schema.Struct({ - code: Schema.Literal(200), - body: Schema.Literal("ok"), - }), - Schema.Struct({ - code: Schema.Literal(500), - body: Schema.Literal("not ok"), - }) - ); - - return this.task($X_SYN_LOCATION_TOKEN, { - taskFn: async (_, context) => { - return await context.fetch(operation, expected); - }, - argsFn: () => [], - }); - })(); - - static styles = css` - main { - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 18px; - padding: 1em 1.2em 1em 1.2em; - height: 100%; - box-sizing: border-box; - } - - #top { - display: flex; - flex-direction: column; - gap: 8px; - } - - #bottom { - display: flex; - justify-content: space-between; - align-items: flex-end; - flex-wrap: wrap; - gap: 8px; - - p { - margin: 0; - } - } - - #image-foundation { - max-width: 192px; - } - - #heading { - h1 { - margin-top: 6px; - } - } - - #disclaimer { - p:not(:last-child) { - margin: 0; - } - } - - #image-device-database::part(img) { - width: 64px; - height: 64px; - border-radius: 8px; - } - - #image-discord { - margin-bottom: 4px; - } - - #tiles { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(176px, 1fr)); - grid-auto-rows: min-height; - gap: 5px; - } - - .tile { - display: flex; - flex-direction: column; - font-weight: 300; - border-radius: 4px; - justify-content: center; - align-items: center; - gap: 4px; - background-color: #e7e7e7; - text-decoration: none; - color: black; - padding: 12px 12px 12px 12px; - - > :first-child { - font-size: 32px; - } - - > :nth-child(3) { - font-size: 12px; - } - - > img { - height: 32px; - } - - &:hover { - background-color: #d7d7d7; - } - } - `; - - render() { - return html`
-
-
-

device database

-
-
-

congratulations, you just stumbled upon the device database!

-

- check out the - wiki - to see what this is all about -

-
- -
- -
- - ${this._healthTask.render({ - pending: () => html`

status: ...

`, - complete: (response) => html`

status: ${response.body}

`, - error: (e) => html`

status: ${e}

`, - })} -
-
`; - } -} diff --git a/project/client/src/schema.ts b/project/client/src/schema.ts deleted file mode 120000 index d403ad4a..00000000 --- a/project/client/src/schema.ts +++ /dev/null @@ -1 +0,0 @@ -../../../schema/out/typescript/schema.ts \ No newline at end of file diff --git a/project/client/src/style.css b/project/client/src/style.css deleted file mode 100644 index b825f29a..00000000 --- a/project/client/src/style.css +++ /dev/null @@ -1,9 +0,0 @@ -html, -body { - margin: 0; - height: 100%; -} - -body { - font-family: sans-serif; -} diff --git a/project/client/src/type/constructor.ts b/project/client/src/type/constructor.ts deleted file mode 100644 index 0869428c..00000000 --- a/project/client/src/type/constructor.ts +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type -- can't be described more accurately -export type Constructor = new (...args: any[]) => T; diff --git a/project/client/src/vendor/@lit-labs/router/README.md b/project/client/src/vendor/@lit-labs/router/README.md deleted file mode 100644 index c3516c3f..00000000 --- a/project/client/src/vendor/@lit-labs/router/README.md +++ /dev/null @@ -1,2 +0,0 @@ -extended to allow `origin` and `pathname` to be specified, which is required for ssr -also removes top-level references to `location`, which causes problems during ssr diff --git a/project/client/src/vendor/@lit-labs/router/router.ts b/project/client/src/vendor/@lit-labs/router/router.ts deleted file mode 100644 index 1ce7af9b..00000000 --- a/project/client/src/vendor/@lit-labs/router/router.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-disable @typescript-eslint/no-floating-promises, @typescript-eslint/no-unused-vars -- ignored in original code */ - -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ - -import type { ReactiveControllerHost } from "lit"; -import type { RouteConfig } from "./routes"; -import { Routes } from "./routes"; - -/** - * A root-level router that installs global event listeners to intercept - * navigation. - * - * This class extends Routes so that it can also have a route configuration. - * - * There should only be one Router instance on a page, since the Router - * installs global event listeners on `window` and `document`. Nested - * routes should be configured with the `Routes` class. - */ -export class Router extends Routes { - private _origin: string; - - constructor( - host: ReactiveControllerHost & HTMLElement, - routes: Array, - options: { - origin: string; - status?: ((code: number) => void) | undefined; - } - ) { - super(host, routes); - this._origin = options.origin; - } - - override hostConnected() { - super.hostConnected(); - window.addEventListener("click", this._onClick); - window.addEventListener("popstate", this._onPopState); - } - - override hostDisconnected() { - super.hostDisconnected(); - window.removeEventListener("click", this._onClick); - window.removeEventListener("popstate", this._onPopState); - } - - private _onClick = (e: MouseEvent) => { - const isNonNavigationClick = - e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey; - if (e.defaultPrevented || isNonNavigationClick) { - return; - } - - const anchor = e - .composedPath() - .find((n) => (n as HTMLElement).tagName === "A") as - | HTMLAnchorElement - | undefined; - if ( - anchor === undefined || - anchor.target !== "" || - anchor.hasAttribute("download") || - anchor.getAttribute("rel") === "external" - ) { - return; - } - - const href = anchor.href; - if (href === "" || href.startsWith("mailto:")) { - return; - } - - const location = window.location; - if (anchor.origin !== this._origin) { - return; - } - - e.preventDefault(); - if (href !== location.href) { - window.history.pushState({}, "", href); - this.goto(anchor.pathname); - } - }; - - private _onPopState = (_e: PopStateEvent) => { - this.goto(window.location.pathname); - }; -} - -/* eslint-enable @typescript-eslint/no-floating-promises, @typescript-eslint/no-unused-vars -- ↑ */ diff --git a/project/client/src/vendor/@lit-labs/router/routes.ts b/project/client/src/vendor/@lit-labs/router/routes.ts deleted file mode 100644 index 75801f1b..00000000 --- a/project/client/src/vendor/@lit-labs/router/routes.ts +++ /dev/null @@ -1,391 +0,0 @@ -/* eslint-disable @typescript-eslint/no-floating-promises, @typescript-eslint/no-unnecessary-condition, @typescript-eslint/restrict-plus-operands -- ignored in original code */ - -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ - -export class RouterPathNotFoundError extends Error { - constructor(private path: string) { - super(`path <${path}> not found`); - } -} - -import type { ReactiveController, ReactiveControllerHost } from "lit"; - -export interface BaseRouteConfig { - name?: string | undefined; - render?: (params: { [key: string]: string | undefined }) => unknown; - enter?: (params: { - [key: string]: string | undefined; - }) => Promise | boolean; -} - -/** - * A RouteConfig that matches against a `path` string. `path` must be a - * [`URLPattern` compatible pathname pattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/pathname). - */ -export interface PathRouteConfig extends BaseRouteConfig { - path: string; -} - -/** - * A RouteConfig that matches against a given [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) - * - * While `URLPattern` can match against protocols, hostnames, and ports, - * routes will only be checked for matches if they're part of the current - * origin. This means that the pattern is limited to checking `pathname` and - * `search`. - */ -export interface URLPatternRouteConfig extends BaseRouteConfig { - pattern: URLPattern; -} - -/** - * A description of a route, which path or pattern to match against, and a - * render() callback used to render a match to the outlet. - */ -export type RouteConfig = PathRouteConfig | URLPatternRouteConfig; - -// A cache of URLPatterns created for PathRouteConfig. -// Rather than converting all given RoutConfigs to URLPatternRouteConfig, this -// lets us make `routes` mutable so users can add new PathRouteConfigs -// dynamically. -const patternCache = new WeakMap(); - -const isPatternConfig = (route: RouteConfig): route is URLPatternRouteConfig => - (route as URLPatternRouteConfig).pattern !== undefined; - -const getPattern = (route: RouteConfig) => { - if (isPatternConfig(route)) { - return route.pattern; - } - let pattern = patternCache.get(route); - if (pattern === undefined) { - patternCache.set( - route, - (pattern = new URLPattern({ pathname: route.path })) - ); - } - return pattern; -}; - -/** - * A reactive controller that performs location-based routing using a - * configuration of URL patterns and associated render callbacks. - */ -export class Routes implements ReactiveController { - private readonly _host: ReactiveControllerHost & HTMLElement; - - /* - * The currently installed set of routes in precedence order. - * - * This array is mutable. To dynamically add a new route you can write: - * - * ```ts - * this._routes.routes.push({ - * path: '/foo', - * render: () => html`

Foo

`, - * }); - * ``` - * - * Mutating this property does not trigger any route transitions. If the - * changes may result is a different route matching for the current path, you - * must instigate a route update with `goto()`. - */ - routes: Array = []; - - /** - * A default fallback route which will always be matched if none of the - * {@link routes} match. Implicitly matches to the path "/*". - */ - fallback?: BaseRouteConfig | undefined; - - /* - * The current set of child Routes controllers. These are connected via - * the routes-connected event. - */ - private readonly _childRoutes: Array = []; - - private _parentRoutes: Routes | undefined; - - private _status?: ((code: number) => void) | undefined; - - /* - * State related to the current matching route. - * - * We keep this so that consuming code can access current parameters, and so - * that we can propagate tail matches to child routes if they are added after - * navigation / matching. - */ - private _currentPathname: string | undefined; - private _currentRoute: RouteConfig | undefined; - private _currentParams: { - [key: string]: string | undefined; - } = {}; - - /** - * Callback to call when this controller is disconnected. - * - * It's critical to call this immediately in hostDisconnected so that this - * controller instance doesn't receive a tail match meant for another route. - */ - // TODO (justinfagnani): Do we need this now that we have a direct reference - // to the parent? We can call `this._parentRoutes.disconnect(this)`. - private _onDisconnect: (() => void) | undefined; - - constructor( - host: ReactiveControllerHost & HTMLElement, - routes: Array, - options?: { - fallback?: BaseRouteConfig; - } - ) { - this.routes = [...routes]; - this.fallback = options?.fallback; - - // host has to be added *after* routes are initialized, otherwise the router's `hostConnected` - // is called before routes are available - (this._host = host).addController(this); - } - - /** - * Returns a URL string of the current route, including parent routes, - * optionally replacing the local path with `pathname`. - */ - link(pathname?: string): string { - if (pathname?.startsWith("/")) { - return pathname; - } - if (pathname?.startsWith(".")) { - throw new Error("Not implemented"); - } - pathname ??= this._currentPathname; - return (this._parentRoutes?.link() ?? "") + pathname; - } - - initialGoto(pathname: string) { - // TODO (justinfagnani): handle absolute vs relative paths separately. - // TODO (justinfagnani): do we need to detect when goto() is called while - // a previous goto() call is still pending? - - // TODO (justinfagnani): generalize this to handle query params and - // fragments. It currently only handles path names because it's easier to - // completely disregard the origin for now. The click handler only does - // an in-page navigation if the origin matches anyway. - let tailGroup: string | undefined; - - if (this.routes.length === 0 && this.fallback === undefined) { - // If a routes controller has none of its own routes it acts like it has - // one route of `/*` so that it passes the whole pathname as a tail - // match. - tailGroup = pathname; - this._currentPathname = ""; - // Simulate a tail group with the whole pathname - this._currentParams = { 0: tailGroup }; - } else { - const route = this._getRoute(pathname); - if (route === undefined) { - throw new RouterPathNotFoundError(pathname); - } - const pattern = getPattern(route); - const result = pattern.exec({ pathname }); - const params = result?.pathname.groups ?? {}; - tailGroup = getTailGroup(params); - - // Only update route state if the enter handler completes successfully - this._currentRoute = route; - this._currentParams = params; - this._currentPathname = - tailGroup === undefined - ? pathname - : pathname.substring(0, pathname.length - tailGroup.length); - } - - // Propagate the tail match to children - if (tailGroup !== undefined) { - for (const childRoutes of this._childRoutes) { - childRoutes.goto(tailGroup); - } - } - } - - /** - * Navigates this routes controller to `pathname`. - * - * This does not navigate parent routes, so it isn't (yet) a general page - * navigation API. It does navigate child routes if pathname matches a - * pattern with a tail wildcard pattern (`/*`). - */ - async goto(pathname: string) { - // TODO (justinfagnani): handle absolute vs relative paths separately. - // TODO (justinfagnani): do we need to detect when goto() is called while - // a previous goto() call is still pending? - - // TODO (justinfagnani): generalize this to handle query params and - // fragments. It currently only handles path names because it's easier to - // completely disregard the origin for now. The click handler only does - // an in-page navigation if the origin matches anyway. - let tailGroup: string | undefined; - - if (this.routes.length === 0 && this.fallback === undefined) { - // If a routes controller has none of its own routes it acts like it has - // one route of `/*` so that it passes the whole pathname as a tail - // match. - tailGroup = pathname; - this._currentPathname = ""; - // Simulate a tail group with the whole pathname - this._currentParams = { 0: tailGroup }; - } else { - const route = this._getRoute(pathname); - if (route === undefined) { - throw new RouterPathNotFoundError(pathname); - } - const pattern = getPattern(route); - const result = pattern.exec({ pathname }); - const params = result?.pathname.groups ?? {}; - tailGroup = getTailGroup(params); - if (typeof route.enter === "function") { - const success = await route.enter(params); - // If enter() returns false, cancel this navigation - if (!success) { - return; - } - } - // Only update route state if the enter handler completes successfully - this._currentRoute = route; - this._currentParams = params; - this._currentPathname = - tailGroup === undefined - ? pathname - : pathname.substring(0, pathname.length - tailGroup.length); - } - - // Propagate the tail match to children - if (tailGroup !== undefined) { - for (const childRoutes of this._childRoutes) { - childRoutes.goto(tailGroup); - } - } - this._host.requestUpdate(); - } - - /** - * The result of calling the current route's render() callback. - */ - outlet() { - return this._currentRoute?.render?.(this._currentParams); - } - - /** - * The current parsed route parameters. - */ - get params() { - return this._currentParams; - } - - /** - * Matches `url` against the installed routes and returns the first match. - */ - private _getRoute(pathname: string): RouteConfig | undefined { - const matchedRoute = this.routes.find((r) => - getPattern(r).test({ pathname: pathname }) - ); - if (matchedRoute || this.fallback === undefined) { - return matchedRoute; - } - if (this.fallback) { - // The fallback route behaves like it has a "/*" path. This is hidden from - // the public API but is added here to return a valid RouteConfig. - return { ...this.fallback, path: "/*" }; - } - return undefined; - } - - hostConnected() { - this._host.addEventListener( - RoutesConnectedEvent.eventName, - this._onRoutesConnected - ); - const event = new RoutesConnectedEvent(this); - this._host.dispatchEvent(event); - this._onDisconnect = event.onDisconnect; - } - - hostDisconnected() { - // When this child routes controller is disconnected because a parent - // outlet rendered a different template, disconnecting will ensure that - // this controller doesn't receive a tail match meant for another route. - this._onDisconnect?.(); - this._parentRoutes = undefined; - } - - private _onRoutesConnected = (e: RoutesConnectedEvent) => { - // Don't handle the event fired by this routes controller, which we get - // because we do this.dispatchEvent(...) - if (e.routes === this) { - return; - } - - const childRoutes = e.routes; - this._childRoutes.push(childRoutes); - childRoutes._parentRoutes = this; - - e.stopImmediatePropagation(); - e.onDisconnect = () => { - // Remove route from this._childRoutes: - // `>>> 0` converts -1 to 2**32-1 - this._childRoutes?.splice( - this._childRoutes.indexOf(childRoutes) >>> 0, - 1 - ); - }; - - const tailGroup = getTailGroup(this._currentParams); - if (tailGroup !== undefined) { - childRoutes.goto(tailGroup); - } - }; -} - -/** - * Returns the tail of a pathname groups object. This is the match from a - * wildcard at the end of a pathname pattern, like `/foo/*` - */ -const getTailGroup = (groups: { [key: string]: string | undefined }) => { - let tailKey: string | undefined; - for (const key of Object.keys(groups)) { - if (/\d+/.test(key) && (tailKey === undefined || key > tailKey)) { - tailKey = key; - } - } - return tailKey && groups[tailKey]; -}; - -/** - * This event is fired from Routes controllers when their host is connected to - * announce the child route and potentially connect to a parent routes controller. - */ -export class RoutesConnectedEvent extends Event { - static readonly eventName = "lit-routes-connected"; - readonly routes: Routes; - onDisconnect?: () => void; - - constructor(routes: Routes) { - super(RoutesConnectedEvent.eventName, { - bubbles: true, - composed: true, - cancelable: false, - }); - this.routes = routes; - } -} - -declare global { - interface HTMLElementEventMap { - [RoutesConnectedEvent.eventName]: RoutesConnectedEvent; - } -} - -/* eslint-enable @typescript-eslint/no-floating-promises, @typescript-eslint/no-unnecessary-condition, @typescript-eslint/restrict-plus-operands -- ↑ */ diff --git a/project/client/tsconfig.json b/project/client/tsconfig.json deleted file mode 100644 index b683bd7c..00000000 --- a/project/client/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "include": ["src/**/*"], - "compilerOptions": { - "target": "ESNext", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "Node", - "isolatedModules": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "exactOptionalPropertyTypes": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "experimentalDecorators": true, - "useDefineForClassFields": false - } -} diff --git a/project/client/watch.ts b/project/client/watch.ts deleted file mode 100644 index 857d89d3..00000000 --- a/project/client/watch.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { createServer, request, type ServerResponse } from "node:http"; -import { dirname, join, parse } from "node:path"; -import { env } from "node:process"; -import { fileURLToPath } from "node:url"; -import { readFile, stat } from "node:fs/promises"; -import type { AddressInfo } from "node:net"; -import type { Stats } from "node:fs"; - -import { typecheckPlugin } from "@jgoz/esbuild-plugin-typecheck"; -import { lookup } from "mime-types"; -import esbuild from "esbuild"; -import type { BuildResult, Plugin } from "esbuild"; -import { buildOptions } from "./build.config.ts"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const STATIC_DIR = "static"; - -const parsedServePort = parseInt(env.SERVE_PORT ?? "", 10); - -// necessary because 0 is a valid port -const SERVE_PORT = Number.isNaN(parsedServePort) ? 8080 : parsedServePort; -const HOSTNAME = "localhost"; - -const stripPrefix = (prefixed: string) => - prefixed.slice(__dirname.length + 1 + STATIC_DIR.length + 1); - -const clients: ServerResponse[] = []; - -let result: BuildResult | undefined; - -const watchServer = createServer((_, res) => { - return clients.push( - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Access-Control-Allow-Origin": "*", - Connection: "keep-alive", - }) - ); -}).listen(0); - -const API_BASE_URL = env.API_BASE_URL - ? env.API_BASE_URL - : `http://${HOSTNAME}:3000`; -const PARSED_API_BASE_URL = new URL(API_BASE_URL); - -const outputFile = (outputFiles: esbuild.OutputFile[], name: string) => { - for (const file of outputFiles) { - // `esbuild` prefixes artifacts with the absolute path to the working directory - const stripped = stripPrefix(file.path); - if (stripped === name) { - return file; - } - } - - return null; -}; - -const serveServer = createServer(async (req, res) => { - const url = new URL(req.url!, `http://${req.headers.host}`); - - const path = url.pathname; - const parsed = parse(path); - - // proxy requests to api to work around CORS - if (parsed.dir.startsWith("/api/")) { - const proxy = request( - { - hostname: PARSED_API_BASE_URL.hostname, - port: PARSED_API_BASE_URL.port, - path: req.url, - method: req.method, - headers: req.headers, - }, - (proxied) => { - if (typeof proxied.statusCode === "undefined") { - return; - } - - res.writeHead(proxied.statusCode, proxied.headers); - proxied.pipe(res, { end: true }); - } - ); - - req.pipe(proxy); - return; - } - - // nuke CORS - res.setHeader("access-control-allow-origin", "*"); - - const staticPrefix = `/${STATIC_DIR}`; - - if (parsed.dir.startsWith(staticPrefix)) { - const contentType = lookup(parsed.ext); - if (!contentType) { - console.error(`mime type unknown for extension "${parsed.ext}"`); - res.writeHead(500, { "Content-Type": "text/plain" }); - res.end("unsupported content type"); - return; - } - - if (parsed.dir === staticPrefix) { - const name = parsed.name + parsed.ext; - const file = outputFile(result?.outputFiles ?? [], name); - if (file !== null) { - res.writeHead(200, { - "Content-Type": contentType, - "Content-Length": file.contents.byteLength, - }); - res.write(file.contents); - res.end(); - - return; - } - } - - // serve directly from public folder as fallback - // mostly for favicons, as there isn't an easy way to include static assets in `outputFiles` - fallback: { - const resolved = join("public", path.slice(staticPrefix.length)); - - let stats: Stats; - try { - stats = await stat(resolved); - } catch { - break fallback; - } - - if (!stats.isFile()) { - break fallback; - } - - const content = await readFile(resolved); - res.writeHead(200, { - "Content-Type": contentType, - "Content-Length": content.length, - }); - res.write(content); - res.end(); - - return; - } - - res.writeHead(404); - res.end("not found"); - } else { - const errors = result?.errors ?? []; - const warnings = result?.warnings ?? []; - - const root = - errors.length > 0 - ? `
${errors.map((item) => JSON.stringify(item)).join("\n")}
` - : ` - - `; - - const data = ` - - - - - - - - - -${root} - - - - - - - - `; - - res.writeHead(200); - res.end(data); - } -}).listen(SERVE_PORT); - -const watchPort = (watchServer.address() as AddressInfo).port; -const servePort = (serveServer.address() as AddressInfo).port; - -const SERVE_URL = env.SERVE_URL || `http://${HOSTNAME}:${servePort}`; - -const sentinelPlugin: Plugin = { - name: "sentinel", - setup: (build) => { - build.onEnd((r) => { - result = r; - - for (const client of clients) { - client.write("data: update\n\n"); - } - }); - }, -}; - -(async () => { - const context = await esbuild.context({ - ...buildOptions, - logLevel: "info", - platform: "browser", - minify: false, - publicPath: `${SERVE_URL}/${STATIC_DIR}`, - outdir: STATIC_DIR, - write: false, - define: { - API_BASE_URL: `"${SERVE_URL}"`, - SSR: "false", - }, - plugins: [ - ...buildOptions.plugins, - typecheckPlugin({ omitStartLog: true, watch: true }), - sentinelPlugin, - ], - }); - - // initial build - result = await context.rebuild(); - - console.log(`[watch] running on: ${SERVE_URL}`); - - // enable watch mode - await context.watch(); -})(); diff --git a/project/server/.codebook.toml b/project/server/.codebook.toml index fa7d8609..34d8e612 100644 --- a/project/server/.codebook.toml +++ b/project/server/.codebook.toml @@ -12,6 +12,7 @@ words = [ "despawn", "despawn", "despawned", + "despawning", "effectful", "errno", "fns", @@ -20,6 +21,7 @@ words = [ "hono", "lifecycle", "mountpoint", + "nestjs", "ohf", "pgsize", "postflight", @@ -31,6 +33,7 @@ words = [ "suspendable", "suspendable", "timespan", + "unconfigured", "unhashed", "unkeyed", "vfs", diff --git a/project/server/Dockerfile b/project/server/Dockerfile index e3dd2feb..17fe6cac 100644 --- a/project/server/Dockerfile +++ b/project/server/Dockerfile @@ -27,14 +27,11 @@ WORKDIR /app COPY package.json ./package.json COPY package-lock.json ./package-lock.json -COPY --from=client package.json ./package/client/package.json -COPY --from=client package-lock.json ./package/client/package-lock.json RUN npm ci --verbose # `COPY` copies directory content, not the directory itself COPY . ./ -COPY --from=client . ./package/client COPY --from=schema-builder /schema/out/schema.json src/ COPY --from=schema-builder /schema/out/typescript/schema.ts src/ diff --git a/project/server/build/client-csr.ts b/project/server/build/client-csr.ts deleted file mode 100644 index 88aa9530..00000000 --- a/project/server/build/client-csr.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { writeFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; - -import esbuild from "esbuild"; - -// this only works if `client` is installed via symlink -// https://docs.npmjs.com/cli/v8/commands/npm-link#caveat -import { buildOptions } from "client/build.config.ts"; -import { OUT_DIR } from "./base.ts"; -import { formatNs } from "../src/utility/format.ts"; - -(async () => { - console.log("[\x1b[36mclient-csr\x1b[0m] building..."); - let start = process.hrtime.bigint(); - - const result = await esbuild.build({ - ...buildOptions, - minify: true, - minifySyntax: true, - sourcemap: false, - metafile: true, - publicPath: "/static", - outdir: join(resolve(OUT_DIR), "client-csr"), - define: { - API_BASE_URL: '""', - SSR: "false", - } - }); - - await writeFile( - join(resolve(OUT_DIR), "client-csr", "meta.json"), - JSON.stringify(result.metafile), - ); - - const end = process.hrtime.bigint(); - - console.log( - `[\x1b[36mclient-csr\x1b[0m] built in ${formatNs(end - start)}s`, - ); -})().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/project/server/build/client-ssr.ts b/project/server/build/client-ssr.ts deleted file mode 100644 index fab50dd5..00000000 --- a/project/server/build/client-ssr.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { join, resolve } from "node:path"; - -import esbuild from "esbuild"; - -// this only works if `client` is installed via symlink -// https://docs.npmjs.com/cli/v8/commands/npm-link#caveat -import { buildOptions } from "client/build.config.ts"; -import { OUT_DIR } from "./base.ts"; -import { formatNs } from "../src/utility/format.ts"; - -(async () => { - console.log("[\x1b[36mclient-ssr\x1b[0m] building..."); - - const start = process.hrtime.bigint(); - - await esbuild.build({ - ...buildOptions, - entryPoints: ["src/entrypoint.ts"], - outExtension: { ".js": ".mjs" }, - platform: "node", - splitting: false, - minify: false, - publicPath: "/static", - sourcemap: "inline", - outdir: join(resolve(OUT_DIR), "client-ssr"), - define: { - API_BASE_URL: '""', - SSR: "true" - }, - // lit expects `TextEncoder` and `TextDecoder` to be defined globally, which they aren't in node.js - banner: { - js: - 'const { TextEncoder, TextDecoder } = await import("util");', - }, - }); - - const end = process.hrtime.bigint(); - - console.log( - `[\x1b[36mclient-ssr\x1b[0m] built in ${formatNs(end-start)}s`, - ); -})().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/project/server/build/server.ts b/project/server/build/server.ts index f126c4d5..1a95aa4b 100644 --- a/project/server/build/server.ts +++ b/project/server/build/server.ts @@ -46,6 +46,7 @@ export const copyPlugin = ( const result = await esbuild.build({ entryPoints: [ { out: "main", in: "src/entrypoint.ts" }, + { out: "main-nest", in: "src/entrypoint-nest.ts" }, { out: "worker-database", in: "src/service/database/worker.ts" }, { out: "repl", in: "src/repl.ts" } ], diff --git a/project/server/docker/entrypoint.sh b/project/server/docker/entrypoint.sh index 1fd5728f..502973c3 100755 --- a/project/server/docker/entrypoint.sh +++ b/project/server/docker/entrypoint.sh @@ -4,4 +4,14 @@ set -e export NODE_OPTIONS="${NODE_OPTIONS:-$(node docker/node-options.ts)}" -node --enable-source-maps out/server/main.mjs +case "${NODE_ENTRYPOINT}" in + "nest") + node --enable-source-maps out/server/main-nest.mjs + ;; + "default" | "") + node --enable-source-maps out/server/main.mjs + ;; + *) + echo "unknown entrypoint \"${NODE_ENTRYPOINT}\"" + ;; +esac diff --git a/project/server/make/common.mk b/project/server/make/common.mk index 2f121a23..aa55b426 100644 --- a/project/server/make/common.mk +++ b/project/server/make/common.mk @@ -2,12 +2,6 @@ SQLC_BIN ?= ../../.ephemeral/go/bin/sqlc -CLIENT_IN := \ - $(shell find -L package/client/src -type f) - -CLIENT_CSR_OUT := out/client-csr/entrypoint.js -CLIENT_SSR_OUT := out/client-ssr/entrypoint.mjs - SERVER_QUERY_QUERY_DIR := src/service/database/query SERVER_QUERY_QUERY_IN := $(wildcard $(SERVER_QUERY_QUERY_DIR)/**/*.sql) SERVER_QUERY_SCHEMA_DIR := src/service/database/schema @@ -27,12 +21,13 @@ SERVER_IN := \ tsconfig.json \ $(realpath src/schema.ts) SERVER_OUT_MAIN := out/server/main.mjs +SERVER_OUT_MAIN_NEST := out/server/main-nest.mjs SERVER_OUT_REPL := out/server/repl.mjs -SERVER_OUT := $(SERVER_OUT_MAIN) +SERVER_OUT := $(SERVER_OUT_MAIN) $(SERVER_OUT_MAIN_NEST) $(SERVER_OUT_REPL) .PRECIOUS: $(SERVER_QUERY_OUT) -BUILD_OUT := $(SERVER_OUT) $(CLIENT_CSR_OUT) $(CLIENT_SSR_OUT) +BUILD_OUT := $(SERVER_OUT) IMAGE_TAG := ohf-device-database/device-database @@ -57,13 +52,5 @@ $(subst .,%,$(SERVER_OUT)): $(SERVER_IN) @npm exec -- tsc --project tsconfig.json --incremental --noEmit @node --experimental-strip-types --disable-warning=ExperimentalWarning build/server.ts -$(CLIENT_CSR_OUT): $(CLIENT_IN) - @npm exec --prefix package/client -- tsc --project tsconfig.json --incremental --noEmit - @node --experimental-strip-types --disable-warning=ExperimentalWarning build/client-csr.ts - -$(CLIENT_SSR_OUT): $(CLIENT_IN) - @npm exec --prefix package/client -- tsc --project tsconfig.json --incremental --noEmit - @node --experimental-strip-types --disable-warning=ExperimentalWarning build/client-ssr.ts - clean: $(RM) -r $(BUILD_OUT) diff --git a/project/server/make/dev.mk b/project/server/make/dev.mk index 363b1a4c..29fadde4 100644 --- a/project/server/make/dev.mk +++ b/project/server/make/dev.mk @@ -33,12 +33,18 @@ export SNAPSHOT_DEFER_OBJECT_STORE_ENDPOINT ?= http://127.0.0.1:$(SNAPSHOT_DEFER secret = node --experimental-strip-types script/secret.ts --name '$(1)' --kind '$(2)' export NODE_OPTIONS ?= "--disable-warning=ExperimentalWarning" +NODE_ENTRYPOINT ?= default start: build @ \ SIGNING_VOUCHER=$(shell $(call secret,voucher,signing-key)) \ node --enable-source-maps $(SERVER_OUT_MAIN) +start-nest: build + @ \ + SIGNING_VOUCHER=$(shell $(call secret,voucher,signing-key)) \ + node --enable-source-maps $(SERVER_OUT_MAIN_NEST) + repl: build @ \ SIGNING_VOUCHER=$(shell $(call secret,voucher,signing-key)) \ @@ -54,6 +60,7 @@ start-container: -e DATABASE_PATH_DERIVED='$(CONTAINER_DATABASE_PATH_DERIVED)' \ -e INITIALLY_CONCURRENT='true' \ -e NODE_OPTIONS='$(NODE_OPTIONS)' \ + -e NODE_ENTRYPOINT='$(NODE_ENTRYPOINT)' \ -e EXTERNAL_AUTHORITY='$(EXTERNAL_AUTHORITY)' \ -e SIGNING_VOUCHER='$(shell $(call secret,voucher,signing-key))' \ -e SNAPSHOT_DEFER_OBJECT_STORE_BUCKET='none' \ diff --git a/project/server/make/ops.mk b/project/server/make/ops.mk index 2881e4ce..e74f08f6 100644 --- a/project/server/make/ops.mk +++ b/project/server/make/ops.mk @@ -6,7 +6,6 @@ build-container: docker build \ -t '$(IMAGE_TAG):latest' \ -f Dockerfile \ - --build-context client='$(shell readlink -f package/client)' \ --build-context schema=../../schema \ --build-context sqlc-plugin='$(shell dirname $(shell readlink -f plugin.wasm))' \ . diff --git a/project/server/nest-cli.json b/project/server/nest-cli.json new file mode 100644 index 00000000..ecdd5de5 --- /dev/null +++ b/project/server/nest-cli.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src/layer", + "entryFile": "../entrypoint-nest", + "compilerOptions": { + "deleteOutDir": true, + "tsConfigPath": "tsconfig.json" + }, + "generateOptions": { + "spec": false + } +} diff --git a/project/server/package-lock.json b/project/server/package-lock.json index 23c0824f..f7df1566 100644 --- a/project/server/package-lock.json +++ b/project/server/package-lock.json @@ -9,15 +9,18 @@ "@aws-sdk/client-s3": "^3.893.0", "@aws-sdk/lib-storage": "^3.893.0", "@hono/node-server": "^1.19.1", - "@lit-labs/ssr": "^3.3.1", "@lppedd/di-wise-neo": "^0.11.1", + "@nestjs/cli": "^11.0.24", + "@nestjs/common": "^11.2.2", + "@nestjs/core": "^11.2.2", + "@nestjs/platform-express": "^11.2.3", "cron-parser": "^5.5.0", "date-fns": "^4.1.0", "effect": "^3.17.13", "hono": "^4.9.6", "prom-client": "^15.1.3", + "reflect-metadata": "^0.2.2", "safe-stable-stringify": "^2.5.0", - "serialize-javascript": "^7.0.0", "stream-json": "^1.9.1", "swagger-ui-dist": "^5.28.1", "undici": "^7.16.0", @@ -25,17 +28,130 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.2", + "@types/express": "^5.0.6", "@types/node": "^24.3.0", - "@types/serialize-javascript": "^5.0.4", "@types/stream-json": "^1.7.8", "@types/swagger-ui-dist": "^3.30.6", - "client": "file:package/client", "esbuild": "^0.25.9", "tsx": "^4.20.6", "type-fest": "^5.0.1", "typescript": "^5.9.2" } }, + "node_modules/@angular-devkit/core": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.27.tgz", + "integrity": "sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==", + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.27.tgz", + "integrity": "sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.27.tgz", + "integrity": "sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -884,13 +1000,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.8.tgz", - "integrity": "sha512-Ql8elcUdYCha83Ol7NznBsgN5GVZnv3vUd86fEc6waU6oUdY0T1O9NODkEEOS/Uaogr87avDrUC6DSeM4oXjZg==", + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz", + "integrity": "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", - "fast-xml-parser": "5.3.6", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { @@ -906,6 +1021,29 @@ "node": ">=18.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@biomejs/biome": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.4.tgz", @@ -1069,6 +1207,16 @@ "node": ">=14.21.3" } }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -1089,40 +1237,6 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -1565,630 +1679,621 @@ "node": ">=18" } }, - "node_modules/@eslint-community/eslint-plugin-eslint-comments": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.5.0.tgz", - "integrity": "sha512-MAhuTKlr4y/CE3WYX26raZjy+I/kS2PLKSzvfmDCGrBLTFHOYwqROZdr4XwPgXwX3K9rjzMr4pSmUWGnzsUyMg==", - "dev": true, + "node_modules/@hono/node-server": { + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", "license": "MIT", - "dependencies": { - "escape-string-regexp": "^4.0.0", - "ignore": "^5.2.4" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18.14.1" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + "hono": "^4" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "@types/node": ">=18" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "license": "MIT", "dependencies": { - "@eslint/core": "^0.16.0" + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", - "dev": true, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, - "funding": { - "url": "https://eslint.org/donate" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "license": "MIT", "dependencies": { - "@eslint/core": "^0.16.0", - "levn": "^0.4.1" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@hono/node-server": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.1.tgz", - "integrity": "sha512-h44e5s+ByUriaRIbeS/C74O8v90m0A95luyYQGMF7KEn96KkYMXO7bZAwombzTpjQTU4e0TkU8U1WBIXlwuwtA==", + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, "engines": { - "node": ">=18.14.1" + "node": ">=18" }, "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=18.18.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, "engines": { - "node": ">=12.22" + "node": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, "engines": { - "node": ">=18.18" + "node": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jgoz/esbuild-plugin-typecheck": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@jgoz/esbuild-plugin-typecheck/-/esbuild-plugin-typecheck-4.0.3.tgz", - "integrity": "sha512-tJzjV3pALNuEQ3+w18jt58Y5MogN+Hm2vmEUR2EcLr9+5PR/X0JLAAg0+6AOw168GRZgKzWKK4zdzY9uMT708Q==", - "dev": true, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, "peerDependencies": { - "@jgoz/esbuild-plugin-livereload": ">=2.1.3", - "esbuild": "0.17.x || 0.18.x || 0.19.x || 0.20.x || 0.21.x || 0.22.x || 0.23.x || 0.24.x || 0.25.x", - "typescript": ">= 3.5" + "@types/node": ">=18" }, "peerDependenciesMeta": { - "@jgoz/esbuild-plugin-livereload": { + "@types/node": { "optional": true } } }, - "node_modules/@lit-labs/ssr": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.3.1.tgz", - "integrity": "sha512-JlF1PempxvzrGEpRFrF+Ki0MHzR3HA51SK8Zv0cFpW9p0bPW4k0FeCwrElCu371UEpXF7RcaE2wgYaE1az0XKg==", - "license": "BSD-3-Clause", + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "license": "MIT", "dependencies": { - "@lit-labs/ssr-client": "^1.1.7", - "@lit-labs/ssr-dom-shim": "^1.3.0", - "@lit/reactive-element": "^2.0.4", - "@parse5/tools": "^0.3.0", - "@types/node": "^16.0.0", - "enhanced-resolve": "^5.10.0", - "lit": "^3.1.2", - "lit-element": "^4.0.4", - "lit-html": "^3.1.2", - "node-fetch": "^3.2.8", - "parse5": "^7.1.1" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": ">=13.9.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@lit-labs/ssr-client": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.7.tgz", - "integrity": "sha512-VvqhY/iif3FHrlhkzEPsuX/7h/NqnfxLwVf0p8ghNIlKegRyRqgeaJevZ57s/u/LiFyKgqksRP5n+LmNvpxN+A==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.0.4", - "lit": "^3.1.2", - "lit-html": "^3.1.2" - } - }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", - "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit-labs/ssr/node_modules/@types/node": { - "version": "16.18.126", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", - "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", - "license": "MIT" - }, - "node_modules/@lit/context": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.6.tgz", - "integrity": "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^1.6.2 || ^2.1.0" - } - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz", - "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.4.0" - } - }, - "node_modules/@lit/task": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lit/task/-/task-1.0.3.tgz", - "integrity": "sha512-1gJGJl8WON+2j0y9xfcD+XsS1rvcy3XDgsIhcdUW++yTR8ESjZW6o7dn8M8a4SZM8NnJe6ynS2cKWwsbfLOurg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^1.0.0 || ^2.0.0" - } - }, - "node_modules/@lppedd/di-wise-neo": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@lppedd/di-wise-neo/-/di-wise-neo-0.11.1.tgz", - "integrity": "sha512-Z4gNiiZWjjE0X8rfu6ce1ip82JQIhWjwp/9Of0o/sOj4oQUKLAWlxxsHejE1cR4UdGTs/MjAJ14P4Bgv80J/JA==", - "license": "MIT" - }, - "node_modules/@napi-rs/image": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image/-/image-1.11.2.tgz", - "integrity": "sha512-i5zlU1EgNBlgRjxMC1CgClZlHyGGnR1upLV64s8t5N+w9/lB7plHcb+rBJ5YmSP9Mho5RvLQpZ/ScaoJNNcnNg==", - "dev": true, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "peerDependencies": { + "@types/node": ">=18" }, - "optionalDependencies": { - "@napi-rs/image-android-arm64": "1.11.2", - "@napi-rs/image-darwin-arm64": "1.11.2", - "@napi-rs/image-darwin-x64": "1.11.2", - "@napi-rs/image-freebsd-x64": "1.11.2", - "@napi-rs/image-linux-arm-gnueabihf": "1.11.2", - "@napi-rs/image-linux-arm64-gnu": "1.11.2", - "@napi-rs/image-linux-arm64-musl": "1.11.2", - "@napi-rs/image-linux-x64-gnu": "1.11.2", - "@napi-rs/image-linux-x64-musl": "1.11.2", - "@napi-rs/image-wasm32-wasi": "1.11.2", - "@napi-rs/image-win32-arm64-msvc": "1.11.2", - "@napi-rs/image-win32-ia32-msvc": "1.11.2", - "@napi-rs/image-win32-x64-msvc": "1.11.2" - } - }, - "node_modules/@napi-rs/image-android-arm64": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-android-arm64/-/image-android-arm64-1.11.2.tgz", - "integrity": "sha512-EUkTeYEayZn9IyzXcn8m5t0MCsiN08+SsPJBhWQR05pQiiuonBRAYuZWB3hJDVHKfKKXogzMRShcTAE70NXGzA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@napi-rs/image-darwin-arm64": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-darwin-arm64/-/image-darwin-arm64-1.11.2.tgz", - "integrity": "sha512-RLnv2bbvkDwaROZHqUEozSto5nE3mhlIS9U2WGHJwepYUneq0gMumibtzp7YjEl6coEgqKnUDoATzRnUovkHqw==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@napi-rs/image-darwin-x64": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-darwin-x64/-/image-darwin-x64-1.11.2.tgz", - "integrity": "sha512-uptDhysXHSB3OTetz15CVeqf0LyeXpUnzqmVFhQimdzTEDkG7CyasUWNM29DOn2XTlZESdwAvg+2YUiQqt0wqA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">= 10" + "node": ">=6.0.0" } }, - "node_modules/@napi-rs/image-freebsd-x64": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-freebsd-x64/-/image-freebsd-x64-1.11.2.tgz", - "integrity": "sha512-W6Sk8MKjS95OVZ0TWurUttF0Kt/chRuZkFKF4iPAbwcW/tk92sIJlBi+7VNMvpjVr8xLTbPkzaPIs2jes19Tbg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/@napi-rs/image-linux-arm-gnueabihf": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm-gnueabihf/-/image-linux-arm-gnueabihf-1.11.2.tgz", - "integrity": "sha512-Oj2l9DWig3d3wEmkX4j6Ecg3H2ElT+n5u5TZ47xm0vEZT0QBb4dcqhVvWx6RTn4rLLU4sxAlwEtb3SR2UmpZwQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, - "node_modules/@napi-rs/image-linux-arm64-gnu": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm64-gnu/-/image-linux-arm64-gnu-1.11.2.tgz", - "integrity": "sha512-/ntkbFvrP4ERrGFJ32PupmYuZxhCoqfSN9y9Nao86kGdxCASjS2zecufDw2IjgUhD8CBigD1o9aoA74lKQbuOw==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/image-linux-arm64-musl": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-arm64-musl/-/image-linux-arm64-musl-1.11.2.tgz", - "integrity": "sha512-8N+PlYpTVMEAyaOHqpx3wtur34TxBvF0YI4i9Cv0zJttzlNgsUOZjFCxVOeH+QVzvz+XOqTb4BR1vYpethFqVQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } + "node_modules/@lppedd/di-wise-neo": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@lppedd/di-wise-neo/-/di-wise-neo-0.11.1.tgz", + "integrity": "sha512-Z4gNiiZWjjE0X8rfu6ce1ip82JQIhWjwp/9Of0o/sOj4oQUKLAWlxxsHejE1cR4UdGTs/MjAJ14P4Bgv80J/JA==", + "license": "MIT" }, - "node_modules/@napi-rs/image-linux-x64-gnu": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-x64-gnu/-/image-linux-x64-gnu-1.11.2.tgz", - "integrity": "sha512-0F/nDFW2UcidE9qh6+M8Ew4a8GIraX2C71xuvYb+dIEAT4lFoqfNWatxKBOmnZFWpsrO/taRhWUBFXJlG7uOyw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/@napi-rs/image-linux-x64-musl": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-linux-x64-musl/-/image-linux-x64-musl-1.11.2.tgz", - "integrity": "sha512-AZEXCQUfmrZXiPQHfTgQE0xTsz4Ox4p15Q5IUM02sCZTvNLKflbE+WmJ2dEIh78q6V2QomUKDVTx7Q6pCZylIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@nestjs/cli": { + "version": "11.0.24", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.24.tgz", + "integrity": "sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@angular-devkit/schematics-cli": "19.2.27", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.106.2", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, "engines": { - "node": ">= 10" + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } } }, - "node_modules/@napi-rs/image-wasm32-wasi": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-wasm32-wasi/-/image-wasm32-wasi-1.11.2.tgz", - "integrity": "sha512-JM/ZFveVEGIBFyIberr1RTp7FHZHAbZFwCLZ7eNbUE5ujpufExkvICAB2uW4a02ZqTwdXTuI2CxmdrUhhN/XkQ==", - "cpu": [ - "wasm32" - ], - "dev": true, + "node_modules/@nestjs/common": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.2.2.tgz", + "integrity": "sha512-U/qJ1cl/rpcN/P0yYNEy1m37fb5rQdu2x2tXVUHmuDCpx5es+9aqKy8odgltpZsQAl4rY5ZwftTXzhjahcQKnw==", "license": "MIT", - "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.3" + "file-type": "21.3.4", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@napi-rs/image-win32-arm64-msvc": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-arm64-msvc/-/image-win32-arm64-msvc-1.11.2.tgz", - "integrity": "sha512-JTWYu5m+a+Pi8nN3jI6c5NJV5gJvpSYoN0kBml2QMQWVktwcDQ2pC1yiTzmDSa+FnFsT57BcSm5S6yhj+42wjQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } } }, - "node_modules/@napi-rs/image-win32-ia32-msvc": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-ia32-msvc/-/image-win32-ia32-msvc-1.11.2.tgz", - "integrity": "sha512-tjTiyeoGD+vk6nt2fagT00+SVV6uvkAsC6CK72up0YQcKTNYBbnpM6K/yUmSDtB708brjo+xtMJ5rwYEpsyeRQ==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/@nestjs/core": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.2.2.tgz", + "integrity": "sha512-U2stdm6un1f5UcbfDMjxtVRnq933kREURc1D8cKSmtz22WOiD2eshgzdvPRSAYWWaWvVAW3Ed62yMlaHFWraig==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, "engines": { - "node": ">= 10" + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } } }, - "node_modules/@napi-rs/image-win32-x64-msvc": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@napi-rs/image-win32-x64-msvc/-/image-win32-x64-msvc-1.11.2.tgz", - "integrity": "sha512-HhGEXPHyuf7EeWgSct1/0B4vy7KckALgI4HCwv2PHisvKcp/v/hcPTMHPCkfwtEe7uBDAugcEuEaMaKZnTP9jQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@nestjs/platform-express": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.2.3.tgz", + "integrity": "sha512-YFQvRXT2de1qNL9LJPUBQ31+RsfI4cJ+sbpU9ENM/hDCgoHSEhm7oxUuGGKmhTZBNZEYm8mDYdfoTFmAH1LIJg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.2.0", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "dev": true, + "node_modules/@nestjs/schematics": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz", + "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==", "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "comment-json": "5.0.0", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "typescript": ">=4.8.2" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz", + "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" }, "engines": { - "node": ">= 8" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz", + "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==", "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, "engines": { - "node": ">= 8" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" + "tslib": "^2.1.0" } }, "node_modules/@opentelemetry/api": { @@ -2200,28 +2305,6 @@ "node": ">=8.0.0" } }, - "node_modules/@parse5/tools": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz", - "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==", - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -2717,9 +2800,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", - "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2967,21 +3050,105 @@ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" - } - }, + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, "license": "MIT" }, @@ -2989,33 +3156,52 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "24.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.14.0" } }, - "node_modules/@types/serialize-javascript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/serialize-javascript/-/serialize-javascript-5.0.4.tgz", - "integrity": "sha512-Z2R7UKFuNWCP8eoa2o9e5rkD3hmWxx/1L0CYz0k2BZzGh0PhEVMp9kfGiqEml/0IglwNERXZ2hwNzIrSz/KHTA==", + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, "license": "MIT" }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/stream-chain": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/stream-chain/-/stream-chain-2.1.0.tgz", @@ -3050,328 +3236,268 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "license": "MIT" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", - "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", - "dev": true, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/type-utils": "8.46.1", - "@typescript-eslint/utils": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.46.1", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", - "engines": { - "node": ">= 4" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", - "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", - "dev": true, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@xtuc/ieee754": "^1.2.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", - "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", - "dev": true, - "license": "MIT", + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.1", - "@typescript-eslint/types": "^8.46.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "@xtuc/long": "4.2.2" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", - "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", - "dev": true, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", - "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", - "dev": true, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", - "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", - "dev": true, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/utils": "8.46.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", - "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", - "dev": true, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", - "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", - "dev": true, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.1", - "@typescript-eslint/tsconfig-utils": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/visitor-keys": "8.46.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, "engines": { - "node": "18 || 20 || >=22" + "node": ">= 0.6" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "dev": true, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=0.4.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", - "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^5.0.2" - }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10.13.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "acorn": "^8.14.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", - "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", - "dev": true, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.1", - "@typescript-eslint/types": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "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": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", - "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", - "dev": true, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.1", - "eslint-visitor-keys": "^4.2.1" + "ajv": "^8.0.0" }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "peerDependencies": { + "ajv": "^8.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "fast-deep-equal": "^3.1.3" }, - "engines": { - "node": ">=0.4.0" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">=6" } }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3383,13 +3509,33 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "license": "MIT" + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -3400,7 +3546,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -3423,47 +3568,119 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz", + "integrity": "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", "license": "MIT" }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "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==", - "dev": true, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=8" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/buffer": { @@ -3476,21 +3693,94 @@ "ieee754": "^1.1.4" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -3503,9 +3793,102 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/client": { - "resolved": "package/client", - "link": true + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } }, "node_modules/color": { "version": "3.2.1", @@ -3521,7 +3904,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3572,136 +3954,141 @@ } }, "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, "engines": { - "node": ">=16" + "node": ">= 6" } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, - "node_modules/cron-parser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.5.0.tgz", - "integrity": "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==", + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], "license": "MIT", "dependencies": { - "luxon": "^3.7.1" - }, - "engines": { - "node": ">=18" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, "engines": { - "node": ">= 8" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/fb55" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "dev": true, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "node": ">= 0.6" } }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "node": ">= 0.6" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { - "css-tree": "~2.2.0" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "license": "MIT", "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "node_modules/cron-parser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.5.0.tgz", + "integrity": "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==", "license": "MIT", + "dependencies": { + "luxon": "^3.7.1" + }, "engines": { - "node": ">= 12" + "node": ">=18" } }, "node_modules/date-fns": { @@ -3718,7 +4105,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3732,124 +4118,155 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "clone": "^1.0.2" }, "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">= 0.8" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "node": ">= 0.4" } }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/effect": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.22.1.tgz", + "integrity": "sha512-TNoXushmPOBAjJlthF5d2QwnX2xBPEtcNJr5XKNKbRLbDvBcOYkXlYDfvGfSA0zriwLFuCll5MDtNMAdZL17PQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.414", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.414.tgz", + "integrity": "sha512-aYlviXiaXBbzvKgyALpcMmqa3Np3sDr0XnZbEG62n2UpZFbEcjQ4EEMOLGzVPhwVnwTz0lvKY+GcARbunuHekw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { - "node": ">=0.12" + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "engines": { + "node": ">= 0.4" } }, "node_modules/esbuild": { @@ -3894,231 +4311,51 @@ "@esbuild/win32-x64": "0.25.9" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", - "@eslint/core": "^0.16.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", - "@eslint/plugin-kit": "^0.4.0", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-lit": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-2.1.1.tgz", - "integrity": "sha512-qmyAOnnTCdS+vDnNxtCoF0icSKIio4GUv6ZLnaCtTX6G/YezRa6Ag6tOQ+MfV5Elvtw9CIXeliRX4mIBSwrPIA==", - "dev": true, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", - "dependencies": { - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1" - }, "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "eslint": ">= 8" + "node": ">=6" } }, - "node_modules/eslint-plugin-lit/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", - "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.7" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-wc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-wc/-/eslint-plugin-wc-3.0.2.tgz", - "integrity": "sha512-siwTrxPTw6GU2JmP3faInw8nhi0ZCnKsiSRM3j7EAkZmBTGYdDAToeseLYsvPrc5Urp/vPz+g7Ewh7XcICLxww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-valid-element-name": "^1.0.0", - "js-levenshtein-esm": "^2.0.0" - }, - "peerDependencies": { - "eslint": ">=8.40.0" - } - }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "estraverse": "^4.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=8.0.0" } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": ">=0.10" + "node": ">=4" } }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -4127,24 +4364,31 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { + "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, "node_modules/events": { @@ -4156,6 +4400,49 @@ "node": ">=0.8.x" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -4182,87 +4469,35 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/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==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, - "node_modules/fast-xml-parser": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", - "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", + "node_modules/fast-uri": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", "funding": [ { "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" } ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } + "license": "BSD-3-Clause" }, "node_modules/fecha": { "version": "4.2.3", @@ -4270,111 +4505,116 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "license": "MIT", "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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==", - "dev": true, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" }, "engines": { - "node": ">=8" + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, "engines": { - "node": ">=16" + "node": ">= 0.8" } }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "license": "MIT", "dependencies": { - "fetch-blob": "^3.1.2" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=12.20.0" + "node": ">=12" } }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "license": "Unlicense" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4390,43 +4630,121 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/get-tsconfig": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", - "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", - "dev": true, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.4" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "node_modules/glob": { + "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": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "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" + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { @@ -4435,32 +4753,84 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hono": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz", - "integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.4.tgz", + "integrity": "sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ==", "license": "MIT", "engines": { "node": ">=16.9.0" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4481,34 +4851,10 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/image-size": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", - "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", - "dev": true, - "license": "MIT", - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -4521,66 +4867,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "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==", - "dev": true, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=8" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, "node_modules/is-stream": { @@ -4595,35 +4924,76 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-valid-element-name": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-element-name/-/is-valid-element-name-1.0.0.tgz", - "integrity": "sha512-GZITEJY2LkSjQfaIPBha7eyZv+ge0PhBR7KITeCCWvy7VBQrCUdFkvpI+HrAPQjVtVjy1LvlEkqQTHckoszruw==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", "dependencies": { - "is-potential-custom-element-name": "^1.0.0" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } }, - "node_modules/js-levenshtein-esm": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-2.0.0.tgz", - "integrity": "sha512-1n4LEPOL4wRXY8rOQcuA7Iuaphe5xCMayvufCzlLAi+hRsnBRDbSS6XPuV58CBVJxj5D9ApFLyjQ7KzFToyHBw==", - "dev": true, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4632,35 +5002,46 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, "node_modules/kuler": { @@ -4669,59 +5050,58 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lit": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz", - "integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" + "node": ">=13.2.0" } }, - "node_modules/lit-element": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz", - "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.4.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/lit-html": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz", - "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { "node": ">=10" @@ -4730,13 +5110,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -4754,6 +5127,15 @@ "node": ">= 12.0.0" } }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -4763,65 +5145,105 @@ "node": ">=12" } }, - "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "dev": true, - "license": "CC0-1.0" + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 0.4" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "license": "Unlicense", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "fs-monkey": "^1.0.4" }, "engines": { - "node": ">=8.6" + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", - "dev": true, + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -4830,121 +5252,241 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "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" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" + "node_modules/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", "engines": { - "node": ">=10.5.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "content-type": "^2.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://opencollective.com/express" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", "license": "MIT", "dependencies": { - "fn.name": "1.x.x" + "lodash": "^4.17.21" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "ee-first": "1.1.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.8" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" }, "engines": { "node": ">=10" @@ -4957,7 +5499,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -4966,50 +5507,63 @@ "node": ">=6" } }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "dev": true, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" + "engines": { + "node": ">= 0.8" } }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true, - "license": "MIT" + "node_modules/path-scurry": { + "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": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "license": "MIT", "engines": { "node": ">=8" @@ -5019,59 +5573,27 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, "engines": { - "node": ">=6.0.0" + "node": ">=4" } }, "node_modules/prom-client": { @@ -5087,11 +5609,23 @@ "node": "^16 || ^18 || >=20" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5113,94 +5647,144 @@ ], "license": "MIT" }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { - "node": ">= 6" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { - "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { - "queue-microtask": "^1.2.2" + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" } }, "node_modules/safe-buffer": { @@ -5232,18 +5816,65 @@ "node": ">=10" } }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, - "license": "ISC" + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5252,36 +5883,139 @@ "node": ">=10" } }, - "node_modules/serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", - "license": "BSD-3-Clause", + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, "engines": { - "node": ">=20.0.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">=8" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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-swizzle": { @@ -5293,11 +6027,29 @@ "is-arrayish": "^0.3.1" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -5312,6 +6064,15 @@ "node": "*" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -5337,6 +6098,14 @@ "stream-chain": "^2.2.5" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5346,68 +6115,67 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "node_modules/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" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "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", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "node_modules/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": { - "has-flag": "^4.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/svgo": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", - "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", - "dev": true, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", "license": "MIT", "dependencies": { - "commander": "^11.1.0", - "css-select": "^5.1.0", - "css-tree": "^3.0.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.1.1", - "sax": "^1.4.1" - }, - "bin": { - "svgo": "bin/svgo.js" + "@tokenizer/token": "^0.3.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/swagger-ui-dist": { @@ -5419,20 +6187,13 @@ "@scarf/scarf": "=1.4.0" } }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" + "node": ">=0.10" } }, "node_modules/tagged-tag": { @@ -5449,105 +6210,725 @@ } }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/terser": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/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==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=18" } }, - "node_modules/tdigest": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", - "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "bintrees": "1.0.2" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "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==", + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">= 14.0.0" + "node": ">=18" } }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" + "node": ">=18" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "node": ">=18" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">= 0.8.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/type-fest": { @@ -5566,11 +6947,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5580,34 +6997,34 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.46.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz", - "integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==", - "dev": true, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.1", - "@typescript-eslint/parser": "8.46.1", - "@typescript-eslint/typescript-estree": "8.46.1", - "@typescript-eslint/utils": "8.46.1" + "@lukeed/csprng": "^1.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -5617,14 +7034,60 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -5636,29 +7099,135 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 0.8" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { - "node-which": "bin/node-which" + "webpack": "bin/webpack.js" }, "engines": { - "node": ">= 8" + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/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==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/winston": { @@ -5697,31 +7266,49 @@ "node": ">= 12.0.0" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "package/client": { - "dev": true, + "extraneous": true, "dependencies": { "@lit-labs/ssr-client": "^1.1.7", "@lit/context": "^1.1.6", diff --git a/project/server/package.json b/project/server/package.json index e7bf86e1..13ce83e6 100644 --- a/project/server/package.json +++ b/project/server/package.json @@ -3,11 +3,10 @@ "type": "module", "devDependencies": { "@biomejs/biome": "^2.4.2", + "@types/express": "^5.0.6", "@types/node": "^24.3.0", - "@types/serialize-javascript": "^5.0.4", - "@types/swagger-ui-dist": "^3.30.6", "@types/stream-json": "^1.7.8", - "client": "file:package/client", + "@types/swagger-ui-dist": "^3.30.6", "esbuild": "^0.25.9", "tsx": "^4.20.6", "type-fest": "^5.0.1", @@ -17,15 +16,18 @@ "@aws-sdk/client-s3": "^3.893.0", "@aws-sdk/lib-storage": "^3.893.0", "@hono/node-server": "^1.19.1", - "@lit-labs/ssr": "^3.3.1", "@lppedd/di-wise-neo": "^0.11.1", + "@nestjs/cli": "^11.0.24", + "@nestjs/common": "^11.2.2", + "@nestjs/core": "^11.2.2", + "@nestjs/platform-express": "^11.2.3", "cron-parser": "^5.5.0", "date-fns": "^4.1.0", "effect": "^3.17.13", "hono": "^4.9.6", "prom-client": "^15.1.3", + "reflect-metadata": "^0.2.2", "safe-stable-stringify": "^2.5.0", - "serialize-javascript": "^7.0.0", "stream-json": "^1.9.1", "swagger-ui-dist": "^5.28.1", "undici": "^7.16.0", diff --git a/project/server/package/client b/project/server/package/client deleted file mode 120000 index 96cd109b..00000000 --- a/project/server/package/client +++ /dev/null @@ -1 +0,0 @@ -../../client \ No newline at end of file diff --git a/project/server/script/load-test/index.ts b/project/server/script/load-test/index.ts index fdda2f26..3a1738b7 100644 --- a/project/server/script/load-test/index.ts +++ b/project/server/script/load-test/index.ts @@ -103,7 +103,7 @@ if (isMainThread) { DATABASE_PATH_DERIVED: databaseDerived, SNAPSHOT_VOUCHER_EXPECTED_AFTER: String(expectedAfter), SNAPSHOT_VOUCHER_TTL: String(ttl), - DERIVE_ENABLE: "false", + SCHEDULER_ENABLE: "false", }, }, ); diff --git a/project/server/src/api/dependency.ts b/project/server/src/api/dependency.ts index c4d3bc6e..0f22b139 100644 --- a/project/server/src/api/dependency.ts +++ b/project/server/src/api/dependency.ts @@ -3,9 +3,9 @@ import type { Hono } from "hono"; import { container } from "../dependency"; import { ICallbackVendorSlack } from "../service/callback/vendor/slack"; import { type IDatabase, IDatabaseStaging } from "../service/database"; -import { IDeriveDerivableDevice } from "../service/derive/derivable/device"; import { IIngress } from "../service/ingress"; import { IIntrospection } from "../service/introspect"; +import { ISchedulerScheduledDeriveDevice } from "../service/scheduler/scheduled/derive/device"; import { ISnapshot } from "../service/snapshot"; import { ISnapshotDeferTarget } from "../service/snapshot/defer/base"; import { IVoucher } from "../service/voucher"; @@ -16,8 +16,8 @@ export type Dependency = { database: { staging: IDatabase<"staging">; }; - derivable: { - device: IDeriveDerivableDevice; + derive: { + device: ISchedulerScheduledDeriveDevice; }; ingress: IIngress; introspection: IIntrospection; @@ -36,8 +36,8 @@ const dependency: Dependency = { database: { staging: container.resolve(IDatabaseStaging), }, - derivable: { - device: container.resolve(IDeriveDerivableDevice), + derive: { + device: container.resolve(ISchedulerScheduledDeriveDevice), }, ingress: container.resolve(IIngress), introspection: container.resolve(IIntrospection), @@ -55,38 +55,16 @@ const dependency: Dependency = { type Handler = (d: Dependency) => DecoratedHandler; -// {"": {"": }} -export type HandlerMap = Record>; - -export type DecoratedRoutes = { +export type Primed = { routers: Hono[]; - handlers: HandlerMap; }; -export const primeRoutes = (...args: Handler[]): DecoratedRoutes => { - const handlers: HandlerMap = {}; +export const primeRoutes = (...args: Handler[]): Primed => { const routers: Hono[] = []; - for (const handler of args) { const primed = handler(dependency); - routers.push(primed.router); - - // TODO: sink endpoints are unsupported for now - if (typeof primed.for === "undefined") { - continue; - } - - handlers[primed.for.path] = { - ...handlers[primed.for.path], - [primed.for.method]: primed.for.handler, - }; } - return { routers, handlers }; -}; - -type UndeclaredHandler = (d: Dependency) => Hono; -export const primeUndeclaredRoute = (handler: UndeclaredHandler) => { - return handler(dependency); + return { routers }; }; diff --git a/project/server/src/api/endpoint/callback/vendor/slack/slash-command/handler.test.ts b/project/server/src/api/endpoint/callback/vendor/slack/slash-command/handler.test.ts index ec599cd3..56413a84 100644 --- a/project/server/src/api/endpoint/callback/vendor/slack/slash-command/handler.test.ts +++ b/project/server/src/api/endpoint/callback/vendor/slack/slash-command/handler.test.ts @@ -1,16 +1,10 @@ -import { randomBytes } from "node:crypto"; import { type TestContext, test } from "node:test"; import { CallbackVendorSlack } from "../../../../../../service/callback/vendor/slack"; -import { Ingress } from "../../../../../../service/ingress"; -import { Voucher } from "../../../../../../service/voucher"; import { postCallbackVendorSlackSlashCommand } from "./handler"; import type { ISnapshotDeferIngest } from "../../../../../../service/snapshot/defer/ingest"; -const voucher = new Voucher(randomBytes(64).toString()); -const ingress = new Ingress({ authority: "foo", secure: true }, voucher); - test("genuine", async (t: TestContext) => { { const primed = postCallbackVendorSlackSlashCommand({ @@ -57,10 +51,7 @@ test("genuine", async (t: TestContext) => { signingKey: "8f742231b10e8888abcd99yyyzzz85a5", botToken: "xoxb-foo", }, - {}, {} as ISnapshotDeferIngest, - ingress, - voucher, ), }, }, diff --git a/project/server/src/api/endpoint/derived/device/handler.ts b/project/server/src/api/endpoint/derived/device/handler.ts index ddc30ed3..14744fe9 100644 --- a/project/server/src/api/endpoint/derived/device/handler.ts +++ b/project/server/src/api/endpoint/derived/device/handler.ts @@ -4,10 +4,10 @@ import type { PickDeep } from "type-fest"; import integrations from "../../../../categorized-integrations.json"; import { logger } from "../../../../logger"; import { - type DerivableDeviceMono, DeviceCategoryIdValue, DeviceConnectivityValue, -} from "../../../../service/derive/derivable/device"; + type SchedulerScheduledDeriveDeviceDeviceMono, +} from "../../../../service/scheduler/scheduled/derive/device"; import { floor, Integer } from "../../../../type/codec/integer"; import { Uuid } from "../../../../type/codec/uuid"; import { isNone, isSome } from "../../../../type/maybe"; @@ -18,7 +18,9 @@ import type { Dependency } from "../../../dependency"; type Integration = keyof typeof integrations; -const mapDevice = (d: Omit) => { +const mapDevice = ( + d: Omit, +) => { const integration = Object.keys(integrations).includes(d.integration) ? integrations[d.integration as Integration] : undefined; @@ -118,7 +120,7 @@ const ParametersDevices = Schema.Struct({ }); export const getDerivedDevices = ( - d: PickDeep, + d: PickDeep, ) => idempotentEndpoint( "/api/unstable/derived/devices", @@ -200,8 +202,8 @@ export const getDerivedDevices = ( const paginated = await paginate(d)({ slice: ({ offset, limit }) => - d.derivable.device.devices.slice(query, { offset, limit }), - count: async () => await d.derivable.device.devices.count(query), + d.derive.device.devices.slice(query, { offset, limit }), + count: async () => await d.derive.device.devices.count(query), })({ path, page, @@ -243,14 +245,14 @@ const ParametersDevice = Schema.Struct({ }); export const getDerivedDevice = ( - d: PickDeep, + d: PickDeep, ) => idempotentEndpoint( "/api/unstable/derived/devices/{id}", "get", ParametersDevice, async ({ path: { id } }) => { - const result = await d.derivable.device.device({ id }); + const result = await d.derive.device.device({ id }); if (isNone(result)) { return { code: 404, @@ -270,8 +272,8 @@ export const getDerivedDevice = ( const paginated = await paginate(d)({ slice: ({ offset, limit }) => - d.derivable.device.devices.slice(query, { offset, limit }), - count: async () => await d.derivable.device.devices.count(query), + d.derive.device.devices.slice(query, { offset, limit }), + count: async () => await d.derive.device.devices.count(query), })({ path: d.ingress.url.device.duplicates(id), page: floor(0), @@ -322,14 +324,14 @@ const ParametersDeviceDuplicates = Schema.Struct({ }); export const getDerivedDeviceDuplicates = ( - d: PickDeep, + d: PickDeep, ) => idempotentEndpoint( "/api/unstable/derived/devices/{id}/duplicates", "get", ParametersDeviceDuplicates, async ({ path: { id }, query: { page, size } }, { path }) => { - const result = await d.derivable.device.device({ id }); + const result = await d.derive.device.device({ id }); if (isNone(result)) { return { code: 404, @@ -343,8 +345,8 @@ export const getDerivedDeviceDuplicates = ( const paginated = await paginate(d)({ slice: ({ offset, limit }) => - d.derivable.device.devices.slice(query, { offset, limit }), - count: async () => await d.derivable.device.devices.count(query), + d.derive.device.devices.slice(query, { offset, limit }), + count: async () => await d.derive.device.devices.count(query), })({ path, page, diff --git a/project/server/src/api/endpoint/dimension/handler.ts b/project/server/src/api/endpoint/dimension/handler.ts index baf21c53..76564e75 100644 --- a/project/server/src/api/endpoint/dimension/handler.ts +++ b/project/server/src/api/endpoint/dimension/handler.ts @@ -4,7 +4,7 @@ import type { PickDeep } from "type-fest"; import { DeviceCategoryIdValue, DeviceConnectivityValue, -} from "../../../service/derive/derivable/device"; +} from "../../../service/scheduler/scheduled/derive/device"; import { idempotentEndpoint } from "../../base"; import type { Dependency } from "../../dependency"; @@ -35,7 +35,7 @@ const Parameters = Schema.Struct({ ), }); -export const getDimensions = (d: PickDeep) => +export const getDimensions = (d: PickDeep) => idempotentEndpoint( "/api/unstable/dimensions", "get", @@ -109,7 +109,7 @@ export const getDimensions = (d: PickDeep) => } as const; return { code: 200, - body: await d.derivable.device.filters(query), + body: await d.derive.device.filters(query), headers: { "cache-control": "max-age=1800", }, diff --git a/project/server/src/api/index.ts b/project/server/src/api/index.ts index f200a2de..0d834241 100644 --- a/project/server/src/api/index.ts +++ b/project/server/src/api/index.ts @@ -11,7 +11,7 @@ import snapshot from "./endpoint/snapshot"; import { middlewareRequestLog } from "./middleware/request-log"; import { middlewareRequestStorage } from "./middleware/request-storage"; -import type { DecoratedRoutes, HandlerMap } from "./dependency"; +import type { Primed } from "./dependency"; export const build = ( app: Hono, @@ -19,12 +19,10 @@ export const build = ( cors: boolean; }, ) => { - let handlers: HandlerMap = {}; - const use = (decorated: DecoratedRoutes) => { + const use = (decorated: Primed) => { for (const router of decorated.routers) { app.route("/", router); } - handlers = { ...handlers, ...decorated.handlers }; }; app.use(requestId()); @@ -45,6 +43,4 @@ export const build = ( use(dimension); use(health); use(snapshot); - - return handlers; }; diff --git a/project/server/src/config/index.ts b/project/server/src/config/index.ts index 61597856..829b85cb 100644 --- a/project/server/src/config/index.ts +++ b/project/server/src/config/index.ts @@ -70,18 +70,18 @@ export const config = () => }, }, vendor: { - slack: { - botToken: env.string(optional("VENDOR_SLACK_BOT_TOKEN")), + slack: env.unite("VENDOR_SLACK", (env) => ({ + botToken: env.string(required("BOT_TOKEN")), callback: { - signingKey: env.string(optional("VENDOR_SLACK_CALLBACK_SIGNING_KEY")), + signingKey: env.string(required("CALLBACK_SIGNING_KEY")), }, - }, + })), }, introspection: { bearerToken: env.string(optional("INTROSPECTION_BEARER_TOKEN")), }, - derive: { - enable: env.boolean(required("DERIVE_ENABLE", true)), + scheduler: { + enable: env.boolean(required("SCHEDULER_ENABLE", true)), }, }) as const; diff --git a/project/server/src/dependency/index.ts b/project/server/src/dependency/index.ts index 5457c250..552bc895 100644 --- a/project/server/src/dependency/index.ts +++ b/project/server/src/dependency/index.ts @@ -16,16 +16,6 @@ import { IDatabaseStaging, } from "../service/database"; import { bake } from "../service/database/base"; -import { DatabaseSnapshotCoordinator } from "../service/database/snapshot-coordinator"; -import { DatabaseSnapshotCoordinators } from "../service/database/snapshot-coordinator/base"; -import { Derive, IDeriveDerived } from "../service/derive"; -import { IDeriveDerivable } from "../service/derive/base"; -import { - DeriveDerivableDevice, - IDeriveDerivableDevice, -} from "../service/derive/derivable/device"; -import { DeriveDerivableSubject } from "../service/derive/derivable/subject"; -import { DeriveDerivableSubmissionFaulty } from "../service/derive/derivable/submission"; import { Dispatch, IDispatch } from "../service/dispatch"; import { IDispatchReporter } from "../service/dispatch/base"; import { DispatchReporterConsole } from "../service/dispatch/reporter/console"; @@ -35,9 +25,14 @@ import { IIntrospectionMixinHono, IntrospectionMixinHono, } from "../service/introspect/mixin-hono"; -import { ISignal, Signal } from "../service/signal"; -import { ISignalProvider } from "../service/signal/base"; -import { SignalProviderSlack } from "../service/signal/provider/slack"; +import { IScheduler, Scheduler } from "../service/scheduler"; +import { ISchedulerScheduled } from "../service/scheduler/base"; +import { + ISchedulerScheduledDeriveDevice, + SchedulerScheduledDeriveDevice, +} from "../service/scheduler/scheduled/derive/device"; +import { SchedulerScheduledDeriveSubject } from "../service/scheduler/scheduled/derive/subject"; +import { SchedulerScheduledDeriveSubmissionFaulty } from "../service/scheduler/scheduled/derive/submission"; import { ISnapshot, Snapshot } from "../service/snapshot"; import { ISnapshotDeferTarget } from "../service/snapshot/defer/base"; import { @@ -64,12 +59,18 @@ container.register(ConfigProvider, { const resolved = config(); -container.register(IDeriveDerivableDevice, { useClass: DeriveDerivableDevice }); +container.register(ISchedulerScheduledDeriveDevice, { + useClass: SchedulerScheduledDeriveDevice, +}); -container.register(IDeriveDerivable, { useExisting: IDeriveDerivableDevice }); -container.register(IDeriveDerivable, { useClass: DeriveDerivableSubject }); -container.register(IDeriveDerivable, { - useClass: DeriveDerivableSubmissionFaulty, +container.register(ISchedulerScheduled, { + useExisting: ISchedulerScheduledDeriveDevice, +}); +container.register(ISchedulerScheduled, { + useClass: SchedulerScheduledDeriveSubject, +}); +container.register(ISchedulerScheduled, { + useClass: SchedulerScheduledDeriveSubmissionFaulty, }); container.register(IDatabaseDerived, { @@ -102,47 +103,30 @@ container.register(IIntrospectionMixinHono, { container.register(IIntrospection, { useExisting: IIntrospectionMixinHono, }); -container.register(ISignal, { useClass: Signal }); -container.register(ISignalProvider, { useClass: SignalProviderSlack }); container.register(ISnapshot, { useClass: Snapshot }); container.register(ISnapshotDeferIngest, { useClass: SnapshotDeferIngest }); container.register(IVoucher, { useClass: Voucher }); -container.register(DatabaseSnapshotCoordinators, { - useFactory: () => ({ - ...(isSome(resolved.database.snapshot.destination.staging) - ? { - staging: new DatabaseSnapshotCoordinator( - container.resolve(IDatabaseStaging), - container.resolve(ISnapshotDeferIngest), - resolved.database.snapshot.destination.staging, - ), - } - : {}), - }), -}); - -if (resolved.derive.enable) { - container.register(IDeriveDerived, { +if (resolved.scheduler.enable) { + container.register(IScheduler, { useFactory: () => - new Derive( - container.resolve(IDatabaseDerived), - container.resolveAll(IDeriveDerivable), + new Scheduler( + container.resolveAll(ISchedulerScheduled), container.resolve(IIntrospection), ), }); } { - const signingKey = resolved.vendor.slack.callback.signingKey; - const botToken = resolved.vendor.slack.botToken; - if (isSome(signingKey) && isSome(botToken)) { + const slack = resolved.vendor.slack; + if (isSome(slack)) { + const { + callback: { signingKey }, + botToken, + } = slack; + container.register(ICallbackVendorSlack, { - useFactory: () => - new CallbackVendorSlack( - { signingKey, botToken }, - container.resolve(DatabaseSnapshotCoordinators), - ), + useFactory: () => new CallbackVendorSlack({ signingKey, botToken }), }); } } diff --git a/project/server/src/entrypoint-nest.ts b/project/server/src/entrypoint-nest.ts new file mode 100644 index 00000000..cd182b3e --- /dev/null +++ b/project/server/src/entrypoint-nest.ts @@ -0,0 +1,49 @@ +import "reflect-metadata"; + +import type { IncomingMessage, ServerResponse } from "node:http"; + +import { NestFactory } from "@nestjs/core"; +import type { NestExpressApplication } from "@nestjs/platform-express"; + +import { ModuleApp } from "./layer/app.module.js"; +import { + REQUEST_BODY_LIMIT, + streamedRouteMatcher, + streamedRoutes, +} from "./layer/body.js"; +import { Config } from "./layer/config/config.module.js"; +import { AdapterLogger } from "./layer/logging.js"; + +void (async () => { + const app = await NestFactory.create(ModuleApp, { + logger: new AdapterLogger(), + rawBody: true, + // registered below, so that `@StreamedBody` routes can opt out of parsing + bodyParser: false, + }); + + // bodies the parser is kept away from are capped by `InterceptorRouteBody` instead + const streamed = streamedRouteMatcher(streamedRoutes(app)); + // overriding `type` replaces the parser's own matching, so the urlencoded + // route has to be kept out of it → matched as strictly as clients send it + app.useBodyParser("json", { + limit: REQUEST_BODY_LIMIT, + type: (request: IncomingMessage) => + !streamed(request) && + request.headers["content-type"] === "application/json", + }); + app.useBodyParser("urlencoded", { + extended: true, + limit: REQUEST_BODY_LIMIT, + }); + + // adapters may advertise themselves (e.g. express sends `x-powered-by`) + // → platform-neutral way of stripping header + app.use((_: unknown, response: ServerResponse, next: () => void) => { + response.removeHeader("x-powered-by"); + next(); + }); + app.enableShutdownHooks(); + const config: Config = await app.resolve(Config); + await app.listen(config.port, config.host); +})(); diff --git a/project/server/src/entrypoint.ts b/project/server/src/entrypoint.ts index c6a449a1..a92c77d6 100644 --- a/project/server/src/entrypoint.ts +++ b/project/server/src/entrypoint.ts @@ -15,12 +15,10 @@ import { container } from "./dependency"; import { logger } from "./logger"; import { IDatabaseDerived, IDatabaseStaging } from "./service/database"; import { DatabaseMigrate } from "./service/database/migrate"; -import { Derive, IDeriveDerived } from "./service/derive"; -import { IIngress } from "./service/ingress"; import { IIntrospectionMixinHono } from "./service/introspect/mixin-hono"; +import { IScheduler, Scheduler } from "./service/scheduler"; import { ISnapshotDeferIngest } from "./service/snapshot/defer/ingest"; import { SuspendableHandle } from "./service/suspendable"; -import { build as buildSsr } from "./ssr"; import { isNone } from "./type/maybe"; import { formatNs } from "./utility/format"; import { unroll } from "./utility/iterable"; @@ -47,16 +45,12 @@ if (!config.secure) { logger.warn("running in insecure mode"); } -const ingress = container.resolve(IIngress); - app.use(container.resolve(IIntrospectionMixinHono).middleware()); buildWeb(app); - -const handlers = buildApi(app, { +buildApi(app, { cors: config.secure, }); -await buildSsr(app, handlers, ingress.origin); app.onError((e, c) => { if (e instanceof HTTPException) { @@ -215,21 +209,21 @@ void (async () => { const ingest = container.resolve(ISnapshotDeferIngest); - const derive = container.resolve(IDeriveDerived, true); - if (typeof derive === "undefined") { + const scheduler = container.resolve(IScheduler, true); + if (typeof scheduler === "undefined") { return; } { await databaseUnlocked; - let epoch = Derive.epoch(); + let epoch = Scheduler.epoch(); while (true) { - epoch = await derive.wait(epoch); - const plan = derive.plan(epoch); + epoch = await scheduler.wait(epoch); + const plan = scheduler.plan(epoch); - if (!Derive.viable(plan)) { - throw new Error(`derive plan not viable <${JSON.stringify(plan)}>`); + if (!Scheduler.viable(plan)) { + throw new Error(`scheduler plan not viable <${JSON.stringify(plan)}>`); } // pause ingesting to prevent wal growth @@ -242,7 +236,7 @@ void (async () => { }); } - for await (const status of derive.act(plan)) { + for await (const status of scheduler.act(plan)) { switch (status.kind) { case "pending": logger.info(`running <${status.id.description}>`, { diff --git a/project/server/src/layer/app.module.ts b/project/server/src/layer/app.module.ts new file mode 100644 index 00000000..fda5ff6f --- /dev/null +++ b/project/server/src/layer/app.module.ts @@ -0,0 +1,52 @@ +import { Module } from "@nestjs/common"; +import { APP_INTERCEPTOR, DiscoveryModule } from "@nestjs/core"; + +import { config, SnapshotDeferTarget } from "../config"; +import { InterceptorRouteBody } from "./body.interceptor"; +import { ModuleCallbackVendorSlack } from "./callback/vendor/slack/slack.module"; +import { ModuleDatabaseCoordinator } from "./database/database-coordinator.module"; +import { ModuleLockfileCoordinator } from "./database/lockfile-corrdinator.module"; +import { ModuleHealth } from "./health/health.module"; +import { ModuleIntrospection } from "./introspection/introspection.module"; +import { ModuleOpenapiExplorer } from "./openapi/explorer/explorer.module"; +import { InterceptorRouteRequest } from "./request.interceptor"; +import { InterceptorEndpointResponse } from "./response.interceptor"; +import { ModuleSchedulerCoordinator } from "./scheduler/scheduler-coordinator.module"; +import { ModuleSnapshotDeferIngestCoordinator } from "./snapshot/defer/ingest-coordinator.module"; +import { ModuleSnapshotDeferTarget } from "./snapshot/defer/target.module"; +import { ModuleSnapshotDeferTargetObjectStore } from "./snapshot/defer/target-object-store.module"; +import { ModuleSnapshot } from "./snapshot/snapshot.module"; + +// resolved eagerly because module metadata is evaluated before the injector exists +const c = config(); + +@Module({ + imports: [ + // intentionally first so it's interceptor wraps all succeeding imports + ModuleIntrospection, + // lets `streamedRoutes` find what handlers declared before routes are mapped + DiscoveryModule, + // needs to be explicitly imported for lifecycle hooks to fire + ModuleDatabaseCoordinator, + ModuleLockfileCoordinator, + ModuleSchedulerCoordinator, + ...(c.snapshot.defer.target === SnapshotDeferTarget.ObjectStore + ? [ + ModuleSnapshotDeferTarget.forRoot( + ModuleSnapshotDeferTargetObjectStore, + ), + ] + : []), + ModuleSnapshotDeferIngestCoordinator, + ModuleSnapshot, + ModuleCallbackVendorSlack.forRoot(c), + ModuleHealth, + ModuleOpenapiExplorer, + ], + providers: [ + { provide: APP_INTERCEPTOR, useClass: InterceptorRouteBody }, + { provide: APP_INTERCEPTOR, useClass: InterceptorRouteRequest }, + { provide: APP_INTERCEPTOR, useClass: InterceptorEndpointResponse }, + ], +}) +export class ModuleApp {} diff --git a/project/server/src/layer/body.interceptor.ts b/project/server/src/layer/body.interceptor.ts new file mode 100644 index 00000000..520b6c2b --- /dev/null +++ b/project/server/src/layer/body.interceptor.ts @@ -0,0 +1,56 @@ +import type { + CallHandler, + ExecutionContext, + NestInterceptor, +} from "@nestjs/common"; +import { Inject, Injectable, PayloadTooLargeException } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { type Observable, throwError } from "rxjs"; +import { catchError } from "rxjs/operators"; + +import { + RequestBodyTooLargeError, + ROUTE_BODY_STREAM, + StreamedBody, + type StreamedRequest, + TransformRequestBodyLimit, +} from "./body"; + +/** applies the cap requested in `@StreamedBody` */ +@Injectable() +export class InterceptorRouteBody implements NestInterceptor { + constructor(@Inject(Reflector) private readonly reflector: Reflector) {} + + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable { + if (context.getType() !== "http") { + return next.handle(); + } + + // absent on any handler that does not declare `@StreamedBody` + const limit = this.reflector.get(StreamedBody, context.getHandler()); + if (typeof limit === "undefined") { + return next.handle(); + } + + const request = context.switchToHttp().getRequest(); + + request[ROUTE_BODY_STREAM] = request.pipe( + new TransformRequestBodyLimit(limit), + ); + + return next + .handle() + .pipe( + catchError((error: unknown) => + throwError(() => + error instanceof RequestBodyTooLargeError + ? new PayloadTooLargeException() + : error, + ), + ), + ); + } +} diff --git a/project/server/src/layer/body.test.ts b/project/server/src/layer/body.test.ts new file mode 100644 index 00000000..93527859 --- /dev/null +++ b/project/server/src/layer/body.test.ts @@ -0,0 +1,226 @@ +import type { IncomingMessage } from "node:http"; +import { Readable } from "node:stream"; +import { text } from "node:stream/consumers"; +import { type TestContext, test } from "node:test"; + +import type { CallHandler, INestApplicationContext } from "@nestjs/common"; +import { PayloadTooLargeException } from "@nestjs/common"; +import { DiscoveryService, MetadataScanner, Reflector } from "@nestjs/core"; +import { firstValueFrom, from, of, throwError } from "rxjs"; + +import { + REQUEST_BODY_LIMIT, + ROUTE_BODY_STREAM, + StreamedBody, + type StreamedRequest, + streamedRouteMatcher, + streamedRoutes, +} from "./body"; +import { InterceptorRouteBody } from "./body.interceptor"; +import { _Route } from "./route"; +import { executionContext } from "./test"; + +import type { RequestStreamStub } from "./test"; + +type Paths = { + readonly "/upload": { + readonly post: { + readonly responses: { + readonly 204: { headers: { readonly [name: string]: unknown } }; + }; + }; + }; + readonly "/thing/{id}": { + readonly put: { + readonly parameters: { readonly path: { readonly id: string } }; + readonly responses: { + readonly 204: { headers: { readonly [name: string]: unknown } }; + }; + }; + }; + readonly "/parsed": { + readonly post: { + readonly responses: { + readonly 204: { headers: { readonly [name: string]: unknown } }; + }; + }; + }; +}; + +const Route = _Route(); + +class Controller { + @Route("post", "/upload") + @StreamedBody() + async upload() { + return { code: 204 } as const; + } + + @Route("put", "/thing/{id}") + @StreamedBody(64) + async replace() { + return { code: 204 } as const; + } + + @Route("post", "/parsed") + async parsed() { + return { code: 204 } as const; + } +} + +type ControllerClass = new (...args: never[]) => unknown; + +const applicationContext = ( + controllers: readonly ControllerClass[], +): INestApplicationContext => { + const discovery = { + getControllers: () => controllers.map((metatype) => ({ metatype })), + }; + + return { + get: (token: unknown) => { + if (token === DiscoveryService) { + return discovery; + } + if (token === MetadataScanner) { + return new MetadataScanner(); + } + + return new Reflector(); + }, + } as unknown as INestApplicationContext; +}; + +/** the platform hands the handler a readable, so the stub has to be one too */ +const request = (chunks: readonly string[]): RequestStreamStub => + Readable.from(chunks) as RequestStreamStub; + +/** what the interceptor is expected to have left behind */ +const body = (stub: StreamedRequest): Readable => { + const prepared = stub[ROUTE_BODY_STREAM]; + if (typeof prepared === "undefined") { + throw new Error("interceptor did not prepare a body"); + } + + return prepared; +}; + +test("a route declaring a streamed body", (t: TestContext) => { + t.test("defaults to the shared limit", (t: TestContext) => { + t.assert.strictEqual( + new Reflector().get(StreamedBody, Controller.prototype.upload), + REQUEST_BODY_LIMIT, + ); + }); + + t.test("keeps a limit of its own", (t: TestContext) => { + t.assert.strictEqual( + new Reflector().get(StreamedBody, Controller.prototype.replace), + 64, + ); + }); + + t.test("is discoverable by its schema path", (t: TestContext) => { + t.assert.deepStrictEqual( + [...streamedRoutes(applicationContext([Controller]))], + ["/upload", "/thing/{id}"], + ); + }); +}); + +test("the parser bypass", (t: TestContext) => { + const streamed = streamedRouteMatcher(new Set(["/upload", "/thing/{id}"])); + const incoming = (url: string | undefined): IncomingMessage => + ({ url }) as IncomingMessage; + + t.test("matches a declared path", (t: TestContext) => { + t.assert.strictEqual(streamed(incoming("/upload")), true); + }); + + t.test("matches regardless of query", (t: TestContext) => { + t.assert.strictEqual(streamed(incoming("/upload?retry=1")), true); + }); + + t.test("matches a templated path segment", (t: TestContext) => { + t.assert.strictEqual(streamed(incoming("/thing/6f1a")), true); + }); + + t.test("does not match across segments", (t: TestContext) => { + t.assert.strictEqual(streamed(incoming("/thing/6f1a/nested")), false); + }); + + t.test("does not match an undeclared path", (t: TestContext) => { + t.assert.strictEqual(streamed(incoming("/parsed")), false); + t.assert.strictEqual(streamed(incoming("/uploaded")), false); + }); + + t.test("tolerates a request without a url", (t: TestContext) => { + t.assert.strictEqual(streamed(incoming(undefined)), false); + }); +}); + +test("the body interceptor", (t: TestContext) => { + const interceptor = new InterceptorRouteBody(new Reflector()); + + const next = (handle: CallHandler["handle"]): CallHandler => + ({ handle }) as CallHandler; + + t.test("leaves a route without a streamed body alone", (t: TestContext) => { + const stub = request(["{}"]); + + interceptor.intercept( + executionContext(Controller.prototype.parsed, stub), + next(() => of("untouched")), + ); + + t.assert.strictEqual(stub[ROUTE_BODY_STREAM], undefined); + }); + + t.test("hands the handler a capped body", async (t: TestContext) => { + const stub = request(["one", "two"]); + + const result = await firstValueFrom( + interceptor.intercept( + executionContext(Controller.prototype.upload, stub), + next(() => of("handled")), + ), + ); + + t.assert.strictEqual(result, "handled"); + t.assert.strictEqual(await text(body(stub)), "onetwo"); + }); + + t.test("fails the body once the limit is passed", async (t: TestContext) => { + // `replace` caps at 64 bytes + const stub = request(["a".repeat(40), "b".repeat(40)]); + + await t.assert.rejects( + async () => + await firstValueFrom( + interceptor.intercept( + executionContext(Controller.prototype.replace, stub), + // a handler that consumes the body, the way a streaming one does + next(() => from(text(body(stub)))), + ), + ), + (error: unknown) => + error instanceof PayloadTooLargeException && error.getStatus() === 413, + ); + }); + + t.test("leaves an unrelated failure as it is", async (t: TestContext) => { + const stub = request(["{}"]); + const failure = new Error("malformed submission"); + + await t.assert.rejects( + async () => + await firstValueFrom( + interceptor.intercept( + executionContext(Controller.prototype.upload, stub), + next(() => throwError(() => failure)), + ), + ), + (error: unknown) => error === failure, + ); + }); +}); diff --git a/project/server/src/layer/body.ts b/project/server/src/layer/body.ts new file mode 100644 index 00000000..edff85b3 --- /dev/null +++ b/project/server/src/layer/body.ts @@ -0,0 +1,161 @@ +import type { IncomingMessage } from "node:http"; +import { type Readable, Transform, type TransformCallback } from "node:stream"; + +import { + createParamDecorator, + type ExecutionContext, + type INestApplicationContext, + InternalServerErrorException, +} from "@nestjs/common"; +import { DiscoveryService, MetadataScanner, Reflector } from "@nestjs/core"; + +import { RouteSchemaMetadata } from "./route"; + +import type { RouteSchema } from "./schema"; + +/** the largest request body a route accepts, in bytes */ +export const REQUEST_BODY_LIMIT = 5120 * 1024; + +/** + * the route consumes the request body itself + * + * the platform's parser is kept away from it, and `limit` is enforced while the + * body is read rather than before the handler is entered + */ +export const StreamedBody = Reflector.createDecorator< + number | undefined, + number +>({ + key: "route:streamed-body", + transform: (limit) => limit ?? REQUEST_BODY_LIMIT, +}); + +/** where the body interceptor leaves the stream it prepared */ +export const ROUTE_BODY_STREAM = Symbol("route:body-stream"); + +/** a platform request the body interceptor has prepared a stream on */ +export type StreamedRequest = Readable & { + [ROUTE_BODY_STREAM]?: Readable; +}; + +export class RequestBodyTooLargeError extends Error { + constructor(public limit: number) { + super(`request body exceeds <${limit}> bytes`); + Object.setPrototypeOf(this, RequestBodyTooLargeError.prototype); + } +} + +/** passes a request body through, throwing {@link RequestBodyTooLargeError} as soon as `limit` is passed */ +export class TransformRequestBodyLimit extends Transform { + private received = 0; + + constructor(private readonly limit: number = REQUEST_BODY_LIMIT) { + super({ objectMode: false }); + } + + _transform( + // biome-ignore lint/suspicious/noExplicitAny: `Transform` isn't constrained further + chunk: any, + _: BufferEncoding, + callback: TransformCallback, + ): void { + this.received += chunk.length; + if (this.received > this.limit) { + callback(new RequestBodyTooLargeError(this.limit)); + return; + } + + this.push(chunk); + callback(); + } +} + +/** picks up the capped body stream from where the body interceptor left it */ +export const RequestBodyStream = createParamDecorator( + (_: unknown, ctx: ExecutionContext): Readable => { + const body = ctx.switchToHttp().getRequest()[ + ROUTE_BODY_STREAM + ]; + if (typeof body === "undefined") { + throw new InternalServerErrorException( + "route did not declare a streamed body", + ); + } + + return body; + }, +); + +/** schema paths of every route that declared {@link StreamedBody} */ +export const streamedRoutes = ( + app: INestApplicationContext, +): ReadonlySet => { + const discovery = app.get(DiscoveryService); + const scanner = app.get(MetadataScanner); + const reflector = app.get(Reflector); + + const paths: Set = new Set(); + + for (const wrapper of discovery.getControllers()) { + const prototype: unknown = wrapper.metatype?.prototype; + if (typeof prototype !== "object" || prototype === null) { + continue; + } + + for (const name of scanner.getAllMethodNames(prototype)) { + const handler = (prototype as Record)[name]; + if (typeof handler !== "function") { + continue; + } + + const limit: number | undefined = reflector.get(StreamedBody, handler); + if (typeof limit === "undefined") { + continue; + } + + // always co-present, as only `@Route` handlers are reachable over http + const route: RouteSchema | undefined = reflector.get( + RouteSchemaMetadata, + handler, + ); + if (typeof route !== "undefined") { + paths.add(route.path); + } + } + } + + return paths; +}; + +const PATH_PARAMETER = /\{[^}]+\}/gu; +const PATTERN_RESERVED = /[.*+?^${}()|[\]\\]/gu; + +/** `/a/{id}` → `/^\/a\/[^\/]+$/` */ +const toPattern = (path: string): RegExp => + new RegExp( + `^${path + .split(PATH_PARAMETER) + .map((literal) => literal.replace(PATTERN_RESERVED, "\\$&")) + .join("[^/]+")}$`, + "u", + ); + +/** whether a request is bound for one of `paths` */ +export const streamedRouteMatcher = ( + paths: ReadonlySet, +): ((request: IncomingMessage) => boolean) => { + const patterns = [...paths].map(toPattern); + + return (request) => { + if (typeof request.url === "undefined") { + return false; + } + + const path = request.url.split("?").at(0); + if (typeof path === "undefined") { + return false; + } + + return patterns.some((pattern) => pattern.test(path)); + }; +}; diff --git a/project/server/src/layer/callback/vendor/slack/slack.controller.test.ts b/project/server/src/layer/callback/vendor/slack/slack.controller.test.ts new file mode 100644 index 00000000..f901b5f8 --- /dev/null +++ b/project/server/src/layer/callback/vendor/slack/slack.controller.test.ts @@ -0,0 +1,138 @@ +import { parse } from "node:querystring"; +import { type TestContext, test } from "node:test"; + +import { InternalServerErrorException } from "@nestjs/common"; + +import { CallbackVendorSlack } from "../../../../service/callback/vendor/slack"; +import { invoke } from "../../../test"; +import { ControllerCallbackVendorSlack } from "./slack.controller"; + +import type { ISnapshotDeferIngest } from "../../../../service/snapshot/defer/ingest"; +import type { RequestStub } from "../../../test"; + +const post = ( + controller: ControllerCallbackVendorSlack, + request: RequestStub, +): unknown => invoke(controller, "post", request); + +// https://docs.slack.dev/authentication/verifying-requests-from-slack +const SIGNING_KEY = "8f742231b10e8888abcd99yyyzzz85a5"; +const TIMESTAMP = 1531420618; +const SIGNATURE = + "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503"; +const PAYLOAD = Buffer.from( + "token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c", + "utf8", +); + +const request = ( + signature: string = SIGNATURE, + timestamp: number = TIMESTAMP, +): RequestStub => ({ + headers: { + "x-slack-signature": signature, + "x-slack-request-timestamp": String(timestamp), + }, + // the platform's parser leaves the decoded form behind, the raw payload is retained alongside + body: parse(PAYLOAD.toString("utf8")), + rawBody: PAYLOAD, +}); + +const controller = () => + new ControllerCallbackVendorSlack( + new CallbackVendorSlack( + { signingKey: SIGNING_KEY, botToken: "xoxb-foo" }, + {} as ISnapshotDeferIngest, + ), + ); + +test("a slash command", async (t: TestContext) => { + await t.test("is not accepted when unconfigured", async (t: TestContext) => { + t.assert.deepStrictEqual( + await post(new ControllerCallbackVendorSlack(undefined), request()), + { + code: 500, + contentType: "text/plain", + body: "callback not configured", + }, + ); + }); + + await t.test( + "is rejected when its timestamp is out of sync", + async (t: TestContext) => { + t.mock.timers.enable({ + apis: ["Date"], + now: TIMESTAMP * 1000 + 10 * 1000, + }); + + t.assert.deepStrictEqual(await post(controller(), request()), { + code: 400, + contentType: "text/plain", + body: "request timestamp too far out of sync", + }); + + t.mock.timers.reset(); + }, + ); + + await t.test( + "is rejected when its signature does not verify", + async (t: TestContext) => { + t.mock.timers.enable({ apis: ["Date"], now: TIMESTAMP * 1000 }); + + t.assert.deepStrictEqual( + await post( + controller(), + request( + "v0=b2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503", + ), + ), + { + code: 400, + contentType: "text/plain", + body: "request payload verification failed", + }, + ); + + t.mock.timers.reset(); + }, + ); + + await t.test("is handled when genuine", async (t: TestContext) => { + t.mock.timers.enable({ apis: ["Date"], now: TIMESTAMP * 1000 }); + + t.assert.deepStrictEqual(await post(controller(), request()), { + body: { + blocks: [ + { + text: { + text: "unknown command 😔", + type: "mrkdwn", + }, + type: "section", + }, + ], + response_type: "ephemeral", + }, + code: 200, + contentType: "application/json", + }); + + t.mock.timers.reset(); + }); + + await t.test( + "is not handled without the payload as it arrived", + (t: TestContext) => { + const { headers, body } = request(); + + t.assert.throws( + () => post(controller(), { headers, body }), + (error: unknown) => + error instanceof InternalServerErrorException && + error.getStatus() === 500, + ); + }, + ); +}); diff --git a/project/server/src/layer/callback/vendor/slack/slack.controller.ts b/project/server/src/layer/callback/vendor/slack/slack.controller.ts new file mode 100644 index 00000000..824561d3 --- /dev/null +++ b/project/server/src/layer/callback/vendor/slack/slack.controller.ts @@ -0,0 +1,93 @@ +import { Controller, Inject, Optional } from "@nestjs/common"; +import { Schema } from "effect"; + +import { RequestBodyRaw } from "../../../http"; +import { Route } from "../../../route"; +import { CallbackVendorSlack } from "./slack.interface"; + +import type { Implements } from "../../../schema"; + +const Parameters = Schema.Struct({ + header: Schema.Struct({ + "x-slack-signature": Schema.String, + "x-slack-request-timestamp": Schema.NumberFromString, + }), +}); +type Parameters = typeof Parameters.Type; + +const RequestBody = Schema.Struct({ + command: Schema.String, + text: Schema.String, + response_url: Schema.String, + user_id: Schema.String, +}); +type RequestBody = typeof RequestBody.Type; + +@Controller() +export class ControllerCallbackVendorSlack + implements Implements<"/api/v1/callback/vendor/slack/slash-command"> +{ + constructor( + @Optional() + @Inject(CallbackVendorSlack) + private readonly callback: CallbackVendorSlack | undefined, + ) {} + + @Route("post", "/api/v1/callback/vendor/slack/slash-command", { + parameters: Parameters, + requestBody: RequestBody, + }) + async post( + parameters: Parameters, + requestBody: RequestBody, + @RequestBodyRaw() raw: Buffer, + ) { + if (typeof this.callback === "undefined") { + return { + code: 500, + contentType: "text/plain", + body: "callback not configured", + } as const; + } + + const timestamp = parameters.header["x-slack-request-timestamp"]; + const signature = Buffer.from( + parameters.header["x-slack-signature"], + "utf-8", + ); + + const genuine = this.callback.genuine(timestamp, signature, raw); + + switch (genuine) { + case "not-genuine-timestamp": + return { + code: 400, + contentType: "text/plain", + body: "request timestamp too far out of sync", + } as const; + case "not-genuine-signature": + return { + code: 400, + contentType: "text/plain", + body: "request payload verification failed", + } as const; + case "genuine": + break; + } + + const response = await this.callback.handle( + requestBody.command, + requestBody.text, + { + responseUrl: requestBody.response_url, + userId: requestBody.user_id, + }, + ); + + return { + code: 200, + contentType: "application/json", + body: response, + } as const; + } +} diff --git a/project/server/src/layer/callback/vendor/slack/slack.interface.ts b/project/server/src/layer/callback/vendor/slack/slack.interface.ts new file mode 100644 index 00000000..cd8c1c76 --- /dev/null +++ b/project/server/src/layer/callback/vendor/slack/slack.interface.ts @@ -0,0 +1,4 @@ +import type { ICallbackVendorSlack } from "../../../../service/callback/vendor/slack"; + +export const CallbackVendorSlack = Symbol("CallbackVendorSlack"); +export type CallbackVendorSlack = ICallbackVendorSlack; diff --git a/project/server/src/layer/callback/vendor/slack/slack.module.ts b/project/server/src/layer/callback/vendor/slack/slack.module.ts new file mode 100644 index 00000000..107a963f --- /dev/null +++ b/project/server/src/layer/callback/vendor/slack/slack.module.ts @@ -0,0 +1,55 @@ +import { type DynamicModule, Module } from "@nestjs/common"; +import type { PickDeep } from "type-fest"; + +import { isNone } from "../../../../type/maybe"; +import { Config, ModuleConfig } from "../../../config/config.module"; +import { ModuleSnapshotDeferIngest } from "../../../snapshot/defer/ingest.module"; +import { ServiceSnapshotDeferIngest } from "../../../snapshot/defer/ingest.service"; +import { ControllerCallbackVendorSlack } from "./slack.controller"; +import { CallbackVendorSlack } from "./slack.interface"; +import { ServiceCallbackVendorSlack } from "./slack.service"; + +@Module({}) +// biome-ignore lint/complexity/noStaticOnlyClass: nestjs convention +export class ModuleCallbackVendorSlack { + static forRoot(config: PickDeep): DynamicModule { + if (isNone(config.vendor.slack)) { + // route stays registered so that it can report the callback as unconfigured + return { + module: ModuleCallbackVendorSlack, + controllers: [ControllerCallbackVendorSlack], + }; + } + + return { + global: true, + module: ModuleCallbackVendorSlack, + imports: [ModuleConfig, ModuleSnapshotDeferIngest], + controllers: [ControllerCallbackVendorSlack], + providers: [ + { + provide: CallbackVendorSlack, + inject: [Config, ServiceSnapshotDeferIngest], + useFactory: ( + config: PickDeep, + ingest: ServiceSnapshotDeferIngest, + ) => { + // guarded by `forRoot` + if (isNone(config.vendor.slack)) { + throw new Error("unreachable"); + } + + return new ServiceCallbackVendorSlack( + { + signingKey: config.vendor.slack.callback.signingKey, + botToken: config.vendor.slack.botToken, + }, + ingest, + ); + }, + }, + ], + exports: [CallbackVendorSlack], + }; + } +} diff --git a/project/server/src/layer/callback/vendor/slack/slack.service.ts b/project/server/src/layer/callback/vendor/slack/slack.service.ts new file mode 100644 index 00000000..dce7fe6a --- /dev/null +++ b/project/server/src/layer/callback/vendor/slack/slack.service.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from "@nestjs/common"; + +import { CallbackVendorSlack } from "../../../../service/callback/vendor/slack"; +import { ServiceSnapshotDeferIngest } from "../../../snapshot/defer/ingest.service"; + +@Injectable() +export class ServiceCallbackVendorSlack extends CallbackVendorSlack { + constructor( + config: { signingKey: string; botToken: string }, + @Inject(ServiceSnapshotDeferIngest) ingest: ServiceSnapshotDeferIngest, + ) { + super(config, ingest); + } +} diff --git a/project/server/src/layer/config/config.module.ts b/project/server/src/layer/config/config.module.ts new file mode 100644 index 00000000..941f5d8b --- /dev/null +++ b/project/server/src/layer/config/config.module.ts @@ -0,0 +1,18 @@ +import { Global, Module } from "@nestjs/common"; + +import { config } from "../../config"; + +export const Config = Symbol("Config"); +export type Config = ReturnType; + +@Global() +@Module({ + providers: [ + { + provide: Config, + useFactory: () => config(), + }, + ], + exports: [Config], +}) +export class ModuleConfig {} diff --git a/project/server/src/layer/database/database-coordinator.module.ts b/project/server/src/layer/database/database-coordinator.module.ts new file mode 100644 index 00000000..0113d5a7 --- /dev/null +++ b/project/server/src/layer/database/database-coordinator.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; + +import { ModuleConfig } from "../config/config.module"; +import { ModuleDatabase } from "./database.module"; +import { ServiceDatabaseCoordinator } from "./database-coordinator.service"; + +@Module({ + imports: [ModuleConfig, ModuleDatabase], + providers: [ServiceDatabaseCoordinator], +}) +export class ModuleDatabaseCoordinator {} diff --git a/project/server/src/layer/database/database-coordinator.service.ts b/project/server/src/layer/database/database-coordinator.service.ts new file mode 100644 index 00000000..a38b51cb --- /dev/null +++ b/project/server/src/layer/database/database-coordinator.service.ts @@ -0,0 +1,76 @@ +import { join } from "node:path"; + +import { + Inject, + Injectable, + type OnApplicationShutdown, + type OnModuleInit, +} from "@nestjs/common"; +import type { PickDeep } from "type-fest"; + +import { logger as parentLogger } from "../../logger"; +import { + DatabaseMigrate, + type DatabaseMigratePlanUnachievable, +} from "../../service/database/migrate"; +import { unroll } from "../../utility/iterable"; +import { Config } from "../config/config.module"; +import { Databases } from "./database.module"; + +import type { DatabaseName } from "../../service/database/base"; + +const logger = parentLogger.child({ label: "database-coordinator" }); + +export class DatabaseMigrationPlanUnachievableError extends Error { + constructor( + public database: DatabaseName, + public plan: DatabaseMigratePlanUnachievable, + ) { + super(`migration plan for <${database}> unachievable`); + Object.setPrototypeOf( + this, + DatabaseMigrationPlanUnachievableError.prototype, + ); + } +} + +@Injectable() +export class ServiceDatabaseCoordinator + implements OnModuleInit, OnApplicationShutdown +{ + constructor( + @Inject(Config) + private readonly config: PickDeep, + @Inject(Databases) private readonly databases: Databases, + ) {} + + async onModuleInit(): Promise { + for (const { database: db, workerCount } of this.databases) { + const migrations = await unroll( + DatabaseMigrate.migrations(join("./migration", db.name)), + ); + const migrate = new DatabaseMigrate(db); + const plan = migrate.plan(migrations); + + if (this.config.database.migrate) { + if (!DatabaseMigrate.viable(plan)) { + throw new DatabaseMigrationPlanUnachievableError(db.name, plan); + } + migrate.act(plan); + } + + await db.spawn(workerCount); + logger.info(`spawned <${db.name}>`, { workerCount }); + } + } + + async onApplicationShutdown(): Promise { + for (const { database: db } of this.databases) { + try { + await db.despawn(); + } catch (error) { + logger.error(`error despawning <${db.name}>`, { error }); + } + } + } +} diff --git a/project/server/src/layer/database/database.module.ts b/project/server/src/layer/database/database.module.ts new file mode 100644 index 00000000..b13cc1ed --- /dev/null +++ b/project/server/src/layer/database/database.module.ts @@ -0,0 +1,89 @@ +import { availableParallelism } from "node:os"; + +import { Module } from "@nestjs/common"; + +import { Database, type IDatabase } from "../../service/database"; +import { bake, type DatabaseName } from "../../service/database/base"; +import { Config, ModuleConfig } from "../config/config.module"; +import { ModuleIntrospection } from "../introspection/introspection.module"; +import { ServiceIntrospection } from "../introspection/introspection.service"; + +import type { Introspection } from "../../service/introspect"; + +export const DatabaseStaging = Symbol("DatabaseStaging"); +export const DatabaseDerived = Symbol("DatabaseDerived"); + +export const Databases = Symbol("Databases"); +export type Databases = readonly { + database: IDatabase; + workerCount: { default: number; background: number }; +}[]; + +const maybeURL = (s: string) => { + try { + return new URL(s); + } catch { + return s; + } +}; + +@Module({ + imports: [ModuleConfig, ModuleIntrospection], + providers: [ + { + provide: DatabaseStaging, + useFactory: (c: Config, introspection: Introspection) => + new Database( + "staging", + bake({ location: maybeURL(c.database.path.staging) }), + {}, + introspection, + ), + inject: [Config, ServiceIntrospection], + }, + { + provide: DatabaseDerived, + useFactory: (c: Config, introspection: Introspection) => + new Database( + "derived", + bake({ location: maybeURL(c.database.path.derived) }), + { + staging: bake({ + location: maybeURL(c.database.path.staging), + readOnly: true, + }), + }, + introspection, + ), + inject: [Config, ServiceIntrospection], + }, + { + provide: Databases, + useFactory: ( + staging: IDatabase<"staging">, + derived: IDatabase<"derived">, + ) => { + const parallelism = availableParallelism(); + return [ + { + database: staging, + workerCount: { + default: parallelism, + background: Math.max(Math.floor(parallelism / 2), 1), + }, + }, + { + database: derived, + workerCount: { + default: parallelism, + background: 1, + }, + }, + ] satisfies Databases; + }, + inject: [DatabaseStaging, DatabaseDerived], + }, + ], + exports: [DatabaseStaging, DatabaseDerived, Databases], +}) +export class ModuleDatabase {} diff --git a/project/server/src/layer/database/lockfile-coordinator.service.ts b/project/server/src/layer/database/lockfile-coordinator.service.ts new file mode 100644 index 00000000..2a86aa47 --- /dev/null +++ b/project/server/src/layer/database/lockfile-coordinator.service.ts @@ -0,0 +1,94 @@ +import { dirname, join } from "node:path"; +import { setTimeout as sleep } from "node:timers/promises"; + +import { Inject, Injectable, type OnApplicationShutdown } from "@nestjs/common"; +import { addMinutes } from "date-fns"; +import type { PickDeep } from "type-fest"; + +import { logger as parentLogger } from "../../logger"; +import { isNone } from "../../type/maybe"; +import { + LockFile, + LockFileAcquiredByOtherProcessError, +} from "../../utility/lockfile"; +import { Config } from "../config/config.module"; +import { DatabaseStaging } from "./database.module"; + +import type { IDatabase } from "../../service/database"; + +const logger = parentLogger.child({ label: "lockfile-coordinator" }); + +@Injectable() +export class ServiceLockfileCoordinator implements OnApplicationShutdown { + // initialized with promise that never resolves to prevent potential race until actual + // promise is assigned on bootstrap + public readonly _unlocked: Promise; + + private lockfile: LockFile | undefined; + + constructor( + @Inject(Config) + private readonly config: PickDeep, + @Inject(DatabaseStaging) db: IDatabase<"staging">, + ) { + const { promise, resolve, reject } = Promise.withResolvers(); + this._unlocked = promise; + + // always unlocked when not initially concurrent or when operating on in-memory database + if (!this.config.initiallyConcurrent) { + resolve(); + return; + } + + const databaseLocation = db.raw.location(); + if ( + // no open file handles can exist for in-memory database + isNone(databaseLocation) + ) { + resolve(); + return; + } + + const lockfile = new LockFile( + join(dirname(databaseLocation), "staging-lock"), + ); + + // when new instance is rolled out, it temporarily runs side-by-side with old instance + // this can lead to busy timeouts and races, as old instance also attempts to lock database + void (async () => { + const deadline = addMinutes(new Date(), 10); + + logger.info("acquiring lock"); + + while (new Date() < deadline) { + try { + await lockfile.acquire(); + } catch (e) { + if (!(e instanceof LockFileAcquiredByOtherProcessError)) { + throw e; + } + + await sleep(1_000); + continue; + } + + logger.info("lock acquired"); + resolve(); + return; + } + + reject(new Error("timeout while acquiring lock for database")); + })(); + + this.lockfile = lockfile; + } + + async onApplicationShutdown(): Promise { + this.lockfile?.release(); + } + + /** resolves when lockfile is successfully acquired */ + get unlocked(): Promise { + return this._unlocked; + } +} diff --git a/project/server/src/layer/database/lockfile-corrdinator.module.ts b/project/server/src/layer/database/lockfile-corrdinator.module.ts new file mode 100644 index 00000000..cb19e6c6 --- /dev/null +++ b/project/server/src/layer/database/lockfile-corrdinator.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; + +import { ModuleConfig } from "../config/config.module"; +import { ModuleDatabase } from "./database.module"; +import { ServiceLockfileCoordinator } from "./lockfile-coordinator.service"; + +@Module({ + imports: [ModuleConfig, ModuleDatabase], + providers: [ServiceLockfileCoordinator], + exports: [ServiceLockfileCoordinator], +}) +export class ModuleLockfileCoordinator {} diff --git a/project/server/src/layer/health/health.controller.ts b/project/server/src/layer/health/health.controller.ts new file mode 100644 index 00000000..2c6a744e --- /dev/null +++ b/project/server/src/layer/health/health.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Inject } from "@nestjs/common"; + +import { Route } from "../route"; +import { ServiceHealth } from "./health.service"; + +import type { Implements } from "../schema"; + +@Controller() +export class ControllerHealth implements Implements<"/api/v1/health"> { + constructor(@Inject(ServiceHealth) private readonly service: ServiceHealth) {} + + @Route("get", "/api/v1/health") + async get() { + await this.service.assertHealthy(); + return { code: 200, contentType: "text/plain", body: "ok" } as const; + } +} diff --git a/project/server/src/layer/health/health.module.ts b/project/server/src/layer/health/health.module.ts new file mode 100644 index 00000000..831e720b --- /dev/null +++ b/project/server/src/layer/health/health.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; + +import { ModuleDatabase } from "../database/database.module"; +import { ControllerHealth } from "./health.controller"; +import { ServiceHealth } from "./health.service"; + +@Module({ + imports: [ModuleDatabase], + controllers: [ControllerHealth], + providers: [ServiceHealth], +}) +export class ModuleHealth {} diff --git a/project/server/src/layer/health/health.service.ts b/project/server/src/layer/health/health.service.ts new file mode 100644 index 00000000..6c14223b --- /dev/null +++ b/project/server/src/layer/health/health.service.ts @@ -0,0 +1,16 @@ +import { Inject, Injectable } from "@nestjs/common"; + +import { DatabaseStaging } from "../database/database.module"; + +import type { IDatabase } from "../../service/database"; + +@Injectable() +export class ServiceHealth { + constructor( + @Inject(DatabaseStaging) private readonly staging: IDatabase<"staging">, + ) {} + + async assertHealthy() { + await this.staging.assertHealthy(); + } +} diff --git a/project/server/src/layer/http.ts b/project/server/src/layer/http.ts new file mode 100644 index 00000000..5a23b07a --- /dev/null +++ b/project/server/src/layer/http.ts @@ -0,0 +1,36 @@ +import { + createParamDecorator, + type ExecutionContext, + InternalServerErrorException, + type RawBodyRequest, +} from "@nestjs/common"; +import type { Request } from "express"; + +declare const HttpResponseTag: unique symbol; + +/** + * opaque handle to the underlying platform's response object + * + * actual shape depends on the installed http adapter + */ +export interface HttpResponse { + readonly [HttpResponseTag]: unknown; +} + +export const RequestPath = createParamDecorator( + (_: unknown, ctx: ExecutionContext) => + ctx.switchToHttp().getRequest().originalUrl, +); + +export const RequestBodyRaw = createParamDecorator( + (_: unknown, ctx: ExecutionContext): Buffer => { + const { rawBody } = ctx + .switchToHttp() + .getRequest>(); + if (typeof rawBody === "undefined") { + throw new InternalServerErrorException("raw request body not retained"); + } + + return rawBody; + }, +); diff --git a/project/server/src/layer/ingress/ingress.module.ts b/project/server/src/layer/ingress/ingress.module.ts new file mode 100644 index 00000000..72c98bcd --- /dev/null +++ b/project/server/src/layer/ingress/ingress.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; + +import { ModuleConfig } from "../config/config.module"; +import { ServiceIngress } from "./ingress.service"; + +@Module({ + imports: [ModuleConfig], + providers: [ServiceIngress], + exports: [ServiceIngress], +}) +export class ModuleIngress {} diff --git a/project/server/src/layer/ingress/ingress.service.ts b/project/server/src/layer/ingress/ingress.service.ts new file mode 100644 index 00000000..7915cac6 --- /dev/null +++ b/project/server/src/layer/ingress/ingress.service.ts @@ -0,0 +1,82 @@ +import { Inject, Injectable } from "@nestjs/common"; +import type { PickDeep } from "type-fest"; + +import { Ingress, peek } from "../../service/ingress"; +import { floor, type Integer } from "../../type/codec/integer"; +import { unroll } from "../../utility/iterable"; +import { Config } from "../config/config.module"; + +type Paginated = { + headers: { + link: string; + "content-range": string; + }; + total: number; + links: { + first: URL; + last: URL; + next?: URL; + prev?: URL; + }; + items: I[]; +}; + +const contentRange = (offset: Integer, size: Integer, count: Integer) => + `items ${offset}-${Math.min(offset + size, count)}/${count}`; + +@Injectable() +export class ServiceIngress extends Ingress { + constructor(@Inject(Config) config: PickDeep) { + super(config.external); + } + + paginate( + { defaults }: { defaults: { size: Integer } } = { + defaults: { size: floor(20) }, + }, + ): ({ + slice, + count, + }: { + slice: ({ + offset, + limit, + }: { + offset: Integer; + limit: Integer; + }) => AsyncIterable; + count: () => Promise; + }) => ({ + path, + page, + size, + }: { + path: string | URL; + page: Integer | undefined; + size: Integer | undefined; + }) => Promise> { + return ({ slice, count }) => + async ({ path, page, size }) => { + const _page = page ?? floor(0); + const _size = size ?? defaults.size; + + const offset = floor(_page * _size); + + const counted = floor(await count()); + + const relationships = this.relationships(path, _page, _size, counted); + const peeked = peek(relationships); + + return { + headers: { + link: this.header.link(relationships), + "content-range": contentRange(offset, _size, counted), + }, + total: counted, + links: peeked, + items: + counted > 0 ? await unroll(slice({ offset, limit: _size })) : [], + }; + }; + } +} diff --git a/project/server/src/layer/introspection/introspection.controller.ts b/project/server/src/layer/introspection/introspection.controller.ts new file mode 100644 index 00000000..fd50b5cc --- /dev/null +++ b/project/server/src/layer/introspection/introspection.controller.ts @@ -0,0 +1,71 @@ +import { Readable } from "node:stream"; + +import { + Controller, + Get, + Inject, + Response, + StreamableFile, + UseGuards, +} from "@nestjs/common"; +import { HttpAdapterHost } from "@nestjs/core"; + +import { GuardIntrospection } from "./introspection.guard"; +import { ServiceIntrospection } from "./introspection.service"; + +import type { IntrospectionRegistry } from "../../service/introspect"; +import type { HttpResponse } from "../http"; + +/** assumes that all registries emit prometheus-style metrics */ +const CONTENT_TYPE_COMBINED = "text/plain; version=0.0.4; charset=utf-8"; + +async function* delimited(metrics: AsyncIterable) { + for await (const scoped of metrics) { + yield `${scoped}\n`; + } +} + +@Controller("metrics") +@UseGuards(GuardIntrospection) +export class ControllerIntrospection { + constructor( + @Inject(ServiceIntrospection) + private readonly introspection: ServiceIntrospection, + @Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost, + ) {} + + private scoped( + registry: IntrospectionRegistry, + response: HttpResponse, + ): Promise { + this.adapterHost.httpAdapter.setHeader( + response, + "content-type", + this.introspection.contentType(registry), + ); + + return this.introspection.metrics(registry); + } + + @Get() + combined(): StreamableFile { + return new StreamableFile( + Readable.from(delimited(this.introspection.metricsCombined())), + { type: CONTENT_TYPE_COMBINED }, + ); + } + + @Get("local") + local( + @Response({ passthrough: true }) response: HttpResponse, + ): Promise { + return this.scoped("local", response); + } + + @Get("global") + global( + @Response({ passthrough: true }) response: HttpResponse, + ): Promise { + return this.scoped("global", response); + } +} diff --git a/project/server/src/layer/introspection/introspection.guard.ts b/project/server/src/layer/introspection/introspection.guard.ts new file mode 100644 index 00000000..478d546b --- /dev/null +++ b/project/server/src/layer/introspection/introspection.guard.ts @@ -0,0 +1,77 @@ +import type { CanActivate, ExecutionContext } from "@nestjs/common"; +import { + Inject, + Injectable, + Logger, + ServiceUnavailableException, + UnauthorizedException, +} from "@nestjs/common"; +import { HttpAdapterHost } from "@nestjs/core"; +import type { PickDeep } from "type-fest"; + +import { isNone, isSome, type Maybe } from "../../type/maybe"; +import { Config } from "../config/config.module"; + +type AuthorizedRequest = { + headers: { authorization?: string }; +}; + +/** denies metrics scraping unless the configured bearer token is presented */ +@Injectable() +export class GuardIntrospection implements CanActivate { + private readonly logger = new Logger(GuardIntrospection.name); + + private readonly configuration: { + bearerToken: Maybe; + secure: boolean; + }; + + constructor( + @Inject(Config) config: PickDeep< + Config, + "introspection.bearerToken" | "secure" + >, + @Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost, + ) { + this.configuration = { + bearerToken: config.introspection.bearerToken, + secure: config.secure, + }; + + if (this.configuration.secure && isNone(this.configuration.bearerToken)) { + this.logger.warn( + "running securely with no provided introspection bearer token, metrics inaccessible", + ); + } + } + + canActivate(context: ExecutionContext): boolean { + if (isSome(this.configuration.bearerToken)) { + const http = context.switchToHttp(); + const { authorization } = http.getRequest().headers; + + // secure / insecure with configured token and valid authorization + if (authorization === `Bearer ${this.configuration.bearerToken}`) { + return true; + } + + // https://community.grafana.com/t/grafana-cloud-metrics-endpoint-error-for-wordpress-plugin/124359/6 + this.adapterHost.httpAdapter.setHeader( + http.getResponse(), + "WWW-Authenticate", + "Bearer", + ); + + // secure / insecure with configured token and invalid authorization + throw new UnauthorizedException(); + } + + // insecure without configured token + if (!this.configuration.secure) { + return true; + } + + // secure without configured token + throw new ServiceUnavailableException(); + } +} diff --git a/project/server/src/layer/introspection/introspection.interceptor.ts b/project/server/src/layer/introspection/introspection.interceptor.ts new file mode 100644 index 00000000..8a4f3ad3 --- /dev/null +++ b/project/server/src/layer/introspection/introspection.interceptor.ts @@ -0,0 +1,106 @@ +import type { + CallHandler, + ExecutionContext, + NestInterceptor, +} from "@nestjs/common"; +import { HttpException, Inject, Injectable } from "@nestjs/common"; +import { HttpAdapterHost, Reflector } from "@nestjs/core"; +import type { Observable } from "rxjs"; +import { tap } from "rxjs/operators"; + +import { RouteSchemaMetadata } from "../route"; +import { ServiceIntrospection } from "./introspection.service"; + +import type { + IntrospectionMetricCounter, + IntrospectionMetricHistogram, +} from "../../service/introspect"; +import type { RouteSchema } from "../schema"; + +type Labels = Record<"method" | "status" | "ok" | "route", string>; + +/** records request duration and count of every route the application serves */ +@Injectable() +export class InterceptorIntrospection implements NestInterceptor { + private readonly requestDuration: IntrospectionMetricHistogram; + private readonly requestsTotal: IntrospectionMetricCounter; + + constructor( + @Inject(ServiceIntrospection) introspection: ServiceIntrospection, + @Inject(Reflector) private readonly reflector: Reflector, + @Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost, + ) { + this.requestDuration = introspection.metric.histogram({ + name: "http_request_duration_seconds", + help: "Duration of HTTP requests in seconds", + labelNames: ["method", "status", "ok", "route"], + registry: "local", + // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration + buckets: [ + 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, + 10, + ], + }); + + this.requestsTotal = introspection.metric.counter({ + name: "http_requests_total", + help: "Total number of HTTP requests", + labelNames: ["method", "status", "ok", "route"], + registry: "local", + }); + } + + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable { + if (context.getType() !== "http") { + return next.handle(); + } + + // the schema path is the only low cardinality route label available: + // → params are kept in their `{id}` form + const route: RouteSchema | undefined = this.reflector.get( + RouteSchemaMetadata, + context.getHandler(), + ); + if (typeof route === "undefined") { + // not defined through `@Route`, skipping + return next.handle(); + } + + const http = context.switchToHttp(); + const method = this.adapterHost.httpAdapter.getRequestMethod( + http.getRequest(), + ); + const started = performance.now(); + + const record = (status: number): void => { + const labels = { + method, + route: route.path, + status: status.toString(), + ok: String(status >= 200 && status < 300), + } as const; + + this.requestDuration.observe( + labels, + (performance.now() - started) / 1000, + ); + this.requestsTotal.increment(labels); + }; + + return next.handle().pipe( + tap({ + next: () => { + // set by `InterceptorEndpointResponse`, or by the adapter's default + const { statusCode } = http.getResponse<{ statusCode: number }>(); + record(statusCode); + }, + error: (error: unknown) => { + record(error instanceof HttpException ? error.getStatus() : 500); + }, + }), + ); + } +} diff --git a/project/server/src/layer/introspection/introspection.module.ts b/project/server/src/layer/introspection/introspection.module.ts new file mode 100644 index 00000000..1a5f85f1 --- /dev/null +++ b/project/server/src/layer/introspection/introspection.module.ts @@ -0,0 +1,18 @@ +import { Module } from "@nestjs/common"; +import { APP_INTERCEPTOR } from "@nestjs/core"; + +import { ModuleConfig } from "../config/config.module"; +import { ControllerIntrospection } from "./introspection.controller"; +import { InterceptorIntrospection } from "./introspection.interceptor"; +import { ServiceIntrospection } from "./introspection.service"; + +@Module({ + imports: [ModuleConfig], + controllers: [ControllerIntrospection], + providers: [ + ServiceIntrospection, + { provide: APP_INTERCEPTOR, useClass: InterceptorIntrospection }, + ], + exports: [ServiceIntrospection], +}) +export class ModuleIntrospection {} diff --git a/project/server/src/layer/introspection/introspection.service.ts b/project/server/src/layer/introspection/introspection.service.ts new file mode 100644 index 00000000..27eb28da --- /dev/null +++ b/project/server/src/layer/introspection/introspection.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@nestjs/common"; + +import { + Introspection, + type IntrospectionRegistry, +} from "../../service/introspect"; +import { progressively } from "../../utility/progressively"; + +/** exposes the otherwise encapsulated registries for scraping */ +@Injectable() +export class ServiceIntrospection extends Introspection { + contentType(registry: IntrospectionRegistry): string { + return this.registries[registry].contentType; + } + + metrics(registry: IntrospectionRegistry): Promise { + return this.registries[registry].metrics(); + } + + /** metrics of every registry, yielded in the order they are collected in */ + metricsCombined(): AsyncIterable { + return progressively( + Object.values(this.registries).map((registry) => registry.metrics()), + ); + } +} diff --git a/project/server/src/layer/logging.ts b/project/server/src/layer/logging.ts new file mode 100644 index 00000000..70c625a1 --- /dev/null +++ b/project/server/src/layer/logging.ts @@ -0,0 +1,79 @@ +import { ConsoleLogger } from "@nestjs/common"; + +import { logger, type logLevels } from "./../logger"; + +type LogLevel = keyof (typeof logLevels)["levels"]; + +const kebabCase = (pascalCase: string) => + pascalCase + .replace(/([a-z\d])([A-Z])/g, "$1-$2") + .replace(/([A-Z]{2,})([A-Z][a-z])/g, "$1-$2") + .toLowerCase(); + +/** biome-ignore-start lint/suspicious/noExplicitAny: base class types */ + +/** adapts log lines produced by nestjs internals + * + * intentionally does not override `.error` to prevent mangling error context + * also delegates to base class when context is not provided + */ +export class AdapterLogger extends ConsoleLogger { + /** forwards to the given app logger level, reporting whether the line was adapted + * + * a line is only adapted when the sole optional param is the context + */ + private adapt( + level: LogLevel, + message: any, + optionalParams: [...any, string?], + ): boolean { + if (optionalParams.length !== 1 || typeof optionalParams[0] !== "string") { + return false; + } + + logger[level](message, { label: kebabCase(optionalParams[0]) }); + return true; + } + + log(message: any, context?: string): void; + log(message: any, ...optionalParams: [...any, string?]): void; + override log(message: any, ...optionalParams: [...any, string?]): void { + if (!this.adapt("info", message, optionalParams)) { + super.log(message, ...optionalParams); + } + } + + warn(message: any, context?: string): void; + warn(message: any, ...optionalParams: [...any, string?]): void; + override warn(message: any, ...optionalParams: [...any, string?]): void { + if (!this.adapt("warn", message, optionalParams)) { + super.warn(message, ...optionalParams); + } + } + + debug(message: any, context?: string): void; + debug(message: any, ...optionalParams: [...any, string?]): void; + override debug(message: any, ...optionalParams: [...any, string?]): void { + if (!this.adapt("debug", message, optionalParams)) { + super.debug(message, ...optionalParams); + } + } + + verbose(message: any, context?: string): void; + verbose(message: any, ...optionalParams: [...any, string?]): void; + override verbose(message: any, ...optionalParams: [...any, string?]): void { + if (!this.adapt("verbose", message, optionalParams)) { + super.verbose(message, ...optionalParams); + } + } + + fatal(message: any, context?: string): void; + fatal(message: any, ...optionalParams: [...any, string?]): void; + override fatal(message: any, ...optionalParams: [...any, string?]): void { + if (!this.adapt("error", message, optionalParams)) { + super.fatal(message, ...optionalParams); + } + } +} + +/** biome-ignore-end lint/suspicious/noExplicitAny: ↑ */ diff --git a/project/server/src/layer/openapi/explorer/explorer.controller.test.ts b/project/server/src/layer/openapi/explorer/explorer.controller.test.ts new file mode 100644 index 00000000..cf1c872f --- /dev/null +++ b/project/server/src/layer/openapi/explorer/explorer.controller.test.ts @@ -0,0 +1,54 @@ +import { type TestContext, test } from "node:test"; + +import { NotFoundException, StreamableFile } from "@nestjs/common"; + +import { ControllerOpenapiExplorer } from "./explorer.controller"; +import { EXPLORER_PATH, ServiceOpenapiExplorer } from "./explorer.service"; + +const service = new ServiceOpenapiExplorer(); + +const controller = () => new ControllerOpenapiExplorer(service); + +test("mount point", (t: TestContext) => { + t.assert.deepStrictEqual(controller().index(), { + url: "/openapi/explorer/index.html", + }); +}); + +test("initializer", (t: TestContext) => { + t.assert.strictEqual( + controller().initializer(), + service.initializer(`/${EXPLORER_PATH}`), + ); +}); + +test("schema", (t: TestContext) => { + t.assert.deepStrictEqual(controller().schema(), service.schema); +}); + +test("assets", async (t: TestContext) => { + await t.test("present", async (t: TestContext) => { + const asset = await controller().asset("swagger-ui.css"); + + t.assert.ok(asset instanceof StreamableFile); + t.assert.strictEqual(asset.getHeaders().type, "text/css; charset=utf-8"); + + asset.getStream().destroy(); + }); + + await t.test("missing", async (t: TestContext) => { + await t.assert.rejects( + controller().asset("absent.js"), + (error: unknown) => error instanceof NotFoundException, + ); + }); + + await t.test("attempted path traversal", async (t: TestContext) => { + for (const name of ["../package.json", "..", "nested/index.html"]) { + await t.assert.rejects( + controller().asset(name), + (error: unknown) => error instanceof NotFoundException, + ); + } + }); +}); diff --git a/project/server/src/layer/openapi/explorer/explorer.controller.ts b/project/server/src/layer/openapi/explorer/explorer.controller.ts new file mode 100644 index 00000000..102dcb81 --- /dev/null +++ b/project/server/src/layer/openapi/explorer/explorer.controller.ts @@ -0,0 +1,52 @@ +import { + Controller, + Get, + Header, + Inject, + NotFoundException, + Param, + Redirect, + StreamableFile, +} from "@nestjs/common"; + +import { isNone } from "../../../type/maybe"; +import { EXPLORER_PATH, ServiceOpenapiExplorer } from "./explorer.service"; + +@Controller(EXPLORER_PATH) +export class ControllerOpenapiExplorer { + constructor( + @Inject(ServiceOpenapiExplorer) + private readonly service: ServiceOpenapiExplorer, + ) {} + + @Get() + @Redirect() + index() { + return { url: `/${EXPLORER_PATH}/index.html` }; + } + + @Get("schema.json") + schema() { + return this.service.schema; + } + + @Get("swagger-initializer.js") + @Header("content-type", "text/javascript") + initializer(): string { + return this.service.initializer(`/${EXPLORER_PATH}`); + } + + // declared last, so the routes above are matched before the distribution + @Get(":asset") + async asset(@Param("asset") name: string): Promise { + const asset = await this.service.asset(name); + if (isNone(asset)) { + throw new NotFoundException(); + } + + return new StreamableFile(asset.stream, { + type: asset.contentType, + length: asset.length, + }); + } +} diff --git a/project/server/src/layer/openapi/explorer/explorer.module.ts b/project/server/src/layer/openapi/explorer/explorer.module.ts new file mode 100644 index 00000000..db92aaa2 --- /dev/null +++ b/project/server/src/layer/openapi/explorer/explorer.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; + +import { ControllerOpenapiExplorer } from "./explorer.controller"; +import { ServiceOpenapiExplorer } from "./explorer.service"; + +@Module({ + controllers: [ControllerOpenapiExplorer], + providers: [ServiceOpenapiExplorer], +}) +export class ModuleOpenapiExplorer {} diff --git a/project/server/src/layer/openapi/explorer/explorer.service.ts b/project/server/src/layer/openapi/explorer/explorer.service.ts new file mode 100644 index 00000000..0d457ee2 --- /dev/null +++ b/project/server/src/layer/openapi/explorer/explorer.service.ts @@ -0,0 +1,92 @@ +import { createReadStream } from "node:fs"; +import { stat } from "node:fs/promises"; +import { dirname, extname, join, resolve } from "node:path"; +import type { Readable } from "node:stream"; + +import { Injectable } from "@nestjs/common"; +import { getAbsoluteFSPath } from "swagger-ui-dist"; + +import schema from "../../../schema.json" with { type: "json" }; + +import type { Maybe } from "../../../type/maybe"; + +/** mount point of the explorer, the paths it serves are relative to */ +export const EXPLORER_PATH = "openapi/explorer"; + +/** only extensions encountered in static `swagger-ui-dist` files */ +const CONTENT_TYPE: Readonly> = { + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".png": "image/png", + ".txt": "text/plain; charset=utf-8", +}; +const CONTENT_TYPE_FALLBACK = "application/octet-stream"; + +// `swagger-ui-dist` ships a hardcoded config at `/swagger-initializer.js` +// serve a customized version instead that points at own schema definition +const initializer = (schemaUrl: string) => `window.onload = function() { + window.ui = SwaggerUIBundle({ + url: ${JSON.stringify(schemaUrl)}, + dom_id: '#swagger-ui', + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + layout: "StandaloneLayout" + }); +};`; + +/** a file of the bundled explorer distribution, opened for reading */ +type Asset = { + readonly stream: Readable; + readonly contentType: string; + readonly length: number; +}; + +@Injectable() +export class ServiceOpenapiExplorer { + /** directory that `swagger-ui-dist` assets are kept in */ + private static readonly root: string = resolve(getAbsoluteFSPath()); + + /** own schema definition, served next to the distribution */ + public readonly schema = schema; + + /** + * `swagger-initializer.js` with substituted schema url + * + * @param mountedAt path the explorer is served under, without trailing slash + */ + initializer(mountedAt: string): string { + return initializer(`${mountedAt}/schema.json`); + } + + /** + * opens an asset of the distribution + * + * @param name file name + */ + async asset(name: string): Promise> { + const path = join(ServiceOpenapiExplorer.root, name); + // ensure directory can't be escaped + if (dirname(path) !== ServiceOpenapiExplorer.root) { + return null; + } + + // anything that is not a readable file is indistinguishable from an absent one + const stats = await stat(path).catch(() => null); + if (stats === null || !stats.isFile()) { + return null; + } + + return { + stream: createReadStream(path), + contentType: + CONTENT_TYPE[extname(path).toLowerCase()] ?? CONTENT_TYPE_FALLBACK, + length: stats.size, + }; + } +} diff --git a/project/server/src/layer/request.interceptor.ts b/project/server/src/layer/request.interceptor.ts new file mode 100644 index 00000000..ab679002 --- /dev/null +++ b/project/server/src/layer/request.interceptor.ts @@ -0,0 +1,84 @@ +import type { + CallHandler, + ExecutionContext, + NestInterceptor, +} from "@nestjs/common"; +import { BadRequestException, Inject, Injectable } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { Schema } from "effect"; +import { isLeft } from "effect/Either"; +import type { Observable } from "rxjs"; + +import { ROUTE_DECODED, RouteCodecMetadata } from "./route"; + +import type { RouteCodec } from "./schema"; + +/** the parts of the platform request a codec is applied to */ +type RequestLike = { + readonly params?: unknown; + readonly query?: unknown; + readonly headers?: Readonly>; + readonly body?: unknown; +}; + +const requestParameters = ( + request: RequestLike, +): Readonly> => ({ + path: request.params ?? {}, + query: request.query ?? {}, + header: request.headers ?? {}, +}); + +const decode = ( + schema: Schema.Schema.AnyNoContext, + section: string, + value: unknown, +): unknown => { + const decoded = Schema.decodeUnknownEither(schema)(value); + if (isLeft(decoded)) { + throw new BadRequestException( + `invalid ${section}: ${decoded.left.message}`, + ); + } + + return decoded.right; +}; + +/** decodes what a `@Route` declares a codec for, before the handler is entered */ +@Injectable() +export class InterceptorRouteRequest implements NestInterceptor { + constructor(@Inject(Reflector) private readonly reflector: Reflector) {} + + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable { + if (context.getType() !== "http") { + return next.handle(); + } + + // absent on any handler that is not a `@Route`, and ones that don't declares schemas + const codec: RouteCodec | undefined = this.reflector.get( + RouteCodecMetadata, + context.getHandler(), + ); + if (typeof codec === "undefined") { + return next.handle(); + } + + const request = context.switchToHttp().getRequest(); + + request[ROUTE_DECODED] = { + parameters: + codec.parameters === undefined + ? undefined + : decode(codec.parameters, "parameters", requestParameters(request)), + requestBody: + codec.requestBody === undefined + ? undefined + : decode(codec.requestBody, "request body", request.body), + }; + + return next.handle(); + } +} diff --git a/project/server/src/layer/response.interceptor.ts b/project/server/src/layer/response.interceptor.ts new file mode 100644 index 00000000..c687909c --- /dev/null +++ b/project/server/src/layer/response.interceptor.ts @@ -0,0 +1,92 @@ +import type { + CallHandler, + ExecutionContext, + NestInterceptor, +} from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; +import { HttpAdapterHost, Reflector } from "@nestjs/core"; +import { Schema } from "effect/index"; +import type { Observable } from "rxjs"; +import { map } from "rxjs/operators"; + +import { RouteSchemaMetadata } from "./route"; + +import type { HttpResponse } from "./http"; +import type { RouteSchema } from "./schema"; + +const Response = Schema.Struct({ + body: Schema.optional(Schema.Unknown), + code: Schema.Number, + contentType: Schema.optional(Schema.String), + headers: Schema.optional( + Schema.Record({ key: Schema.String, value: Schema.String }), + ), +}); +const isResponse = Schema.is(Response); + +/** a `@Route` handler resolved to something other than an endpoint response */ +export class EndpointResponseMalformedError extends Error { + constructor( + public route: RouteSchema, + public result: unknown, + ) { + super( + `<${route.method} ${route.path}> did not resolve to an endpoint response`, + ); + Object.setPrototypeOf(this, EndpointResponseMalformedError.prototype); + } +} + +/** unwraps the `{ code, contentType, body, headers }` union returned by `@Route` handlers into response */ +@Injectable() +export class InterceptorEndpointResponse + implements NestInterceptor +{ + constructor( + @Inject(Reflector) private readonly reflector: Reflector, + @Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost, + ) {} + + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable { + if (context.getType() !== "http") { + return next.handle(); + } + + const route: RouteSchema | undefined = this.reflector.get( + RouteSchemaMetadata, + context.getHandler(), + ); + if (typeof route === "undefined") { + // not defined through `@Route`, skipping + return next.handle(); + } + + const response = context.switchToHttp().getResponse(); + const { httpAdapter } = this.adapterHost; + + return next.handle().pipe( + map((result) => { + if (!isResponse(result)) { + throw new EndpointResponseMalformedError(route, result); + } + + for (const [name, value] of Object.entries(result.headers ?? {})) { + if (value !== undefined) { + httpAdapter.setHeader(response, name, String(value)); + } + } + + const { contentType } = result; + if (contentType !== undefined) { + httpAdapter.setHeader(response, "content-type", contentType); + } + + httpAdapter.status(response, result.code); + return result.body; + }), + ); + } +} diff --git a/project/server/src/layer/route.test.ts b/project/server/src/layer/route.test.ts new file mode 100644 index 00000000..6b7db8c5 --- /dev/null +++ b/project/server/src/layer/route.test.ts @@ -0,0 +1,280 @@ +import { type TestContext, test } from "node:test"; + +import { BadRequestException } from "@nestjs/common"; +import { PATH_METADATA } from "@nestjs/common/constants"; +import { Reflector } from "@nestjs/core"; +import { Schema } from "effect"; + +import { _Route, ROUTE_DECODED, RouteSchemaMetadata } from "./route"; +import { intercept, invoke, routeArguments } from "./test"; + +import type { _Implements } from "./schema"; +import type { RequestStub } from "./test"; + +const declaredArguments = ( + controller: new (...args: never[]) => unknown, + propertyKey: string, +): readonly (readonly [number, unknown])[] => + routeArguments(controller, propertyKey).map( + ({ index, data }) => [index, data] as const, + ); + +test("an operation declaring parameters and a request body", (t: TestContext) => { + type Paths = { + readonly "/command": { + readonly post: { + readonly parameters: { + readonly header: { + readonly "x-signature": string; + readonly "x-timestamp": string; + }; + }; + readonly requestBody: { + readonly content: { + readonly "application/json": { + readonly command: string; + readonly text: string; + }; + }; + }; + readonly responses: { + readonly 200: { + headers: { readonly [name: string]: unknown }; + content: { + readonly "application/json": { readonly type: string }; + }; + }; + }; + }; + }; + }; + + const Parameters = Schema.Struct({ + header: Schema.Struct({ + "x-signature": Schema.String, + "x-timestamp": Schema.NumberFromString, + }), + }); + type Parameters = typeof Parameters.Type; + + const RequestBody = Schema.Struct({ + command: Schema.String, + text: Schema.Trim, + }); + type RequestBody = typeof RequestBody.Type; + + const Route = _Route(); + + class Controller implements _Implements { + received: readonly unknown[] = []; + + @Route("post", "/command", { + parameters: Parameters, + requestBody: RequestBody, + }) + // a decorator cannot contextually type the parameters of the method it + // decorates, so they are annotated with what the schemas decode to; the + // annotations are checked against `HandlerArguments` + async post(parameters: Parameters, requestBody: RequestBody) { + this.received = [parameters, requestBody]; + return { + code: 200, + contentType: "application/json", + body: { type: "ephemeral" }, + } as const; + } + } + + const request: RequestStub = { + headers: { "x-signature": "v0=deadbeef", "x-timestamp": "1700000000" }, + body: { command: "/device", text: " shelly " }, + }; + + t.test( + "hands both decoded sections to the handler", + async (t: TestContext) => { + const controller = new Controller(); + + const response = await invoke(controller, "post", { + ...request, + }); + + t.assert.deepStrictEqual(controller.received, [ + { + header: { + "x-signature": "v0=deadbeef", + // transformed by the schema + "x-timestamp": 1700000000, + }, + }, + { command: "/device", text: "shelly" }, + ]); + t.assert.deepStrictEqual(response, { + code: 200, + contentType: "application/json", + body: { type: "ephemeral" }, + }); + }, + ); + + t.test("rejects parameters the schema does not accept", (t: TestContext) => { + const controller = new Controller(); + + t.assert.throws( + () => + invoke(controller, "post", { + ...request, + headers: { "x-signature": "v0=deadbeef" }, + }), + (error: unknown) => + error instanceof BadRequestException && + error.getStatus() === 400 && + error.message.startsWith("invalid parameters:"), + ); + t.assert.deepStrictEqual(controller.received, []); + }); + + t.test( + "rejects a request body the schema does not accept", + (t: TestContext) => { + const controller = new Controller(); + + t.assert.throws( + () => + invoke(controller, "post", { + ...request, + body: { command: "/device" }, + }), + (error: unknown) => + error instanceof BadRequestException && + error.getStatus() === 400 && + error.message.startsWith("invalid request body:"), + ); + t.assert.deepStrictEqual(controller.received, []); + }, + ); + + t.test( + "records the schema metadata the response interceptor reads", + (t: TestContext) => { + t.assert.deepStrictEqual( + new Reflector().get(RouteSchemaMetadata, Controller.prototype.post), + { path: "/command", method: "post" }, + ); + }, + ); + + t.test( + "binds both sections to the positions the handler declares", + (t: TestContext) => { + t.assert.deepStrictEqual(declaredArguments(Controller, "post"), [ + [0, "parameters"], + [1, "requestBody"], + ]); + }, + ); +}); + +test("an operation declaring parameters only", (t: TestContext) => { + type Paths = { + readonly "/search": { + readonly get: { + readonly parameters: { + readonly query: { + readonly q: string; + readonly tag?: readonly string[]; + }; + }; + readonly responses: { + readonly 204: { headers: { readonly [name: string]: unknown } }; + }; + }; + }; + }; + + const Parameters = Schema.Struct({ + query: Schema.Struct({ + q: Schema.Trim, + // a repeatable query parameter arrives in unary form when it occurs once + tag: Schema.optional( + Schema.Union(Schema.String, Schema.Array(Schema.String)), + ), + }), + }); + type Parameters = typeof Parameters.Type; + + const Route = _Route(); + + class Controller implements _Implements { + received: unknown = undefined; + + @Route("get", "/search", { parameters: Parameters }) + async get(parameters: Parameters) { + this.received = parameters; + return { code: 204 } as const; + } + } + + t.test( + "hands the sole decoded section to the handler", + async (t: TestContext) => { + const controller = new Controller(); + + await invoke(controller, "get", { + query: { q: " shelly ", tag: "relay" }, + cookies: { session: "parsed" }, + }); + + t.assert.deepStrictEqual(controller.received, { + query: { q: "shelly", tag: "relay" }, + }); + }, + ); + + t.test("does not provide request body when not given", (t: TestContext) => { + t.assert.deepStrictEqual(declaredArguments(Controller, "get"), [ + [0, "parameters"], + ]); + }); +}); + +test("an operation declaring no codec", (t: TestContext) => { + type Paths = { + readonly "/thing/{id}": { + readonly get: { + readonly parameters: { readonly path: { readonly id: string } }; + readonly responses: { + readonly 204: { headers: { readonly [name: string]: unknown } }; + }; + }; + }; + }; + + const Route = _Route(); + + class Controller implements _Implements { + @Route("get", "/thing/{id}") + async get() { + return { code: 204 } as const; + } + } + + t.test("templates the path the way nest expects", (t: TestContext) => { + t.assert.strictEqual( + Reflect.getMetadata(PATH_METADATA, Controller.prototype.get), + "/thing/:id", + ); + }); + + t.test("binds no parameters at all", (t: TestContext) => { + t.assert.deepStrictEqual(declaredArguments(Controller, "get"), []); + }); + + t.test("is left alone by the request interceptor", (t: TestContext) => { + const request: RequestStub = { params: { id: "a" } }; + + intercept(Controller.prototype.get, request); + + t.assert.strictEqual(request[ROUTE_DECODED], undefined); + }); +}); diff --git a/project/server/src/layer/route.ts b/project/server/src/layer/route.ts new file mode 100644 index 00000000..85fe8d69 --- /dev/null +++ b/project/server/src/layer/route.ts @@ -0,0 +1,147 @@ +import type { ExecutionContext } from "@nestjs/common"; +import { + createParamDecorator, + Delete, + Get, + Patch, + Post, + Put, +} from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import type { Schema } from "effect"; + +import type { paths } from "../schema"; +import type { + _Codec, + _EndpointResponse, + _MethodKeyOf, + HandlerArguments, + HttpMethod, + RouteCodec, + RouteSchema, +} from "./schema"; + +/** metadata key used by response interceptor to indicate that a route was registered through {@link Route} */ +export const RouteSchemaMetadata = Reflector.createDecorator({ + key: "route:schema", +}); + +/** metadata key used by request interceptor to retrieve codecs */ +export const RouteCodecMetadata = Reflector.createDecorator({ + key: "route:codec", +}); + +/** where the request interceptor leaves what it decoded */ +export const ROUTE_DECODED = Symbol("route:decoded"); + +/** section of a request the codec decodes */ +export type DecodedSection = "parameters" | "requestBody"; + +/** a platform request the request interceptor has decoded onto */ +export type DecodedRequest = { + [ROUTE_DECODED]?: { + readonly [Section in DecodedSection]: unknown; + }; +}; + +/** picks up decoded parameters from where interceptor left them to be used as handler function parameters */ +const DecodedParameter = createParamDecorator( + (section: DecodedSection, context: ExecutionContext) => + context.switchToHttp().getRequest()[ROUTE_DECODED]?.[ + section + ], +); + +const NEST_ROUTE = { + get: Get, + put: Put, + post: Post, + patch: Patch, + delete: Delete, +} as const satisfies Record MethodDecorator>; + +/** `{id}` → `:id` */ +const toPath = (path: string): string => path.replace(/\{([^}]+)\}/gu, ":$1"); + +/** + * unbound schema guard for `@Get()` / `@Post()` / ... + * + * type inference fails when not bound explicitly + * ```typescript + * // bind explicitly before using as decorator + * const Route = _Route(); + * ```` + * + * @param method http method + * @param path schema path + * @param codec {@link _Codec} + */ +export const _Route = + () => + < + Path extends keyof Paths, + Method extends _MethodKeyOf, + CodecParameters extends Schema.Schema.AnyNoContext, + CodecRequestBody extends Schema.Schema.AnyNoContext, + >( + method: Method, + path: Path, + codec?: _Codec, + ) => + < + Handler extends ( + ...args: HandlerArguments + ) => + | _EndpointResponse + | Promise<_EndpointResponse>, + >( + target: object, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, + ): void => { + const untyped = descriptor as TypedPropertyDescriptor; + + const parameters = codec?.parameters as + | Schema.Schema.AnyNoContext + | undefined; + const requestBody = codec?.requestBody as + | Schema.Schema.AnyNoContext + | undefined; + if ( + typeof parameters !== "undefined" || + typeof requestBody !== "undefined" + ) { + RouteCodecMetadata({ parameters, requestBody })( + target, + propertyKey, + untyped, + ); + } + + // enables accessing decoded parameters / request body through handle function parameters + if (typeof parameters !== "undefined") { + DecodedParameter("parameters")(target, propertyKey, 0); + } + if (typeof requestBody !== "undefined") { + DecodedParameter("requestBody")(target, propertyKey, 1); + } + + NEST_ROUTE[String(method) as HttpMethod](toPath(String(path)))( + target, + propertyKey, + untyped, + ); + RouteSchemaMetadata({ + path: String(path), + method: String(method), + })(target, propertyKey, untyped); + }; + +/** + * schema guard for `@Get()` / `@Post()` / ... + * + * @param method http method + * @param path schema path + * @param codec {@link _Codec} + */ +export const Route = _Route(); diff --git a/project/server/src/layer/scheduler/scheduled/derive/device.controller.ts b/project/server/src/layer/scheduler/scheduled/derive/device.controller.ts new file mode 100644 index 00000000..ff11aadf --- /dev/null +++ b/project/server/src/layer/scheduler/scheduled/derive/device.controller.ts @@ -0,0 +1,493 @@ +import { Controller, Inject } from "@nestjs/common"; +import { Schema } from "effect"; + +import integrations from "../../../../categorized-integrations.json"; +import { logger as parentLogger } from "../../../../logger"; +import { + DeviceCategoryIdValue, + DeviceConnectivityValue, + type SchedulerScheduledDeriveDeviceDeviceMono, +} from "../../../../service/scheduler/scheduled/derive/device"; +import { floor, Integer } from "../../../../type/codec/integer"; +import { Uuid } from "../../../../type/codec/uuid"; +import { isNone, isSome } from "../../../../type/maybe"; +import { RequestPath } from "../../../http"; +import { ServiceIngress } from "../../../ingress/ingress.service"; +import { Route } from "../../../route"; +import { ServiceSchedulerScheduledDeriveDevice } from "./device.service"; + +type Integration = keyof typeof integrations; + +const logger = parentLogger.child({ + label: "controller-scheduler-scheduled-derive-device", +}); + +const ParametersDevices = Schema.Struct({ + query: Schema.partial( + Schema.Struct({ + term: Schema.String, + canonical: Schema.BooleanFromString, + manufacturer: Schema.Union(Schema.Array(Schema.String), Schema.String), + "!manufacturer": Schema.Union(Schema.Array(Schema.String), Schema.String), + category: Schema.Union( + Schema.Array(DeviceCategoryIdValue), + DeviceCategoryIdValue, + ), + "!category": Schema.Union( + Schema.Array(DeviceCategoryIdValue), + DeviceCategoryIdValue, + ), + connectivity: Schema.Union( + Schema.Array(DeviceConnectivityValue), + DeviceConnectivityValue, + ), + "!connectivity": Schema.Union( + Schema.Array(DeviceConnectivityValue), + DeviceConnectivityValue, + ), + page: Schema.compose(Schema.NumberFromString, Integer), + size: Schema.compose( + Schema.NumberFromString.pipe(Schema.between(10, 50)), + Integer, + ), + }), + ), +}); +type ParametersDevices = typeof ParametersDevices.Type; + +const ParametersDevice = Schema.Struct({ + path: Schema.Struct({ + id: Uuid, + }), +}); +type ParametersDevice = typeof ParametersDevice.Type; + +const ParametersDeviceDuplicates = Schema.Struct({ + path: Schema.Struct({ + id: Uuid, + }), + query: Schema.partial( + Schema.Struct({ + page: Schema.compose(Schema.NumberFromString, Integer), + size: Schema.compose( + Schema.NumberFromString.pipe(Schema.between(10, 50)), + Integer, + ), + }), + ), +}); +type ParametersDeviceDuplicates = typeof ParametersDeviceDuplicates.Type; + +const ParametersDimensions = Schema.Struct({ + query: Schema.partial( + Schema.Struct({ + term: Schema.String, + manufacturer: Schema.Union(Schema.Array(Schema.String), Schema.String), + "!manufacturer": Schema.Union(Schema.Array(Schema.String), Schema.String), + category: Schema.Union( + Schema.Array(DeviceCategoryIdValue), + DeviceCategoryIdValue, + ), + "!category": Schema.Union( + Schema.Array(DeviceCategoryIdValue), + DeviceCategoryIdValue, + ), + connectivity: Schema.Union( + Schema.Array(DeviceConnectivityValue), + DeviceConnectivityValue, + ), + "!connectivity": Schema.Union( + Schema.Array(DeviceConnectivityValue), + DeviceConnectivityValue, + ), + }), + ), +}); +type ParametersDimensions = typeof ParametersDimensions.Type; + +@Controller() +export class ControllerSchedulerScheduledDeriveDevice { + constructor( + @Inject(ServiceSchedulerScheduledDeriveDevice) + private readonly service: ServiceSchedulerScheduledDeriveDevice, + @Inject(ServiceIngress) + private readonly ingress: ServiceIngress, + ) {} + + private static map( + d: Omit, + ) { + const integration = Object.keys(integrations).includes(d.integration) + ? integrations[d.integration as Integration] + : undefined; + + if (typeof integration === "undefined") { + logger.warn(`integration definition missing for <${d.integration}>`, { + integration: d.integration, + }); + + return null; + } + + const independent = { + integration: { + name: integration.title, + domain: d.integration, + }, + manufacturer: d.manufacturer, + first_encountered: d.firstEncounteredAt.toISOString(), + categories: d.categories, + connectivity: d.connectivity, + versions: { + software: d.versions.software.map((item) => ({ + version: item.version, + active: item.active, + first_encountered: item.firstEncounteredAt.toISOString(), + })), + hardware: d.versions.hardware.map((item) => ({ + version: item.version, + first_encountered: item.firstEncounteredAt.toISOString(), + })), + }, + entities: d.entities.map((item) => ({ + domain: item.domain, + original_device_class: item.originalDeviceClass, + })), + count: d.count, + } as const; + + if (typeof d.model !== "undefined" && typeof d.modelId !== "undefined") { + return { + ...independent, + model: d.model, + model_id: d.modelId, + } as const; + } else if ( + typeof d.model !== "undefined" && + typeof d.modelId === "undefined" + ) { + return { + ...independent, + model: d.model, + } as const; + } else if ( + typeof d.model === "undefined" && + typeof d.modelId !== "undefined" + ) { + return { + ...independent, + model_id: d.modelId, + } as const; + } + + return null; + } + + @Route("get", "/api/unstable/derived/devices", { + parameters: ParametersDevices, + }) + async devices( + { + query: { + page, + size, + term, + canonical, + category: includeCategory, + "!category": excludeCategory, + connectivity: includeConnectivity, + "!connectivity": excludeConnectivity, + manufacturer: includeManufacturer, + "!manufacturer": excludeManufacturer, + }, + }: ParametersDevices, + @RequestPath() path: string, + ) { + const query = { + term, + canonical: canonical ?? true, + include: { + categories: + typeof includeCategory !== "undefined" + ? new Set( + typeof includeCategory === "string" + ? [includeCategory] + : includeCategory, + ) + : undefined, + connectivities: + typeof includeConnectivity !== "undefined" + ? new Set( + typeof includeConnectivity === "string" + ? [includeConnectivity] + : includeConnectivity, + ) + : undefined, + manufacturers: + typeof includeManufacturer !== "undefined" + ? new Set( + typeof includeManufacturer === "string" + ? [includeManufacturer] + : includeManufacturer, + ) + : undefined, + }, + exclude: { + categories: + typeof excludeCategory !== "undefined" + ? new Set( + typeof excludeCategory === "string" + ? [excludeCategory] + : excludeCategory, + ) + : undefined, + connectivities: + typeof excludeConnectivity !== "undefined" + ? new Set( + typeof excludeConnectivity === "string" + ? [excludeConnectivity] + : excludeConnectivity, + ) + : undefined, + manufacturers: + typeof excludeManufacturer !== "undefined" + ? new Set( + typeof excludeManufacturer === "string" + ? [excludeManufacturer] + : excludeManufacturer, + ) + : undefined, + }, + } as const; + + const paginated = await this.ingress.paginate()({ + slice: ({ offset, limit }) => + this.service.devices.slice(query, { offset, limit }), + count: async () => await this.service.devices.count(query), + })({ + path, + page, + size, + }); + + const mapped = paginated.items.flatMap((device) => { + const mapped = ControllerSchedulerScheduledDeriveDevice.map(device); + return isSome(mapped) + ? [ + { + ...mapped, + id: device.id, + url: this.ingress.url.device.self(device.id).toString(), + duplicates: device.duplicates.map((id) => ({ + id, + url: this.ingress.url.device.self(id).toString(), + })), + }, + ] + : []; + }); + + return { + code: 200, + body: mapped, + contentType: "application/json", + headers: { + "cache-control": "max-age=1800", + ...paginated.headers, + }, + } as const; + } + + @Route("get", "/api/unstable/derived/devices/{id}", { + parameters: ParametersDevice, + }) + async device({ path: { id } }: ParametersDevice) { + const result = await this.service.device({ id }); + if (isNone(result)) { + return { + code: 404, + body: "not found", + } as const; + } + + const device = ControllerSchedulerScheduledDeriveDevice.map(result); + if (isNone(device)) { + return { + code: 404, + body: "not found", + } as const; + } + + const query = { canonical: new Set([id]) }; + + const paginated = await this.ingress.paginate()({ + slice: ({ offset, limit }) => + this.service.devices.slice(query, { offset, limit }), + count: async () => await this.service.devices.count(query), + })({ + path: this.ingress.url.device.duplicates(id), + page: floor(0), + size: undefined, + }); + + return { + code: 200, + contentType: "application/json", + body: { + ...device, + duplicates: { + items: paginated.items.flatMap((device) => { + const mapped = ControllerSchedulerScheduledDeriveDevice.map(device); + return isSome(mapped) + ? [ + { + ...mapped, + id: device.id, + url: this.ingress.url.device.self(device.id).toString(), + }, + ] + : []; + }), + total: paginated.total, + next: paginated.links.next?.toString(), + }, + }, + headers: { + "cache-control": "max-age=1800", + }, + } as const; + } + + @Route("get", "/api/unstable/derived/devices/{id}/duplicates", { + parameters: ParametersDeviceDuplicates, + }) + async deviceDuplicates( + { path: { id }, query: { page, size } }: ParametersDeviceDuplicates, + @RequestPath() path: string, + ) { + const result = await this.service.device({ id }); + if (isNone(result)) { + return { + code: 404, + body: "not found", + } as const; + } + + const query = { + canonical: new Set([id]), + }; + + const paginated = await this.ingress.paginate()({ + slice: ({ offset, limit }) => + this.service.devices.slice(query, { offset, limit }), + count: async () => await this.service.devices.count(query), + })({ + path, + page, + size, + }); + + const mapped = paginated.items.flatMap((device) => { + const mapped = ControllerSchedulerScheduledDeriveDevice.map(device); + return isSome(mapped) + ? [ + { + ...mapped, + id: device.id, + url: this.ingress.url.device.self(device.id).toString(), + }, + ] + : []; + }); + + return { + code: 200, + body: mapped, + contentType: "application/json", + headers: { + "cache-control": "max-age=1800", + ...paginated.headers, + }, + } as const; + } + + @Route("get", "/api/unstable/dimensions", { + parameters: ParametersDimensions, + }) + async dimensions({ + query: { + term, + category: includeCategory, + "!category": excludeCategory, + connectivity: includeConnectivity, + "!connectivity": excludeConnectivity, + manufacturer: includeManufacturer, + "!manufacturer": excludeManufacturer, + }, + }: ParametersDimensions) { + const query = { + term, + canonical: true, + include: { + categories: + typeof includeCategory !== "undefined" + ? new Set( + typeof includeCategory === "string" + ? [includeCategory] + : includeCategory, + ) + : undefined, + connectivities: + typeof includeConnectivity !== "undefined" + ? new Set( + typeof includeConnectivity === "string" + ? [includeConnectivity] + : includeConnectivity, + ) + : undefined, + manufacturers: + typeof includeManufacturer !== "undefined" + ? new Set( + typeof includeManufacturer === "string" + ? [includeManufacturer] + : includeManufacturer, + ) + : undefined, + }, + exclude: { + categories: + typeof excludeCategory !== "undefined" + ? new Set( + typeof excludeCategory === "string" + ? [excludeCategory] + : excludeCategory, + ) + : undefined, + connectivities: + typeof excludeConnectivity !== "undefined" + ? new Set( + typeof excludeConnectivity === "string" + ? [excludeConnectivity] + : excludeConnectivity, + ) + : undefined, + manufacturers: + typeof excludeManufacturer !== "undefined" + ? new Set( + typeof excludeManufacturer === "string" + ? [excludeManufacturer] + : excludeManufacturer, + ) + : undefined, + }, + } as const; + return { + code: 200, + body: await this.service.filters(query), + contentType: "application/json", + headers: { + "cache-control": "max-age=1800", + }, + } as const; + } +} diff --git a/project/server/src/layer/scheduler/scheduled/derive/device.module.ts b/project/server/src/layer/scheduler/scheduled/derive/device.module.ts new file mode 100644 index 00000000..22ecf968 --- /dev/null +++ b/project/server/src/layer/scheduler/scheduled/derive/device.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; + +import { ModuleDatabase } from "../../../database/database.module"; +import { ModuleIngress } from "../../../ingress/ingress.module"; +import { ControllerSchedulerScheduledDeriveDevice } from "./device.controller"; +import { ServiceSchedulerScheduledDeriveDevice } from "./device.service"; + +@Module({ + imports: [ModuleDatabase, ModuleIngress], + providers: [ServiceSchedulerScheduledDeriveDevice], + controllers: [ControllerSchedulerScheduledDeriveDevice], + exports: [ServiceSchedulerScheduledDeriveDevice], +}) +export class ModuleSchedulerScheduledDeriveDevice {} diff --git a/project/server/src/layer/scheduler/scheduled/derive/device.service.ts b/project/server/src/layer/scheduler/scheduled/derive/device.service.ts new file mode 100644 index 00000000..3eee1128 --- /dev/null +++ b/project/server/src/layer/scheduler/scheduled/derive/device.service.ts @@ -0,0 +1,13 @@ +import { Inject, Injectable } from "@nestjs/common"; + +import { SchedulerScheduledDeriveDevice } from "../../../../service/scheduler/scheduled/derive/device"; +import { DatabaseDerived } from "../../../database/database.module"; + +import type { IDatabase } from "../../../../service/database"; + +@Injectable() +export class ServiceSchedulerScheduledDeriveDevice extends SchedulerScheduledDeriveDevice { + constructor(@Inject(DatabaseDerived) db: IDatabase<"derived">) { + super(db); + } +} diff --git a/project/server/src/layer/scheduler/scheduler-coordinator.module.ts b/project/server/src/layer/scheduler/scheduler-coordinator.module.ts new file mode 100644 index 00000000..482ff3f3 --- /dev/null +++ b/project/server/src/layer/scheduler/scheduler-coordinator.module.ts @@ -0,0 +1,17 @@ +import { Module } from "@nestjs/common"; + +import { ModuleLockfileCoordinator } from "../database/lockfile-corrdinator.module"; +import { ModuleSnapshotDeferIngest } from "../snapshot/defer/ingest.module"; +import { ModuleScheduler } from "./scheduler.module"; +import { ServiceSchedulerCoordinator } from "./scheduler-coordinator.service"; + +@Module({ + imports: [ + ModuleLockfileCoordinator, + ModuleScheduler, + ModuleSnapshotDeferIngest, + ], + providers: [ServiceSchedulerCoordinator], + exports: [ServiceSchedulerCoordinator], +}) +export class ModuleSchedulerCoordinator {} diff --git a/project/server/src/layer/scheduler/scheduler-coordinator.service.ts b/project/server/src/layer/scheduler/scheduler-coordinator.service.ts new file mode 100644 index 00000000..fe13f4a0 --- /dev/null +++ b/project/server/src/layer/scheduler/scheduler-coordinator.service.ts @@ -0,0 +1,92 @@ +import { hrtime } from "node:process"; + +import { Inject, Injectable, type OnApplicationShutdown } from "@nestjs/common"; +import type { PickDeep } from "type-fest"; + +import { logger as parentLogger } from "../../logger"; +import { SuspendableHandle } from "../../service/suspendable"; +import { formatNs } from "../../utility/format"; +import { Config } from "../config/config.module"; +import { ServiceLockfileCoordinator } from "../database/lockfile-coordinator.service"; +import { ServiceSnapshotDeferIngest } from "../snapshot/defer/ingest.service"; +import { ServiceScheduler } from "./scheduler.service"; + +const logger = parentLogger.child({ label: "scheduler-coordinator" }); + +@Injectable() +export class ServiceSchedulerCoordinator implements OnApplicationShutdown { + private controller = new AbortController(); + + constructor( + @Inject(Config) config: PickDeep, + @Inject(ServiceScheduler) scheduler: ServiceScheduler, + @Inject(ServiceLockfileCoordinator) + lockfileCoordinator: ServiceLockfileCoordinator, + @Inject(ServiceSnapshotDeferIngest) ingest: ServiceSnapshotDeferIngest, + ) { + if (!config.scheduler.enable) { + return; + } + + void (async () => { + await lockfileCoordinator.unlocked; + + logger.info("started"); + + const handle = new SuspendableHandle(Symbol("Derive")); + + let epoch = ServiceScheduler.epoch(); + while (true) { + if (this.controller.signal.aborted) { + break; + } + + epoch = await scheduler.wait(epoch); + const plan = scheduler.plan(epoch); + + if (!ServiceScheduler.viable(plan)) { + throw new Error( + `scheduler plan not viable <${JSON.stringify(plan)}>`, + ); + } + + // pause ingesting to prevent wal growth + { + const start = hrtime.bigint(); + await ingest.suspend(handle); + const end = hrtime.bigint(); + logger.debug(`paused ingestion in ${formatNs(end - start)}s`, { + took: end - start, + }); + } + + for await (const status of scheduler.act(plan)) { + switch (status.kind) { + case "pending": + logger.info(`running <${status.id.description}>`, { + identifier: status.id, + }); + break; + case "success": + logger.info( + `ran <${status.id.description}> in ${formatNs(status.took)}s`, + { identifier: status.id, took: status.took }, + ); + break; + case "error": + logger.error(`error while running <${status.id.description}>`, { + identifier: status.id, + }); + console.error(status.error); + } + } + + ingest.resume(handle); + } + })(); + } + + async onApplicationShutdown(): Promise { + this.controller.abort(); + } +} diff --git a/project/server/src/layer/scheduler/scheduler.module.ts b/project/server/src/layer/scheduler/scheduler.module.ts new file mode 100644 index 00000000..f45e8fc7 --- /dev/null +++ b/project/server/src/layer/scheduler/scheduler.module.ts @@ -0,0 +1,25 @@ +import { Module } from "@nestjs/common"; + +import { ModuleIntrospection } from "../introspection/introspection.module"; +import { ModuleSchedulerScheduledDeriveDevice } from "./scheduled/derive/device.module"; +import { ServiceSchedulerScheduledDeriveDevice } from "./scheduled/derive/device.service"; +import { SchedulerScheduled } from "./scheduler.registry"; +import { ServiceScheduler } from "./scheduler.service"; + +import type { SchedulerScheduledInstance } from "../../service/scheduler/base"; + +@Module({ + imports: [ModuleIntrospection, ModuleSchedulerScheduledDeriveDevice], + providers: [ + { + provide: SchedulerScheduled, + useFactory: ( + ...units: SchedulerScheduledInstance[] + ): SchedulerScheduled => units, + inject: [ServiceSchedulerScheduledDeriveDevice], + }, + ServiceScheduler, + ], + exports: [ServiceScheduler], +}) +export class ModuleScheduler {} diff --git a/project/server/src/layer/scheduler/scheduler.registry.ts b/project/server/src/layer/scheduler/scheduler.registry.ts new file mode 100644 index 00000000..f09110e3 --- /dev/null +++ b/project/server/src/layer/scheduler/scheduler.registry.ts @@ -0,0 +1,4 @@ +import type { SchedulerScheduledInstance } from "../../service/scheduler/base"; + +export const SchedulerScheduled = Symbol("SchedulerScheduled"); +export type SchedulerScheduled = readonly SchedulerScheduledInstance[]; diff --git a/project/server/src/layer/scheduler/scheduler.service.ts b/project/server/src/layer/scheduler/scheduler.service.ts new file mode 100644 index 00000000..eadd35fc --- /dev/null +++ b/project/server/src/layer/scheduler/scheduler.service.ts @@ -0,0 +1,15 @@ +import { Inject, Injectable } from "@nestjs/common"; + +import { Scheduler } from "../../service/scheduler"; +import { ServiceIntrospection } from "../introspection/introspection.service"; +import { SchedulerScheduled } from "./scheduler.registry"; + +@Injectable() +export class ServiceScheduler extends Scheduler { + constructor( + @Inject(SchedulerScheduled) scheduled: SchedulerScheduled, + @Inject(ServiceIntrospection) introspection: ServiceIntrospection, + ) { + super(scheduled, introspection); + } +} diff --git a/project/server/src/layer/schema.test.ts b/project/server/src/layer/schema.test.ts new file mode 100644 index 00000000..96498126 --- /dev/null +++ b/project/server/src/layer/schema.test.ts @@ -0,0 +1,358 @@ +import type { Schema } from "effect"; + +import type { + _Codec, + _ContentTypeOf, + _EndpointResponse, + _Implements, + _MethodOf, + _ParametersInputOf, + _ParametersOf, + _PathParamOf, + _QueryParamOf, + _RequestBodyOf, + _StatusOf, +} from "./schema"; + +type Equal = + (() => G extends A ? 1 : 2) extends () => G extends B ? 1 : 2 + ? true + : false; +type Expect = T; + +// mirrors the shape openapi-typescript emits, including `?: never` for the +// verbs a path does not declare +export type MockPaths = { + readonly "/thing": { + readonly parameters: { + readonly query?: never; + readonly header?: never; + readonly path?: never; + readonly cookie?: never; + }; + readonly get: { + readonly parameters: { + readonly query?: { readonly page?: string; readonly size?: string }; + readonly header?: never; + readonly path?: never; + readonly cookie?: never; + }; + readonly requestBody?: never; + readonly responses: { + readonly 200: { + headers: { + readonly "x-total": string; + readonly [name: string]: unknown; + }; + content: { readonly "application/json": { readonly id: string } }; + }; + readonly 404: { + headers: { readonly [name: string]: unknown }; + content: { readonly "text/plain": "missing" }; + }; + }; + }; + readonly put?: never; + readonly post: { + readonly parameters: { + readonly query?: never; + readonly header?: never; + readonly path?: never; + readonly cookie?: never; + }; + readonly requestBody: { + readonly content: { + readonly "application/json": { readonly name: string }; + }; + }; + readonly responses: { + readonly 201: { + headers: { readonly [name: string]: unknown }; + content: { readonly "application/json": { readonly id: string } }; + }; + }; + }; + readonly delete?: never; + readonly options?: never; + readonly head?: never; + readonly patch?: never; + readonly trace?: never; + }; + readonly "/thing/{id}": { + readonly parameters: { + readonly query?: never; + readonly header?: never; + readonly path?: never; + readonly cookie?: never; + }; + readonly get: { + readonly parameters: { + readonly query?: never; + readonly header?: never; + readonly path: { readonly id: string }; + readonly cookie?: never; + }; + readonly requestBody?: never; + readonly responses: { + readonly 204: { + headers: { readonly [name: string]: unknown }; + }; + }; + }; + readonly put?: never; + readonly post?: never; + readonly delete?: never; + readonly options?: never; + readonly head?: never; + readonly patch?: never; + readonly trace?: never; + }; + readonly "/search": { + readonly parameters: { + readonly query?: never; + readonly header?: never; + readonly path?: never; + readonly cookie?: never; + }; + readonly get: { + readonly parameters: { + readonly query: { + readonly q: string; + readonly tag?: readonly string[]; + }; + readonly header?: never; + readonly path?: never; + readonly cookie: { readonly session: string }; + }; + readonly requestBody?: never; + readonly responses: { + readonly 204: { + headers: { readonly [name: string]: unknown }; + }; + }; + }; + readonly put?: never; + readonly post?: never; + readonly delete?: never; + readonly options?: never; + readonly head?: never; + readonly patch?: never; + readonly trace?: never; + }; +}; + +// only declared verbs survive; the `?: never` ones are filtered out +export type AssertMethod = Expect< + Equal<_MethodOf, "get" | "post"> +>; +export type AssertMethodSingle = Expect< + Equal<_MethodOf, "get"> +>; + +export type AssertStatus = Expect< + Equal<_StatusOf, 200 | 404> +>; + +export type AssertQuery = Expect< + Equal<_QueryParamOf, "page" | "size"> +>; +export type AssertNoQuery = Expect< + Equal<_QueryParamOf, never> +>; + +export type AssertPathParam = Expect< + Equal<_PathParamOf, "id"> +>; +export type AssertNoPathParam = Expect< + Equal<_PathParamOf, never> +>; + +export type AssertRequestBody = Expect< + Equal<_RequestBodyOf, { readonly name: string }> +>; +export type AssertNoRequestBody = Expect< + Equal<_RequestBodyOf, never> +>; + +export type AssertParameters = Expect< + Equal< + _ParametersOf, + { + readonly query?: { readonly page?: string; readonly size?: string }; + readonly header?: never; + readonly path?: never; + readonly cookie?: never; + } + > +>; + +// a section the operation does not declare collapses to `undefined`, so a +// validation schema may not declare it either; optional properties are widened +// with `undefined`, because that is what `Schema.optional` encodes to +export type AssertParametersInput = Expect< + Equal< + _ParametersInputOf, + { + readonly query?: + | { + readonly page?: string | undefined; + readonly size?: string | undefined; + } + | undefined; + readonly header?: undefined; + readonly path?: undefined; + readonly cookie?: undefined; + } + > +>; + +// a repeatable query parameter may be validated in its unary form as well +export type AssertParametersInputQuery = Expect< + Equal< + _ParametersInputOf, + { + readonly query: { + readonly q: string; + readonly tag?: string | readonly string[] | undefined; + }; + readonly header?: undefined; + readonly path?: undefined; + readonly cookie: { readonly session: string }; + } + > +>; + +// a validation schema may narrow what the operation declares ... +export type AssertParametersInputAccepted = Expect< + { + readonly query: { readonly q: "a" | "b"; readonly tag: readonly string[] }; + readonly cookie: { readonly session: string }; + } extends _ParametersInputOf + ? true + : false +>; + +// ... but not accept something the operation does not declare +export type AssertParametersInputRejected = Expect< + Equal< + { + readonly query: { readonly undeclared: string }; + } extends _ParametersInputOf + ? true + : false, + false + > +>; + +// `@Route` infers the validation schemas rather than taking them as type +// arguments, so the check that they encode to what the operation declares is +// the one `_Validation` performs +type ValidatedParameters = Exclude< + _Codec["parameters"], + undefined +>; +type ValidatedRequestBody = Exclude< + _Codec["requestBody"], + undefined +>; + +type PageQuery = Schema.Schema< + { readonly page: number }, + { readonly query: { readonly page?: string | undefined } }, + never +>; +type UndeclaredQuery = Schema.Schema< + { readonly undeclared: number }, + { readonly query: { readonly undeclared: string } }, + never +>; + +export type AssertValidationAccepted = Expect< + Equal, PageQuery> +>; +// a schema that does not encode to the declared parameters collapses to `never` +export type AssertValidationRejected = Expect< + Equal, never> +>; +// the operation declares no request body, so no schema is accepted for it +export type AssertValidationBodilessRejected = Expect< + Equal, never> +>; + +// one entry per status code that declares a body, valued by that code's content type +export type AssertContentType = Expect< + Equal< + _ContentTypeOf, + { readonly 200: "application/json"; readonly 404: "text/plain" } + > +>; +// the 204 declares no content, so no entry is required +export type AssertContentTypeEmpty = Expect< + Equal, never> +>; + +type ThingGet = _EndpointResponse; + +export const accepted: ThingGet[] = [ + // 200 declares a concrete `x-total` header, so headers are required + { + code: 200, + contentType: "application/json", + body: { id: "a" }, + headers: { "x-total": "1" }, + }, + // 404 declares only an index signature, so headers may be omitted + { code: 404, contentType: "text/plain", body: "missing" }, +]; + +// @ts-expect-error 200 requires the declared `x-total` header +export const missingHeader: ThingGet = { + code: 200, + contentType: "application/json", + body: { id: "a" }, +}; + +// @ts-expect-error the response declares a body, so `contentType` is required +export const missingContentType: ThingGet = { code: 404, body: "missing" }; + +// @ts-expect-error body must match the content type paired with it +export const wrongBodyForCode: ThingGet = { + code: 404, + contentType: "text/plain", + body: { id: "a" }, +}; + +// @ts-expect-error 404 declares text/plain, not application/json +export const wrongContentTypeForCode: ThingGet = { + code: 404, + contentType: "application/json", + body: "missing", +}; + +// @ts-expect-error 500 is not declared for this operation +export const undeclaredCode: ThingGet = { code: 500, body: "x" }; + +type ThingIdGet = _EndpointResponse; + +// a status code declaring no body takes neither `contentType` nor `body` +export const bodiless: ThingIdGet = { code: 204 }; + +// @ts-expect-error 204 declares no content +export const bodilessWithBody: ThingIdGet = { code: 204, body: "x" }; + +export class MockController implements _Implements { + async get(): Promise { + return { code: 404, contentType: "text/plain", body: "missing" }; + } + + async post(): Promise<_EndpointResponse> { + return { code: 201, contentType: "application/json", body: { id: "a" } }; + } +} + +// @ts-expect-error `post` is declared by the mock schema but not implemented +export class PartialController implements _Implements { + async get(): Promise { + return { code: 404, contentType: "text/plain", body: "missing" }; + } +} diff --git a/project/server/src/layer/schema.ts b/project/server/src/layer/schema.ts new file mode 100644 index 00000000..5ea28614 --- /dev/null +++ b/project/server/src/layer/schema.ts @@ -0,0 +1,337 @@ +import type { Schema } from "effect"; + +import type { paths } from "../schema"; + +export type HttpMethod = "get" | "put" | "post" | "patch" | "delete"; + +/** http methods a path actually declares in schema */ +export type _MethodOf = Exclude< + { + [Method in Extract< + keyof Paths[Path], + HttpMethod + >]: Paths[Path][Method] extends undefined | never ? never : Method; + }[Extract], + undefined +>; + +export type _MethodKeyOf = _MethodOf< + Paths, + Path +> & + keyof Paths[Path]; + +type _OperationOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = Paths[Path][Method]; + +type _ResponsesOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = "responses" extends keyof _OperationOf + ? _OperationOf["responses"] + : never; + +/** status codes declared for the operation */ +export type _StatusOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = keyof _ResponsesOf; + +// https://github.com/openapi-ts/openapi-typescript/issues/2457 +type LaxOptionalProperty = + T extends Record + ? { + [K in keyof T]: Omit extends T + ? LaxOptionalProperty | undefined + : LaxOptionalProperty; + } + : T extends ReadonlyArray + ? readonly LaxOptionalProperty[] + : T; + +type Body = + T extends ReadonlyArray + ? readonly LaxOptionalProperty[] + : LaxOptionalProperty; + +// only require `headers` to be supplied when the response declares at least one concrete header +// (bare `[name: string]: unknown` index signature does not count) +type HeadersProperty = + { + [H in keyof T as unknown extends T[H] ? never : H]: T[H]; + } extends Record + ? { readonly headers?: undefined } + : { readonly headers: T }; + +type _ContentMapOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], + Code extends keyof _ResponsesOf, +> = "content" extends keyof _ResponsesOf[Code] + ? _ResponsesOf[Code]["content"] + : never; + +type _HeadersOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], + Code extends keyof _ResponsesOf, +> = "headers" extends keyof _ResponsesOf[Code] + ? HeadersProperty<_ResponsesOf[Code]["headers"]> + : { readonly headers?: undefined }; + +// one member per content type the status code declares, so `body` is tied to the `contentType` actually chosen rather than to the union of all of them +type _ResponseOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], + Code extends keyof _ResponsesOf, +> = [_ContentMapOf] extends [never] + ? { + readonly code: Code; + readonly contentType?: undefined; + readonly body?: undefined; + } + : { + [ContentType in keyof _ContentMapOf]: { + readonly code: Code; + readonly contentType: ContentType; + readonly body: Body< + _ContentMapOf[ContentType] + >; + }; + }[keyof _ContentMapOf]; + +/** discriminated union over the status codes the operation declares, optionally narrowed to `Code` */ +export type _EndpointResponse< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], + Code extends keyof _ResponsesOf = keyof _ResponsesOf< + Paths, + Path, + Method + >, +> = { + [C in Code]: _ResponseOf & + _HeadersOf; +}[Code]; + +/** names of the path parameters declared for the operation */ +export type _PathParamOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = "parameters" extends keyof _OperationOf + ? "path" extends keyof _OperationOf["parameters"] + ? keyof _OperationOf["parameters"]["path"] + : never + : never; + +/** names of the query parameters declared for the operation */ +export type _QueryParamOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = "parameters" extends keyof _OperationOf + ? "query" extends keyof _OperationOf["parameters"] + ? [_OperationOf["parameters"]["query"]] extends [ + undefined, + ] + ? never + : keyof NonNullable< + _OperationOf["parameters"]["query"] + > + : never + : never; + +/** parameter sections declared for the operation */ +export type _ParametersOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = "parameters" extends keyof _OperationOf + ? _OperationOf["parameters"] + : never; + +// a query parameter that may repeat is indistinguishable from a single occurrence at runtime, so both the unary and the array form are accepted +type QueryInput = { + [Name in keyof T]: NonNullable extends ReadonlyArray + ? Value | readonly Value[] + : T[Name]; +}; + +/** encoded (input) shape a parameters validation schema has to accept */ +export type _ParametersInputOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = [_ParametersOf] extends [never] + ? never + : LaxOptionalProperty<{ + [Section in keyof _ParametersOf< + Paths, + Path, + Method + >]: Section extends "query" + ? QueryInput[Section]>> + : _ParametersOf[Section]; + }>; + +/** request body of the operation */ +export type _RequestBodyOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = "requestBody" extends keyof _OperationOf + ? "content" extends keyof _OperationOf["requestBody"] + ? Body< + _OperationOf< + Paths, + Path, + Method + >["requestBody"]["content"][keyof _OperationOf< + Paths, + Path, + Method + >["requestBody"]["content"]] + > + : never + : never; + +/** shape a controller class can implement to adhere with schema-defined routes */ +export type _Implements = { + [Method in _MethodKeyOf]: ( + ...args: never[] + ) => + | _EndpointResponse + | Promise<_EndpointResponse>; +}; + +/** content type to respond with, per declared status code */ +export type _ContentTypeOf< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], +> = { + readonly [Code in keyof _ResponsesOf< + Paths, + Path, + Method + > as "content" extends keyof _ResponsesOf[Code] + ? Code + : never]: "content" extends keyof _ResponsesOf[Code] + ? keyof _ResponsesOf[Code]["content"] & string + : never; +}; + +/** effect schemas the request is validated against before the handler runs */ +export type _Codec< + Paths, + Path extends keyof Paths, + Method extends keyof Paths[Path], + CodecParameters extends Schema.Schema.AnyNoContext, + CodecRequestBody extends Schema.Schema.AnyNoContext, +> = { + /** codec for `{ path, query, header }` */ + readonly parameters?: Schema.Schema.Encoded extends _ParametersInputOf< + Paths, + Path, + Method + > + ? CodecParameters + : never; + /** codec for request body */ + readonly requestBody?: Schema.Schema.Encoded extends _RequestBodyOf< + Paths, + Path, + Method + > + ? CodecRequestBody + : never; +}; + +// what the handler is handed for a section, `undefined` when not provided +type Decoded = [Codec] extends [never] + ? undefined + : Schema.Schema.Type; + +export type HandlerArguments = [ + RequestBodySchema, +] extends [never] + ? [ParametersSchema] extends [never] + ? [...bound: never[]] + : [parameters: Decoded, ...bound: never[]] + : [ + parameters: Decoded, + requestBody: Decoded, + ...bound: never[], + ]; + +/** http methods a path actually declares in schema */ +export type MethodOf = _MethodOf; +export type MethodKeyOf = _MethodKeyOf; +/** status codes declared for the operation */ +export type StatusOf< + Path extends keyof paths, + Method extends keyof paths[Path], +> = _StatusOf; +/** + * discriminated union over the status codes the operation declares, optionally + * narrowed to `Code` + */ +export type EndpointResponse< + Path extends keyof paths, + Method extends keyof paths[Path], + Code extends StatusOf = StatusOf, +> = _EndpointResponse; +/** names of the path parameters declared for the operation */ +export type PathParamOf< + Path extends keyof paths, + Method extends keyof paths[Path], +> = _PathParamOf; +/** names of the query parameters declared for the operation */ +export type QueryParamOf< + Path extends keyof paths, + Method extends keyof paths[Path], +> = _QueryParamOf; +/** parameter sections declared for the operation */ +export type ParametersOf< + Path extends keyof paths, + Method extends keyof paths[Path], +> = _ParametersOf; +/** encoded (input) shape a parameters validation schema has to accept */ +export type ParametersInputOf< + Path extends keyof paths, + Method extends keyof paths[Path], +> = _ParametersInputOf; +/** request body of the operation */ +export type RequestBodyOf< + Path extends keyof paths, + Method extends keyof paths[Path], +> = _RequestBodyOf; +/** shape a controller class can implement to adhere with schema-defined routes */ +export type Implements = _Implements; +/** content type to respond with, per declared status code */ +export type ContentTypeOf< + Path extends keyof paths, + Method extends keyof paths[Path], +> = _ContentTypeOf; + +/** schema descriptor recorded by `@Route` */ +export type RouteSchema = { + readonly path: string; + readonly method: string; +}; + +/** codecs recorded by `@Route` */ +export type RouteCodec = { + readonly parameters?: Schema.Schema.AnyNoContext | undefined; + readonly requestBody?: Schema.Schema.AnyNoContext | undefined; +}; diff --git a/project/server/src/layer/snapshot/defer/ingest-coordinator.module.ts b/project/server/src/layer/snapshot/defer/ingest-coordinator.module.ts new file mode 100644 index 00000000..64f19ef9 --- /dev/null +++ b/project/server/src/layer/snapshot/defer/ingest-coordinator.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; + +import { ModuleConfig } from "../../config/config.module"; +import { ModuleLockfileCoordinator } from "../../database/lockfile-corrdinator.module"; +import { ModuleSnapshotDeferIngest } from "./ingest.module"; +import { ServiceSnapshotDeferIngestCoordinator } from "./ingest-coordinator.service"; + +@Module({ + imports: [ModuleConfig, ModuleSnapshotDeferIngest, ModuleLockfileCoordinator], + providers: [ServiceSnapshotDeferIngestCoordinator], + exports: [ServiceSnapshotDeferIngestCoordinator], +}) +export class ModuleSnapshotDeferIngestCoordinator {} diff --git a/project/server/src/layer/snapshot/defer/ingest-coordinator.service.ts b/project/server/src/layer/snapshot/defer/ingest-coordinator.service.ts new file mode 100644 index 00000000..198169c4 --- /dev/null +++ b/project/server/src/layer/snapshot/defer/ingest-coordinator.service.ts @@ -0,0 +1,71 @@ +import { + type BeforeApplicationShutdown, + Inject, + Injectable, + type OnApplicationBootstrap, +} from "@nestjs/common"; +import type { PickDeep } from "type-fest"; + +import race from "../../../utility/race-as-promised"; +import { Config } from "../../config/config.module"; +import { ServiceLockfileCoordinator } from "../../database/lockfile-coordinator.service"; +import { ServiceSnapshotDeferIngest } from "./ingest.service"; + +@Injectable() +export class ServiceSnapshotDeferIngestCoordinator + implements OnApplicationBootstrap, BeforeApplicationShutdown +{ + private controller = new AbortController(); + + constructor( + @Inject(Config) + private readonly config: PickDeep, + @Inject(ServiceSnapshotDeferIngest) + private ingest: ServiceSnapshotDeferIngest, + @Inject(ServiceLockfileCoordinator) + private lockfileCoordinator: ServiceLockfileCoordinator, + ) {} + + onApplicationBootstrap(): void { + if (!this.config.snapshot.defer.process) { + return; + } + + void (async () => { + const aborted = new Promise((resolve) => + this.controller.signal.addEventListener("abort", () => resolve()), + ); + + await race([this.lockfileCoordinator._unlocked, aborted]); + + if (this.controller.signal.aborted) { + return; + } + + for await (const step of this.ingest.ingest()) { + let delay; + switch (step) { + case "idle": + delay = 5_000; + break; + case "acted": + delay = 100; + break; + } + + await race([ + new Promise((resolve) => setTimeout(resolve, delay)), + aborted, + ]); + + if (this.controller.signal.aborted) { + return; + } + } + })(); + } + + async beforeApplicationShutdown(): Promise { + this.controller.abort(); + } +} diff --git a/project/server/src/layer/snapshot/defer/ingest.module.ts b/project/server/src/layer/snapshot/defer/ingest.module.ts new file mode 100644 index 00000000..64778482 --- /dev/null +++ b/project/server/src/layer/snapshot/defer/ingest.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; + +import { ModuleIntrospection } from "../../introspection/introspection.module"; +import { ModuleSnapshot } from "../snapshot.module"; +import { ServiceSnapshotDeferIngest } from "./ingest.service"; + +@Module({ + // `SnapshotDeferTarget` is resolved from the global registration so that concrete implementation + // can be selected at app root + imports: [ModuleSnapshot, ModuleIntrospection], + providers: [ServiceSnapshotDeferIngest], + exports: [ServiceSnapshotDeferIngest], +}) +export class ModuleSnapshotDeferIngest {} diff --git a/project/server/src/layer/snapshot/defer/ingest.service.ts b/project/server/src/layer/snapshot/defer/ingest.service.ts new file mode 100644 index 00000000..169f9b78 --- /dev/null +++ b/project/server/src/layer/snapshot/defer/ingest.service.ts @@ -0,0 +1,19 @@ +import { Inject, Injectable, Optional } from "@nestjs/common"; + +import { SnapshotDeferIngest } from "../../../service/snapshot/defer/ingest"; +import { ServiceIntrospection } from "../../introspection/introspection.service"; +import { ServiceSnapshot } from "../snapshot.service"; +import { SnapshotDeferTarget } from "./target.interface"; + +@Injectable() +export class ServiceSnapshotDeferIngest extends SnapshotDeferIngest { + constructor( + @Inject(ServiceSnapshot) snapshot: ServiceSnapshot, + @Optional() + @Inject(SnapshotDeferTarget) + target: SnapshotDeferTarget | undefined, + @Inject(ServiceIntrospection) introspection: ServiceIntrospection, + ) { + super(snapshot, target, introspection); + } +} diff --git a/project/server/src/layer/snapshot/defer/target-object-store.module.ts b/project/server/src/layer/snapshot/defer/target-object-store.module.ts new file mode 100644 index 00000000..2f74b9b6 --- /dev/null +++ b/project/server/src/layer/snapshot/defer/target-object-store.module.ts @@ -0,0 +1,21 @@ +import { Module } from "@nestjs/common"; + +import { ModuleConfig } from "../../config/config.module"; +import { ModuleSnapshot } from "../snapshot.module"; +import { + _ModuleSnapshotDeferTarget, + SnapshotDeferTarget, +} from "./target.interface"; +import { ServiceSnapshotDeferTargetObjectStore } from "./target-object-store.service"; + +@Module({ + imports: [ModuleConfig, ModuleSnapshot], + providers: [ + { + provide: SnapshotDeferTarget, + useClass: ServiceSnapshotDeferTargetObjectStore, + }, + ], + exports: [SnapshotDeferTarget], +}) +export class ModuleSnapshotDeferTargetObjectStore extends _ModuleSnapshotDeferTarget {} diff --git a/project/server/src/layer/snapshot/defer/target-object-store.service.ts b/project/server/src/layer/snapshot/defer/target-object-store.service.ts new file mode 100644 index 00000000..73a8650d --- /dev/null +++ b/project/server/src/layer/snapshot/defer/target-object-store.service.ts @@ -0,0 +1,16 @@ +import { Inject, Injectable } from "@nestjs/common"; +import type { PickDeep } from "type-fest"; + +import { SnapshotDeferTargetObjectStore } from "../../../service/snapshot/defer/object-store"; +import { Config } from "../../config/config.module"; +import { ServiceSnapshot } from "../snapshot.service"; + +@Injectable() +export class ServiceSnapshotDeferTargetObjectStore extends SnapshotDeferTargetObjectStore { + constructor( + @Inject(Config) config: PickDeep, + @Inject(ServiceSnapshot) snapshot: ServiceSnapshot, + ) { + super(snapshot, config.snapshot.defer.objectStore); + } +} diff --git a/project/server/src/layer/snapshot/defer/target.interface.ts b/project/server/src/layer/snapshot/defer/target.interface.ts new file mode 100644 index 00000000..c0bdf14a --- /dev/null +++ b/project/server/src/layer/snapshot/defer/target.interface.ts @@ -0,0 +1,15 @@ +import type { ISnapshotDeferTarget } from "../../../service/snapshot/defer/base"; + +export const SnapshotDeferTarget = Symbol("SnapshotDeferTarget"); +export type SnapshotDeferTarget = ISnapshotDeferTarget; + +/** + * nominal marker for modules that export {@link SnapshotDeferTarget}. + * + * the `protected` member is never assigned — it only exists so that solely + * subclasses are assignable, letting the composition root be constrained to + * modules that claim to provide the port. + */ +export abstract class _ModuleSnapshotDeferTarget { + protected declare readonly provides: SnapshotDeferTarget; +} diff --git a/project/server/src/layer/snapshot/defer/target.module.ts b/project/server/src/layer/snapshot/defer/target.module.ts new file mode 100644 index 00000000..61679ed7 --- /dev/null +++ b/project/server/src/layer/snapshot/defer/target.module.ts @@ -0,0 +1,16 @@ +import { type DynamicModule, Module, type Type } from "@nestjs/common"; + +import type { _ModuleSnapshotDeferTarget } from "./target.interface"; + +@Module({}) +// biome-ignore lint/complexity/noStaticOnlyClass: nestjs convention +export class ModuleSnapshotDeferTarget { + static forRoot(adapter: Type<_ModuleSnapshotDeferTarget>): DynamicModule { + return { + global: true, + module: ModuleSnapshotDeferTarget, + imports: [adapter], + exports: [adapter], + }; + } +} diff --git a/project/server/src/layer/snapshot/snapshot.controller.test.ts b/project/server/src/layer/snapshot/snapshot.controller.test.ts new file mode 100644 index 00000000..c169409b --- /dev/null +++ b/project/server/src/layer/snapshot/snapshot.controller.test.ts @@ -0,0 +1,554 @@ +import { Readable } from "node:stream"; +import { type TestContext, test } from "node:test"; + +import { BadRequestException } from "@nestjs/common"; + +import { logger } from "../../logger"; +import { Voucher } from "../../service/voucher"; +import { RequestBodyTooLargeError } from "../body"; +import { invoke } from "../test"; +import { ControllerSnapshot } from "./snapshot.controller"; + +import type { + SnapshotAttachableDevice, + SnapshotAttachableEntity, + SnapshotCreateResult, + SnapshotHandleAttachableUnhashed, + SnapshotHash, + SnapshotVoucher, +} from "../../service/snapshot"; +import type { Uuid } from "../../type/codec/uuid"; +import type { ServiceIntrospection } from "../introspection/introspection.service"; +import type { RequestStreamStub } from "../test"; +import type { SnapshotDeferTarget } from "./defer/target.interface"; +import type { ServiceSnapshot } from "./snapshot.service"; + +logger.silent = true; + +const SIGNING_KEY = "voucher-signing-key"; +const SUBJECT = "3f2504e0-4f89-11d3-9a0c-0305e82c3301" as Uuid; +const INITIAL = "3f2504e0-4f89-11d3-9a0c-0305e82c3302" as Uuid; +const RESUMED = "3f2504e0-4f89-11d3-9a0c-0305e82c3303" as Uuid; +const SUBSEQUENT = "3f2504e0-4f89-11d3-9a0c-0305e82c3304" as Uuid; + +const MALFORMED_IDENTIFIER = "not-a-voucher"; +const USER_AGENT = "home-assistant/2024.1.0"; +const VERSION = "2024.1.0"; + +const HANDLE = {} as SnapshotHandleAttachableUnhashed; + +const sealed = (id: Uuid): SnapshotVoucher => + new Voucher(SIGNING_KEY).create("snapshot-submission", new Date(0), { + id, + sub: SUBJECT, + }); + +type Attached = + | { + kind: "device"; + integration: string; + device: SnapshotAttachableDevice; + entities: readonly SnapshotAttachableEntity[]; + } + | { kind: "entity"; integration: string; entity: SnapshotAttachableEntity }; + +type Finalized = { + handle: unknown; + hash: SnapshotHash; + hassVersion: string; +}; + +class StubSnapshot { + created: SnapshotCreateResult = { + kind: "success", + handle: HANDLE, + }; + + readonly attached: Attached[] = []; + readonly finalized: Finalized[] = []; + readonly deleted: Uuid[] = []; + /** identifiers of the vouchers a successor was derived from */ + readonly renewed: Uuid[] = []; + + readonly voucher = { + initial: () => sealed(INITIAL), + subsequent: (voucher: SnapshotVoucher) => { + this.renewed.push(Voucher.peek(voucher).id); + return sealed(SUBSEQUENT); + }, + serialize: (voucher: SnapshotVoucher) => + `voucher:${Voucher.peek(voucher).id}`, + deserialize: (serialized: string) => + serialized === MALFORMED_IDENTIFIER + ? ({ kind: "error", cause: "malformed" } as const) + : ({ kind: "success", voucher: sealed(RESUMED) } as const), + expired: () => false, + expiresAt: () => new Date(0), + }; + + readonly attach = { + device: async ( + _: unknown, + integration: string, + device: SnapshotAttachableDevice, + entities: readonly SnapshotAttachableEntity[], + ) => { + this.attached.push({ kind: "device", integration, device, entities }); + }, + entity: async ( + _: unknown, + integration: string, + entity: SnapshotAttachableEntity, + ) => { + this.attached.push({ kind: "entity", integration, entity }); + }, + }; + + async create() { + return this.created; + } + + async delete(id: Uuid) { + this.deleted.push(id); + } + + async finalize(handle: unknown, hash: SnapshotHash, hassVersion: string) { + this.finalized.push({ handle, hash, hassVersion }); + } +} + +type Observation = { + name: string; + labels: Readonly>; + value: number; +}; + +const introspection = (observed: Observation[]): ServiceIntrospection => { + const metric = (name: string) => ({ + increment: (labels: Observation["labels"], by = 1) => { + observed.push({ name, labels, value: by }); + }, + set: (labels: Observation["labels"], value: number) => { + observed.push({ name, labels, value }); + }, + observe: (labels: Observation["labels"], value: number) => { + observed.push({ name, labels, value }); + }, + }); + + return { + metric: { + counter: ({ name }: { name: string }) => metric(name), + gauge: ({ name }: { name: string }) => metric(name), + histogram: ({ name }: { name: string }) => metric(name), + }, + } as unknown as ServiceIntrospection; +}; + +type Deferred = { + id: Uuid; + hassVersion: string; + parts: number; +}; + +const deferTarget = (received: Deferred[]): SnapshotDeferTarget => + ({ + put: async ( + voucher: SnapshotVoucher, + hassVersion: string, + snapshot: AsyncIterable, + ) => { + let parts = 0; + for await (const _ of snapshot) { + parts += 1; + } + + received.push({ id: Voucher.peek(voucher).id, hassVersion, parts }); + }, + }) as unknown as SnapshotDeferTarget; + +type Context = { + controller: ControllerSnapshot; + snapshot: StubSnapshot; + observed: Observation[]; + deferred: Deferred[]; +}; + +const context = (defer = false): Context => { + const snapshot = new StubSnapshot(); + const observed: Observation[] = []; + const deferred: Deferred[] = []; + + return { + snapshot, + observed, + deferred, + controller: new ControllerSnapshot( + snapshot as unknown as ServiceSnapshot, + defer ? deferTarget(deferred) : undefined, + introspection(observed), + ), + }; +}; + +const request = ( + body: unknown, + headers: Readonly> = { "user-agent": USER_AGENT }, +): RequestStreamStub => + Object.assign( + Readable.from([typeof body === "string" ? body : JSON.stringify(body)]), + { headers }, + ) as RequestStreamStub; + +const post = async ( + { controller }: Context, + stub: RequestStreamStub, +): Promise => await invoke(controller, "post", stub); + +const ENTITY: SnapshotAttachableEntity = { + assumed_state: null, + domain: "light", + entity_category: null, + has_entity_name: false, + original_device_class: null, + unit_of_measurement: null, +}; + +const DEVICE: SnapshotAttachableDevice = { + entry_type: null, + has_configuration_url: false, + hw_version: null, + manufacturer: "Philips", + model: "Hue Go", + model_id: null, + sw_version: null, + via_device: null, +}; + +const EMPTY_DEVICE: SnapshotAttachableDevice = { + ...DEVICE, + manufacturer: null, + model: null, +}; + +test("initial submission", async (t: TestContext) => { + const c = context(); + + const response = await post( + c, + request({ + hue: { + devices: [{ ...DEVICE, entities: [ENTITY] }], + entities: [ENTITY], + }, + }), + ); + + await t.test("starts a new attribution chain", (t: TestContext) => { + t.assert.deepStrictEqual(c.snapshot.renewed, [INITIAL]); + }); + + await t.test("attaches what the body declared", (t: TestContext) => { + t.assert.deepStrictEqual(c.snapshot.attached, [ + { + kind: "device", + integration: "hue", + device: DEVICE, + entities: [ENTITY], + }, + { kind: "entity", integration: "hue", entity: ENTITY }, + ]); + }); + + await t.test("finalizes with the version it was told", (t: TestContext) => { + t.assert.strictEqual(c.snapshot.finalized.length, 1); + + const [finalized] = c.snapshot.finalized; + t.assert.strictEqual(finalized?.handle, HANDLE); + t.assert.strictEqual(finalized?.hassVersion, VERSION); + t.assert.strictEqual(finalized?.hash.version, 1); + t.assert.ok(Buffer.isBuffer(finalized?.hash.hash)); + }); + + await t.test("hands back the successor", (t: TestContext) => { + t.assert.deepStrictEqual(response, { + code: 200, + contentType: "application/json", + body: { submission_identifier: `voucher:${SUBSEQUENT}` }, + }); + }); + + await t.test("measures what it received", (t: TestContext) => { + const size = c.observed.filter( + ({ name }) => name === "snapshot_submission_size_bytes", + ); + + t.assert.strictEqual(size.length, 1); + t.assert.ok((size[0]?.value ?? 0) > 0); + }); +}); + +test("subsequent submission", async (t: TestContext) => { + await t.test("continues that attribution chain", async (t: TestContext) => { + const c = context(); + + const response = await post( + c, + request( + { hue: { devices: [], entities: [] } }, + { + "user-agent": USER_AGENT, + "x-device-database-submission-identifier": "previous", + }, + ), + ); + + t.assert.deepStrictEqual(c.snapshot.renewed, [RESUMED]); + t.assert.deepStrictEqual(response, { + code: 200, + contentType: "application/json", + body: { submission_identifier: `voucher:${SUBSEQUENT}` }, + }); + }); + + await t.test("is rejected when it is malformed", async (t: TestContext) => { + const c = context(); + + const response = await post( + c, + request( + { hue: { devices: [], entities: [] } }, + { + "user-agent": USER_AGENT, + "x-device-database-submission-identifier": MALFORMED_IDENTIFIER, + }, + ), + ); + + t.assert.deepStrictEqual(response, { + code: 400, + contentType: "application/json", + body: { + kind: "invalid-submission-identifier", + message: "invalid submission identifier", + }, + }); + t.assert.deepStrictEqual(c.snapshot.attached, []); + t.assert.deepStrictEqual(c.snapshot.finalized, []); + }); +}); + +test("submission with invalid identifier", async (t: TestContext) => { + const cases = [ + ["voucher-expired", "expired submission identifier"], + ["voucher-used", "reuse of submission identifier"], + ] as const; + + for (const [reason, message] of cases) { + await t.test(`is rejected as ${reason}`, async (t: TestContext) => { + const c = context(); + c.snapshot.created = { kind: "failure", reason }; + + t.assert.deepStrictEqual( + await post(c, request({ hue: { devices: [DEVICE], entities: [] } })), + { + code: 400, + contentType: "application/json", + body: { kind: "invalid-submission-identifier", message }, + }, + ); + t.assert.deepStrictEqual(c.snapshot.attached, []); + t.assert.deepStrictEqual(c.snapshot.finalized, []); + t.assert.deepStrictEqual(c.snapshot.deleted, []); + }); + } +}); + +test("unreadable submission", async (t: TestContext) => { + const c = context(); + + const response = await post(c, request('{"hue": {"devices": [')); + + await t.test("is answered as malformed", (t: TestContext) => { + t.assert.deepStrictEqual(response, { + code: 400, + contentType: "application/json", + body: { kind: "malformed-submission", message: "malformed submission" }, + }); + }); + + await t.test("leaves nothing behind", (t: TestContext) => { + t.assert.deepStrictEqual(c.snapshot.deleted, [INITIAL]); + t.assert.deepStrictEqual(c.snapshot.finalized, []); + }); +}); + +test("oversized submission", async (t: TestContext) => { + const c = context(); + + // `@StreamedBody` caps at `REQUEST_BODY_LIMIT`, so the body has to pass it + const devices = Array.from({ length: 40_000 }, () => DEVICE); + + await t.test( + "is left for the interceptor to answer", + async (t: TestContext) => { + await t.assert.rejects( + async () => await post(c, request({ hue: { devices, entities: [] } })), + (error: unknown) => error instanceof RequestBodyTooLargeError, + ); + }, + ); + + await t.test("is cleaned up all the same", (t: TestContext) => { + t.assert.deepStrictEqual(c.snapshot.deleted, [INITIAL]); + t.assert.deepStrictEqual(c.snapshot.finalized, []); + }); +}); + +test("deferred submission", async (t: TestContext) => { + const c = context(true); + + const response = await post( + c, + request({ + hue: { + devices: [{ ...DEVICE, entities: [] }], + entities: [ENTITY], + }, + }), + ); + + await t.test("is handed to the target whole", (t: TestContext) => { + t.assert.deepStrictEqual(c.deferred, [ + { id: INITIAL, hassVersion: VERSION, parts: 2 }, + ]); + }); + + await t.test("is not ingested directly", (t: TestContext) => { + t.assert.deepStrictEqual(c.snapshot.attached, []); + t.assert.deepStrictEqual(c.snapshot.finalized, []); + }); + + await t.test("is acknowledged all the same", (t: TestContext) => { + t.assert.deepStrictEqual(response, { + code: 200, + contentType: "application/json", + body: { submission_identifier: `voucher:${SUBSEQUENT}` }, + }); + }); +}); + +test("what a submission is measured by", async (t: TestContext) => { + const c = context(); + + await post( + c, + request({ + hue: { + devices: [ + // links to itself + { ...DEVICE, via_device: ["hue", 0], entities: [] }, + { ...EMPTY_DEVICE, entities: [] }, + // links to a device that is not part of the submission + { + ...DEVICE, + model: "Hue Bloom", + via_device: ["hue", 9], + entities: [], + }, + { manufacturer: 5 }, + ], + entities: [ENTITY, { ...ENTITY, domain: "sensor" }, { domain: 5 }], + }, + }), + ); + + const observed = (name: string) => c.observed.filter((o) => o.name === name); + + await t.test("devices carrying nothing", (t: TestContext) => { + t.assert.deepStrictEqual(observed("snapshot_empty_device_total"), [ + { + name: "snapshot_empty_device_total", + labels: { integration: "hue", version: VERSION }, + value: 1, + }, + ]); + }); + + await t.test("devices and entities it could not read", (t: TestContext) => { + t.assert.deepStrictEqual(observed("snapshot_malformed_device_total"), [ + { + name: "snapshot_malformed_device_total", + labels: { integration: "hue", version: VERSION }, + value: 1, + }, + ]); + t.assert.deepStrictEqual(observed("snapshot_malformed_entity_total"), [ + { + name: "snapshot_malformed_entity_total", + labels: { integration: "hue", version: VERSION }, + value: 1, + }, + ]); + }); + + await t.test("links it could not resolve", (t: TestContext) => { + t.assert.deepStrictEqual(observed("snapshot_circular_device_link_total"), [ + { + name: "snapshot_circular_device_link_total", + labels: { integration: "hue", version: VERSION }, + value: 1, + }, + ]); + t.assert.deepStrictEqual(observed("snapshot_dangling_device_link_total"), [ + { + name: "snapshot_dangling_device_link_total", + labels: { integration: "hue", version: VERSION }, + value: 1, + }, + ]); + }); + + await t.test("entities per integration and domain", (t: TestContext) => { + t.assert.deepStrictEqual(observed("snapshot_integration_entity_total"), [ + { + name: "snapshot_integration_entity_total", + labels: { + integration: "hue", + version: VERSION, + has_devices: "true", + entity_domain: "light", + }, + value: 1, + }, + { + name: "snapshot_integration_entity_total", + labels: { + integration: "hue", + version: VERSION, + has_devices: "true", + entity_domain: "sensor", + }, + value: 1, + }, + ]); + }); +}); + +test("unexpected user agent", (t: TestContext) => { + const c = context(); + + t.assert.throws( + () => + invoke( + c.controller, + "post", + request( + { hue: { devices: [], entities: [] } }, + { "user-agent": "curl/8.7.1" }, + ), + ), + (error: unknown) => + error instanceof BadRequestException && error.getStatus() === 400, + ); +}); diff --git a/project/server/src/layer/snapshot/snapshot.controller.ts b/project/server/src/layer/snapshot/snapshot.controller.ts new file mode 100644 index 00000000..35a585bd --- /dev/null +++ b/project/server/src/layer/snapshot/snapshot.controller.ts @@ -0,0 +1,318 @@ +import type { Readable } from "node:stream"; + +import { Controller, Inject, Optional } from "@nestjs/common"; +import { Schema } from "effect"; +import { ArrayFormatter } from "effect/ParseResult"; + +import { logger as parentLogger } from "../../logger"; +import { stream } from "../../service/snapshot/stream"; +import { Voucher } from "../../service/voucher"; +import { isNone, isSome } from "../../type/maybe"; +import { + RequestBodyStream, + RequestBodyTooLargeError, + StreamedBody, +} from "../body"; +import { ServiceIntrospection } from "../introspection/introspection.service"; +import { Route } from "../route"; +import { SnapshotDeferTarget } from "./defer/target.interface"; +import { ServiceSnapshot } from "./snapshot.service"; + +import type { IIntrospection } from "../../service/introspect"; +import type { SnapshotVoucher } from "../../service/snapshot"; +import type { SnapshotRequestTransformOut } from "../../service/snapshot/stream"; +import type { Implements } from "../schema"; + +const logger = parentLogger.child({ label: "controller-snapshot" }); + +const Parameters = Schema.Struct({ + header: Schema.Struct({ + "user-agent": Schema.String.pipe(Schema.pattern(/^home-assistant\/.+/)), + "x-device-database-submission-identifier": Schema.optional(Schema.String), + }), +}); +type Parameters = typeof Parameters.Type; + +const metrics = (introspection: IIntrospection) => + ({ + circularDeviceLinks: introspection.metric.counter({ + name: "snapshot_circular_device_link_total", + help: "amount of circular device links", + labelNames: ["integration", "version"], + registry: "local", + }), + danglingDeviceLinks: introspection.metric.counter({ + name: "snapshot_dangling_device_link_total", + help: "amount of dangling device links", + labelNames: ["integration", "version"], + registry: "local", + }), + emptyDevice: introspection.metric.counter({ + name: "snapshot_empty_device_total", + help: "amount of empty devices", + labelNames: ["integration", "version"], + registry: "local", + }), + integrationEntity: introspection.metric.gauge({ + name: "snapshot_integration_entity_total", + help: "amount of integration entities", + labelNames: ["integration", "version", "has_devices", "entity_domain"], + registry: "local", + }), + malformedDevice: introspection.metric.counter({ + name: "snapshot_malformed_device_total", + help: "amount of malformed devices", + labelNames: ["integration", "version"], + registry: "local", + }), + malformedEntity: introspection.metric.counter({ + name: "snapshot_malformed_entity_total", + help: "amount of malformed entities", + labelNames: ["integration", "version"], + registry: "local", + }), + submissionSize: introspection.metric.histogram({ + name: "snapshot_submission_size_bytes", + help: "size of snapshot submissions", + labelNames: [], + registry: "local", + buckets: [ + 1, 2, 5, 11, 26, 58, 131, 296, 668, 1507, 3398, 7662, 17276, 38954, + 87836, 198058, 446593, 1007004, 2270652, 5120000, + ], + }), + }) as const; + +@Controller() +export class ControllerSnapshot implements Implements<"/api/v1/snapshot/1"> { + private readonly metrics: ReturnType; + + constructor( + @Inject(ServiceSnapshot) private readonly snapshot: ServiceSnapshot, + @Optional() + @Inject(SnapshotDeferTarget) + private readonly deferTarget: SnapshotDeferTarget | undefined, + @Inject(ServiceIntrospection) introspection: ServiceIntrospection, + ) { + this.metrics = metrics(introspection); + } + + @Route("post", "/api/v1/snapshot/1", { parameters: Parameters }) + @StreamedBody() + async post( + parameters: Parameters, + @RequestBodyStream() requestBody: Readable, + ) { + const submissionIdentifier = + parameters.header["x-device-database-submission-identifier"]; + const hassVersion = parameters.header["user-agent"].replace( + "home-assistant/", + "", + ); + + let voucher: SnapshotVoucher; + if (typeof submissionIdentifier !== "undefined") { + const deserialized = + this.snapshot.voucher.deserialize(submissionIdentifier); + + if (deserialized.kind === "success") { + voucher = deserialized.voucher; + } else { + switch (deserialized.cause) { + case "malformed": + return { + code: 400, + contentType: "application/json", + body: { + kind: "invalid-submission-identifier", + message: "invalid submission identifier", + }, + } as const; + } + } + } else { + voucher = this.snapshot.voucher.initial(); + } + + const { id, sub } = Voucher.peek(voucher); + + // integrations that contained at least one device + const integrations: Set = new Set(); + // integration → (domain → count) + const integrationEntities: Map> = new Map(); + + const chained = stream(requestBody); + chained.on("device", (item) => { + if ( + isNone(item.device.entry_type) && + isNone(item.device.hw_version) && + isNone(item.device.manufacturer) && + isNone(item.device.model) && + isNone(item.device.model_id) && + isNone(item.device.sw_version) && + isNone(item.device.via_device) + ) { + this.metrics.emptyDevice.increment({ + integration: item.integration, + version: hassVersion, + }); + } + + integrations.add(item.integration); + }); + chained.on("entity", (item) => { + const bucket = integrationEntities.get(item.integration); + if (typeof bucket === "undefined") { + integrationEntities.set( + item.integration, + new Map([[item.entity.domain, 1]]), + ); + } else { + bucket.set( + item.entity.domain, + (bucket.get(item.entity.domain) ?? 0) + 1, + ); + } + }); + chained.on("end", () => { + for (const [integration, domainCount] of integrationEntities) { + const hasDevices = integrations.has(integration); + + for (const [domain, count] of domainCount) { + this.metrics.integrationEntity.set( + { + integration, + version: hassVersion, + has_devices: hasDevices ? "true" : "false", + entity_domain: domain, + }, + count, + ); + } + } + }); + chained.on("malformed-device", ({ integration, error }) => { + logger.warn(`submission <${id}> → malformed device`, { + submissionId: id, + subject: sub, + error: ArrayFormatter.formatErrorSync(error), + }); + this.metrics.malformedDevice.increment({ + integration, + version: hassVersion, + }); + }); + chained.on("malformed-entity", ({ integration, error }) => { + logger.warn(`submission <${id}> → malformed entity`, { + submissionId: id, + subject: sub, + error: ArrayFormatter.formatErrorSync(error), + }); + this.metrics.malformedEntity.increment({ + integration, + version: hassVersion, + }); + }); + chained.on("malformed-link", ({ kind, integration }) => { + switch (kind) { + case "circular": + this.metrics.circularDeviceLinks.increment({ + integration, + version: hassVersion, + }); + break; + case "dangling": + this.metrics.danglingDeviceLinks.increment({ + integration, + version: hassVersion, + }); + break; + } + }); + chained.once("size", (s: number) => { + this.metrics.submissionSize.observe({}, s); + }); + + if (typeof this.deferTarget !== "undefined") { + await this.deferTarget.put(voucher, hassVersion, chained); + } else { + const created = await this.snapshot.create(voucher); + if (created.kind !== "success") { + let message; + switch (created.reason) { + case "voucher-expired": + message = "expired submission identifier"; + break; + case "voucher-used": + message = "reuse of submission identifier"; + break; + } + + return { + code: 400, + contentType: "application/json", + body: { + kind: "invalid-submission-identifier", + message, + }, + } as const; + } + + try { + for await (const part of chained) { + const cast = part as SnapshotRequestTransformOut; + + if ("device" in cast) { + await this.snapshot.attach.device( + created.handle, + cast.integration, + cast.device, + cast.entities, + ); + } else { + await this.snapshot.attach.entity( + created.handle, + cast.integration, + cast.entity, + ); + } + } + } catch (err) { + await this.snapshot.delete(id); + + // not a malformed submission → left to `InterceptorRouteBody` to answer + if (err instanceof RequestBodyTooLargeError) { + throw err; + } + + logger.warn("stream consumption error", { + message: + typeof err === "object" && isSome(err) && "message" in err + ? err.message + : "unknown error", + }); + + return { + code: 400, + contentType: "application/json", + body: { + kind: "malformed-submission", + message: "malformed submission", + }, + } as const; + } + + await this.snapshot.finalize(created.handle, chained.hash(), hassVersion); + } + + return { + code: 200, + contentType: "application/json", + body: { + submission_identifier: this.snapshot.voucher.serialize( + this.snapshot.voucher.subsequent(voucher), + ), + }, + } as const; + } +} diff --git a/project/server/src/layer/snapshot/snapshot.module.ts b/project/server/src/layer/snapshot/snapshot.module.ts new file mode 100644 index 00000000..83c1c08d --- /dev/null +++ b/project/server/src/layer/snapshot/snapshot.module.ts @@ -0,0 +1,16 @@ +import { Module } from "@nestjs/common"; + +import { ModuleConfig } from "../config/config.module"; +import { ModuleDatabase } from "../database/database.module"; +import { ModuleIntrospection } from "../introspection/introspection.module"; +import { ModuleVoucher } from "../voucher/voucher.module"; +import { ControllerSnapshot } from "./snapshot.controller"; +import { ServiceSnapshot } from "./snapshot.service"; + +@Module({ + imports: [ModuleConfig, ModuleDatabase, ModuleIntrospection, ModuleVoucher], + controllers: [ControllerSnapshot], + providers: [ServiceSnapshot], + exports: [ServiceSnapshot], +}) +export class ModuleSnapshot {} diff --git a/project/server/src/layer/snapshot/snapshot.service.ts b/project/server/src/layer/snapshot/snapshot.service.ts new file mode 100644 index 00000000..873b1f45 --- /dev/null +++ b/project/server/src/layer/snapshot/snapshot.service.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from "@nestjs/common"; +import type { PickDeep } from "type-fest"; + +import { Snapshot } from "../../service/snapshot"; +import { Config } from "../config/config.module"; +import { DatabaseStaging } from "../database/database.module"; +import { ServiceIntrospection } from "../introspection/introspection.service"; +import { ServiceVoucher } from "../voucher/voucher.service"; + +import type { IDatabase } from "../../service/database"; + +@Injectable() +export class ServiceSnapshot extends Snapshot { + constructor( + @Inject(Config) config: PickDeep< + Config, + "snapshot.voucher.expectedAfter" | "snapshot.voucher.ttl" + >, + @Inject(DatabaseStaging) database: IDatabase<"staging">, + @Inject(ServiceIntrospection) introspection: ServiceIntrospection, + @Inject(ServiceVoucher) voucher: ServiceVoucher, + ) { + super(database, introspection, voucher, { + voucher: config.snapshot.voucher, + }); + } +} diff --git a/project/server/src/layer/test.ts b/project/server/src/layer/test.ts new file mode 100644 index 00000000..dcf3422b --- /dev/null +++ b/project/server/src/layer/test.ts @@ -0,0 +1,88 @@ +import type { Readable } from "node:stream"; + +import type { CallHandler, ExecutionContext } from "@nestjs/common"; +import { ROUTE_ARGS_METADATA } from "@nestjs/common/constants"; +import { Reflector } from "@nestjs/core"; +import { EMPTY } from "rxjs"; + +import { InterceptorRouteBody } from "./body.interceptor"; +import { InterceptorRouteRequest } from "./request.interceptor"; + +import type { StreamedRequest } from "./body"; +import type { DecodedRequest } from "./route"; + +// only the sections a codec and the parameter decorators read matter, so stub rather than taken from platform adapter +export type RequestStub = DecodedRequest & { + readonly headers?: Readonly> | undefined; + readonly rawBody?: Buffer | undefined; + /** whatever else an adapter leaves behind (e.g. `params`, `query`, `body`, ...) */ + readonly [section: string]: unknown; +}; + +/** what a `@StreamedBody` route is handed, as the platform's request is a readable */ +export type RequestStreamStub = Readable & RequestStub & StreamedRequest; + +/** the http context nest enters an interceptor and a handler with */ +export const executionContext = ( + handler: unknown, + request: RequestStub, +): ExecutionContext => + ({ + getType: () => "http", + getHandler: () => handler, + switchToHttp: () => ({ getRequest: () => request }), + }) as unknown as ExecutionContext; + +const next: CallHandler = { handle: () => EMPTY }; + +const interceptContext = (context: ExecutionContext): void => { + // in the order the composition root registers them in + new InterceptorRouteBody(new Reflector()).intercept(context, next); + new InterceptorRouteRequest(new Reflector()).intercept(context, next); +}; + +/** decodes onto the request the way the interceptors do before a handler is entered */ +export const intercept = (handler: unknown, request: RequestStub): void => { + interceptContext(executionContext(handler, request)); +}; + +/** an evaluated parameter decorator */ +type RouteArgument = { + readonly index: number; + readonly factory: (data: unknown, context: ExecutionContext) => unknown; + readonly data: unknown; +}; + +type ControllerClass = new (...args: never[]) => unknown; + +/** the parameter decorators a handler declares, in the positions they are bound to */ +export const routeArguments = ( + controller: ControllerClass, + propertyKey: string, +): readonly RouteArgument[] => + Object.values( + (Reflect.getMetadata(ROUTE_ARGS_METADATA, controller, propertyKey) ?? + {}) as Readonly>, + ).sort((left, right) => left.index - right.index); + +/** invokes handler by running request interceptor first, then handler with every parameter decorator resolved against the same request */ +export const invoke = ( + controller: Controller, + propertyKey: keyof Controller & string, + request: RequestStub, +): unknown => { + const handler = controller[propertyKey] as unknown as ( + this: Controller, + ...args: readonly unknown[] + ) => unknown; + const context = executionContext(handler, request); + + interceptContext(context); + + return handler.apply( + controller, + routeArguments(controller.constructor as ControllerClass, propertyKey).map( + ({ factory, data }) => factory(data, context), + ), + ); +}; diff --git a/project/server/src/layer/voucher/voucher.module.ts b/project/server/src/layer/voucher/voucher.module.ts new file mode 100644 index 00000000..ad664dd2 --- /dev/null +++ b/project/server/src/layer/voucher/voucher.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; + +import { ModuleConfig } from "../config/config.module"; +import { ServiceVoucher } from "./voucher.service"; + +@Module({ + imports: [ModuleConfig], + providers: [ServiceVoucher], + exports: [ServiceVoucher], +}) +export class ModuleVoucher {} diff --git a/project/server/src/layer/voucher/voucher.service.ts b/project/server/src/layer/voucher/voucher.service.ts new file mode 100644 index 00000000..789b6a7f --- /dev/null +++ b/project/server/src/layer/voucher/voucher.service.ts @@ -0,0 +1,12 @@ +import { Inject, Injectable } from "@nestjs/common"; +import type { PickDeep } from "type-fest"; + +import { Voucher } from "../../service/voucher"; +import { Config } from "../config/config.module"; + +@Injectable() +export class ServiceVoucher extends Voucher { + constructor(@Inject(Config) config: PickDeep) { + super(config.signing.voucher); + } +} diff --git a/project/server/src/logger.ts b/project/server/src/logger.ts index 6828ec93..eca49794 100644 --- a/project/server/src/logger.ts +++ b/project/server/src/logger.ts @@ -7,7 +7,7 @@ import { requestStorage } from "./utility/request-storage"; // logger configuration needs no test coverage /* node:coverage disable */ -const logLevels = { +export const logLevels = { levels: { error: 1, warn: 2, @@ -22,7 +22,7 @@ const logLevels = { debug: "blue", verbose: "gray", }, -}; +} as const; winston.addColors(logLevels.colors); // https://no-color.org/ (https://web.archive.org/web/20260616201813/https://no-color.org/) @@ -51,31 +51,29 @@ const formatPretty = format.combine( ); const formatJson = format.combine( - format.printf(({ level, message, timestamp, label, ...rest }) => - // biome-ignore lint/style/noNonNullAssertion: stringify only returns undefined for undefined input - stringify({ - timestamp, - level, - ...(label ? { label } : {}), - message, - request: rest.request ?? requestId(), - ...rest, - })!, + format.printf( + ({ level, message, timestamp, label, ...rest }) => + // biome-ignore lint/style/noNonNullAssertion: stringify only returns undefined for undefined input + stringify({ + timestamp, + level, + ...(label ? { label } : {}), + message, + request: rest.request ?? requestId(), + ...rest, + })!, ), ); export const logger = createLogger({ levels: logLevels.levels, defaultMeta: {}, - format: format.combine( - format.timestamp(), - format.errors({ stack: true }), - ), + format: format.combine(format.timestamp(), format.errors({ stack: true })), transports: [ - new transports.Console({ - format: - // https://nodejs.org/api/tty.html#tty_tty - process.stdout.isTTY ? formatPretty : formatJson, + new transports.Console({ + format: + // https://nodejs.org/api/tty.html#tty_tty + process.stdout.isTTY ? formatPretty : formatJson, }), ], }); diff --git a/project/server/src/repl.ts b/project/server/src/repl.ts index 1e1061b2..db9a325e 100644 --- a/project/server/src/repl.ts +++ b/project/server/src/repl.ts @@ -1,8 +1,8 @@ import { container } from "./dependency"; import { logger } from "./logger"; import { IDatabaseDerived, IDatabaseStaging } from "./service/database"; -import { IDeriveDerived } from "./service/derive"; -import { IDeriveDerivableDevice } from "./service/derive/derivable/device"; +import { IScheduler } from "./service/scheduler"; +import { ISchedulerScheduledDeriveDevice } from "./service/scheduler/scheduled/derive/device"; import { ISnapshot } from "./service/snapshot"; import { ISnapshotDeferTarget } from "./service/snapshot/defer/base"; import { IVoucher } from "./service/voucher"; @@ -19,11 +19,9 @@ logger.level = "debug"; derived: IDatabaseDerived, staging: IDatabaseStaging, }, - derive: { - derived: IDeriveDerived, - }, + scheduler: IScheduler, derivable: { - device: IDeriveDerivableDevice, + device: ISchedulerScheduledDeriveDevice, }, voucher: IVoucher, snapshot: ISnapshot, diff --git a/project/server/src/service/callback/vendor/slack.test.ts b/project/server/src/service/callback/vendor/slack.test.ts index 78ac4335..02364161 100644 --- a/project/server/src/service/callback/vendor/slack.test.ts +++ b/project/server/src/service/callback/vendor/slack.test.ts @@ -1,16 +1,10 @@ -import { randomBytes } from "node:crypto"; import { mock, type TestContext, test } from "node:test"; -import { Ingress } from "../../ingress"; -import { Voucher } from "../../voucher"; import { CallbackVendorSlack } from "./slack"; import type { ISnapshotDeferIngest } from "../../snapshot/defer/ingest"; test("genuine", (t: TestContext) => { - const voucher = new Voucher(randomBytes(64).toString()); - const ingress = new Ingress({ authority: "foo", secure: true }, voucher); - { const timestamp = 1531420618; const signature = Buffer.from( @@ -25,10 +19,7 @@ test("genuine", (t: TestContext) => { const slack = new CallbackVendorSlack( { signingKey: "8f742231b10e8888abcd99yyyzzz85a5", botToken: "xoxb-foo" }, - {}, {} as ISnapshotDeferIngest, - ingress, - voucher, ); t.mock.timers.enable({ apis: ["Date"] }); @@ -65,10 +56,7 @@ test("genuine", (t: TestContext) => { const slack = new CallbackVendorSlack( { signingKey: "9f742231b10e8888abcd99yyyzzz85a5", botToken: "xoxb-foo" }, - {}, {} as ISnapshotDeferIngest, - ingress, - voucher, ); t.mock.timers.enable({ apis: ["Date"] }); @@ -84,15 +72,9 @@ test("genuine", (t: TestContext) => { }); test("command handling", async (t) => { - const voucher = new Voucher("dd934b01b7bbe1ff59aaa892a6021115"); - const ingress = new Ingress({ authority: "foo", secure: true }, voucher); - const slack = new CallbackVendorSlack( { signingKey: "8f742231b10e8888abcd99yyyzzz85a5", botToken: "xoxb-foo" }, - {}, {} as ISnapshotDeferIngest, - ingress, - voucher, ); // `DateFromSelf` can't decode tap's mocked dates → use builtin mocking instead diff --git a/project/server/src/service/callback/vendor/slack.ts b/project/server/src/service/callback/vendor/slack.ts index 4777eb07..9fe43edf 100644 --- a/project/server/src/service/callback/vendor/slack.ts +++ b/project/server/src/service/callback/vendor/slack.ts @@ -1,22 +1,11 @@ import { createHmac, timingSafeEqual } from "node:crypto"; -import { setTimeout } from "node:timers/promises"; import { createType, inject } from "@lppedd/di-wise-neo"; -import { formatDistanceToNow } from "date-fns"; import { Schema } from "effect"; import { isLeft } from "effect/Either"; -import { isNone } from "../../../type/maybe"; -import { - DatabaseSnapshotCoordinatorName, - DatabaseSnapshotCoordinators, -} from "../../database/snapshot-coordinator/base"; -import { IIngress } from "../../ingress"; import { ISnapshotDeferIngest } from "../../snapshot/defer/ingest"; import { SuspendableHandle } from "../../suspendable"; -import { IVoucher } from "../../voucher"; - -import type { IDatabaseSnapshotCoordinator } from "../../database/snapshot-coordinator"; type BlockText = { type: "mrkdwn"; @@ -57,25 +46,14 @@ type HandleContext = { userId: string; }; -const parseableCommandDatabaseSnapshot = "/database-snapshot" as const; const parseableCommandDatabaseIngest = "/database-ingest" as const; -type ParseableCommandDatabaseSnapshot = typeof parseableCommandDatabaseSnapshot; type ParseableCommandDatabaseIngest = typeof parseableCommandDatabaseIngest; -type ParseableCommand = - | ParseableCommandDatabaseSnapshot - | ParseableCommandDatabaseIngest; +type ParseableCommand = ParseableCommandDatabaseIngest; type ParsedCommandTextParsed = { kind: "parsed"; inner: T; }; -type ParsedCommandTextCommandDatabaseSnapshot = ParsedCommandTextParsed<{ - coordinator: { - self: IDatabaseSnapshotCoordinator; - name: DatabaseSnapshotCoordinatorName; - }; - age: "fresh" | "stale"; -}>; type ParsedCommandTextCommandDatabaseIngest = ParsedCommandTextParsed<{ action: "suspend" | "resume"; }>; @@ -83,9 +61,7 @@ type ParsedCommandTextError = { kind: "error"; blocks: Block[]; }; -type ParsedCommandTextCommand = - | ParsedCommandTextCommandDatabaseSnapshot - | ParsedCommandTextCommandDatabaseIngest; +type ParsedCommandTextCommand = ParsedCommandTextCommandDatabaseIngest; const ResponseConversationOpen = Schema.Struct({ ok: Schema.Literal(true), @@ -131,10 +107,7 @@ const ephemeral = (...blocks: Block[]): Handled => ({ export class CallbackVendorSlack implements ICallbackVendorSlack { constructor( private readonly secrets: { signingKey: string; botToken: string }, - private readonly coodinators = inject(DatabaseSnapshotCoordinators), private ingest = inject(ISnapshotDeferIngest), - private ingress = inject(IIngress), - private voucher = inject(IVoucher), ) {} // https://docs.slack.dev/authentication/verifying-requests-from-slack @@ -216,18 +189,6 @@ export class CallbackVendorSlack implements ICallbackVendorSlack { return decoded.right.ts; } - private async updateMessage( - channelId: string, - ts: string, - blocks: Block[], - ): Promise { - await this.post("chat.update", { channel: channelId, ts, blocks }); - } - - private parseCommandText( - command: ParseableCommandDatabaseSnapshot, - text: string, - ): ParsedCommandTextCommandDatabaseSnapshot | ParsedCommandTextError; private parseCommandText( command: ParseableCommandDatabaseIngest, text: string, @@ -237,74 +198,6 @@ export class CallbackVendorSlack implements ICallbackVendorSlack { text: string, ): ParsedCommandTextCommand | ParsedCommandTextError { switch (command) { - case parseableCommandDatabaseSnapshot: { - const split = text.split(" "); - - const snapshotName = split.at(0); - if (typeof snapshotName === "undefined") { - return { - kind: "error", - blocks: [ - mrkdwnBlock( - `missing snapshot name (supported: ${Object.keys( - this.coodinators, - ) - .map((item) => `\`${item}\``) - .join(", ")})`, - ), - ], - }; - } - - if (!Schema.is(DatabaseSnapshotCoordinatorName)(snapshotName)) { - return { - kind: "error", - blocks: [ - mrkdwnBlock( - `unknown snapshot name (supported: ${Object.keys( - this.coodinators, - ) - .map((item) => `\`${item}\``) - .join(", ")})`, - ), - ], - }; - } - - const age = split.at(1); - if ( - !(typeof age === "undefined" || age === "stale" || age === "fresh") - ) { - return { - kind: "error", - blocks: [mrkdwnBlock(`unsupported age (supported: stale, fresh)`)], - }; - } - - const coordinator = this.coodinators[snapshotName]; - if (typeof coordinator === "undefined") { - return { - kind: "error", - blocks: [ - mrkdwnBlock( - `unknown snapshot name (supported: ${Object.keys( - this.coodinators, - ) - .map((item) => `\`${item}\``) - .join(", ")})`, - ), - ], - }; - } - - return { - kind: "parsed", - inner: { - coordinator: { self: coordinator, name: snapshotName }, - age: age ?? "stale", - }, - }; - } case parseableCommandDatabaseIngest: { const trimmed = text.trim(); switch (trimmed) { @@ -330,111 +223,6 @@ export class CallbackVendorSlack implements ICallbackVendorSlack { } } - private static progressBar(percentage: number, length = 20): string { - const blocks = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"]; - - const clamped = Math.max(0, Math.min(1, percentage)); - const total = clamped * length; - - const full = Math.floor(total); - const remainder = total - full; - - const partialIndex = Math.round(remainder * (blocks.length - 1)); - - let bar = "█".repeat(full); - - if (full < length && partialIndex > 0) { - bar += blocks[partialIndex]; - } - - const used = full + (partialIndex > 0 ? 1 : 0); - bar += " ".repeat(length - used); - - return `\`|${bar}|\` ${Math.floor(percentage * 100)}%`; - } - - private async handleCommandDatabaseSnapshotStale( - parsed: ParsedCommandTextCommandDatabaseSnapshot, - ): Promise { - const handle = await parsed.inner.coordinator.self.stale(); - if (isNone(handle)) { - return ephemeral( - mrkdwnBlock( - `no stale snapshot available, use \`${parseableCommandDatabaseSnapshot} ${parsed.inner.coordinator.name} fresh\` to request a new snapshot`, - ), - ); - } else { - const stat = await handle.stat(); - - await handle.close(); - - const voucher = this.voucher.create("database-snapshot", new Date(), { - coordinator: parsed.inner.coordinator.name, - }); - const url = this.ingress.url.databaseSnapshot(voucher); - - return ephemeral( - mrkdwnBlock( - `database snapshot was created ${formatDistanceToNow(stat.birthtime)} ago`, - ), - mrkdwnBlock( - `use <${url}|this link> to download snapshot (it expires quickly!)`, - ), - ); - } - } - - private async handleCommandDatabaseSnapshot( - parsed: ParsedCommandTextCommandDatabaseSnapshot, - ctx: Pick, - ): Promise { - if (parsed.inner.age === "stale") { - return await this.handleCommandDatabaseSnapshotStale(parsed); - } - - const channelId = await this.openConversation(ctx.userId); - if (channelId === null) { - return ephemeral(mrkdwnBlock("could not open conversation 😰")); - } - - const messageTs = await this.postMessage(channelId, [ - mrkdwnBlock(CallbackVendorSlack.progressBar(0)), - ]); - if (messageTs === null) { - return ephemeral(mrkdwnBlock("could not create initial message 😰")); - } - - void (async () => { - for await (const progress of parsed.inner.coordinator.self.fresh()) { - await this.updateMessage(channelId, messageTs, [ - mrkdwnBlock( - CallbackVendorSlack.progressBar( - progress.currentSnapshotSize / progress.originalSizeEstimate, - ), - ), - ]); - - await setTimeout(5_000); - } - - await this.updateMessage(channelId, messageTs, [ - mrkdwnBlock(CallbackVendorSlack.progressBar(1)), - ]); - - await this.postMessage(channelId, [ - mrkdwnBlock( - `snapshot complete, request download link with \`${parseableCommandDatabaseSnapshot} ${parsed.inner.coordinator.name} stale\``, - ), - ]); - })(); - - return ephemeral( - mrkdwnBlock( - `head over to <#${channelId}> to observe snapshotting status ⌛️`, - ), - ); - } - private async handleCommandDatabaseIngest( parsed: ParsedCommandTextCommandDatabaseIngest, ctx: Pick, @@ -489,14 +277,6 @@ export class CallbackVendorSlack implements ICallbackVendorSlack { ctx: HandleContext, ): Promise { switch (command) { - case parseableCommandDatabaseSnapshot: { - const parsed = this.parseCommandText(command, text); - if (parsed.kind === "error") { - return ephemeral(...parsed.blocks); - } - - return this.handleCommandDatabaseSnapshot(parsed, ctx); - } case parseableCommandDatabaseIngest: { const parsed = this.parseCommandText(command, text); if (parsed.kind === "error") { diff --git a/project/server/src/service/database/migrate/index.ts b/project/server/src/service/database/migrate/index.ts index e8141c76..d3ac023b 100644 --- a/project/server/src/service/database/migrate/index.ts +++ b/project/server/src/service/database/migrate/index.ts @@ -55,7 +55,7 @@ type DatabaseMigratePlanUnachievableMalformedMigration = { migration: DatabaseMigrateMigration; }; -type DatabaseMigratePlanUnachievable = +export type DatabaseMigratePlanUnachievable = | DatabaseMigratePlanUnachievableTableIntegrityViolation | DatabaseMigratePlanUnachievableDuplicateIdentifier | DatabaseMigratePlanUnachievableMalformedMigration diff --git a/project/server/src/service/database/snapshot-coordinator/base.ts b/project/server/src/service/database/snapshot-coordinator/base.ts deleted file mode 100644 index a6064e7d..00000000 --- a/project/server/src/service/database/snapshot-coordinator/base.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createType } from "@lppedd/di-wise-neo"; -import { Schema } from "effect"; - -import type { IDatabaseSnapshotCoordinator } from "."; - -export const DatabaseSnapshotCoordinatorName = Schema.Literal("staging"); -export type DatabaseSnapshotCoordinatorName = - typeof DatabaseSnapshotCoordinatorName.Type; - -export const DatabaseSnapshotCoordinators = createType< - Partial> ->("DatabaseSnapshotCoordinators"); diff --git a/project/server/src/service/database/snapshot-coordinator/index.ts b/project/server/src/service/database/snapshot-coordinator/index.ts deleted file mode 100644 index 467f4e8f..00000000 --- a/project/server/src/service/database/snapshot-coordinator/index.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { ENOENT } from "node:constants"; -import type { Mode, PathLike } from "node:fs"; -import { type FileHandle, open, rename, rm } from "node:fs/promises"; - -import { createType } from "@lppedd/di-wise-neo"; - -import { type ISuspendable, SuspendableHandle } from "../../suspendable"; - -import type { Maybe } from "../../../type/maybe"; -import type { IDatabase } from "../"; -import type { DatabaseName } from "../base"; - -type FreshProgress = { - originalSizeEstimate: number; - currentSnapshotSize: number; -}; - -export interface IDatabaseSnapshotCoordinator { - get destination(): string; - - // return value of AsyncIterable is not observable in `for await` loop → tagged union in `T` position - fresh(): AsyncIterable; - stale(): Promise>; -} - -export const IDatabaseSnapshotCoordinator = - createType("IDatabaseSnapshotCoordinator"); - -const RaceSentinel = Symbol("RaceSentinel"); - -// `FileHandle` does not expose that the underlying file descriptor has been closed → wrapper to manage closing -class FileHandleBox { - private _closed: boolean = false; - - public static async new( - path: PathLike, - flags?: string | number, - mode?: Mode, - ) { - return new FileHandleBox(await open(path, flags, mode)); - } - - private constructor(private wrapped: FileHandle) {} - - public get handle(): Omit { - return this.wrapped; - } - - public get closed(): boolean { - return this._closed; - } - - public async close() { - if (this._closed) { - return; - } - - await this.wrapped.close(); - this._closed = true; - } -} - -type Running = { - originalSizeEstimate: number; - snapshot: Promise; - // assignment to `this.running` can't suspend, otherwise the "not set" branch - // can be reached by multiple callers at once → store as promise that is awaited when observing - handle: Promise; -}; - -const DatabaseSnapshotCoordinatorSymbol = Symbol("DatabaseSnapshotCoordinator"); - -export class DatabaseSnapshotCoordinator - implements IDatabaseSnapshotCoordinator -{ - // already running snapshot operation - private running: Running | undefined; - - constructor( - private database: IDatabase, - private suspendable: ISuspendable, - public readonly destination: string, - ) {} - - fresh(): AsyncIterable { - let running: Running; - if (typeof this.running === "undefined") { - const tmpPath = `${this.destination}.tmp`; - - const handle = (async () => { - await rm(tmpPath, { force: true }); - return await FileHandleBox.new( - tmpPath, - // creates file if it doesn't yet exist - // required because snapshotting will only start _after_ handle has been acquired - "a+", - ); - })(); - - running = { - originalSizeEstimate: this.database.sizeEstimate, - handle, - snapshot: (async () => { - // wait until temporary file has been removed - await handle; - - const suspendHandle = new SuspendableHandle( - DatabaseSnapshotCoordinatorSymbol, - ); - - await this.suspendable.suspend(suspendHandle); - - try { - await this.database.snapshot(tmpPath); - await rename(tmpPath, this.destination); - } finally { - this.suspendable.resume(suspendHandle); - } - })(), - }; - this.running = running; - } else { - running = this.running; - } - - const { originalSizeEstimate, snapshot, handle } = running; - return { - [Symbol.asyncIterator]: () => { - return { - next: async () => { - const h = await handle; - - // conclude the iterable - if (h.closed) { - return { - value: undefined, - done: true, - }; - } - - const raced = await Promise.race([snapshot, RaceSentinel]); - // snapshot completed - if (raced !== RaceSentinel) { - await h.close(); - this.running = undefined; - - return { - value: undefined, - done: true, - }; - } - - const stat = await h.handle.stat(); - return { - value: { - originalSizeEstimate: originalSizeEstimate, - currentSnapshotSize: stat.size, - }, - done: false, - }; - }, - }; - }, - }; - } - - async stale(): Promise> { - // get file descriptor to prevent deletion from causing race between stat and opening stream - let handle; - try { - handle = await open(this.destination, "r"); - } catch (e) { - if ( - !( - typeof e === "object" && - e !== null && - "errno" in e && - e.errno === -ENOENT - ) - ) { - throw e; - } - } - - return handle ?? null; - } -} diff --git a/project/server/src/service/derive/derivable/meta.ts b/project/server/src/service/derive/derivable/meta.ts deleted file mode 100644 index 29ff401f..00000000 --- a/project/server/src/service/derive/derivable/meta.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { inject } from "@lppedd/di-wise-neo"; - -import { type DatabaseTransaction, IDatabaseDerived } from "../../database"; -import { deleteDerivedMetaEntityStats } from "../../database/query/derived/meta-delete"; -import { getDerivedMetaEntityStats } from "../../database/query/derived/meta-get"; -import { IIntrospection } from "../../introspect"; - -import type { DeriveDerivable } from "../base"; - -export class DeriveDerivableMetaEntityStat - implements DeriveDerivable<"derived", typeof DeriveDerivableMetaEntityStat> -{ - static readonly id = Symbol("DeriveDerivableMetaEntityStat"); - - static readonly prerequisites = []; - static readonly schedule = { - minute: "*/5", - } as const; - - constructor( - private db = inject(IDatabaseDerived), - introspection: IIntrospection = inject(IIntrospection), - ) { - introspection.metric.gauge( - { - name: "database_staging_size_total", - help: "size of database", - labelNames: ["entity"], - registry: "global", - }, - async (collector) => { - const bound = getDerivedMetaEntityStats.bind.anonymous([], { - rowMode: "tuple", - }); - - for await (const row of this.db.run(bound, "background")) { - collector.set({ entity: row[0] }, row[1]); - } - }, - ); - } - - async derive(t: DatabaseTransaction<"derived", "w">): Promise { - await t.run(deleteDerivedMetaEntityStats.bind.anonymous([])); - await t.run({ - database: "derived", - name: "InsertDeriveMetaEntityStat", - query: `insert into derived_meta_entity_stat - select name, pgsize from dbstat where aggregate = true and schema = 'staging'`, - parameters: [], - connectionMode: "w", - resultMode: "none", - rowMode: "tuple", - integerMode: "number", - }); - } -} diff --git a/project/server/src/service/derive/derivable/subject.ts b/project/server/src/service/derive/derivable/subject.ts deleted file mode 100644 index 62e73c64..00000000 --- a/project/server/src/service/derive/derivable/subject.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { deleteDerivedSubjects } from "../../database/query/derived/subject-delete"; -import { insertDerivedSubjects } from "../../database/query/derived/subject-insert"; - -import type { DatabaseTransaction } from "../../database"; -import type { DeriveDerivable } from "../base"; - -export class DeriveDerivableSubject - implements DeriveDerivable<"derived", typeof DeriveDerivableSubject> -{ - static readonly id = Symbol("DeriveDerivableSubject"); - - static readonly prerequisites = []; - - async derive(t: DatabaseTransaction<"derived", "w">): Promise { - await t.run(deleteDerivedSubjects.bind.anonymous([])); - await t.run(insertDerivedSubjects.bind.named({ window: 60 * 60 * 25 })); - } -} diff --git a/project/server/src/service/derive/derivable/submission.ts b/project/server/src/service/derive/derivable/submission.ts deleted file mode 100644 index bb142e45..00000000 --- a/project/server/src/service/derive/derivable/submission.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { inject } from "@lppedd/di-wise-neo"; - -import { type DatabaseTransaction, IDatabaseDerived } from "../../database"; -import { deleteDerivedSubmissions } from "../../database/query/derived/submission-delete"; -import { getDerivedSubmissions } from "../../database/query/derived/submission-get"; -import { insertDerivedSubmission } from "../../database/query/derived/submission-insert"; -import { IIntrospection } from "../../introspect"; - -import type { DeriveDerivable } from "../base"; - -export class DeriveDerivableSubmissionFaulty - implements DeriveDerivable<"derived", typeof DeriveDerivableSubmissionFaulty> -{ - static readonly id = Symbol("DeriveDerivableSubmissionFaulty"); - - static readonly prerequisites = []; - - constructor( - private db = inject(IDatabaseDerived), - introspection: IIntrospection = inject(IIntrospection), - ) { - introspection.metric.gauge( - { - name: "snapshot_faulty_submissions_total", - help: "amount of faulty submissions", - labelNames: ["state"], - registry: "global", - }, - async (collector) => { - const bound = getDerivedSubmissions.bind.anonymous([]); - - for await (const row of this.db.run(bound)) { - collector.set({ state: row.state }, row.count); - } - }, - ); - } - - async derive(t: DatabaseTransaction<"derived", "w">): Promise { - await t.run(deleteDerivedSubmissions.bind.anonymous([])); - await t.run(insertDerivedSubmission.bind.anonymous([])); - } -} diff --git a/project/server/src/service/derive/index.test.ts b/project/server/src/service/derive/index.test.ts deleted file mode 100644 index 65918bf2..00000000 --- a/project/server/src/service/derive/index.test.ts +++ /dev/null @@ -1,423 +0,0 @@ -import { type TestContext, test } from "node:test"; - -import { unroll } from "../../utility/iterable"; -import { Database, type DatabaseTransaction } from "../database"; -import { bake } from "../database/base"; -import { testDatabase } from "../database/utility"; -import { StubIntrospection } from "../introspect/stub"; -import { Derive, DeriveWaitLateError } from "."; - -import type { DeriveDerivable } from "./base"; - -test("plan", (t: TestContext) => { - // unscheduled derivable isn't included in plan unless its a prerequisite of another derivable - t.test("unscheduled", (t: TestContext) => { - class A implements DeriveDerivable { - static id = Symbol("A"); - - static prerequisites = []; - - async derive(): Promise {} - } - - class B implements DeriveDerivable { - static id = Symbol("B"); - static schedule = { minute: "30" } as const; - - static prerequisites = [A.id]; - - async derive(): Promise {} - } - - { - const derive = new Derive( - new Database( - undefined, - bake({ location: new URL("file:?mode=memory") }), - {}, - ), - [new A()], - new StubIntrospection(), - ); - - const epoch = derive.next( - Derive.epoch(new Date("2026-03-03T17:29:00.000Z")), - ); - const plan = derive.plan(epoch); - t.assert.ok(Derive.viable(plan)); - t.assert.deepStrictEqual( - Derive.peek(plan).pending.map((item) => item.id), - [], - ); - } - - { - const derive = new Derive( - new Database( - undefined, - bake({ location: new URL("file:?mode=memory") }), - {}, - ), - [new A(), new B()], - new StubIntrospection(), - ); - - const epoch = derive.next( - Derive.epoch(new Date("2026-03-03T17:29:00.000Z")), - ); - t.assert.deepStrictEqual(Derive.peek(epoch), { - next: new Date("2026-03-03T17:30:00.000Z"), - }); - const plan = derive.plan(epoch); - t.assert.ok(Derive.viable(plan)); - t.assert.deepStrictEqual( - Derive.peek(plan).pending.map((item) => item.id), - [A.id, B.id], - ); - } - }); - - t.test("satisfiable", (t: TestContext) => { - class A implements DeriveDerivable { - static id = Symbol("A"); - - static prerequisites = []; - - async derive(): Promise {} - } - - class B implements DeriveDerivable { - static id = Symbol("B"); - static schedule = { minute: "30" } as const; - - static prerequisites = [A.id]; - - async derive(): Promise {} - } - - class C implements DeriveDerivable { - static id = Symbol("C"); - static schedule = {} as const; - - static prerequisites = [A.id]; - - async derive(): Promise {} - } - - class D implements DeriveDerivable { - static id = Symbol("D"); - static schedule = { minute: "*/2" } as const; - - static prerequisites = [C.id]; - - async derive(): Promise {} - } - - const derive = new Derive( - new Database( - undefined, - bake({ location: new URL("file:?mode=memory") }), - {}, - ), - [new A(), new B(), new C(), new D()], - new StubIntrospection(), - ); - - let epoch = derive.next(Derive.epoch(new Date("2026-03-03T17:29:00.000Z"))); - t.assert.deepStrictEqual(Derive.peek(epoch), { - next: new Date("2026-03-03T17:30:00.000Z"), - }); - let plan = derive.plan(epoch); - t.assert.ok(Derive.viable(plan)); - t.assert.deepStrictEqual( - Derive.peek(plan).pending.map((item) => item.id), - [A.id, C.id, B.id, D.id], - ); - t.assert.deepStrictEqual( - Derive.peek(plan).reasons, - new Map([ - [A.id, new Set(["dependency"])], - [B.id, new Set(["schedule"])], - [C.id, new Set(["schedule", "dependency"])], - [D.id, new Set(["schedule"])], - ]), - ); - - epoch = derive.next(epoch); - t.assert.deepStrictEqual(Derive.peek(epoch), { - next: new Date("2026-03-03T17:31:00.000Z"), - }); - plan = derive.plan(epoch); - t.assert.ok(Derive.viable(plan)); - t.assert.deepStrictEqual( - Derive.peek(plan).pending.map((item) => item.id), - [A.id, C.id], - ); - t.assert.deepStrictEqual( - Derive.peek(plan).reasons, - new Map([ - [A.id, new Set(["dependency"])], - [C.id, new Set(["schedule"])], - ]), - ); - - epoch = derive.next(epoch); - t.assert.deepStrictEqual(Derive.peek(epoch), { - next: new Date("2026-03-03T17:32:00.000Z"), - }); - plan = derive.plan(epoch); - t.assert.ok(Derive.viable(plan)); - t.assert.deepStrictEqual( - Derive.peek(plan).pending.map((item) => item.id), - [A.id, C.id, D.id], - ); - t.assert.deepStrictEqual( - Derive.peek(plan).reasons, - new Map([ - [A.id, new Set(["dependency"])], - [C.id, new Set(["schedule", "dependency"])], - [D.id, new Set(["schedule"])], - ]), - ); - - epoch = derive.next(epoch); - t.assert.deepStrictEqual(Derive.peek(epoch), { - next: new Date("2026-03-03T17:33:00.000Z"), - }); - plan = derive.plan(epoch); - t.assert.ok(Derive.viable(plan)); - t.assert.deepStrictEqual( - Derive.peek(plan).pending.map((item) => item.id), - [A.id, C.id], - ); - t.assert.deepStrictEqual( - Derive.peek(plan).reasons, - new Map([ - [A.id, new Set(["dependency"])], - [C.id, new Set(["schedule"])], - ]), - ); - }); - - test("wait", async (t: TestContext) => { - t.test("not late", async (t: TestContext) => { - t.mock.timers.enable({ - apis: ["setTimeout", "Date"], - now: new Date("2026-03-03T17:29:00.000Z"), - }); - - class A implements DeriveDerivable { - static id = Symbol("A"); - static schedule = {} as const; - - static prerequisites = []; - - async derive(): Promise {} - } - - const derive = new Derive( - new Database( - undefined, - bake({ location: new URL("file:?mode=memory") }), - {}, - ), - [new A()], - new StubIntrospection(), - ); - - const waiting = derive.wait(Derive.epoch(), { late: "throw" }); - t.mock.timers.tick(60_000); - t.assert.deepStrictEqual( - await Promise.race([waiting, "sentinel"]), - "sentinel", - ); - - const raced = await Promise.race([waiting, "sentinel"] as const); - t.assert.ok(raced !== "sentinel"); - t.assert.deepStrictEqual(Derive.peek(raced), { - next: new Date("2026-03-03T17:30:00.000Z"), - }); - }); - - t.test("late", async (t: TestContext) => { - t.mock.timers.enable({ - apis: ["setTimeout", "Date"], - now: new Date("2026-03-03T17:29:00.000Z"), - }); - - class A implements DeriveDerivable { - static id = Symbol("A"); - static schedule = {} as const; - - static prerequisites = []; - - async derive(): Promise {} - } - - const derive = new Derive( - new Database( - undefined, - bake({ location: new URL("file:?mode=memory") }), - {}, - ), - [new A()], - new StubIntrospection(), - ); - - const epoch = Derive.epoch(); - t.mock.timers.tick(60_000); - await t.assert.rejects( - derive.wait(epoch, { late: "throw" }), - DeriveWaitLateError, - ); - }); - }); - - t.test("missing prerequisite", (t: TestContext) => { - class A implements DeriveDerivable { - static id = Symbol("A"); - static schedule = {} as const; - - static prerequisites = []; - - async derive(): Promise {} - } - - class B implements DeriveDerivable { - static id = Symbol("B"); - static schedule = {} as const; - - static prerequisites = [A.id]; - - async derive(): Promise {} - } - - const derive = new Derive( - new Database( - undefined, - bake({ location: new URL("file:?mode=memory") }), - {}, - ), - [new B()], - new StubIntrospection(), - ); - - const epoch = Derive.epoch(); - const next = derive.next(epoch); - const plan = derive.plan(next); - - t.assert.ok("kind" in plan && plan.kind === "missing-prerequisite"); - }); - - t.test("circular prerequisites", (t: TestContext) => { - const bId = Symbol("B"); - - class A implements DeriveDerivable { - static id = Symbol("A"); - static schedule = {} as const; - - static prerequisites = [bId]; - - async derive(): Promise {} - } - - class B implements DeriveDerivable { - static id = bId; - static schedule = {} as const; - - static prerequisites = [A.id]; - - async derive(): Promise {} - } - - const derive = new Derive( - new Database( - undefined, - bake({ location: new URL("file:?mode=memory") }), - {}, - ), - [new A(), new B()], - new StubIntrospection(), - ); - - const epoch = Derive.epoch(); - const next = derive.next(epoch); - const plan = derive.plan(next); - t.assert.ok("kind" in plan && plan.kind === "circular-prerequisites"); - }); -}); - -test("act", async (t: TestContext) => { - await using db = await testDatabase(undefined, false); - db.raw.exec( - "create table a (value text primary key not null) strict, without rowid", - ); - db.raw.exec( - "create table b (value text primary key not null) strict, without rowid", - ); - - const mockA = t.mock.fn< - (t: DatabaseTransaction) => Promise - >(async (t: DatabaseTransaction) => { - await t.run({ - database: undefined, - name: "InsertA", - query: "insert into a values ('foo')", - connectionMode: "w", - parameters: [], - rowMode: "tuple", - resultMode: "none", - integerMode: "number", - }); - }); - class A implements DeriveDerivable { - static id = Symbol("A"); - static schedule = {} as const; - - static prerequisites = []; - - derive = mockA; - } - - const mockB = t.mock.fn< - (t: DatabaseTransaction) => Promise - >(async (t: DatabaseTransaction) => { - await t.run({ - database: undefined, - name: "InsertB", - query: "insert into b select value from a", - connectionMode: "w", - parameters: [], - rowMode: "tuple", - resultMode: "none", - integerMode: "number", - }); - }); - class B implements DeriveDerivable { - static id = Symbol("B"); - static schedule = {} as const; - - static prerequisites = [A.id]; - - derive = mockB; - } - - const derive = new Derive(db, [new A(), new B()], new StubIntrospection()); - - const next = derive.next(Derive.epoch()); - const plan = derive.plan(next); - t.assert.ok(Derive.viable(plan)); - - t.assert.partialDeepStrictEqual(await unroll(derive.act(plan)), [ - { id: A.id }, - { id: B.id }, - ]); - - t.assert.deepStrictEqual(mockA.mock.callCount(), 1); - t.assert.deepStrictEqual(mockB.mock.callCount(), 1); - - t.assert.deepStrictEqual( - [...db.raw.query("select value from b", { returnArray: true }, {})], - [["foo"]], - ); -}); diff --git a/project/server/src/service/ingress/index.test.ts b/project/server/src/service/ingress/index.test.ts index 501a91a9..843cda96 100644 --- a/project/server/src/service/ingress/index.test.ts +++ b/project/server/src/service/ingress/index.test.ts @@ -1,85 +1,10 @@ import { type TestContext, test } from "node:test"; import { floor } from "../../type/codec/integer"; -import { isSome } from "../../type/maybe"; -import { DatabaseSnapshotVoucherPayload } from "../../web/database/snapshot/base"; -import { Voucher } from "../voucher"; import { Ingress } from "."; -test("ingress", (t: TestContext) => { - const voucher = new Voucher("09734462143c5e195c36299bb6892ec2"); - - // `DateFromSelf` can't decode tap's mocked dates → use builtin mocking instead - t.mock.timers.enable({ apis: ["Date"], now: 1760005665000 }); - - { - const ingress = new Ingress({ authority: "foo", secure: true }, voucher); - t.assert.snapshot( - ingress.url.databaseSnapshot( - voucher.create("database-snapshot", new Date(), { - coordinator: "staging", - }), - ), - ); - } - - { - const ingress = new Ingress({ authority: "foo", secure: false }, voucher); - t.assert.snapshot( - ingress.url.databaseSnapshot( - voucher.create("database-snapshot", new Date(), { - coordinator: "staging", - }), - ), - ); - } - - { - const ingress = new Ingress({ authority: "foo", secure: false }, voucher); - const url = new URL( - ingress.url.databaseSnapshot( - voucher.create("database-snapshot", new Date(), { - coordinator: "staging", - }), - ), - ); - - const serialized = url.searchParams.get("voucher"); - t.assert.ok(isSome(serialized)); - - { - const deserialized = voucher.deserialize( - serialized, - "database-snapshot", - 10, - DatabaseSnapshotVoucherPayload, - ); - - t.assert.ok(deserialized.kind === "success"); - } - - t.mock.timers.tick(10 * 1000); - - { - const deserialized = voucher.deserialize( - serialized, - "database-snapshot", - 10, - ); - - t.assert.ok( - deserialized.kind === "error" && deserialized.cause === "expired", - ); - } - } -}); - test("formats link header style pagination", (t: TestContext) => { - const voucher = new Voucher("09734462143c5e195c36299bb6892ec2"); - const ingress = new Ingress( - { authority: "example.com", secure: true }, - voucher, - ); + const ingress = new Ingress({ authority: "example.com", secure: true }); t.test("first page of multi-page collection", (t: TestContext) => { const link = ingress.header.link( diff --git a/project/server/src/service/ingress/index.test.ts.snapshot b/project/server/src/service/ingress/index.test.ts.snapshot deleted file mode 100644 index 345b845c..00000000 --- a/project/server/src/service/ingress/index.test.ts.snapshot +++ /dev/null @@ -1,7 +0,0 @@ -exports[`ingress 1`] = ` -"https://foo/system/database/snapshot/staging.db?voucher=6yriH76Mf80qoz3Pnp4y7djwJGA7amHDbx4ph6TE0tw%7CeyJyb2xlIjoiZGF0YWJhc2Utc25hcHNob3QiLCJhdCI6MTc2MDAwNTY2NSwiY29vcmRpbmF0b3IiOiJzdGFnaW5nIn0" -`; - -exports[`ingress 2`] = ` -"http://foo/system/database/snapshot/staging.db?voucher=6yriH76Mf80qoz3Pnp4y7djwJGA7amHDbx4ph6TE0tw%7CeyJyb2xlIjoiZGF0YWJhc2Utc25hcHNob3QiLCJhdCI6MTc2MDAwNTY2NSwiY29vcmRpbmF0b3IiOiJzdGFnaW5nIn0" -`; diff --git a/project/server/src/service/ingress/index.ts b/project/server/src/service/ingress/index.ts index 0ae3281e..2d5d2a0f 100644 --- a/project/server/src/service/ingress/index.ts +++ b/project/server/src/service/ingress/index.ts @@ -2,9 +2,6 @@ import { createType, inject } from "@lppedd/di-wise-neo"; import { ConfigProvider } from "../../config"; import { ceil, floor, type Integer } from "../../type/codec/integer"; -import { type Parameters, paths } from "../../web/base"; -import { DatabaseSnapshotVoucherPayload } from "../../web/database/snapshot/base"; -import { IVoucher, type SealedVoucher, Voucher } from "../voucher"; import type { Uuid } from "../../type/codec/uuid"; @@ -43,23 +40,13 @@ export interface IIngress { self(id: Uuid): URL; duplicates(id: Uuid): URL; }; - - databaseSnapshot( - sealed: SealedVoucher< - "database-snapshot", - DatabaseSnapshotVoucherPayload - >, - ): URL; }; } export const IIngress = createType("IIngress"); export class Ingress implements IIngress { - constructor( - private external = inject(ConfigProvider)((c) => c.external), - private voucher = inject(IVoucher), - ) {} + constructor(private external = inject(ConfigProvider)((c) => c.external)) {} get origin() { return `${this.external.secure ? "https" : "http"}://${this.external.authority}`; @@ -153,28 +140,10 @@ export class Ingress implements IIngress { ); } - private urlDatabaseSnapshotStale( - sealed: SealedVoucher<"database-snapshot", DatabaseSnapshotVoucherPayload>, - ): URL { - const path = paths["database-snapshot"]; - const query = { - voucher: this.voucher.serialize(sealed, DatabaseSnapshotVoucherPayload), - } satisfies Parameters["database-snapshot"]["query"]; - - const peeked = Voucher.peek(sealed); - - // `:name` is set so that name of downloaded file reflects the coordinator name - return new URL( - `${path.replace(":name", `${peeked.coordinator}.db`)}?${new URLSearchParams(query).toString()}`, - this.origin, - ); - } - url = { device: { self: this.urlDeviceSelf.bind(this), duplicates: this.urlDeviceDuplicates.bind(this), }, - databaseSnapshot: this.urlDatabaseSnapshotStale.bind(this), }; } diff --git a/project/server/src/service/derive/base.ts b/project/server/src/service/scheduler/base.ts similarity index 55% rename from project/server/src/service/derive/base.ts rename to project/server/src/service/scheduler/base.ts index 04244372..f0cf3cd1 100644 --- a/project/server/src/service/derive/base.ts +++ b/project/server/src/service/scheduler/base.ts @@ -1,10 +1,7 @@ import { createType } from "@lppedd/di-wise-neo"; -import type { DatabaseTransaction } from "../database"; -import type { DatabaseName } from "../database/base"; - -export interface DeriveDerivableInstance { - derive(t: DatabaseTransaction): Promise; +export interface SchedulerScheduledInstance { + run(): Promise; } type ZeroToSix = 0 | 1 | 2 | 3 | 4 | 5 | 6; @@ -24,7 +21,7 @@ type Day = | `*`; type Week = `${ZeroToSix}`; type Month = `${OneToNine}` | `1${ZeroToTwo}`; -export type DeriveSchedule = { +export type SchedulerSchedule = { minute?: Minute | `*` | `*/${Minute}`; hour?: Hour | `*` | `*/${Hour}`; day?: Day | `*` | `*/${Day}`; @@ -32,21 +29,20 @@ export type DeriveSchedule = { month?: Month | `*` | `*/${Month}`; }; -interface _DeriveDerivableClass { +interface _SchedulerScheduledClass { // biome-ignore lint/suspicious/noExplicitAny: can't constrain further - new (...args: any[]): DeriveDerivableInstance; + new (...args: any[]): SchedulerScheduledInstance; get id(): symbol; - schedule?: DeriveSchedule; + schedule?: SchedulerSchedule; - /* identifiers of derivables that should be satisfied before deriving */ + /* identifiers of scheduled units that should be run before running scheduled unit */ get prerequisites(): readonly symbol[]; } -export type DeriveDerivable< - DB extends DatabaseName | undefined, - _C extends _DeriveDerivableClass, -> = InstanceType<_DeriveDerivableClass>; +export type SchedulerScheduled<_C extends _SchedulerScheduledClass> = + InstanceType<_SchedulerScheduledClass>; -export const IDeriveDerivable = - createType>("IDeriveDerivable"); +export const ISchedulerScheduled = createType( + "ISchedulerScheduled", +); diff --git a/project/server/src/service/scheduler/index.test.ts b/project/server/src/service/scheduler/index.test.ts new file mode 100644 index 00000000..2386b4f2 --- /dev/null +++ b/project/server/src/service/scheduler/index.test.ts @@ -0,0 +1,376 @@ +import { type TestContext, test } from "node:test"; + +import { unroll } from "../../utility/iterable"; +import { testDatabase } from "../database/utility"; +import { StubIntrospection } from "../introspect/stub"; +import { Scheduler, SchedulerWaitLateError } from "."; + +import type { SchedulerScheduled } from "./base"; + +test("plan", (t: TestContext) => { + // unscheduled scheduled units are not included in plan unless they are a prerequisite of another scheduled unit + t.test("unscheduled", (t: TestContext) => { + class A implements SchedulerScheduled { + static id = Symbol("A"); + + static prerequisites = []; + + async run(): Promise {} + } + + class B implements SchedulerScheduled { + static id = Symbol("B"); + static schedule = { minute: "30" } as const; + + static prerequisites = [A.id]; + + async run(): Promise {} + } + + { + const scheduler = new Scheduler([new A()], new StubIntrospection()); + + const epoch = scheduler.next( + Scheduler.epoch(new Date("2026-03-03T17:29:00.000Z")), + ); + const plan = scheduler.plan(epoch); + t.assert.ok(Scheduler.viable(plan)); + t.assert.deepStrictEqual( + Scheduler.peek(plan).pending.map((item) => item.id), + [], + ); + } + + { + const scheduler = new Scheduler( + [new A(), new B()], + new StubIntrospection(), + ); + + const epoch = scheduler.next( + Scheduler.epoch(new Date("2026-03-03T17:29:00.000Z")), + ); + t.assert.deepStrictEqual(Scheduler.peek(epoch), { + next: new Date("2026-03-03T17:30:00.000Z"), + }); + const plan = scheduler.plan(epoch); + t.assert.ok(Scheduler.viable(plan)); + t.assert.deepStrictEqual( + Scheduler.peek(plan).pending.map((item) => item.id), + [A.id, B.id], + ); + } + }); + + t.test("satisfiable", (t: TestContext) => { + class A implements SchedulerScheduled { + static id = Symbol("A"); + + static prerequisites = []; + + async run(): Promise {} + } + + class B implements SchedulerScheduled { + static id = Symbol("B"); + static schedule = { minute: "30" } as const; + + static prerequisites = [A.id]; + + async run(): Promise {} + } + + class C implements SchedulerScheduled { + static id = Symbol("C"); + static schedule = {} as const; + + static prerequisites = [A.id]; + + async run(): Promise {} + } + + class D implements SchedulerScheduled { + static id = Symbol("D"); + static schedule = { minute: "*/2" } as const; + + static prerequisites = [C.id]; + + async run(): Promise {} + } + + const scheduler = new Scheduler( + [new A(), new B(), new C(), new D()], + new StubIntrospection(), + ); + + let epoch = scheduler.next( + Scheduler.epoch(new Date("2026-03-03T17:29:00.000Z")), + ); + t.assert.deepStrictEqual(Scheduler.peek(epoch), { + next: new Date("2026-03-03T17:30:00.000Z"), + }); + let plan = scheduler.plan(epoch); + t.assert.ok(Scheduler.viable(plan)); + t.assert.deepStrictEqual( + Scheduler.peek(plan).pending.map((item) => item.id), + [A.id, C.id, B.id, D.id], + ); + t.assert.deepStrictEqual( + Scheduler.peek(plan).reasons, + new Map([ + [A.id, new Set(["dependency"])], + [B.id, new Set(["schedule"])], + [C.id, new Set(["schedule", "dependency"])], + [D.id, new Set(["schedule"])], + ]), + ); + + epoch = scheduler.next(epoch); + t.assert.deepStrictEqual(Scheduler.peek(epoch), { + next: new Date("2026-03-03T17:31:00.000Z"), + }); + plan = scheduler.plan(epoch); + t.assert.ok(Scheduler.viable(plan)); + t.assert.deepStrictEqual( + Scheduler.peek(plan).pending.map((item) => item.id), + [A.id, C.id], + ); + t.assert.deepStrictEqual( + Scheduler.peek(plan).reasons, + new Map([ + [A.id, new Set(["dependency"])], + [C.id, new Set(["schedule"])], + ]), + ); + + epoch = scheduler.next(epoch); + t.assert.deepStrictEqual(Scheduler.peek(epoch), { + next: new Date("2026-03-03T17:32:00.000Z"), + }); + plan = scheduler.plan(epoch); + t.assert.ok(Scheduler.viable(plan)); + t.assert.deepStrictEqual( + Scheduler.peek(plan).pending.map((item) => item.id), + [A.id, C.id, D.id], + ); + t.assert.deepStrictEqual( + Scheduler.peek(plan).reasons, + new Map([ + [A.id, new Set(["dependency"])], + [C.id, new Set(["schedule", "dependency"])], + [D.id, new Set(["schedule"])], + ]), + ); + + epoch = scheduler.next(epoch); + t.assert.deepStrictEqual(Scheduler.peek(epoch), { + next: new Date("2026-03-03T17:33:00.000Z"), + }); + plan = scheduler.plan(epoch); + t.assert.ok(Scheduler.viable(plan)); + t.assert.deepStrictEqual( + Scheduler.peek(plan).pending.map((item) => item.id), + [A.id, C.id], + ); + t.assert.deepStrictEqual( + Scheduler.peek(plan).reasons, + new Map([ + [A.id, new Set(["dependency"])], + [C.id, new Set(["schedule"])], + ]), + ); + }); + + test("wait", async (t: TestContext) => { + t.test("not late", async (t: TestContext) => { + t.mock.timers.enable({ + apis: ["setTimeout", "Date"], + now: new Date("2026-03-03T17:29:00.000Z"), + }); + + class A implements SchedulerScheduled { + static id = Symbol("A"); + static schedule = {} as const; + + static prerequisites = []; + + async run(): Promise {} + } + + const scheduler = new Scheduler([new A()], new StubIntrospection()); + + const waiting = scheduler.wait(Scheduler.epoch(), { late: "throw" }); + t.mock.timers.tick(60_000); + t.assert.deepStrictEqual( + await Promise.race([waiting, "sentinel"]), + "sentinel", + ); + + const raced = await Promise.race([waiting, "sentinel"] as const); + t.assert.ok(raced !== "sentinel"); + t.assert.deepStrictEqual(Scheduler.peek(raced), { + next: new Date("2026-03-03T17:30:00.000Z"), + }); + }); + + t.test("late", async (t: TestContext) => { + t.mock.timers.enable({ + apis: ["setTimeout", "Date"], + now: new Date("2026-03-03T17:29:00.000Z"), + }); + + class A implements SchedulerScheduled { + static id = Symbol("A"); + static schedule = {} as const; + + static prerequisites = []; + + async run(): Promise {} + } + + const scheduler = new Scheduler([new A()], new StubIntrospection()); + + const epoch = Scheduler.epoch(); + t.mock.timers.tick(60_000); + await t.assert.rejects( + scheduler.wait(epoch, { late: "throw" }), + SchedulerWaitLateError, + ); + }); + }); + + t.test("missing prerequisite", (t: TestContext) => { + class A implements SchedulerScheduled { + static id = Symbol("A"); + static schedule = {} as const; + + static prerequisites = []; + + async run(): Promise {} + } + + class B implements SchedulerScheduled { + static id = Symbol("B"); + static schedule = {} as const; + + static prerequisites = [A.id]; + + async run(): Promise {} + } + + const scheduler = new Scheduler([new B()], new StubIntrospection()); + + const epoch = Scheduler.epoch(); + const next = scheduler.next(epoch); + const plan = scheduler.plan(next); + + t.assert.ok("kind" in plan && plan.kind === "missing-prerequisite"); + }); + + t.test("circular prerequisites", (t: TestContext) => { + const bId = Symbol("B"); + + class A implements SchedulerScheduled { + static id = Symbol("A"); + static schedule = {} as const; + + static prerequisites = [bId]; + + async run(): Promise {} + } + + class B implements SchedulerScheduled { + static id = bId; + static schedule = {} as const; + + static prerequisites = [A.id]; + + async run(): Promise {} + } + + const scheduler = new Scheduler( + [new A(), new B()], + new StubIntrospection(), + ); + + const epoch = Scheduler.epoch(); + const next = scheduler.next(epoch); + const plan = scheduler.plan(next); + t.assert.ok("kind" in plan && plan.kind === "circular-prerequisites"); + }); +}); + +test("act", async (t: TestContext) => { + await using db = await testDatabase(undefined, false); + db.raw.exec( + "create table a (value text primary key not null) strict, without rowid", + ); + db.raw.exec( + "create table b (value text primary key not null) strict, without rowid", + ); + + const runA = t.mock.fn<() => Promise>(async () => { + await db.begin("w", async (t) => { + await t.run({ + database: undefined, + name: "InsertA", + query: "insert into a values ('foo')", + connectionMode: "w", + parameters: [], + rowMode: "tuple", + resultMode: "none", + integerMode: "number", + }); + }); + }); + class A implements SchedulerScheduled { + static id = Symbol("A"); + static schedule = {} as const; + + static prerequisites = []; + + run = runA; + } + + const runB = t.mock.fn<() => Promise>(async () => { + await db.begin("w", async (t) => { + await t.run({ + database: undefined, + name: "InsertB", + query: "insert into b select value from a", + connectionMode: "w", + parameters: [], + rowMode: "tuple", + resultMode: "none", + integerMode: "number", + }); + }); + }); + class B implements SchedulerScheduled { + static id = Symbol("B"); + static schedule = {} as const; + + static prerequisites = [A.id]; + + run = runB; + } + + const scheduler = new Scheduler([new A(), new B()], new StubIntrospection()); + + const next = scheduler.next(Scheduler.epoch()); + const plan = scheduler.plan(next); + t.assert.ok(Scheduler.viable(plan)); + + t.assert.partialDeepStrictEqual(await unroll(scheduler.act(plan)), [ + { id: A.id }, + { id: B.id }, + ]); + + t.assert.deepStrictEqual(runA.mock.callCount(), 1); + t.assert.deepStrictEqual(runB.mock.callCount(), 1); + + t.assert.deepStrictEqual( + [...db.raw.query("select value from b", { returnArray: true }, {})], + [["foo"]], + ); +}); diff --git a/project/server/src/service/derive/index.ts b/project/server/src/service/scheduler/index.ts similarity index 53% rename from project/server/src/service/derive/index.ts rename to project/server/src/service/scheduler/index.ts index 6dbe4221..f5aa839d 100644 --- a/project/server/src/service/derive/index.ts +++ b/project/server/src/service/scheduler/index.ts @@ -9,155 +9,143 @@ import { injectOrStub } from "../../utility/dependency-injection"; import { IIntrospection } from "../introspect"; import { StubIntrospection } from "../introspect/stub"; -import type { DatabaseTransaction, IDatabase } from "../database"; -import type { DatabaseName } from "../database/base"; -import type { DeriveDerivableInstance, DeriveSchedule } from "./base"; - -type DeriveDeriveActPending = { kind: "pending"; id: symbol }; -type DeriveDeriveActSuccess = { kind: "success"; id: symbol; took: bigint }; -type DeriveDeriveActError = { kind: "error"; id: symbol; error: unknown }; -type DeriveDeriveActStatus = - | DeriveDeriveActPending - | DeriveDeriveActSuccess - | DeriveDeriveActError; - -const DeriveEpochSymbol = Symbol("DeriveEpochSymbol"); -const DerivePlanSymbol = Symbol("DerivePlanSymbol"); - -export class DeriveWaitLateError extends Error { +import type { SchedulerSchedule, SchedulerScheduledInstance } from "./base"; + +type SchedulerActPending = { kind: "pending"; id: symbol }; +type SchedulerActSuccess = { kind: "success"; id: symbol; took: bigint }; +type SchedulerActError = { kind: "error"; id: symbol; error: unknown }; +type SchedulerActStatus = + | SchedulerActPending + | SchedulerActSuccess + | SchedulerActError; + +const SchedulerEpochSymbol = Symbol("SchedulerEpochSymbol"); +const SchedulerPlanSymbol = Symbol("SchedulerPlanSymbol"); + +export class SchedulerWaitLateError extends Error { constructor( public desired: Date, public now: Date, ) { super(`late for slot at <${desired}>, is <${now}>`); - Object.setPrototypeOf(this, DeriveWaitLateError.prototype); + Object.setPrototypeOf(this, SchedulerWaitLateError.prototype); } } -type DeriveEpochInner = { +type SchedulerEpochInner = { next: Date; }; -export type DeriveEpoch = { - [DeriveEpochSymbol]: DeriveEpochInner; +export type SchedulerEpoch = { + [SchedulerEpochSymbol]: SchedulerEpochInner; }; -type DerivePlanUnachievableCircularPrerequisites = { +type SchedulerPlanUnachievableCircularPrerequisites = { kind: "circular-prerequisites"; }; -type DerivePlanUnachievableMissingPrerequisite = { +type SchedulerPlanUnachievableMissingPrerequisite = { kind: "missing-prerequisite"; id: symbol; }; -type DerivePlanUnachievable = - | DerivePlanUnachievableCircularPrerequisites - | DerivePlanUnachievableMissingPrerequisite; +type SchedulerPlanUnachievable = + | SchedulerPlanUnachievableCircularPrerequisites + | SchedulerPlanUnachievableMissingPrerequisite; -type Derivable = { +type Scheduled = { id: symbol; - schedule?: DeriveSchedule | undefined; - derive: (t: DatabaseTransaction) => Promise; + schedule?: SchedulerSchedule | undefined; + run: () => Promise; }; -type DerivePlanStrategyInnerReason = "schedule" | "dependency"; -type DerivePlanStrategyInner = { - pending: readonly Derivable[]; - reasons: ReadonlyMap>; +type SchedulerPlanStrategyInnerReason = "schedule" | "dependency"; +type SchedulerPlanStrategyInner = { + pending: readonly Scheduled[]; + reasons: ReadonlyMap>; }; -type DerivePlanStrategy = { - [DerivePlanSymbol]: DerivePlanStrategyInner; +type SchedulerPlanStrategy = { + [SchedulerPlanSymbol]: SchedulerPlanStrategyInner; }; -type DerivePlan = - | DerivePlanStrategy - | DerivePlanUnachievable; +type SchedulerPlan = SchedulerPlanStrategy | SchedulerPlanUnachievable; -export class DeriveNoDerivablesError extends Error { +export class SchedulerNoScheduledError extends Error { constructor() { - super("no derivables provided"); - Object.setPrototypeOf(this, DeriveNoDerivablesError.prototype); + super("no scheduled units provided"); + Object.setPrototypeOf(this, SchedulerNoScheduledError.prototype); } } -export type IDerive = { +export type IScheduler = { /** waits until earliest scheduled execution and returns said execution */ wait( - epoch: DeriveEpoch, + epoch: SchedulerEpoch, options?: | { signal?: AbortSignal | undefined; late?: "throw" | undefined; } | undefined, - ): Promise; + ): Promise; /** returns earliest scheduled execution */ - next(epoch: DeriveEpoch): DeriveEpoch; - plan(epoch: DeriveEpoch): DerivePlan; - plan(id: symbol): DerivePlan; - act(strategy: DerivePlanStrategy): AsyncIterable; + next(epoch: SchedulerEpoch): SchedulerEpoch; + plan(epoch: SchedulerEpoch): SchedulerPlan; + plan(id: symbol): SchedulerPlan; + act(strategy: SchedulerPlanStrategy): AsyncIterable; }; -const logger = parentLogger.child({ label: "derive" }); +const logger = parentLogger.child({ label: "scheduler" }); -export const IDeriveDerived = createType>("IDeriveDerived"); +export const IScheduler = createType("IScheduler"); const metrics = (introspection: IIntrospection) => ({ runs: introspection.metric.counter({ - name: "derivable_runs_total", - help: "amount of derivable runs", + name: "scheduler_runs_total", + help: "amount of scheduled unit runs", labelNames: ["id", "result"], registry: "local", }), runDuration: introspection.metric.histogram({ - name: "derivable_run_duration_seconds", - help: "execution time of derivable", + name: "scheduler_run_duration_seconds", + help: "execution time of scheduled units", labelNames: ["id"], buckets: [1, 2.5, 5, 7.5, 10, 30, 60, 120, 240], registry: "local", }), }) as const; -export class Derive - implements IDerive -{ - private identified: Map> = new Map(); +export class Scheduler implements IScheduler { + private identified: Map = new Map(); // child → parents private prerequisites: Map = new Map(); private metrics: ReturnType; constructor( - private database: IDatabase, - derivables: DeriveDerivableInstance[], + scheduled: readonly SchedulerScheduledInstance[], introspect = injectOrStub(IIntrospection, () => new StubIntrospection()), ) { - outer: for (const derivable of derivables) { - if ( - !( - "id" in derivable.constructor && - typeof derivable.constructor.id === "symbol" - ) - ) { + outer: for (const s of scheduled) { + if (!("id" in s.constructor && typeof s.constructor.id === "symbol")) { logger.error( - `malformed derivable <${derivable.constructor.name}>, missing #id`, + `malformed scheduled unit <${s.constructor.name}>, missing #id`, { - name: derivable.constructor.name, + name: s.constructor.name, }, ); continue; } if ( - "schedule" in derivable.constructor && - typeof derivable.constructor.schedule !== "object" + "schedule" in s.constructor && + typeof s.constructor.schedule !== "object" ) { logger.error( - `malformed derivable <${derivable.constructor.name}>, misshapen #schedule`, + `malformed scheduled unit <${s.constructor.name}>, misshapen #schedule`, { - name: derivable.constructor.name, - description: derivable.constructor.id.description, + name: s.constructor.name, + description: s.constructor.id.description, }, ); continue; @@ -166,26 +154,26 @@ export class Derive const _prerequisites: symbol[] = []; if ( !( - "prerequisites" in derivable.constructor && - Array.isArray(derivable.constructor.prerequisites) + "prerequisites" in s.constructor && + Array.isArray(s.constructor.prerequisites) ) ) { logger.error( - `malformed derivable <${derivable.constructor.name}>, missing #prerequisites`, + `malformed scheduled unit <${s.constructor.name}>, missing #prerequisites`, { - name: derivable.constructor.name, - description: derivable.constructor.id.description, + name: s.constructor.name, + description: s.constructor.id.description, }, ); continue; } - for (const prerequisite of derivable.constructor.prerequisites) { + for (const prerequisite of s.constructor.prerequisites) { if (typeof prerequisite !== "symbol") { logger.error( - `malformed derivable <${derivable.constructor.name}>, #prerequisites should be symbols`, + `malformed scheduled unit <${s.constructor.name}>, #prerequisites should be symbols`, { - name: derivable.constructor.name, - description: derivable.constructor.id.description, + name: s.constructor.name, + description: s.constructor.id.description, }, ); continue outer; @@ -194,50 +182,48 @@ export class Derive } } - this.identified.set(derivable.constructor.id, { - id: derivable.constructor.id, + this.identified.set(s.constructor.id, { + id: s.constructor.id, schedule: - "schedule" in derivable.constructor - ? (derivable.constructor.schedule as DeriveSchedule) + "schedule" in s.constructor + ? (s.constructor.schedule as SchedulerSchedule) : undefined, - derive: derivable.derive.bind(derivable), + run: s.run.bind(s), }); - this.prerequisites.set(derivable.constructor.id, _prerequisites); + this.prerequisites.set(s.constructor.id, _prerequisites); } if (this.identified.size === 0) { - throw new DeriveNoDerivablesError(); + throw new SchedulerNoScheduledError(); } this.metrics = metrics(introspect); } - public static viable( - plan: DerivePlan, - ): plan is DerivePlanStrategy { - return DerivePlanSymbol in plan; + public static viable(plan: SchedulerPlan): plan is SchedulerPlanStrategy { + return SchedulerPlanSymbol in plan; } - public static peek(epoch: DeriveEpoch): DeriveEpochInner; + public static peek(epoch: SchedulerEpoch): SchedulerEpochInner; public static peek( - strategy: DerivePlanStrategy, - ): DerivePlanStrategyInner; + strategy: SchedulerPlanStrategy, + ): SchedulerPlanStrategyInner; public static peek( - arg0: DeriveEpoch | DerivePlanStrategy, - ): DeriveEpochInner | DerivePlanStrategyInner { - if (DeriveEpochSymbol in arg0) { - return arg0[DeriveEpochSymbol]; + arg0: SchedulerEpoch | SchedulerPlanStrategy, + ): SchedulerEpochInner | SchedulerPlanStrategyInner { + if (SchedulerEpochSymbol in arg0) { + return arg0[SchedulerEpochSymbol]; } - return arg0[DerivePlanSymbol]; + return arg0[SchedulerPlanSymbol]; } - public static epoch(now?: Date): DeriveEpoch { - return { [DeriveEpochSymbol]: { next: now ?? new Date() } }; + public static epoch(now?: Date): SchedulerEpoch { + return { [SchedulerEpochSymbol]: { next: now ?? new Date() } }; } private static parseSchedule( - schedule: DeriveSchedule, + schedule: SchedulerSchedule, now?: Date, ): CronExpression { return CronExpressionParser.parse( @@ -246,8 +232,8 @@ export class Derive ); } - private pending(schedule: DeriveSchedule, now: Date): boolean { - const parsed = Derive.parseSchedule(schedule, now); + private pending(schedule: SchedulerSchedule, now: Date): boolean { + const parsed = Scheduler.parseSchedule(schedule, now); // obtain current slot by progressing once, and then going back parsed.next(); @@ -257,24 +243,24 @@ export class Derive } public async wait( - epoch: DeriveEpoch, + epoch: SchedulerEpoch, options?: | { signal?: AbortSignal | undefined; late?: "throw" | undefined; } | undefined, - ): Promise { + ): Promise { const next = this.next(epoch); - const peeked = Derive.peek(next); + const peeked = Scheduler.peek(next); - const waited = new Promise((resolve) => { + const waited = new Promise((resolve) => { const now = new Date(); const delay = peeked.next.getTime() - now.getTime(); if (delay <= 0) { switch (options?.late) { case "throw": - throw new DeriveWaitLateError(peeked.next, now); + throw new SchedulerWaitLateError(peeked.next, now); case undefined: logger.warn("late for slot", { desired: peeked.next, now }); break; @@ -285,7 +271,7 @@ export class Derive setTimeout(() => resolve(next), delay); } }); - const aborted = new Promise((_, reject) => + const aborted = new Promise((_, reject) => options?.signal?.addEventListener("abort", () => reject(options?.signal?.reason), ), @@ -294,17 +280,17 @@ export class Derive return await Promise.race([waited, aborted]); } - public next(epoch: DeriveEpoch): DeriveEpoch { - const peeked = Derive.peek(epoch); + public next(epoch: SchedulerEpoch): SchedulerEpoch { + const peeked = Scheduler.peek(epoch); let next: Date | undefined; for (const { schedule } of this.identified.values()) { - // encountered derivable that doesn't have a set schedule + // encountered scheduled unit that doesn't have a set schedule if (typeof schedule === "undefined") { continue; } - const parsed = Derive.parseSchedule(schedule, peeked.next); + const parsed = Scheduler.parseSchedule(schedule, peeked.next); const date = parsed.next().toDate(); if (typeof next === "undefined" || date < next) { @@ -313,23 +299,23 @@ export class Derive } return { - [DeriveEpochSymbol]: { + [SchedulerEpochSymbol]: { // biome-ignore lint/style/noNonNullAssertion: constructor enforces that `identified` has at least one element next: next!, }, }; } - private ordered(candidates: Map>) { + private ordered(candidates: Map) { const discovered = new Set(); // cycle detection const visiting = new Set(); - const ordered: Derivable[] = []; + const ordered: Scheduled[] = []; // https://en.wikipedia.org/wiki/Depth-first_search - const visit = (identifier: symbol): Maybe => { - const derivable = candidates.get(identifier); - if (typeof derivable === "undefined") { + const visit = (identifier: symbol): Maybe => { + const scheduled = candidates.get(identifier); + if (typeof scheduled === "undefined") { return null; } @@ -357,7 +343,7 @@ export class Derive visiting.delete(identifier); discovered.add(identifier); - ordered.push(derivable); + ordered.push(scheduled); return null; }; @@ -372,19 +358,22 @@ export class Derive return ordered; } - private planByIdentifier(id: symbol): DerivePlan { + private planByIdentifier(id: symbol): SchedulerPlan { const target = this.identified.get(id); if (typeof target === "undefined") { return { kind: "missing-prerequisite", id }; } - const candidates: Map> = new Map(); - const reasons: Map> = new Map(); + const candidates: Map = new Map(); + const reasons: Map< + symbol, + Set + > = new Map(); candidates.set(id, target); reasons.set(id, new Set(["schedule"])); - const visit = (identifier: symbol): Maybe => { + const visit = (identifier: symbol): Maybe => { const parents = this.prerequisites.get(identifier) ?? []; for (const parentId of parents) { if (candidates.has(parentId)) { @@ -419,19 +408,22 @@ export class Derive } return { - [DerivePlanSymbol]: { pending, reasons }, + [SchedulerPlanSymbol]: { pending, reasons }, }; } - private planByEpoch(epoch: DeriveEpoch): DerivePlan { - const { next } = Derive.peek(epoch); + private planByEpoch(epoch: SchedulerEpoch): SchedulerPlan { + const { next } = Scheduler.peek(epoch); - // derivables that need to run due to their own schedule, or due to schedule of - // other derivables that list them as prerequisites - const candidates: Map> = new Map(); - const reasons: Map> = new Map(); + // scheduled units that need to run due to their own schedule, or due to schedule of + // other scheduled units that list them as prerequisites + const candidates: Map = new Map(); + const reasons: Map< + symbol, + Set + > = new Map(); { - // derivable → derivables listing that derivable as prerequisite + // scheduled unit → scheduled units listing that scheduled unit as prerequisite const dependencies: Map = new Map(); for (const childIdentifier of this.identified.keys()) { const parentIdentifiers = this.prerequisites.get(childIdentifier); @@ -509,8 +501,8 @@ export class Derive } } - // handle derivables that are neither listed as prerequisites, nor have prerequisites themselves - for (const [identifier, derivable] of this.identified) { + // handle scheduled units that are neither listed as prerequisites, nor have prerequisites themselves + for (const [identifier, scheduled] of this.identified) { if (dependencies.has(identifier)) { continue; } @@ -519,14 +511,14 @@ export class Derive continue; } - if (typeof derivable.schedule === "undefined") { + if (typeof scheduled.schedule === "undefined") { continue; } - if (!this.pending(derivable.schedule, next)) { + if (!this.pending(scheduled.schedule, next)) { continue; } - candidates.set(identifier, derivable); + candidates.set(identifier, scheduled); reasons.set(identifier, new Set(["schedule"])); } } @@ -537,12 +529,12 @@ export class Derive } return { - [DerivePlanSymbol]: { pending, reasons }, + [SchedulerPlanSymbol]: { pending, reasons }, }; } - public plan(epoch: DeriveEpoch): DerivePlan; - public plan(id: symbol): DerivePlan; - public plan(arg: DeriveEpoch | symbol): DerivePlan { + public plan(epoch: SchedulerEpoch): SchedulerPlan; + public plan(id: symbol): SchedulerPlan; + public plan(arg: SchedulerEpoch | symbol): SchedulerPlan { if (typeof arg === "symbol") { return this.planByIdentifier(arg); } @@ -551,26 +543,26 @@ export class Derive } async *act( - strategy: DerivePlanStrategy, - ): AsyncIterable { - const peeked = strategy[DerivePlanSymbol]; - for (const derivable of peeked.pending) { - yield { kind: "pending", id: derivable.id }; + strategy: SchedulerPlanStrategy, + ): AsyncIterable { + const peeked = strategy[SchedulerPlanSymbol]; + for (const scheduled of peeked.pending) { + yield { kind: "pending", id: scheduled.id }; // description is used as external identifier - const description = derivable.id.description; + const description = scheduled.id.description; if (typeof description === "undefined") { - logger.warn(`undefined description for derivable <${derivable}>`, { - derivable, + logger.warn(`undefined description for scheduled unit <${scheduled}>`, { + scheduled, }); } try { const start = hrtime.bigint(); - await this.database.begin("w", derivable.derive); + await scheduled.run(); const end = hrtime.bigint(); - yield { kind: "success", id: derivable.id, took: end - start }; + yield { kind: "success", id: scheduled.id, took: end - start }; if (typeof description !== "undefined") { this.metrics.runs.increment({ id: description, result: "success" }); @@ -580,7 +572,7 @@ export class Derive ); } } catch (error) { - yield { kind: "error", id: derivable.id, error }; + yield { kind: "error", id: scheduled.id, error }; if (typeof description !== "undefined") { this.metrics.runs.increment({ id: description, result: "failure" }); diff --git a/project/server/src/service/derive/derivable/device/index.ts b/project/server/src/service/scheduler/scheduled/derive/device/index.ts similarity index 85% rename from project/server/src/service/derive/derivable/device/index.ts rename to project/server/src/service/scheduler/scheduled/derive/device/index.ts index 9440710a..fffcf865 100644 --- a/project/server/src/service/derive/derivable/device/index.ts +++ b/project/server/src/service/scheduler/scheduled/derive/device/index.ts @@ -3,31 +3,27 @@ import { Schema } from "effect"; import { isLeft } from "effect/Either"; import { parseJson } from "effect/Schema"; -import { Category } from "../../../../categories"; -import categories from "../../../../categories.json" with { type: "json" }; -import categorizedIntegrations from "../../../../categorized-integrations.json" with { +import { Category } from "../../../../../categories"; +import categories from "../../../../../categories.json" with { type: "json" }; +import categorizedIntegrations from "../../../../../categorized-integrations.json" with { type: "json", }; -import { DateFromUnixTime } from "../../../../type/codec/date"; -import { floor, Integer } from "../../../../type/codec/integer"; -import { Uuid } from "../../../../type/codec/uuid"; -import { isNone, isSome, type Maybe } from "../../../../type/maybe"; -import { - counted, - type DatabaseTransaction, - IDatabaseDerived, -} from "../../../database"; -import { deleteDerivedDevices } from "../../../database/query/derived/device-delete"; +import { DateFromUnixTime } from "../../../../../type/codec/date"; +import { floor, Integer } from "../../../../../type/codec/integer"; +import { Uuid } from "../../../../../type/codec/uuid"; +import { isNone, isSome, type Maybe } from "../../../../../type/maybe"; +import { counted, IDatabaseDerived } from "../../../../database"; +import { deleteDerivedDevices } from "../../../../database/query/derived/device-delete"; import { getDerivedDevice, getDerivedDevices, getDerivedDevicesFiltersCounted, -} from "../../../database/query/derived/device-get"; -import { insertDerivedDevices } from "../../../database/query/derived/device-insert"; -import { DeriveDerivableSubject } from "../subject"; +} from "../../../../database/query/derived/device-get"; +import { insertDerivedDevices } from "../../../../database/query/derived/device-insert"; +import { SchedulerScheduledDeriveSubject } from "../subject"; import { alias, literal, pattern } from "./rules"; -import type { DeriveDerivable } from "../../base"; +import type { SchedulerScheduled } from "../../../base"; type DeviceModel = | { model: string; modelId: string } @@ -56,7 +52,7 @@ export type DeviceConnectivityValue = typeof DeviceConnectivityValue.Type; const isDeviceConnectivityValue = Schema.is(DeviceConnectivityValue); -export type DerivableDeviceMono = { +export type SchedulerScheduledDeriveDeviceDeviceMono = { integration: string; manufacturer: string; categories?: DeviceCategory[] | undefined; @@ -78,7 +74,7 @@ export type DerivableDeviceMono = { duplicates: Uuid[]; } & DeviceModel; -type PolyDevice = DerivableDeviceMono & { +type PolyDevice = SchedulerScheduledDeriveDeviceDeviceMono & { id: Uuid; }; @@ -176,7 +172,7 @@ type Filters = { connectivity: Partial>; }; -export interface IDeriveDerivableDevice { +export interface ISchedulerScheduledDeriveDevice { devices: { slice: ( query: QueryPolyDevice, @@ -190,7 +186,9 @@ export interface IDeriveDerivableDevice { ) => AsyncIterable; count(query: QueryPolyDevice): Promise; }; - device(query: QueryMonoDevice): Promise>; + device( + query: QueryMonoDevice, + ): Promise>; filters(query: QueryPolyDevice): Promise; } @@ -342,18 +340,19 @@ for (const [integration, manifest] of Object.entries(categorizedIntegrations)) { } } -export const IDeriveDerivableDevice = createType( - "IDeriveDerivableDevice", -); +export const ISchedulerScheduledDeriveDevice = + createType( + "ISchedulerScheduledDeriveDevice", + ); -export class DeriveDerivableDevice +export class SchedulerScheduledDeriveDevice implements - DeriveDerivable<"derived", typeof DeriveDerivableDevice>, - IDeriveDerivableDevice + SchedulerScheduled, + ISchedulerScheduledDeriveDevice { - static readonly id = Symbol("DeriveDerivableDevice"); + static readonly id = Symbol("SchedulerScheduledDevice"); - static readonly prerequisites = [DeriveDerivableSubject.id]; + static readonly prerequisites = [SchedulerScheduledDeriveSubject.id]; static readonly schedule = { minute: "0", hour: "0", @@ -361,18 +360,20 @@ export class DeriveDerivableDevice constructor(private db = inject(IDatabaseDerived)) {} - async derive(t: DatabaseTransaction<"derived", "w">): Promise { - await t.run(deleteDerivedDevices.bind.anonymous([])); - await t.run( - insertDerivedDevices.bind.named({ - ruleLiteralIntegration: JSON.stringify(literal.integration), - ruleLiteralManufacturer: JSON.stringify(literal.manufacturer), - ruleLiteralModel: JSON.stringify(literal.model), - rulePatternManufacturer: JSON.stringify(pattern.manufacturer), - rulePatternModel: JSON.stringify(pattern.model), - ruleAliasManufacturer: JSON.stringify(alias.manufacturer), - }), - ); + async run(): Promise { + await this.db.begin("w", async (t) => { + await t.run(deleteDerivedDevices.bind.anonymous([])); + await t.run( + insertDerivedDevices.bind.named({ + ruleLiteralIntegration: JSON.stringify(literal.integration), + ruleLiteralManufacturer: JSON.stringify(literal.manufacturer), + ruleLiteralModel: JSON.stringify(literal.model), + rulePatternManufacturer: JSON.stringify(pattern.manufacturer), + rulePatternModel: JSON.stringify(pattern.model), + ruleAliasManufacturer: JSON.stringify(alias.manufacturer), + }), + ); + }); } private static decoderDevice = Schema.decodeUnknownEither(DeviceCodec); @@ -438,13 +439,13 @@ export class DeriveDerivableDevice exclude, }: QueryPolyDevice) { const includeIntegrations = - DeriveDerivableDevice.queryParameterIntegrations( + SchedulerScheduledDeriveDevice.queryParameterIntegrations( include?.categories, include?.connectivities, ); const excludeIntegrations = - DeriveDerivableDevice.queryParameterIntegrations( + SchedulerScheduledDeriveDevice.queryParameterIntegrations( exclude?.categories, exclude?.connectivities, ); @@ -491,13 +492,13 @@ export class DeriveDerivableDevice }, ): AsyncIterable { const bound = getDerivedDevices.bind.named({ - ...DeriveDerivableDevice.queryParameters(query), + ...SchedulerScheduledDeriveDevice.queryParameters(query), offset, limit, }); for await (const device of this.db.run(bound)) { - const decoded = DeriveDerivableDevice.decoderDevice(device); + const decoded = SchedulerScheduledDeriveDevice.decoderDevice(device); if (isLeft(decoded)) { continue; } @@ -548,7 +549,7 @@ export class DeriveDerivableDevice private async devicesCount(query: QueryPolyDevice): Promise { const bound = counted(getDerivedDevices).bind.named( { - ...DeriveDerivableDevice.queryParameters(query), + ...SchedulerScheduledDeriveDevice.queryParameters(query), offset: null, limit: null, }, @@ -565,9 +566,11 @@ export class DeriveDerivableDevice count: this.devicesCount.bind(this), }; - async device(query: QueryMonoDevice): Promise> { + async device( + query: QueryMonoDevice, + ): Promise> { const device = await this.db.run(getDerivedDevice.bind.named(query)); - const decoded = DeriveDerivableDevice.decoderDevice(device); + const decoded = SchedulerScheduledDeriveDevice.decoderDevice(device); if (isLeft(decoded)) { return null; } @@ -614,7 +617,7 @@ export class DeriveDerivableDevice public async filters(query: QueryPolyDevice): Promise { const bound = getDerivedDevicesFiltersCounted.bind.named({ - ...DeriveDerivableDevice.queryParameters(query), + ...SchedulerScheduledDeriveDevice.queryParameters(query), }); const countedManufacturer: Map = new Map(); diff --git a/project/server/src/service/derive/derivable/device/rules.ts b/project/server/src/service/scheduler/scheduled/derive/device/rules.ts similarity index 100% rename from project/server/src/service/derive/derivable/device/rules.ts rename to project/server/src/service/scheduler/scheduled/derive/device/rules.ts diff --git a/project/server/src/service/scheduler/scheduled/derive/meta.ts b/project/server/src/service/scheduler/scheduled/derive/meta.ts new file mode 100644 index 00000000..b298a20f --- /dev/null +++ b/project/server/src/service/scheduler/scheduled/derive/meta.ts @@ -0,0 +1,59 @@ +import { inject } from "@lppedd/di-wise-neo"; + +import { IDatabaseDerived } from "../../../database"; +import { deleteDerivedMetaEntityStats } from "../../../database/query/derived/meta-delete"; +import { getDerivedMetaEntityStats } from "../../../database/query/derived/meta-get"; +import { IIntrospection } from "../../../introspect"; + +import type { SchedulerScheduled } from "../../base"; + +export class SchedulerScheduledDeriveMetaEntityStat + implements SchedulerScheduled +{ + static readonly id = Symbol("SchedulerScheduledDeriveMetaEntityStat"); + + static readonly prerequisites = []; + static readonly schedule = { + minute: "*/5", + } as const; + + constructor( + private db = inject(IDatabaseDerived), + introspection: IIntrospection = inject(IIntrospection), + ) { + introspection.metric.gauge( + { + name: "database_staging_size_total", + help: "size of database", + labelNames: ["entity"], + registry: "global", + }, + async (collector) => { + const bound = getDerivedMetaEntityStats.bind.anonymous([], { + rowMode: "tuple", + }); + + for await (const row of this.db.run(bound, "background")) { + collector.set({ entity: row[0] }, row[1]); + } + }, + ); + } + + async run(): Promise { + await this.db.begin("w", async (t) => { + await t.run(deleteDerivedMetaEntityStats.bind.anonymous([])); + await t.run({ + database: "derived", + name: "InsertDeriveMetaEntityStat", + query: `insert into derived_meta_entity_stat + select name, pgsize from dbstat where aggregate = true and schema = 'staging'`, + parameters: [], + connectionMode: "w", + resultMode: "none", + rowMode: "tuple", + integerMode: "number", + }); + }); + } +} diff --git a/project/server/src/service/scheduler/scheduled/derive/subject.ts b/project/server/src/service/scheduler/scheduled/derive/subject.ts new file mode 100644 index 00000000..09745862 --- /dev/null +++ b/project/server/src/service/scheduler/scheduled/derive/subject.ts @@ -0,0 +1,24 @@ +import { inject } from "@lppedd/di-wise-neo"; + +import { IDatabaseDerived } from "../../../database"; +import { deleteDerivedSubjects } from "../../../database/query/derived/subject-delete"; +import { insertDerivedSubjects } from "../../../database/query/derived/subject-insert"; + +import type { SchedulerScheduled } from "../../base"; + +export class SchedulerScheduledDeriveSubject + implements SchedulerScheduled +{ + static readonly id = Symbol("SchedulerScheduledDeriveSubject"); + + static readonly prerequisites = []; + + constructor(private db = inject(IDatabaseDerived)) {} + + async run(): Promise { + await this.db.begin("w", async (t) => { + await t.run(deleteDerivedSubjects.bind.anonymous([])); + await t.run(insertDerivedSubjects.bind.named({ window: 60 * 60 * 25 })); + }); + } +} diff --git a/project/server/src/service/scheduler/scheduled/derive/submission.ts b/project/server/src/service/scheduler/scheduled/derive/submission.ts new file mode 100644 index 00000000..97c160c4 --- /dev/null +++ b/project/server/src/service/scheduler/scheduled/derive/submission.ts @@ -0,0 +1,45 @@ +import { inject } from "@lppedd/di-wise-neo"; + +import { IDatabaseDerived } from "../../../database"; +import { deleteDerivedSubmissions } from "../../../database/query/derived/submission-delete"; +import { getDerivedSubmissions } from "../../../database/query/derived/submission-get"; +import { insertDerivedSubmission } from "../../../database/query/derived/submission-insert"; +import { IIntrospection } from "../../../introspect"; + +import type { SchedulerScheduled } from "../../base"; + +export class SchedulerScheduledDeriveSubmissionFaulty + implements SchedulerScheduled +{ + static readonly id = Symbol("SchedulerScheduledDeriveSubmissionFaulty"); + + static readonly prerequisites = []; + + constructor( + private db = inject(IDatabaseDerived), + introspection: IIntrospection = inject(IIntrospection), + ) { + introspection.metric.gauge( + { + name: "snapshot_faulty_submissions_total", + help: "amount of faulty submissions", + labelNames: ["state"], + registry: "global", + }, + async (collector) => { + const bound = getDerivedSubmissions.bind.anonymous([]); + + for await (const row of this.db.run(bound)) { + collector.set({ state: row.state }, row.count); + } + }, + ); + } + + async run(): Promise { + await this.db.begin("w", async (t) => { + await t.run(deleteDerivedSubmissions.bind.anonymous([])); + await t.run(insertDerivedSubmission.bind.anonymous([])); + }); + } +} diff --git a/project/server/src/service/signal/base.ts b/project/server/src/service/signal/base.ts deleted file mode 100644 index 32ddd54a..00000000 --- a/project/server/src/service/signal/base.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createType } from "@lppedd/di-wise-neo"; - -export type EventSubmission = { - kind: "no-op"; -}; - -export type Event = EventSubmission; -export type EventKind = Event["kind"]; - -export type ISignalProvider = { - send(event: Event): Promise; - supported(event: Event): boolean; -}; - -export const ISignalProvider = createType("ISignalProvider"); diff --git a/project/server/src/service/signal/index.test.ts b/project/server/src/service/signal/index.test.ts deleted file mode 100644 index 86a3f320..00000000 --- a/project/server/src/service/signal/index.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { type TestContext, test } from "node:test"; - -import { Signal } from "."; - -import type { Event, ISignalProvider } from "./base"; - -test("send", async (t: TestContext) => { - class Provider implements ISignalProvider { - constructor(private isSupported: boolean) {} - - async send(event: Event): Promise {} - supported(event: Event): boolean { - return this.isSupported; - } - } - - const p0 = new Provider(true); - const p1 = new Provider(false); - - const m0 = t.mock.method(p0, "send", async () => {}); - const m1 = t.mock.method(p1, "send", async () => {}); - - const signal = new Signal([p0, p1]); - - await signal.send({ - kind: "no-op", - }); - - t.assert.strictEqual(m0.mock.callCount(), 1); - t.assert.strictEqual(m1.mock.callCount(), 0); -}); diff --git a/project/server/src/service/signal/index.ts b/project/server/src/service/signal/index.ts deleted file mode 100644 index d413b4ca..00000000 --- a/project/server/src/service/signal/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createType, injectAll } from "@lppedd/di-wise-neo"; - -import { type Event, ISignalProvider } from "./base"; - -export const ISignal = createType("ISignal"); - -export interface ISignal { - send(event: Event): Promise; -} - -export class Signal implements ISignal { - constructor(private providers = injectAll(ISignalProvider)) {} - - async send(event: Event): Promise { - for (const provider of this.providers) { - if (!provider.supported(event)) { - continue; - } - - await provider.send(event); - } - } -} diff --git a/project/server/src/service/signal/provider/slack/index.ts b/project/server/src/service/signal/provider/slack/index.ts deleted file mode 100644 index 4ef430b7..00000000 --- a/project/server/src/service/signal/provider/slack/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { inject } from "@lppedd/di-wise-neo"; - -import { ConfigProvider } from "../../../../config"; - -import type { Event, ISignalProvider } from "../../base"; - -const supported = [] as const; -type SupportedEventKind = (typeof supported)[number]; - -// biome-ignore lint/correctness/noUnusedVariables: will be used again -type SupportedEvent = Extract< - Event, - // biome-ignore lint/suspicious/noExplicitAny: distributive union - SupportedEventKind extends any ? { kind: SupportedEventKind } : never ->; - -type WebhookUrl = { [K in SupportedEventKind]: string | undefined }; - -export interface ISignalProviderSlack extends ISignalProvider {} - -export const templateMarkdown = (markdown: string) => - ({ - blocks: [ - { - type: "section", - text: { - type: "mrkdwn", - text: markdown, - }, - }, - ], - }) as const; - -export class SignalProviderSlack implements ISignalProviderSlack { - constructor( - private webhookUrl: WebhookUrl = inject(ConfigProvider)(() => ({})), - ) {} - - async send(): Promise { - return; - } - - supported(event: Event): boolean { - return (Object.keys(this.webhookUrl) as readonly string[]).includes( - event.kind, - ); - } -} diff --git a/project/server/src/service/snapshot/defer/ingest.ts b/project/server/src/service/snapshot/defer/ingest.ts index 3bdcd3cc..1dd55d76 100644 --- a/project/server/src/service/snapshot/defer/ingest.ts +++ b/project/server/src/service/snapshot/defer/ingest.ts @@ -1,4 +1,4 @@ -import { createType, inject, optional } from "@lppedd/di-wise-neo"; +import { createType, inject } from "@lppedd/di-wise-neo"; import { logger as parentLogger } from "../../../logger"; import { isNone, isSome } from "../../../type/maybe"; @@ -44,7 +44,11 @@ export class SnapshotDeferIngest constructor( private snapshot = inject(ISnapshot), - private snapshotDeferTarget = optional(ISnapshotDeferTarget), + // di-wise-neo's optional() only works when there is an injection context + private snapshotDeferTarget = injectOrStub( + ISnapshotDeferTarget, + () => undefined, + ), introspection: IIntrospection = injectOrStub( IIntrospection, () => new StubIntrospection(), diff --git a/project/server/src/ssr/index.ts b/project/server/src/ssr/index.ts deleted file mode 100644 index f26179de..00000000 --- a/project/server/src/ssr/index.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { createHash, randomUUID } from "node:crypto"; -import { readFile } from "node:fs/promises"; -import { join, relative } from "node:path"; -import { Readable } from "node:stream"; - -import { serveStatic } from "@hono/node-server/serve-static"; -import { html, render } from "@lit-labs/ssr"; -import { installWindowOnGlobal } from "@lit-labs/ssr/lib/dom-shim"; -import { collectResult } from "@lit-labs/ssr/lib/render-result"; -import { RenderResultReadable } from "@lit-labs/ssr/lib/render-result-readable.js"; -import type { Hono } from "hono"; -import { stream } from "hono/streaming"; -import type { StatusCode } from "hono/utils/http-status"; -import { unsafeHTML } from "lit/directives/unsafe-html.js"; -import serialize from "serialize-javascript"; - -import { logger as parentLogger } from "../logger"; -import { CSSStyleSheet } from "./vendor/@lit-labs/ssr-dom-shim/css"; - -import type { HandlerMap } from "../api/dependency"; - -installWindowOnGlobal(); - -// biome-ignore-start lint/suspicious/noExplicitAny: https://github.com/lit/lit.dev/pull/1390 -(globalThis as any).litSsrCallConnectedCallback = true; -(globalThis as any).CSSStyleSheet = CSSStyleSheet; -// biome-ignore-end lint/suspicious/noExplicitAny: ↑ - -const csrPath = join(import.meta.dirname, "..", "client-csr"); -const ssrPath = join(import.meta.dirname, "..", "client-ssr"); - -const { entrypointTemplate } = await import(join(ssrPath, "entrypoint.mjs")); - -const logger = parentLogger.child({ label: "ssr" }); - -type Resources = { - "entrypoint-js": string; - "style-css": string; -}; - -// manually copied from `client` -type RequestBody = { - contentType: C; - body: B; -}; -type BuiltOperation = { - name: string; - path: string; - method: string; - parameters: { - query: Record; - path: Record; - header: Record; - }; - body?: RequestBody; -}; - -type EntrypointTemplateContext = { - io: (built: BuiltOperation, signal?: AbortSignal) => Promise; - resolve?: (locationToken: string, task: () => Promise) => void; - resolved?: Record; - location?: { - origin: string; - pathname: string; - status?: (code: StatusCode) => void; - }; -}; - -const template = (resources: Resources, context: EntrypointTemplateContext) => - html` - - - - - ${unsafeHTML(` - - `)} - ${ - typeof context.resolved !== "undefined" - ? unsafeHTML(` - - `) - : "" - } - ${unsafeHTML(` - - `)} - - - - ${entrypointTemplate(context)} - - `; - -const buildIo = - (handlers: HandlerMap) => - async (built: BuiltOperation): Promise => { - const { path, method, parameters, body } = built; - - const handler = handlers[path][method] as ( - parameters: unknown, - requestBody: unknown, - ) => Promise; - - const requestId = randomUUID(); - - logger.debug("processing ssr io", { - requestId, - path, - method, - }); - - try { - return (await handler(parameters, body?.body)) as T; - } catch (e) { - logger.error("ssr io error", { requestId }); - throw e; - } finally { - logger.debug("processed ssr io", { requestId }); - } - }; - -export const build = async ( - app: Hono, - handlers: HandlerMap, - origin: string, -) => { - const staticPath = "/static/*"; - const staticPathLength = staticPath.length; - - app.use( - staticPath, - serveStatic({ - // `serveStatic` expects relative paths - root: relative(".", csrPath), - rewriteRequestPath: (path) => { - // mountpoint also includes the `/*` portion - return path.slice(staticPathLength - 2); - }, - }), - ); - - const csrEntrypointPath = join(csrPath, "entrypoint.js"); - const csrEntrypoint = await readFile(csrEntrypointPath, "utf8"); - - const csrStylePath = join(csrPath, "style.css"); - const csrStyle = await readFile(csrStylePath, "utf8"); - - let csrEntrypointHash; - - { - const hash = createHash("sha256"); - hash.update(csrEntrypoint); - csrEntrypointHash = hash.digest("hex"); - } - - let csrStyleHash; - { - const hash = createHash("sha256"); - hash.update(csrStyle); - csrStyleHash = hash.digest("hex"); - } - - // for cache-busting - const csrEntrypointAliasPath = `/static/entrypoint-${csrEntrypointHash.slice(0, 8)}.js`; - const csrStyleAliasPath = `/static/style-${csrStyleHash.slice(0, 8)}.css`; - - app.get(csrEntrypointAliasPath, (c) => { - c.header("Content-Type", "text/javascript"); - return c.body(csrEntrypoint); - }); - - app.get(csrStyleAliasPath, (c) => { - c.header("Content-Type", "text/css"); - return c.body(csrStyle); - }); - - // `serveStatic` calls `next` when a path isn't found → register handler on same path that returns 404 - app.get(staticPath, (c) => { - return c.notFound(); - }); - - app.get("/*", async (c) => { - const resources: Resources = { - "entrypoint-js": csrEntrypointAliasPath, - "style-css": csrStyleAliasPath, - }; - - const resolving: Map Promise> = new Map(); - - let settled: PromiseSettledResult[]; - - let status: StatusCode = 200; - - const result = render( - template(resources, { - io: buildIo(handlers), - resolve: (token, bound) => { - resolving.set(token, bound); - }, - location: { - origin, - pathname: c.req.path, - status: (code: StatusCode) => { - status = code; - }, - }, - }), - ); - - await collectResult(result); - - if (status !== 200) { - return c.text("not found", status); - } - - settled = await Promise.allSettled( - resolving - .entries() - .map(([token, bound]) => - (async () => [token, await bound()] as const)(), - ), - ); - - return stream(c, async (stream) => { - const resolved = Object.fromEntries( - settled.flatMap((item) => - item.status === "fulfilled" ? [item.value] : [], - ), - ); - - const rendered = render( - template(resources, { - io: buildIo(handlers), - resolved, - location: { - origin, - pathname: c.req.path, - }, - }), - ); - - c.header("Content-Type", "text/html"); - - const readable = new RenderResultReadable(rendered); - await stream.pipe(Readable.toWeb(readable)); - }); - }); -}; diff --git a/project/server/src/ssr/vendor/@lit-labs/ssr-dom-shim/README.md b/project/server/src/ssr/vendor/@lit-labs/ssr-dom-shim/README.md deleted file mode 100644 index c707cf43..00000000 --- a/project/server/src/ssr/vendor/@lit-labs/ssr-dom-shim/README.md +++ /dev/null @@ -1 +0,0 @@ -vendored because version that contains `CSSStyleSheetShim` is not yet released (28.10.2025) diff --git a/project/server/src/ssr/vendor/@lit-labs/ssr-dom-shim/css.ts b/project/server/src/ssr/vendor/@lit-labs/ssr-dom-shim/css.ts deleted file mode 100644 index 3ca593fc..00000000 --- a/project/server/src/ssr/vendor/@lit-labs/ssr-dom-shim/css.ts +++ /dev/null @@ -1,178 +0,0 @@ -// @ts-nocheck - -/** - * @license - * Copyright 2024 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ - -/** - * This is a limited implemenation of the CSSStyleSheet class - * and associated functionality. - */ - -type MediaListInterface = MediaList; - -const MediaListShim = class MediaList - extends Array - implements MediaListInterface -{ - get mediaText(): string { - return this.join(", "); - } - toString(): string { - return this.mediaText; - } - appendMedium(medium: string): void { - if (!this.includes(medium)) { - this.push(medium); - } - } - deleteMedium(medium: string): void { - const index = this.indexOf(medium); - if (index !== -1) { - this.splice(index, 1); - } - } - item(index: number): string | null { - return this[index] ?? null; - } -}; -const MediaListShimWithRealType = MediaListShim as object as typeof MediaList; -export { MediaListShimWithRealType as MediaList }; - -type StyleSheetInterface = StyleSheet; - -const StyleSheetShim = class StyleSheet implements StyleSheetInterface { - private __media = new MediaListShim(); - - disabled: boolean = false; - get href(): string | null { - return null; - } - get media(): MediaList { - return this.__media; - } - get ownerNode(): Element | ProcessingInstruction | null { - return null; - } - get parentStyleSheet(): CSSStyleSheet | null { - return null; - } - get title(): string | null { - return null; - } - get type(): string { - return "text/css"; - } -}; - -const StyleSheetShimWithRealType = - StyleSheetShim as object as typeof StyleSheet; -export { StyleSheetShimWithRealType as StyleSheet }; - -type CSSRuleInterface = CSSRule; - -const CSSRuleShim = class CSSRule implements CSSRuleInterface { - static readonly STYLE_RULE: 1 = 1 as const; - static readonly CHARSET_RULE: 2 = 2 as const; - static readonly IMPORT_RULE: 3 = 3 as const; - static readonly MEDIA_RULE: 4 = 4 as const; - static readonly FONT_FACE_RULE: 5 = 5 as const; - static readonly PAGE_RULE: 6 = 6 as const; - static readonly NAMESPACE_RULE: 10 = 10 as const; - static readonly KEYFRAMES_RULE: 7 = 7 as const; - static readonly KEYFRAME_RULE: 8 = 8 as const; - static readonly SUPPORTS_RULE: 12 = 12 as const; - static readonly COUNTER_STYLE_RULE: 11 = 11 as const; - static readonly FONT_FEATURE_VALUES_RULE: 14 = 14 as const; - readonly STYLE_RULE: 1 = 1 as const; - readonly CHARSET_RULE: 2 = 2 as const; - readonly IMPORT_RULE: 3 = 3 as const; - readonly MEDIA_RULE: 4 = 4 as const; - readonly FONT_FACE_RULE: 5 = 5 as const; - readonly PAGE_RULE: 6 = 6 as const; - readonly NAMESPACE_RULE: 10 = 10 as const; - readonly KEYFRAMES_RULE: 7 = 7 as const; - readonly KEYFRAME_RULE: 8 = 8 as const; - readonly SUPPORTS_RULE: 12 = 12 as const; - readonly COUNTER_STYLE_RULE: 11 = 11 as const; - readonly FONT_FEATURE_VALUES_RULE: 14 = 14 as const; - __parentStyleSheet: CSSStyleSheet | null = null; - - cssText: string = ""; - get parentRule(): CSSRule | null { - return null; - } - get parentStyleSheet(): CSSStyleSheet | null { - return this.__parentStyleSheet; - } - get type(): number { - return 0; - } -}; - -const CSSRuleShimWithRealType = CSSRuleShim as object as typeof CSSRule; -export { CSSRuleShimWithRealType as CSSRule }; - -type CSSRuleListInterface = CSSRuleList; - -const CSSRuleListShim = class CSSRuleList - extends Array - implements CSSRuleListInterface -{ - item(index: number): CSSRule | null { - return this[index] ?? null; - } -}; - -const CSSRuleListShimWithRealType = - CSSRuleListShim as object as typeof CSSRuleList; -export { CSSRuleListShimWithRealType as CSSRuleList }; - -type CSSStyleSheetInterface = CSSStyleSheet; - -const CSSStyleSheetShim = class CSSStyleSheet - extends StyleSheetShim - implements CSSStyleSheetInterface -{ - private __rules = new CSSRuleListShim(); - get cssRules(): CSSRuleList { - return this.__rules; - } - get ownerRule(): CSSRule | null { - return null; - } - get rules(): CSSRuleList { - return this.cssRules; - } - addRule(_selector?: string, _style?: string, _index?: number): number { - throw new Error("Method not implemented."); - } - deleteRule(_index: number): void { - throw new Error("Method not implemented."); - } - insertRule(_rule: string, _index?: number): number { - throw new Error("Method not implemented."); - } - removeRule(_index?: number): void { - throw new Error("Method not implemented."); - } - replace(text: string): Promise { - this.replaceSync(text); - return Promise.resolve(this); - } - replaceSync(text: string): void { - this.__rules.length = 0; - const rule = new CSSRuleShim(); - rule.cssText = text; - this.__rules.push(rule); - } -}; - -const CSSStyleSheetShimWithRealType = - CSSStyleSheetShim as object as typeof CSSStyleSheet; -export { - CSSStyleSheetShimWithRealType as CSSStyleSheet, - CSSStyleSheetShimWithRealType as CSSStyleSheetShim, -}; diff --git a/project/server/src/utility/dependency-injection.ts b/project/server/src/utility/dependency-injection.ts index 0097b7fd..68b2cb25 100644 --- a/project/server/src/utility/dependency-injection.ts +++ b/project/server/src/utility/dependency-injection.ts @@ -1,7 +1,6 @@ import { type Constructor, createType, - inject, optional, type Type, } from "@lppedd/di-wise-neo"; @@ -27,7 +26,9 @@ export const injectOrStub = (token: Token, stub: () => T): T => { } if (containerExists) { - return inject(token); + // token might not exist even though container exists + // → also use stub + return optional(token) ?? stub(); } return stub(); diff --git a/project/server/src/web/base.ts b/project/server/src/web/base.ts deleted file mode 100644 index 4134e55e..00000000 --- a/project/server/src/web/base.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Schema } from "effect"; - -import { Query as QueryDatabaseSnapshot } from "./database/snapshot/base"; - -export const paths = { - "database-snapshot": "/system/database/snapshot/:name", -} as const; -export const Paths = typeof paths; - -type ParameterValue = { - query?: Schema.Any; - path?: Schema.Any; - header?: Schema.Any; -}; -type DereferenceParameters> = { - [K0 in keyof T]: { - [K1 in keyof T[K0]]: "Type" extends keyof T[K0][K1] - ? T[K0][K1]["Type"] - : never; - }; -}; - -export const parameters = { - "database-snapshot": { query: QueryDatabaseSnapshot }, -} as const; -export type Parameters = DereferenceParameters; diff --git a/project/server/src/web/database/snapshot/base.ts b/project/server/src/web/database/snapshot/base.ts deleted file mode 100644 index 980b63aa..00000000 --- a/project/server/src/web/database/snapshot/base.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Schema } from "effect"; - -import { DatabaseSnapshotCoordinatorName } from "../../../service/database/snapshot-coordinator/base"; - -export const Query = Schema.Struct({ - voucher: Schema.String, -}); - -export const DatabaseSnapshotVoucherPayload = Schema.Struct({ - coordinator: DatabaseSnapshotCoordinatorName, -}); -export type DatabaseSnapshotVoucherPayload = - typeof DatabaseSnapshotVoucherPayload.Type; diff --git a/project/server/src/web/database/snapshot/index.ts b/project/server/src/web/database/snapshot/index.ts deleted file mode 100644 index 0bfb7bed..00000000 --- a/project/server/src/web/database/snapshot/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Readable } from "node:stream"; - -import { Schema } from "effect"; -import { isLeft } from "effect/Either"; -import { Hono } from "hono"; -import { stream } from "hono/streaming"; - -import { container } from "../../../dependency"; -import { DatabaseSnapshotCoordinators } from "../../../service/database/snapshot-coordinator/base"; -import { IVoucher, Voucher } from "../../../service/voucher"; -import { isNone } from "../../../type/maybe"; -import { DatabaseSnapshotVoucherPayload, Query } from "./base"; - -export const router = () => { - const router = new Hono(); - - const coordinators = container.resolve(DatabaseSnapshotCoordinators); - - const voucher = container.resolve(IVoucher); - - router.get("/", async (c) => { - const decoder = Schema.decodeUnknownEither(Query); - const decoded = decoder(c.req.query()); - if (isLeft(decoded)) { - return c.text(decoded.left.message, 400); - } - - const unpacked = voucher.deserialize( - decoded.right.voucher, - "database-snapshot", - 10, - DatabaseSnapshotVoucherPayload, - ); - if (unpacked.kind !== "success") { - return c.text("invalid voucher", 400); - } - - const peeked = Voucher.peek(unpacked.voucher); - - const coordinator = coordinators[peeked.coordinator]; - if (typeof coordinator === "undefined") { - return c.text("snapshot coordinator not configured", 500); - } - - const handle = await coordinator.stale(); - if (isNone(handle)) { - return c.text("snapshot became unexpectedly unavailable", 500); - } - - const controller = new AbortController(); - - const snapshotStream = handle.createReadStream({ - highWaterMark: 16 * 1024, - signal: controller.signal, - }); - - return stream(c, async (stream) => { - // `stream.onAbort` doesn't appear to work 🫠 - // https://github.com/honojs/hono/issues/1770 - c.req.raw.signal.addEventListener("abort", () => { - controller.abort(); - }); - - await stream.pipe(Readable.toWeb(snapshotStream)); - }); - }); - - return router; -}; diff --git a/project/server/src/web/index.ts b/project/server/src/web/index.ts index e96a8806..1081e37c 100644 --- a/project/server/src/web/index.ts +++ b/project/server/src/web/index.ts @@ -1,7 +1,5 @@ import type { Hono } from "hono"; -import { paths } from "./base"; -import { router as routerDatabaseSnapshot } from "./database/snapshot"; import { router as routerMetrics } from "./metrics"; import { router as routerExplorer } from "./openapi-explorer"; import { router as routerResource } from "./resource"; @@ -9,6 +7,5 @@ import { router as routerResource } from "./resource"; export const build = (app: Hono) => { app.route("/metrics", routerMetrics()); app.route("/openapi/explorer", routerExplorer()); - app.route(paths["database-snapshot"], routerDatabaseSnapshot()); app.route("/resource", routerResource()); }; diff --git a/project/server/tsconfig.json b/project/server/tsconfig.json index 4c384bde..8ee437c6 100644 --- a/project/server/tsconfig.json +++ b/project/server/tsconfig.json @@ -11,6 +11,7 @@ "strict": true, "exactOptionalPropertyTypes": true, "skipLibCheck": true, - "resolveJsonModule": true - } + "resolveJsonModule": true, + "experimentalDecorators": true, + }, } diff --git a/project/server/vendor/aws/ecs-task-definition.json b/project/server/vendor/aws/ecs-task-definition.json index 4fd539c1..827c0fd3 100644 --- a/project/server/vendor/aws/ecs-task-definition.json +++ b/project/server/vendor/aws/ecs-task-definition.json @@ -55,7 +55,7 @@ "value": "/volume/snapshot/staging.db" }, { - "name": "DERIVE_ENABLE", + "name": "SCHEDULER_ENABLE", "value": "true" } ],