From f6f51a34e663ffe48e78b876b7bfb8b1250e0f50 Mon Sep 17 00:00:00 2001 From: Fabian Meyer <3982806+meyfa@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:55:59 +0200 Subject: [PATCH] feat: Add project files and source (#1) --- .dockerignore | 19 + .gitattributes | 5 + .github/workflows/build.yml | 30 + .github/workflows/test.yml | 24 + .gitignore | 20 + Dockerfile | 40 + README.md | 128 +- backend/.gitignore | 2 + backend/package.json | 29 + backend/src/annotations.ts | 6 + backend/src/api/auth/local-login.ts | 13 + backend/src/api/auth/logout.ts | 16 + backend/src/api/auth/me.ts | 24 + backend/src/api/auth/oidc.ts | 12 + backend/src/api/auth/strategies.ts | 14 + backend/src/api/cronjob.ts | 41 + backend/src/api/errors.ts | 25 + backend/src/api/job-pods.ts | 51 + backend/src/api/jobs.ts | 107 + backend/src/api/pod-logs.ts | 86 + backend/src/api/pod-progress.ts | 108 + backend/src/api/trigger.ts | 43 + backend/src/auth/common.ts | 31 + backend/src/auth/local-strategy.ts | 69 + backend/src/auth/oidc-strategy.ts | 56 + backend/src/backend-config.ts | 29 + backend/src/controllers.ts | 31 + backend/src/controllers/cronjob.ts | 23 + backend/src/controllers/job.ts | 50 + backend/src/controllers/logs.ts | 36 + backend/src/controllers/pod.ts | 60 + backend/src/controllers/trigger.ts | 62 + backend/src/index.ts | 112 + backend/src/kubernetes/api.ts | 186 + backend/src/routes.ts | 13 + backend/src/util/cache.ts | 113 + backend/src/util/mutex.ts | 30 + backend/src/util/system.ts | 19 + backend/tsconfig.json | 17 + backend/tsconfig.lint.json | 7 + frontend/.gitignore | 2 + frontend/index.html | 12 + frontend/package.json | 34 + frontend/public/favicon.svg | 1 + frontend/public/robots.txt | 2 + frontend/src/App.tsx | 97 + frontend/src/api/api.ts | 105 + frontend/src/api/dispatch.ts | 38 + frontend/src/api/fetch.ts | 111 + frontend/src/api/subscription.ts | 56 + frontend/src/components/Annotation.tsx | 15 + frontend/src/components/Button.tsx | 42 + frontend/src/components/ColoredSkeleton.tsx | 12 + frontend/src/components/ErrorMessage.tsx | 9 + frontend/src/components/Heading.tsx | 9 + frontend/src/components/Icon.tsx | 12 + frontend/src/components/InfoCard.tsx | 13 + frontend/src/components/InputInfo.tsx | 9 + frontend/src/components/JobPanel.tsx | 47 + frontend/src/components/Label.tsx | 12 + frontend/src/components/LogDialog.tsx | 123 + frontend/src/components/Navigation.tsx | 125 + frontend/src/components/StatusDot.tsx | 25 + frontend/src/components/TextInput.tsx | 16 + frontend/src/components/ToggleSwitch.tsx | 26 + frontend/src/index.css | 24 + frontend/src/index.tsx | 24 + frontend/src/pages/Home.tsx | 81 + frontend/src/pages/Job.tsx | 284 + frontend/src/pages/Jobs.tsx | 25 + frontend/src/pages/Login.tsx | 115 + frontend/src/pages/Trigger.tsx | 75 + frontend/src/util/navigation.ts | 10 + frontend/tailwind.config.ts | 18 + frontend/tsconfig.json | 26 + frontend/tsconfig.lint.json | 8 + frontend/vite.config.ts | 25 + package-lock.json | 5642 +++++++++++++++++++ package.json | 54 + renovate.json | 6 + src/config.ts | 139 + src/environment.ts | 16 + src/handle-error.ts | 33 + src/kube-config.ts | 47 + src/server.ts | 91 + tsconfig.json | 17 + tsconfig.lint.json | 7 + 87 files changed, 9506 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 backend/.gitignore create mode 100644 backend/package.json create mode 100644 backend/src/annotations.ts create mode 100644 backend/src/api/auth/local-login.ts create mode 100644 backend/src/api/auth/logout.ts create mode 100644 backend/src/api/auth/me.ts create mode 100644 backend/src/api/auth/oidc.ts create mode 100644 backend/src/api/auth/strategies.ts create mode 100644 backend/src/api/cronjob.ts create mode 100644 backend/src/api/errors.ts create mode 100644 backend/src/api/job-pods.ts create mode 100644 backend/src/api/jobs.ts create mode 100644 backend/src/api/pod-logs.ts create mode 100644 backend/src/api/pod-progress.ts create mode 100644 backend/src/api/trigger.ts create mode 100644 backend/src/auth/common.ts create mode 100644 backend/src/auth/local-strategy.ts create mode 100644 backend/src/auth/oidc-strategy.ts create mode 100644 backend/src/backend-config.ts create mode 100644 backend/src/controllers.ts create mode 100644 backend/src/controllers/cronjob.ts create mode 100644 backend/src/controllers/job.ts create mode 100644 backend/src/controllers/logs.ts create mode 100644 backend/src/controllers/pod.ts create mode 100644 backend/src/controllers/trigger.ts create mode 100644 backend/src/index.ts create mode 100644 backend/src/kubernetes/api.ts create mode 100644 backend/src/routes.ts create mode 100644 backend/src/util/cache.ts create mode 100644 backend/src/util/mutex.ts create mode 100644 backend/src/util/system.ts create mode 100644 backend/tsconfig.json create mode 100644 backend/tsconfig.lint.json create mode 100644 frontend/.gitignore create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/robots.txt create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/api.ts create mode 100644 frontend/src/api/dispatch.ts create mode 100644 frontend/src/api/fetch.ts create mode 100644 frontend/src/api/subscription.ts create mode 100644 frontend/src/components/Annotation.tsx create mode 100644 frontend/src/components/Button.tsx create mode 100644 frontend/src/components/ColoredSkeleton.tsx create mode 100644 frontend/src/components/ErrorMessage.tsx create mode 100644 frontend/src/components/Heading.tsx create mode 100644 frontend/src/components/Icon.tsx create mode 100644 frontend/src/components/InfoCard.tsx create mode 100644 frontend/src/components/InputInfo.tsx create mode 100644 frontend/src/components/JobPanel.tsx create mode 100644 frontend/src/components/Label.tsx create mode 100644 frontend/src/components/LogDialog.tsx create mode 100644 frontend/src/components/Navigation.tsx create mode 100644 frontend/src/components/StatusDot.tsx create mode 100644 frontend/src/components/TextInput.tsx create mode 100644 frontend/src/components/ToggleSwitch.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/pages/Home.tsx create mode 100644 frontend/src/pages/Job.tsx create mode 100644 frontend/src/pages/Jobs.tsx create mode 100644 frontend/src/pages/Login.tsx create mode 100644 frontend/src/pages/Trigger.tsx create mode 100644 frontend/src/util/navigation.ts create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.lint.json create mode 100644 frontend/vite.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 renovate.json create mode 100644 src/config.ts create mode 100644 src/environment.ts create mode 100644 src/handle-error.ts create mode 100644 src/kube-config.ts create mode 100644 src/server.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.lint.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..74cdc3c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +# Default ignored files +.env +.idea +node_modules/ +renovate.json +.github +README.md +.gitignore +.gitattributes + +# build folders and files +/dist +.nyc_output +/tmp +/coverage +/junit.xml + +# configuration +/config diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..046f103 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +.env text eol=lf +*.sh text eol=lf +*.bat text eol=crlf +*.conf text eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..01b1e53 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Build + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build + id: docker-build + uses: docker/build-push-action@v6 + with: + context: ./ + push: false + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c889216 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run build + - run: npm run lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ac3cd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +.idea +.fleet +node_modules/ +.DS_Store +npm-debug.log* +.npmrc +.env + +# build folders and files +.nyc_output +/tmp +/coverage +/junit.xml +/build +/dist +/backend/dist +/frontend/dist + +# configuration files +config/*.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..91a122d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# -- compilation -- +FROM node:20.15.0-alpine AS build +WORKDIR /app + +# install dependencies +COPY package*.json ./ +COPY backend/package*.json ./backend/ +COPY frontend/package*.json ./frontend/ +RUN npm ci + +# copy in app code and build it +COPY . . +RUN npm run build + + +# -- execution -- +FROM node:20.15.0-alpine +WORKDIR /app + +# install PRODUCTION dependencies +COPY package*.json ./ +COPY backend/package*.json ./backend/ +COPY frontend/package*.json ./frontend/ +RUN npm ci --omit=dev +RUN apk add --no-cache tini + +# add the already compiled code and the default config +# (custom config must be set via volume) +COPY --from=build /app/dist ./dist +COPY --from=build /app/backend/dist ./backend/dist +COPY --from=build /app/frontend/dist ./frontend/dist + +# config +USER node +ENV PORT=8080 +EXPOSE 8080 + +# use tini as init process since Node.js isn't designed to be run as PID 1 +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["node", "--disable-proto=delete", "dist/server.js"] diff --git a/README.md b/README.md index 4848919..4d6e765 100644 --- a/README.md +++ b/README.md @@ -1 +1,127 @@ -# foreman \ No newline at end of file +# foreman + +A Kubernetes application for managing Renovate jobs, developed by [Contane](https://contane.net). + +Foreman is intended to be deployed together with self-hosted [Mend Renovate](https://www.mend.io/renovate/), either in +the same or a secondary Kubernetes cluster. +It provides a user interface for monitoring the Renovate CronJob including interactive progress and logs as well as the +ability to trigger custom jobs, speeding up the dependency upgrade cycle in the case of many repositories. + +## Instructions + +To build and run: + +```sh +$ npm run build +$ npm start +``` + +Note that you will need to authenticate on the web interface. +During development, it is recommended to set a password for local login. See the configuration section below. + +## Configuration + +Create a YAML configuration file inside the `config` directory. The file can have any name ending with `.yaml`. +If there are multiple configuration files, they will be merged in alphabetical order. For example, if you have +`config/00-config.yaml` and `config/01-config.yaml`, the settings in `config/01-config.yaml` will take precedence. +The following sections describe the available configuration options. + +Environment variables can be set on the command line or in an `.env` file in the root directory of the project. + +### KubeConfig + +Foreman supports multiple KubeConfig sources. The source to use can be specified by setting the `kubeConfig.source` +to one of the following values: + +- `in-cluster`: Use the KubeConfig from the Kubernetes cluster environment. This is the default. +- `file`: Use the KubeConfig from the file specified by the `KUBECONFIG` environment variable, or `~/.kube/config`. + +In the `in-cluster` mode, Foreman will look at the `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` environment +variables to determine the location of the Kubernetes API server. These variables are automatically set by Kubernetes. + +You can use the `file` mode when developing Foreman locally, or when deploying it outside of Kubernetes. +If necessary, override the `KUBECONFIG` environment variable to point to the correct file. + +You can select a context by setting `kubeConfig.context`. This defaults to the KubeConfig file's current context. + +### CronJob Selection + +Foreman queries a defined CronJob resource to determine which Renovate jobs to manage. By default, this is +`renovate/renovate` (namespace/name). You can override this by setting `cronJob.namespace` and `cronJob.name`. + +### Session Cookies + +Foreman authentication is stateless. Client auth information is stored in a cookie, which is encrypted using a key. +The key is generated on startup and is stored in memory. This means that the key will be different on each startup, +and that all clients will be logged out when Foreman is restarted. Note that key generation consumes about 256 MiB of +memory. + +To avoid this, you can set `cookies.key` to a fixed value. This will cause the same key to be used on each startup, +and will allow clients to stay logged in across restarts, and less memory to be used. + +`cookies.key` must be base64-encoded. To generate a key on Linux, run: + +```sh +$ npx --yes @fastify/secure-session | base64 +``` + +Sessions are valid for 24 hours by default. You can change this by setting `cookies.maxAge` to a duration string +such as "6h" or "14 days". + +### Local Authentication + +Foreman supports local authentication, which allows users to log in using a username and password. This is disabled by +default. To enable it, set the following config options: + +```yaml +auth: + local: + enabled: true + username: "any username (default: admin)" + password: "any password" +``` + +### OIDC Authentication + +Foreman supports authentication via OpenID Connect. To enable it, set the following config options: + +```yaml +auth: + oidc: + enabled: true + issuer: "https://oidc.example.com" + clientId: "client-id" + clientSecret: "client-secret" + publicUrl: "https://foreman.example.com" +``` + +The following configuration must be set in the OIDC provider, assuming Foreman is running at +`https://foreman.example.com`: + +- Response Type: Code +- Grant Type: Authorization Code +- Authentication Method: Basic +- Auth Token Type: Bearer +- Redirect URL: `https://foreman.example.com/api/auth/oidc/callback` + +It is assumed that your OIDC provider restricts which users can log in to Foreman, as no additional role checks are +performed by Foreman. + +### GitLab integration + +Some GitLab-related functionality can be enabled by setting the following configuration options: + +```yaml +gitlab: + host: "https://gitlab.example.com" +``` + +This will enable the following features: + +* Linking to GitLab repositories in the Renovate job list. + +### Other settings + +The following additional environment variables can be set: + +- `PORT`: The port to listen on. Defaults to `8080`. diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..8225baa --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/dist diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..616923f --- /dev/null +++ b/backend/package.json @@ -0,0 +1,29 @@ +{ + "name": "backend", + "version": "0.0.0", + "type": "module", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./routes": { + "types": "./dist/routes.d.ts", + "import": "./dist/routes.js" + } + }, + "scripts": { + "build": "node -e \"fs.rmSync('./dist',{force:true,recursive:true})\" && tsc", + "lint": "tsc --noEmit -p tsconfig.lint.json" + }, + "dependencies": { + "pino-pretty": "11.2.1", + "secure-json-parse": "2.7.0" + } +} diff --git a/backend/src/annotations.ts b/backend/src/annotations.ts new file mode 100644 index 0000000..0eed6d7 --- /dev/null +++ b/backend/src/annotations.ts @@ -0,0 +1,6 @@ +export enum ForemanAnnotations { + Manual = 'foreman.contane.net/manual', + TriggeredBy = 'foreman.contane.net/triggered-by', + RepositoryScope = 'foreman.contane.net/repository-scope', + DebugLogging = 'foreman.contane.net/debug-logging' +} diff --git a/backend/src/api/auth/local-login.ts b/backend/src/api/auth/local-login.ts new file mode 100644 index 0000000..bf06278 --- /dev/null +++ b/backend/src/api/auth/local-login.ts @@ -0,0 +1,13 @@ +import { FastifyPluginAsync } from 'fastify' +import { authenticateLocal, LocalStrategyRequestBody } from '../../auth/local-strategy.js' + +export interface LocalLoginRoute { + Body: LocalStrategyRequestBody + Reply: {} +} + +export const localLoginAuthRoute = (): FastifyPluginAsync => async (app) => { + app.post('/', { preValidation: authenticateLocal() }, () => { + return {} + }) +} diff --git a/backend/src/api/auth/logout.ts b/backend/src/api/auth/logout.ts new file mode 100644 index 0000000..0eb851b --- /dev/null +++ b/backend/src/api/auth/logout.ts @@ -0,0 +1,16 @@ +import { FastifyPluginAsync } from 'fastify' +import { authenticateSession } from '../../auth/common.js' + +export interface LogoutRoute { + Body: {} + Reply: {} +} + +export const logoutAuthRoute = (): FastifyPluginAsync => async (app) => { + app.addHook('preValidation', authenticateSession()) + + app.post('/', async (request, reply) => { + await request.logout() + return {} + }) +} diff --git a/backend/src/api/auth/me.ts b/backend/src/api/auth/me.ts new file mode 100644 index 0000000..fbca20b --- /dev/null +++ b/backend/src/api/auth/me.ts @@ -0,0 +1,24 @@ +import { FastifyPluginAsync } from 'fastify' +import { forbidden } from '../errors.js' +import { authenticateSession } from '../../auth/common.js' + +export interface MeRoute { + Reply: { + username: string + strategy: string + } +} + +export const meAuthRoute = (): FastifyPluginAsync => async (app) => { + app.addHook('preValidation', authenticateSession()) + + app.get('/', async (request, reply) => { + if (request.user == null) { + return await forbidden(reply) + } + return { + username: request.user.username, + strategy: request.user.strategy + } + }) +} diff --git a/backend/src/api/auth/oidc.ts b/backend/src/api/auth/oidc.ts new file mode 100644 index 0000000..c4ad3aa --- /dev/null +++ b/backend/src/api/auth/oidc.ts @@ -0,0 +1,12 @@ +import { FastifyPluginAsync } from 'fastify' +import { authenticateOidc } from '../../auth/oidc-strategy.js' +import { AuthStrategy } from '../../auth/common.js' + +export const oidcAuthRoute = (): FastifyPluginAsync => async (app) => { + app.get('/', authenticateOidc()) + + app.get('/callback', authenticateOidc({ + successRedirect: '/', + failureRedirect: '/login?login_error=' + AuthStrategy.OIDC + })) +} diff --git a/backend/src/api/auth/strategies.ts b/backend/src/api/auth/strategies.ts new file mode 100644 index 0000000..5a4df1a --- /dev/null +++ b/backend/src/api/auth/strategies.ts @@ -0,0 +1,14 @@ +import { FastifyPluginAsync } from 'fastify' +import { AuthStrategy } from '../../auth/common.js' + +export interface StrategiesRoute { + Reply: string[] +} + +export const strategiesAuthRoute = (strategies: AuthStrategy[]): FastifyPluginAsync => async (app) => { + // route is not authenticated - provides a list of available strategies to log in with + + app.get('/', async () => { + return strategies + }) +} diff --git a/backend/src/api/cronjob.ts b/backend/src/api/cronjob.ts new file mode 100644 index 0000000..c1ae682 --- /dev/null +++ b/backend/src/api/cronjob.ts @@ -0,0 +1,41 @@ +import { FastifyPluginAsync } from 'fastify' +import { Controllers } from '../controllers.js' +import cronParser from 'cron-parser' +import { forbidden, notFound } from './errors.js' +import { authenticateSession } from '../auth/common.js' + +export interface CronJobRoute { + Reply: { + schedule?: string + timeZone?: string + suspend: boolean + nextScheduleTime: string + } +} + +export const cronjobRoute = ({ cronJobController }: Controllers): FastifyPluginAsync => async (app) => { + app.addHook('preValidation', authenticateSession()) + + app.get('/', async (request, reply) => { + if (request.user == null) { + return await forbidden(reply) + } + + const cronJob = await cronJobController.getCronJob() + if (cronJob == null) { + return await notFound(reply) + } + + const schedule = typeof cronJob.spec?.schedule === 'string' ? cronJob.spec.schedule : undefined + const interval = cronParser.parseExpression(schedule ?? '', { + tz: typeof cronJob.spec?.timeZone === 'string' ? cronJob.spec.timeZone : undefined + }) + + return { + schedule, + timeZone: cronJob.spec?.timeZone, + suspend: cronJob.spec?.suspend ?? false, + nextScheduleTime: interval.next().toISOString() + } + }) +} diff --git a/backend/src/api/errors.ts b/backend/src/api/errors.ts new file mode 100644 index 0000000..3abffdd --- /dev/null +++ b/backend/src/api/errors.ts @@ -0,0 +1,25 @@ +import { FastifyReply } from 'fastify' + +export async function badRequest (reply: FastifyReply): Promise { + return await reply.code(400).send({ error: 'Bad Request' }) +} + +export async function forbidden (reply: FastifyReply): Promise { + return await reply.code(403).send({ error: 'Forbidden' }) +} + +export async function notFound (reply: FastifyReply): Promise { + return await reply.code(404).send({ error: 'Not Found' }) +} + +export async function payloadTooLarge (reply: FastifyReply): Promise { + return await reply.code(413).send({ error: 'Payload Too Large' }) +} + +export async function unsupportedMediaType (reply: FastifyReply): Promise { + return await reply.code(415).send({ error: 'Unsupported Media Type' }) +} + +export async function internalServerError (reply: FastifyReply): Promise { + return await reply.code(500).send({ error: 'Internal Server Error' }) +} diff --git a/backend/src/api/job-pods.ts b/backend/src/api/job-pods.ts new file mode 100644 index 0000000..243c2ef --- /dev/null +++ b/backend/src/api/job-pods.ts @@ -0,0 +1,51 @@ +import { FastifyPluginAsync } from 'fastify' +import { Controllers } from '../controllers.js' +import { forbidden, notFound } from './errors.js' +import { authenticateSession } from '../auth/common.js' +import { V1Pod } from '@kubernetes/client-node' +import assert from 'node:assert' +import { DEFAULT_NAMESPACE } from '../kubernetes/api.js' + +interface JobPodsItem { + namespace: string + name: string + status?: 'active' | 'success' | 'failure' + startTime?: string +} + +export interface JobPodsRoute { + Reply: JobPodsItem[] +} + +function project (pod: V1Pod): JobPodsItem { + assert.ok(pod.metadata?.name != null) + + const isFailure = pod.status?.phase === 'Failed' + const isSuccess = pod.status?.phase === 'Succeeded' + const isActive = pod.status?.phase === 'Running' + + return { + namespace: pod.metadata.namespace ?? DEFAULT_NAMESPACE, + name: pod.metadata.name, + status: isFailure ? 'failure' : isSuccess ? 'success' : isActive ? 'active' : undefined, + startTime: pod.status?.startTime?.toISOString() + } +} + +export const jobPodsRoute = ({ podController }: Controllers): FastifyPluginAsync => async (app) => { + app.addHook('preValidation', authenticateSession()) + + app.get('/:namespace/:name', async (request, reply) => { + if (request.user == null) { + return await forbidden(reply) + } + const pods = await podController.getPodsForJob({ namespace: request.params.namespace, name: request.params.name }) + if (pods == null) { + return await notFound(reply) + } + return pods.map(project) + }) +} diff --git a/backend/src/api/jobs.ts b/backend/src/api/jobs.ts new file mode 100644 index 0000000..9581966 --- /dev/null +++ b/backend/src/api/jobs.ts @@ -0,0 +1,107 @@ +import { FastifyPluginAsync } from 'fastify' +import { Controllers } from '../controllers.js' +import { forbidden, notFound } from './errors.js' +import { authenticateSession } from '../auth/common.js' +import { V1Job } from '@kubernetes/client-node' +import assert from 'node:assert' +import { DEFAULT_NAMESPACE } from '../kubernetes/api.js' +import { ForemanAnnotations } from '../annotations.js' + +interface JobsItem { + namespace: string + name: string + manual: boolean + settings: { + repositoryScope?: string + debugLogging?: boolean + } + triggeredBy?: { + username: string + strategy: string + } + status?: 'success' | 'failure' | 'active' + startTime?: string + completionTime?: string +} + +export interface JobsRoute { + Reply: JobsItem[] +} + +export interface JobsItemRoute { + Params: { + namespace: string + name: string + } + Reply: JobsItem +} + +function project (job: V1Job): JobsItem { + assert.ok(job.metadata?.name != null) + + const isFailure = typeof job.status?.failed === 'number' && job.status.failed > 0 + const isSuccess = typeof job.status?.succeeded === 'number' && job.status.succeeded > 0 + const isActive = typeof job.status?.active === 'number' && job.status.active > 0 + + const isManual = job.metadata?.annotations?.[ForemanAnnotations.Manual] === 'true' ?? false + const repositoryScope = job.metadata?.annotations?.[ForemanAnnotations.RepositoryScope] + const debugLogging = job.metadata?.annotations?.[ForemanAnnotations.DebugLogging] != null + ? job.metadata.annotations[ForemanAnnotations.DebugLogging] === 'true' + : undefined + const triggeredBy = job.metadata?.annotations?.[ForemanAnnotations.TriggeredBy] != null + ? parseLogin(job.metadata.annotations[ForemanAnnotations.TriggeredBy]) + : undefined + + return { + namespace: job.metadata.namespace ?? DEFAULT_NAMESPACE, + name: job.metadata.name, + manual: isManual, + settings: { + repositoryScope, + debugLogging + }, + triggeredBy, + status: isSuccess ? 'success' : isFailure ? 'failure' : isActive ? 'active' : undefined, + startTime: job.status?.startTime?.toISOString(), + completionTime: job.status?.completionTime?.toISOString() + } +} + +interface UserDescriptor { + username: string + strategy: string +} + +function parseLogin (login: string): UserDescriptor { + const [strategy, username] = login.split(':') + return { strategy: strategy ?? '', username: username ?? '' } +} + +export const jobsRoute = ({ jobController }: Controllers): FastifyPluginAsync => async (app) => { + app.addHook('preValidation', authenticateSession()) + + app.get('/', async (request, reply) => { + if (request.user == null) { + return await forbidden(reply) + } + const jobs = await jobController.getJobsFromCronJob() + if (jobs == null) { + return await notFound(reply) + } + return jobs.map(project).sort((a, b) => { + // sort newest jobs first + return (b.startTime ?? '').localeCompare(a.startTime ?? '', 'en-US') + }) + }) + + app.get('/:namespace/:name', async (request, reply) => { + if (request.user == null) { + return await forbidden(reply) + } + const job = await jobController.findJob({ namespace: request.params.namespace, name: request.params.name }) + if (job == null) { + return await notFound(reply) + } + return project(job) + }) +} diff --git a/backend/src/api/pod-logs.ts b/backend/src/api/pod-logs.ts new file mode 100644 index 0000000..3cd278f --- /dev/null +++ b/backend/src/api/pod-logs.ts @@ -0,0 +1,86 @@ +import { FastifyPluginAsync } from 'fastify' +import { Controllers } from '../controllers.js' +import { badRequest, forbidden, notFound } from './errors.js' +import { authenticateSession } from '../auth/common.js' +import pinoPretty from 'pino-pretty' +import { Writable } from 'node:stream' +import { enums, optional, type } from 'superstruct' +import { WeakCache } from '../util/cache.js' + +export interface PodLogsRoute { + Reply: string +} + +const logsQuerystringSchema = type({ + pretty: optional(enums(['true', 'false'])) +}) + +export const podLogsRoute = ({ logsController }: Controllers): FastifyPluginAsync => async (app) => { + app.addHook('preValidation', authenticateSession()) + + // Prettifying is an expensive operation. Cache the results. + const prettyLogsCache = new WeakCache<{ data: string }>(5000) + + app.get('/:namespace/:name', async (request, reply) => { + if (request.user == null) { + return await forbidden(reply) + } + const [err, query] = logsQuerystringSchema.validate(request.query, { coerce: true }) + if (err != null) { + return await badRequest(reply) + } + const logs = await logsController.getPodLogs({ namespace: request.params.namespace, name: request.params.name }) + if (logs == null) { + return await notFound(reply) + } + if (query.pretty === 'true') { + const result = await prettyLogsCache.lazyCompute([request.params.namespace, request.params.name], async () => { + return { + data: await prettifyLogs(logs) + } + }) + return result?.data + } + return logs + }) +} + +async function prettifyLogs (logs: string): Promise { + // TODO: This consumes a lot of memory. We should use streaming instead. + // TODO: There should be a way for clients to request logs over WebSocket instead of polling. + const chunks: string[] = [] + const prettyLogs = pinoPretty({ + translateTime: true, + ignore: 'v,name,pid,hostname,logContext', + colorize: false, + destination: new Writable({ + write (chunk, enc, cb) { + chunks.push(chunk.toString()) + cb() + } + }) + }) + // Process the string in chunks to avoid blocking the event loop. + for (const chunk of chunked(logs)) { + await new Promise((resolve) => { + if (!prettyLogs.write(chunk, 'utf8')) { + prettyLogs.once('drain', resolve) + } else { + setImmediate(resolve) + } + }) + } + await new Promise((resolve) => prettyLogs.end(resolve)) + return chunks.join('') +} + +function * chunked (str: string, size = 4096): Iterable { + for (let i = 0; i < str.length; i += size) { + yield str.slice(i, i + size) + } +} diff --git a/backend/src/api/pod-progress.ts b/backend/src/api/pod-progress.ts new file mode 100644 index 0000000..274db8a --- /dev/null +++ b/backend/src/api/pod-progress.ts @@ -0,0 +1,108 @@ +import { FastifyPluginAsync } from 'fastify' +import { Controllers } from '../controllers.js' +import { forbidden, notFound } from './errors.js' +import { authenticateSession } from '../auth/common.js' +import sjson from 'secure-json-parse' +import { BackendConfig } from '../backend-config.js' + +interface ProgressItem { + repository: string + repositoryUrl?: string + state: 'pending' | 'started' | 'finished' + duration?: number +} + +export interface PodProgressRoute { + Reply: ProgressItem[] +} + +export const podProgressRoute = ({ logsController }: Controllers, config: BackendConfig): FastifyPluginAsync => async (app) => { + app.addHook('preValidation', authenticateSession()) + + app.get('/:namespace/:name', async (request, reply) => { + if (request.user == null) { + return await forbidden(reply) + } + const logs = await logsController.getPodLogs({ namespace: request.params.namespace, name: request.params.name }) + if (logs == null) { + return await notFound(reply) + } + const progress = getProgress(logs, config) + if (progress == null) { + // In case the log cannot be parsed, progress is unavailable, so return a 404. + return await notFound((reply)) + } + return progress + }) +} + +function getProgress (logs: string, config: BackendConfig): ProgressItem[] | undefined { + const repositories = new Map() + let foundRepositories = false + for (const line of lines(logs)) { + const message = tryParseLogMessage(line) + // Note: While it looks ugly, manual validation is more than 3x faster than using superstruct. + // This is worth it here as there is lots of data to process. + if (message == null || typeof message !== 'object' || !('msg' in message)) { + continue + } + // The initial log message is for autodiscovery and tells us about the repositories that will be processed. + if ('repositories' in message && Array.isArray(message.repositories) && + message.repositories.every(msg => typeof msg === 'string')) { + foundRepositories = true + for (const repository of message.repositories) { + const item: ProgressItem = { repository, state: 'pending' } + if (config.gitlab.host != null) { + item.repositoryUrl = new URL(repository, config.gitlab.host).toString() + } + repositories.set(repository, item) + } + } + // Log messages associated with a repository have a repository property, and update the item's status. + if (!('repository' in message) || typeof message.repository !== 'string') { + continue + } + const item = repositories.get(message.repository) + if (item == null) { + continue // what? + } + switch (message.msg) { + case 'Repository started': + item.state = 'started' + break + case 'Repository finished': + item.state = 'finished' + break + } + } + if (!foundRepositories) { + return undefined + } + // Note: JS Map always returns values in insertion order. + return Array.from(repositories.values()) +} + +function * lines (str: string): Generator { + let lineFrom = 0 + while (lineFrom < str.length) { + let lineTo = str.indexOf('\n', lineFrom) + if (lineTo === -1) { + lineTo = str.length + } + yield str.slice(lineFrom, lineTo) + lineFrom = lineTo + 1 + } +} + +function tryParseLogMessage (str: string): unknown { + try { + return sjson.parse(str) + } catch { + return undefined + } +} diff --git a/backend/src/api/trigger.ts b/backend/src/api/trigger.ts new file mode 100644 index 0000000..6bf30e6 --- /dev/null +++ b/backend/src/api/trigger.ts @@ -0,0 +1,43 @@ +import { FastifyPluginAsync } from 'fastify' +import { Controllers } from '../controllers.js' +import { boolean, Infer, object, optional, pattern, string } from 'superstruct' +import { badRequest, forbidden } from './errors.js' +import { authenticateSession } from '../auth/common.js' +import assert from 'node:assert' +import { DEFAULT_NAMESPACE } from '../kubernetes/api.js' + +const triggerSchema = object({ + repositoryScope: optional(pattern(string(), /^[a-z0-9-]+(\/[a-z0-9-]+)*$/i)), + debugLogging: optional(boolean()) +}) + +export interface TriggerRoute { + Body: Infer + Reply: { + namespace: string + name: string + } +} + +export const triggerRoute = ({ triggerController }: Controllers): FastifyPluginAsync => async (app) => { + app.addHook('preValidation', authenticateSession()) + + app.post('/', async (request, reply) => { + if (request.user == null) { + return await forbidden(reply) + } + const [err, body] = triggerSchema.validate(request.body, { coerce: true }) + if (err != null) { + return await badRequest(reply) + } + const createdJob = await triggerController.trigger(body, { + username: request.user.username, + strategy: request.user.strategy + }) + assert.ok(createdJob.metadata?.name != null) + return { + namespace: createdJob.metadata.namespace ?? DEFAULT_NAMESPACE, + name: createdJob.metadata.name + } + }) +} diff --git a/backend/src/auth/common.ts b/backend/src/auth/common.ts new file mode 100644 index 0000000..8cbde97 --- /dev/null +++ b/backend/src/auth/common.ts @@ -0,0 +1,31 @@ +import { RouteHandlerMethod } from 'fastify' +import fastifyPassport from '@fastify/passport' + +export enum AuthStrategy { + LOCAL = 'local', + OIDC = 'oidc' +} + +// handled by @fastify/secure-session +export const authenticateSession = (): RouteHandlerMethod => fastifyPassport.authenticate('session') + +export interface User { + /** + * The strategy used for login. + */ + strategy: AuthStrategy + + /** + * The username used for login. + */ + username: string + + /** + * The time when the session was created (in milliseconds since the Unix epoch). + */ + createdAt: number +} + +declare module 'fastify' { + interface PassportUser extends User {} +} diff --git a/backend/src/auth/local-strategy.ts b/backend/src/auth/local-strategy.ts new file mode 100644 index 0000000..34aea61 --- /dev/null +++ b/backend/src/auth/local-strategy.ts @@ -0,0 +1,69 @@ +import { Strategy as LocalStrategy } from 'passport-local' +import crypto, { BinaryLike, timingSafeEqual } from 'node:crypto' +import { Mutex } from '../util/mutex.js' +import { AuthStrategy, User } from './common.js' +import { RouteHandlerMethod } from 'fastify' +import fastifyPassport from '@fastify/passport' +import { promisify } from 'node:util' + +export const authenticateLocal = (): RouteHandlerMethod => fastifyPassport.authenticate(AuthStrategy.LOCAL) + +export interface LocalStrategyRequestBody { + username: string + password: string +} + +export async function makeLocalStrategy (adminUser: { + username: string + password: string +}): Promise { + const options = { + usernameField: 'username' satisfies keyof LocalStrategyRequestBody, + passwordField: 'password' satisfies keyof LocalStrategyRequestBody + } + + // If the admin user has no password, disable the local strategy. + if (adminUser.password === '') { + return new LocalStrategy(options, function (username, password, done) { + done(null, false) + }) + } + + // Otherwise, hash the password with scrypt. + // We don't want to perform direct comparisons on login requests because that would speed up brute force attacks. + const salt = crypto.randomBytes(16) + const hash = await hashPassword(adminUser.password, salt) + + return new LocalStrategy(options, function (username, password, done) { + if (username !== adminUser.username) { + done(null, false) + return + } + // Hash the password with the same salt and compare the hashes. + hashPassword(password, salt).then((hash2) => { + if (timingSafeEqual(hash, hash2)) { + done(null, { + strategy: AuthStrategy.LOCAL, + username, + createdAt: Date.now() + } satisfies User) + return + } + done(null, false) + }).catch(done) + }) +} + +const hashingMutex = new Mutex() + +const promiseScrypt = promisify(crypto.scrypt) + +async function hashPassword (password: string, salt: Buffer): Promise { + // Since hashing is resource intensive, we perform it serially (one at a time). + const unlock = await hashingMutex.lock() + try { + return await promiseScrypt(password, salt, 64) + } finally { + unlock() + } +} diff --git a/backend/src/auth/oidc-strategy.ts b/backend/src/auth/oidc-strategy.ts new file mode 100644 index 0000000..c3e1559 --- /dev/null +++ b/backend/src/auth/oidc-strategy.ts @@ -0,0 +1,56 @@ +import { custom, Issuer, Strategy as OpenIdStrategy, TokenSet, UserinfoResponse } from 'openid-client' +import { AuthStrategy, User } from './common.js' +import { AuthenticateOptions } from '@fastify/passport/dist/AuthenticationRoute.js' +import { RouteHandlerMethod } from 'fastify' +import fastifyPassport from '@fastify/passport' + +export const authenticateOidc = (options?: AuthenticateOptions): RouteHandlerMethod => fastifyPassport.authenticate(AuthStrategy.OIDC, options) + +export async function makeOidcStrategy (options: { + issuer: string + clientId: string + clientSecret: string + redirectUri: string +}): Promise> { + const issuerUrl = options.issuer.includes('.well-known') + ? options.issuer + : new URL('/.well-known/openid-configuration', options.issuer).toString() + + const issuer = await Issuer.discover(issuerUrl) + + const client = new issuer.Client({ + client_id: options.clientId, + client_secret: options.clientSecret, + redirect_uris: [options.redirectUri], + response_types: ['code'], + token_endpoint_auth_method: 'client_secret_basic' + }) + + // There are some really slow OIDC providers out there. + client[custom.http_options] = () => { + return { timeout: 30_000 } + } + + return new OpenIdStrategy({ + client, + params: { + scope: 'openid email' + } + }, (tokenset: TokenSet, userinfo: UserinfoResponse, done: (err: any, user?: User) => void) => { + if (tokenset.expired()) { + done(new Error('Token expired')) + return + } + const claims = tokenset.claims() + const username = claims.email ?? userinfo.email + if (username == null) { + done(new Error('No username or email provided')) + return + } + done(null, { + strategy: AuthStrategy.OIDC, + username, + createdAt: Date.now() + }) + }) +} diff --git a/backend/src/backend-config.ts b/backend/src/backend-config.ts new file mode 100644 index 0000000..f46a42f --- /dev/null +++ b/backend/src/backend-config.ts @@ -0,0 +1,29 @@ +interface LocalAuthOptions { + username: string + password: string +} + +interface OidcAuthOptions { + publicUrl: string + issuer: string + clientId: string + clientSecret: string +} + +export interface BackendConfig { + cronJob: { + namespace: string + name: string + } + cookies: { + key?: Buffer + maxAge: number + } + auth: { + local: ({ enabled: true } & LocalAuthOptions) | ({ enabled: false } & Partial) + oidc: ({ enabled: true } & OidcAuthOptions) | ({ enabled: false } & Partial) + } + gitlab: { + host?: string + } +} diff --git a/backend/src/controllers.ts b/backend/src/controllers.ts new file mode 100644 index 0000000..5df95fa --- /dev/null +++ b/backend/src/controllers.ts @@ -0,0 +1,31 @@ +import { CronJobController } from './controllers/cronjob.js' +import { JobController } from './controllers/job.js' +import { PodController } from './controllers/pod.js' +import { KubernetesApi } from './kubernetes/api.js' +import { LogsController } from './controllers/logs.js' +import { TriggerController } from './controllers/trigger.js' +import { BackendConfig } from './index.js' + +export interface Controllers { + cronJobController: CronJobController + jobController: JobController + podController: PodController + logsController: LogsController + triggerController: TriggerController +} + +export function createControllers (k8s: KubernetesApi, config: BackendConfig): Controllers { + const cronJobController = new CronJobController(k8s, config.cronJob) + const jobController = new JobController(k8s, cronJobController) + const podController = new PodController(k8s, jobController) + const logsController = new LogsController(k8s, podController) + const triggerController = new TriggerController(k8s, cronJobController, jobController) + + return { + cronJobController, + jobController, + podController, + logsController, + triggerController + } +} diff --git a/backend/src/controllers/cronjob.ts b/backend/src/controllers/cronjob.ts new file mode 100644 index 0000000..6f92b27 --- /dev/null +++ b/backend/src/controllers/cronjob.ts @@ -0,0 +1,23 @@ +import { KubernetesApi } from '../kubernetes/api.js' +import { StrongCache } from '../util/cache.js' +import { BackendConfig } from '../index.js' +import { V1CronJob } from '@kubernetes/client-node' + +export class CronJobController { + private readonly cache = new StrongCache(5000) + + constructor ( + private readonly k8s: KubernetesApi, + private readonly config: BackendConfig['cronJob'] + ) { + } + + async getCronJob (): Promise { + return await this.cache.lazyCompute([], async () => { + return await this.k8s.getCronJob({ + namespace: this.config.namespace, + name: this.config.name + }) + }) + } +} diff --git a/backend/src/controllers/job.ts b/backend/src/controllers/job.ts new file mode 100644 index 0000000..82e5160 --- /dev/null +++ b/backend/src/controllers/job.ts @@ -0,0 +1,50 @@ +import { CronJobController } from './cronjob.js' +import { KubernetesApi } from '../kubernetes/api.js' +import assert from 'node:assert' +import { StrongCache } from '../util/cache.js' +import { V1CronJob, V1Job } from '@kubernetes/client-node' + +export class JobController { + private readonly cache = new StrongCache(5000) + + constructor ( + private readonly k8s: KubernetesApi, + private readonly cronJobController: CronJobController + ) { + } + + async getJobsFromCronJob (): Promise { + const cronJob = await this.cronJobController.getCronJob() + if (cronJob == null) { + return undefined + } + + const namespace = cronJob.metadata?.namespace + assert.ok(namespace != null) + + return await this.cache.lazyCompute([namespace], async () => { + return await this.k8s.getJobs({ + namespace + // TODO ensure owner + }) + }) + } + + async findJob (job: { + namespace: string + name: string + }): Promise { + const jobs = await this.getJobsFromCronJob() + if (jobs == null) { + return undefined + } + return jobs.find(j => j.metadata?.namespace === job.namespace && j.metadata?.name === job.name) + } + + invalidateCache (cronJob: V1CronJob): void { + const namespace = cronJob.metadata?.namespace + if (namespace != null) { + this.cache.invalidate([namespace]) + } + } +} diff --git a/backend/src/controllers/logs.ts b/backend/src/controllers/logs.ts new file mode 100644 index 0000000..6662e3e --- /dev/null +++ b/backend/src/controllers/logs.ts @@ -0,0 +1,36 @@ +import { KubernetesApi } from '../kubernetes/api.js' +import { PodController } from './pod.js' +import { WeakCache } from '../util/cache.js' + +export class LogsController { + // The log strings may be large, so we use a weak cache to allow them to be garbage collected. + // However, it's impossible to create a WeakRef to a string, so we wrap the string in an object. + private readonly cache = new WeakCache<{ data: string }>(5000) + + constructor ( + private readonly k8s: KubernetesApi, + private readonly podController: PodController + ) { + } + + async getPodLogs (pod: { + namespace: string + name: string + }): Promise { + // Input must relate to an existing pod + const podItem = await this.podController.findPod(pod) + if (podItem == null) { + return undefined + } + // Requesting logs for a pod that is starting is invalid and will result in a 400 error. + if (podItem.status?.phase === 'Pending' || podItem.status?.phase === 'Unknown') { + return undefined + } + const result = await this.cache.lazyCompute([pod.namespace, pod.name], async () => { + return { + data: await this.k8s.getPodLogs(pod) + } + }) + return result?.data + } +} diff --git a/backend/src/controllers/pod.ts b/backend/src/controllers/pod.ts new file mode 100644 index 0000000..2d7bc61 --- /dev/null +++ b/backend/src/controllers/pod.ts @@ -0,0 +1,60 @@ +import { KubernetesApi } from '../kubernetes/api.js' +import { JobController } from './job.js' +import { StrongCache } from '../util/cache.js' +import assert from 'node:assert' +import { V1Pod } from '@kubernetes/client-node' + +export class PodController { + private readonly cache = new StrongCache(5000) + + constructor ( + private readonly k8s: KubernetesApi, + private readonly jobController: JobController + ) { + } + + async getPodsForJob (job: { + namespace: string + name: string + }): Promise { + // Input must relate to an existing job + if (await this.jobController.findJob(job) == null) { + return undefined + } + return await this.cache.lazyCompute([job.namespace, job.name], async () => { + return await this.k8s.getPodsForJob(job) + }) + } + + async listAllPods (): Promise { + const jobs = await this.jobController.getJobsFromCronJob() + if (jobs == null) { + return [] + } + const result: V1Pod[] = [] + for (const job of jobs) { + assert.ok(job.metadata?.namespace != null) + assert.ok(job.metadata?.name != null) + const pods = await this.getPodsForJob({ + namespace: job.metadata.namespace, + name: job.metadata.name + }) + if (pods != null) { + result.push(...pods) + } + } + return result + } + + async findPod (podMetadata: { + namespace: string + name: string + }): Promise { + for (const pod of await this.listAllPods()) { + if (pod.metadata?.namespace === podMetadata.namespace && pod.metadata?.name === podMetadata.name) { + return pod + } + } + return undefined + } +} diff --git a/backend/src/controllers/trigger.ts b/backend/src/controllers/trigger.ts new file mode 100644 index 0000000..5a09bc4 --- /dev/null +++ b/backend/src/controllers/trigger.ts @@ -0,0 +1,62 @@ +import { KubernetesApi } from '../kubernetes/api.js' +import { CronJobController } from './cronjob.js' +import assert from 'node:assert' +import { randomBytes } from 'node:crypto' +import { V1Job } from '@kubernetes/client-node' +import { ForemanAnnotations } from '../annotations.js' +import { JobController } from './job.js' + +export class TriggerController { + constructor ( + private readonly k8s: KubernetesApi, + private readonly cronJobController: CronJobController, + private readonly jobController: JobController + ) { + } + + async trigger (options: { + repositoryScope?: string + debugLogging?: boolean + }, user: { + username: string + strategy: string + }): Promise { + const cronJob = await this.cronJobController.getCronJob() + assert.ok(cronJob?.metadata?.name != null) + + const annotations: Record = {} + annotations[ForemanAnnotations.Manual] = 'true' + annotations[ForemanAnnotations.TriggeredBy] = `${user.strategy}:${user.username}` + + const env: Record = {} + + if (options.repositoryScope != null) { + env.RENOVATE_AUTODISCOVER_FILTER = options.repositoryScope + annotations[ForemanAnnotations.RepositoryScope] = options.repositoryScope + } + + if (options.debugLogging === true) { + env.LOG_LEVEL = 'debug' + annotations[ForemanAnnotations.DebugLogging] = 'true' + } + + const result = await this.k8s.triggerCronJob({ + cronJob, + jobName: `${cronJob.metadata.name}-foreman-${randomId()}`, + annotations, + env + }) + + // Avoid 404 errors when the client immediately tries to list the job + this.jobController.invalidateCache(cronJob) + + return result + } +} + +/** + * Generate 6 random lowercase characters for use in a job name. + */ +function randomId (): string { + return randomBytes(16).toString('base64').toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 6) +} diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..2c69f75 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,112 @@ +import k8s from '@kubernetes/client-node' +import { KubernetesApi } from './kubernetes/api.js' +import { cronjobRoute } from './api/cronjob.js' +import { podLogsRoute } from './api/pod-logs.js' +import { jobsRoute } from './api/jobs.js' +import { jobPodsRoute } from './api/job-pods.js' +import { createControllers } from './controllers.js' +import { triggerRoute } from './api/trigger.js' +import { FastifyPluginAsync } from 'fastify' +import { fastifySecureSession } from '@fastify/secure-session' +import fastifyPassport from '@fastify/passport' +import { randomBytes } from 'node:crypto' +import { meAuthRoute } from './api/auth/me.js' +import { localLoginAuthRoute } from './api/auth/local-login.js' +import { logoutAuthRoute } from './api/auth/logout.js' +import { podProgressRoute } from './api/pod-progress.js' +import { getAvailableMemory } from './util/system.js' +import { oidcAuthRoute } from './api/auth/oidc.js' +import { makeLocalStrategy } from './auth/local-strategy.js' +import { makeOidcStrategy } from './auth/oidc-strategy.js' +import { AuthStrategy, User } from './auth/common.js' +import { strategiesAuthRoute } from './api/auth/strategies.js' +import { BackendConfig } from './backend-config.js' + +export type { BackendConfig } from './backend-config.js' +export * from './api/errors.js' + +export const backend = (kubeConfig: k8s.KubeConfig, config: BackendConfig): FastifyPluginAsync => async (app) => { + // Note: NEVER log the config here, as it contains the admin password! + app.log.info('backend_init') + + const api = await KubernetesApi.create(app.log, { kubeConfig }) + const controllers = createControllers(api, config) + + // If no key is provided, generate a new key each time. + if (config.cookies.key == null) { + app.log.warn('cookie_key_random_generation') + // Key generation needs at least 256 MiB of free memory, which can be problematic in containerized environments. + const requiredMemory = 256 * 1024 * 1024 + const availableMemory = getAvailableMemory() + if (availableMemory != null && availableMemory < requiredMemory) { + throw new Error(`Cookie key generation requires at least 256 MiB of free memory. Memory available: ${availableMemory.toLocaleString('en-US')} bytes.`) + } + } + const keyOptions = config.cookies.key != null + ? { key: config.cookies.key } + : { secret: randomBytes(32), salt: randomBytes(16) } + + await app.register(fastifySecureSession, { + ...keyOptions, + cookie: { + // the cookie is scoped to the API, so that it's not sent with requests to static files + path: app.prefix, + // do not allow JavaScript to access the cookie + httpOnly: true, + // if the request was made via HTTPS, the cookie should only be sent via HTTPS in future requests + secure: 'auto', + maxAge: config.cookies.maxAge / 1000 + } + }) + await app.register(fastifyPassport.initialize()) + await app.register(fastifyPassport.secureSession()) + + fastifyPassport.registerUserSerializer(async (user: User) => user) + fastifyPassport.registerUserDeserializer(async (user: User) => { + // From https://github.com/fastify/fastify-passport: + // "if a deserializer returns null or false, @fastify/passport interprets that as a missing but expected user, and + // resets the session to log the user out" + if (!Number.isSafeInteger(user.createdAt) || user.createdAt + config.cookies.maxAge < Date.now()) { + return null + } + return user + }) + + const enabledAuthStrategies = [] + + if (config.auth.local.enabled) { + if (config.auth.local.password.trim().length === 0) { + throw new Error('Local auth password must not be empty.') + } + enabledAuthStrategies.push(AuthStrategy.LOCAL) + fastifyPassport.use(AuthStrategy.LOCAL, await makeLocalStrategy({ + username: config.auth.local.username, + password: config.auth.local.password + })) + } + + if (config.auth.oidc.enabled) { + enabledAuthStrategies.push(AuthStrategy.OIDC) + fastifyPassport.use(AuthStrategy.OIDC, await makeOidcStrategy({ + ...config.auth.oidc, + redirectUri: new URL(`${app.prefix}/auth/oidc/callback`, config.auth.oidc.publicUrl).toString() + })) + } + + await app.register(strategiesAuthRoute(enabledAuthStrategies), { prefix: '/auth/strategies' }) + await app.register(meAuthRoute(), { prefix: '/auth/me' }) + await app.register(logoutAuthRoute(), { prefix: '/auth/logout' }) + if (enabledAuthStrategies.includes(AuthStrategy.LOCAL)) { + await app.register(localLoginAuthRoute(), { prefix: '/auth/local' }) + } + if (enabledAuthStrategies.includes(AuthStrategy.OIDC)) { + await app.register(oidcAuthRoute(), { prefix: '/auth/oidc' }) + } + + await app.register(cronjobRoute(controllers), { prefix: '/cronjob' }) + await app.register(jobsRoute(controllers), { prefix: '/jobs' }) + await app.register(jobPodsRoute(controllers), { prefix: '/job-pods' }) + await app.register(podLogsRoute(controllers), { prefix: '/pod-logs' }) + await app.register(podProgressRoute(controllers, config), { prefix: '/pod-progress' }) + await app.register(triggerRoute(controllers), { prefix: '/trigger' }) +} diff --git a/backend/src/kubernetes/api.ts b/backend/src/kubernetes/api.ts new file mode 100644 index 0000000..277d4e2 --- /dev/null +++ b/backend/src/kubernetes/api.ts @@ -0,0 +1,186 @@ +import k8s, { HttpError, V1CronJob, V1EnvVar, V1Job, V1Pod } from '@kubernetes/client-node' +import assert from 'node:assert' +import { BaseLogger } from 'pino' + +export const DEFAULT_NAMESPACE = 'default' + +export interface KubernetesApiOptions { + kubeConfig: k8s.KubeConfig +} + +export class KubernetesApi { + constructor ( + private readonly log: BaseLogger, + private readonly coreApi: k8s.CoreV1Api, + private readonly batchApi: k8s.BatchV1Api + ) { + } + + static async create (log: BaseLogger, options: KubernetesApiOptions): Promise { + const kubeConfig = options.kubeConfig + const coreApi = kubeConfig.makeApiClient(k8s.CoreV1Api) + const batchApi = kubeConfig.makeApiClient(k8s.BatchV1Api) + return new KubernetesApi(log, coreApi, batchApi) + } + + private async request (fn: () => Promise): Promise { + // The Kubernetes client throws HTTP errors that contain the request and response headers. + // This is a security risk if the error is logged, so we catch and rethrow the error without these objects. + try { + return await fn() + } catch (err) { + if (err instanceof HttpError) { + // Force TypeScript to cause an error if the response property is missing + ((err satisfies { response: object }) as any).response = undefined + } + throw err + } + } + + async getCronJob (options: { + namespace: string + name: string + }): Promise { + this.log.debug({ options }, 'k8s_getCronJob') + const result = await this.request(async () => { + return await this.batchApi.readNamespacedCronJob(options.name, options.namespace) + }) + return result.body + } + + async getJobs (options: { + namespace: string + }): Promise { + this.log.debug({ options }, 'k8s_getJobs') + const result = await this.request(async () => { + return await this.batchApi.listNamespacedJob(options.namespace) + }) + return result.body.items.map((item) => ({ + ...item, + kind: result.body.kind?.replace(/List$/, ''), + apiVersion: result.body.apiVersion + })) + } + + async getPodsForJob (options: { + namespace: string + name: string + }): Promise { + this.log.debug({ options }, 'k8s_getPodsForJob') + const labelSelector = `job-name=${options.name}` + const result = await this.request(async () => { + return await this.coreApi.listNamespacedPod(options.namespace, undefined, undefined, undefined, undefined, labelSelector) + }) + return result.body.items.map((item) => ({ + ...item, + kind: result.body.kind?.replace(/List$/, ''), + apiVersion: result.body.apiVersion + })) + } + + async getPodLogs (options: { + namespace: string + name: string + }): Promise { + this.log.debug({ options }, 'k8s_getPodLogs') + const result = await this.request(async () => { + return await this.coreApi.readNamespacedPodLog(options.name, options.namespace) + }) + return result.body + } + + async triggerCronJob (options: { + cronJob: V1CronJob + jobName: string + annotations?: Record + env: Record + }): Promise { + assert.ok(options.cronJob.metadata?.namespace != null) + assert.ok(options.cronJob.metadata?.name != null) + const { name, namespace } = options.cronJob.metadata + assert.ok(options.cronJob.spec?.jobTemplate?.metadata != null) + this.log.info({ + options: { + namespace, + name, + jobName: options.jobName, + env: JSON.stringify(options.env) + } + }, 'k8s_triggerCronJob') + let jobBody = options.cronJob.spec.jobTemplate + jobBody = applyMetadataName(jobBody, options.jobName) + jobBody = applyMetadataAnnotations(jobBody, options.annotations ?? {}) + jobBody = applyEnvToJobContainers(jobBody, options.env) + // Cleanup job after 1 hour + jobBody = applyTtl(jobBody, 60 * 60) + const result = await this.request(async () => { + return await this.batchApi.createNamespacedJob(namespace, jobBody) + }) + return result.body + } +} + +function applyMetadataAnnotations (jobBody: k8s.V1Job, annotations: Record): k8s.V1Job { + return { + ...jobBody, + metadata: { + ...jobBody.metadata, + annotations: { + ...jobBody.metadata?.annotations, + ...annotations + } + } + } +} + +function applyMetadataName (jobBody: k8s.V1Job, name: string): k8s.V1Job { + return { + ...jobBody, + metadata: { + ...jobBody.metadata, + name + } + } +} + +function applyEnvToJobContainers (jobBody: k8s.V1Job, env: Record): k8s.V1Job { + if (jobBody.spec?.template?.spec?.containers == null) { + // No containers to apply env to + return jobBody + } + return { + ...jobBody, + spec: { + ...jobBody.spec, + template: { + ...jobBody.spec.template, + spec: { + ...jobBody.spec.template.spec, + containers: jobBody.spec.template.spec.containers.map((container) => ({ + ...container, + env: mergeEnv(container.env ?? [], env) + })) + } + } + } + } +} + +function applyTtl (jobBody: k8s.V1Job, ttlSecondsAfterFinished: number): k8s.V1Job { + if (jobBody.spec == null) { + return jobBody + } + return { + ...jobBody, + spec: { + ...jobBody.spec, + ttlSecondsAfterFinished + } + } +} + +function mergeEnv (env: V1EnvVar[], newEnv: Record): V1EnvVar[] { + // Remove env vars that will be overwritten, then add new env vars + return env.filter((envVar) => newEnv[envVar.name] == null) + .concat(Object.entries(newEnv).map(([name, value]) => ({ name, value }))) +} diff --git a/backend/src/routes.ts b/backend/src/routes.ts new file mode 100644 index 0000000..640c057 --- /dev/null +++ b/backend/src/routes.ts @@ -0,0 +1,13 @@ +// This file is intended for import from the frontend. It must be kept free of runtime code. + +export type { StrategiesRoute } from './api/auth/strategies.js' +export type { MeRoute } from './api/auth/me.js' +export type { LogoutRoute } from './api/auth/logout.js' +export type { LocalLoginRoute } from './api/auth/local-login.js' + +export type { CronJobRoute } from './api/cronjob.js' +export type { JobsRoute, JobsItemRoute } from './api/jobs.js' +export type { JobPodsRoute } from './api/job-pods.js' +export type { PodLogsRoute } from './api/pod-logs.js' +export type { PodProgressRoute } from './api/pod-progress.js' +export type { TriggerRoute } from './api/trigger.js' diff --git a/backend/src/util/cache.ts b/backend/src/util/cache.ts new file mode 100644 index 0000000..af26343 --- /dev/null +++ b/backend/src/util/cache.ts @@ -0,0 +1,113 @@ +import assert from 'node:assert' + +export interface Cache { + store: (key: string[], value: T) => void + lookup: (key: string[]) => T | undefined + invalidate: (key: string[]) => void + lazyCompute: (key: string[], compute: () => PromiseLike) => Promise +} + +interface CacheEntry { + readonly key: string[] + readonly value: T + readonly createdAt: number +} + +abstract class BaseCache implements Cache { + private readonly entries: Array> = [] + + constructor ( + private readonly duration: number + ) { + assert.ok(duration > 0) + } + + abstract store (key: string[], value: T): void + + abstract lookup (key: string[]): T | undefined + + invalidate (key: string[]): void { + const index = this.findIndex(key) + if (index != null) { + this.entries.splice(index, 1) + } + } + + async lazyCompute (key: string[], compute: () => PromiseLike): Promise { + const cached = this.lookup(key) + if (cached != null) { + return cached + } + const value = await compute() + if (value == null) { + return undefined + } + this.store(key, value) + return value + } + + protected baseStore (key: string[], value: TStored): void { + const entry = { + key, + value, + createdAt: Date.now() + } + const index = this.findIndex(key) + if (index != null) { + this.entries[index] = entry + } else { + this.entries.push(entry) + } + } + + protected baseLookup (key: string[]): TStored | undefined { + const index = this.findIndex(key) + return index != null ? this.entries[index].value : undefined + } + + private findIndex (key: string[]): number | undefined { + const now = Date.now() + for (let i = 0; i < this.entries.length; i++) { + const entry = this.entries[i] + // remove expired entries + if (entry.createdAt + this.duration < now) { + this.entries.splice(i, 1) + i-- + continue + } + // find matching entry + if (entry.key.length === key.length && entry.key.every((value, index) => value === key[index])) { + return i + } + } + return undefined + } +} + +/** + * A cache implementation that stores entries in memory for a given duration. + */ +export class StrongCache extends BaseCache { + override store (key: string[], value: T): void { + super.baseStore(key, value) + } + + override lookup (key: string[]): T | undefined { + return super.baseLookup(key) + } +} + +/** + * A cache implementation that stores weak references to entries in memory for a given duration. + * This cache is useful for caching large objects that should be allowed to be garbage collected. + */ +export class WeakCache extends BaseCache> { + override store (key: string[], value: T): void { + super.baseStore(key, new WeakRef(value)) + } + + override lookup (key: string[]): T | undefined { + const value = super.baseLookup(key) + return value?.deref() + } +} diff --git a/backend/src/util/mutex.ts b/backend/src/util/mutex.ts new file mode 100644 index 0000000..a69b515 --- /dev/null +++ b/backend/src/util/mutex.ts @@ -0,0 +1,30 @@ +type Unlock = () => void + +/** + * Implementation of a mutex. Calling lock() will wait until the mutex is unlocked, then lock it and return a function + * that can be called to unlock it again. + */ +export class Mutex { + private currentPromise = Promise.resolve() + + async lock (): Promise { + // Note about async functions, such as this one: + // "Top-level code, up to and including the first await expression (if there is one), is run synchronously." + // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function + + // Obtain a Promise that can be resolved at a later time. + let unlock: () => void + const nextPromise = new Promise((resolve) => { + unlock = () => resolve() + }) + + // currentPromise resolves when the previous lock holder unlocks the mutex, at which point we can lock it, + // and return a function to unlock it again. + const returnedPromise = this.currentPromise.then(() => unlock) + + // Once lock() has been called, the caller is in queue to get the lock, and subsequent calls must wait for it. + this.currentPromise = nextPromise + + return await returnedPromise + } +} diff --git a/backend/src/util/system.ts b/backend/src/util/system.ts new file mode 100644 index 0000000..7bf41bc --- /dev/null +++ b/backend/src/util/system.ts @@ -0,0 +1,19 @@ +// If the memory constraint is reported to be > 1 TB, it is likely that the process is not constrained. +const MAX_CONSTRAINED_MEMORY = 1024 * 1024 * 1024 * 1024 + +/** + * Returns the available memory in bytes if the process is constrained, otherwise undefined. + * + * @param reserve The amount of memory to subtract from the available memory for other purposes. + */ +export function getAvailableMemory (reserve = 0): number | undefined { + // Note: process.constrainedMemory() returns UINT64_MAX if the process is not constrained. + // See https://docs.libuv.org/en/v1.x/misc.html#c.uv_get_constrained_memory + // It is important that we don't return a gigantic number here, as the scrypt algorithm doesn't allow it. + const constrainedMemory = process.constrainedMemory() + if (constrainedMemory != null && constrainedMemory > 0 && constrainedMemory <= MAX_CONSTRAINED_MEMORY) { + const memoryUsage = process.memoryUsage.rss() + return Math.max(0, constrainedMemory - memoryUsage - reserve) + } + return undefined +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..84ca40f --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "lib": ["ES2022"], + "module": "ES2022", + "target": "ES2022", + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} diff --git a/backend/tsconfig.lint.json b/backend/tsconfig.lint.json new file mode 100644 index 0000000..f33590b --- /dev/null +++ b/backend/tsconfig.lint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src", + "test" + ] +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..8225baa --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/dist diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..f7101df --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Foreman + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..310b73b --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "frontend", + "version": "0.0.0", + "type": "module", + "private": true, + "scripts": { + "build": "vite build", + "lint": "tsc --noEmit -p tsconfig.lint.json" + }, + "devDependencies": { + "@types/luxon": "3.4.2", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "@vitejs/plugin-react": "4.3.1", + "autoprefixer": "10.4.19", + "postcss": "8.4.39", + "tailwindcss": "3.4.4", + "vite": "5.3.3", + "vite-plugin-favicons-inject": "2.2.0" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@fortawesome/react-fontawesome": "0.2.2", + "@headlessui/react": "2.1.2", + "@uidotdev/usehooks": "2.4.1", + "clsx": "2.1.1", + "cronstrue": "2.50.0", + "luxon": "3.4.4", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-loading-skeleton": "3.4.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..54d7b08 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..fe66774 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,97 @@ +import { FunctionComponent, useEffect, useState } from 'react' +import { BrowserRouter, Link, Navigate, Route, Routes } from 'react-router-dom' +import { Home } from './pages/Home.js' +import { Navigation } from './components/Navigation.js' +import { Job } from './pages/Job.js' +import { Trigger } from './pages/Trigger.js' +import { Login } from './pages/Login.js' +import { useApiSubscription } from './api/subscription.js' +import { useApiDispatch } from './api/dispatch.js' +import { api } from './api/api.js' +import { Button } from './components/Button.js' +import { Jobs } from './pages/Jobs.js' +import clsx from 'clsx' +import Icon from './components/Icon.js' +import { faChevronDown, faChevronUp, faPaintRoller } from '@fortawesome/free-solid-svg-icons' +import { ColoredSkeleton } from './components/ColoredSkeleton.js' +import { useMobileNavigation } from './util/navigation.js' + +export const App: FunctionComponent = () => { + const { loading, data: userInfo } = useApiSubscription({ interval: 60_000 }, api.userInfo) + + const { dispatch: dispatchLogout, data: logoutResult, inProgress: logoutInProgress } = useApiDispatch(api.logout) + + // refresh the page when logging out + useEffect(() => { + if (logoutResult != null) { + window.location.reload() + } + }, [logoutResult]) + + const [asideExpanded, setAsideExpanded] = useState(false) + + // close the aside when the screen size is large enough to show it always, so it isn't unexpectedly open next time + const isMobileNavigation = useMobileNavigation() + useEffect(() => { + if (!isMobileNavigation) { + setAsideExpanded(false) + } + }, [isMobileNavigation]) + + return ( + +
+ +
+ + {!loading && userInfo === false && ( + <> + } /> + } /> + + )} + {!loading && userInfo !== false && ( + <> + } /> + } /> + } /> + } /> + } /> + + )} + +
+
+
+ ) +} diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts new file mode 100644 index 0000000..244ef06 --- /dev/null +++ b/frontend/src/api/api.ts @@ -0,0 +1,105 @@ +import { ApiCallError, ApiRoute } from './fetch.js' +import type { + CronJobRoute, + JobPodsRoute, + JobsRoute, + JobsItemRoute, + LocalLoginRoute, + LogoutRoute, + MeRoute, + PodLogsRoute, + PodProgressRoute, + StrategiesRoute, + TriggerRoute +} from 'backend/routes' + +export type TriggerJobOptions = TriggerRoute['Body'] + +// helper for better type inference +const route = (route: ApiRoute): ApiRoute => route + +export const api = { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + userInfo: route({ + request: () => ({ url: 'auth/me' }), + transformResponse: async (response): Promise => await response.json(), + transformError: (error) => { + if (error instanceof ApiCallError && error.statusCode === 403) { + return false + } + return error + } + }), + + authStrategies: route({ + request: () => ({ url: 'auth/strategies' }), + transformResponse: async (response): Promise => await response.json() + }), + + localLogin: route({ + request: (body: LocalLoginRoute['Body']) => ({ url: 'auth/local', method: 'POST', body }), + transformResponse: async (response): Promise => await response.json() + }), + + logout: route({ + request: () => ({ url: 'auth/logout', method: 'POST' }), + transformResponse: async (response): Promise => await response.json() + }), + + cronJob: route({ + request: () => ({ url: 'cronjob' }), + transformResponse: async (response): Promise => await response.json() + }), + + jobs: route({ + request: () => ({ url: 'jobs' }), + transformResponse: async (response): Promise => await response.json() + }), + + job: route({ + request: (job: { + namespace: string + name: string + }) => ({ url: `jobs/${encodeURIComponent(job.namespace)}/${encodeURIComponent(job.name)}` }), + transformResponse: async (response): Promise => await response.json() + }), + + podsForJob: route({ + request: (job: { + namespace: string + name: string + }) => ({ url: `job-pods/${encodeURIComponent(job.namespace)}/${encodeURIComponent(job.name)}` }), + transformResponse: async (response): Promise => await response.json() + }), + + podLogs: route({ + request: (pod: { + namespace: string + name: string + }, options?: { + pretty?: boolean + }) => { + const searchParams = new URLSearchParams() + if (options?.pretty != null) { + searchParams.set('pretty', options.pretty.toString()) + } + return { + url: `pod-logs/${encodeURIComponent(pod.namespace)}/${encodeURIComponent(pod.name)}?${searchParams.toString()}` + } + }, + transformResponse: async (response): Promise => await response.text() + }), + + podProgress: route({ + request: (pod: { + namespace: string + name: string + }) => ({ url: `pod-progress/${encodeURIComponent(pod.namespace)}/${encodeURIComponent(pod.name)}` }), + transformResponse: async (response): Promise => await response.json() + }), + + triggerJob: route({ + request: (options: TriggerJobOptions) => ({ url: 'trigger', method: 'POST', body: options }), + transformResponse: async (response): Promise => await response.json() + }) +} diff --git a/frontend/src/api/dispatch.ts b/frontend/src/api/dispatch.ts new file mode 100644 index 0000000..f3f622b --- /dev/null +++ b/frontend/src/api/dispatch.ts @@ -0,0 +1,38 @@ +import { ApiRoute, FetchState, useApiFetcher } from './fetch.js' +import { useCallback, useEffect, useMemo, useState } from 'react' + +export type ApiDispatchHook = FetchState & { + dispatch: (...args: Args) => void + inProgress: boolean +} + +/** + * A hook with a dispatch function that will send a request to the API and update the state with the response. + * This hook is intended for requests that trigger actions or cause data to change on the server. + * + * @param route The route to dispatch to. + * @returns An object with a dispatch method, and the current state of the request. + */ +export function useApiDispatch (route: ApiRoute): ApiDispatchHook { + const fetcher = useApiFetcher() + const [inProgress, setInProgress] = useState(false) + + // when the dispatch hook is unmounted, abort any pending requests + const abortController = useMemo(() => new AbortController(), []) + useEffect(() => { + return () => { + abortController.abort() + setInProgress(false) + } + }, [abortController]) + + const dispatch = useCallback['dispatch']>((...args) => { + const { url, method, body } = route.request(...args) + setInProgress(true) + fetcher.fetch({ url, method, body }, route, abortController.signal, () => { + setInProgress(false) + }) + }, [fetcher, route, abortController]) + + return { ...fetcher, dispatch, inProgress } +} diff --git a/frontend/src/api/fetch.ts b/frontend/src/api/fetch.ts new file mode 100644 index 0000000..9e700d8 --- /dev/null +++ b/frontend/src/api/fetch.ts @@ -0,0 +1,111 @@ +import { useCallback, useState } from 'react' + +export class ApiCallError extends Error { + constructor ( + public readonly statusCode: number, + message: string + ) { + super(message) + this.name = 'ApiCallError' + } +} + +export interface FetchTransform { + transformResponse: (response: Response) => PromiseLike + transformError?: (error: Error) => Error | T +} + +export interface ApiRoute extends FetchTransform { + request: (...args: Args) => RequestDescriptor +} + +export interface RequestDescriptor { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' + url: string + body?: object +} + +export interface FetchState { + data: T | undefined + error: Error | undefined +} + +export type FetchHook = FetchState & { + fetch: (req: RequestDescriptor, transform: FetchTransform, signal: AbortSignal | undefined, cb?: () => void) => void + reset: () => void +} + +export function useApiFetcher (): FetchHook { + const [state, setState] = useState>({ + data: undefined, + error: undefined + }) + + const fetch = useCallback['fetch']>((req, transform, signal, cb) => { + sendRequest(req, signal) + .then(async (response): Promise => { + // the response is not an error + return await transform.transformResponse(response) + }) + .catch(async (error) => { + // the response is an error, but may still become a valid response via the transform + const transformed: T | Error = transform.transformError?.(error) ?? error + if (transformed instanceof Error) { + throw transformed + } + return transformed + }) + .then(async (data) => { + // all transforms are applied and the response is valid + if (signal?.aborted === true) return + setState((state) => ({ + ...state, + data, + error: undefined + })) + }) + .catch(async (error) => { + // all transforms are applied and the response is an error + if (signal?.aborted === true) return + setState((state) => ({ + ...state, + data: undefined, + error + })) + }) + .finally(() => { + cb?.() + }) + }, []) + + const reset = useCallback['reset']>(() => { + setState({ + data: undefined, + error: undefined + }) + }, []) + + return { ...state, fetch, reset } +} + +async function sendRequest (request: RequestDescriptor, signal?: AbortSignal): Promise { + const { url, method, body } = request + const init: RequestInit = { method, credentials: 'include', signal } + if ((method === 'POST' || method === 'PUT') && body != null) { + init.headers = { 'Content-Type': 'application/json' } + init.body = JSON.stringify(body) + } + const response = await fetch(`/api/${url}`, init) + if (!response.ok) { + throw await toApiCallError(response) + } + return response +} + +async function toApiCallError (response: Response): Promise { + const body = await response.json() + if (typeof body?.error === 'string') { + return new ApiCallError(response.status, body.error) + } + return new Error(`unknown error (status=${response.status})`) +} diff --git a/frontend/src/api/subscription.ts b/frontend/src/api/subscription.ts new file mode 100644 index 0000000..55d45a3 --- /dev/null +++ b/frontend/src/api/subscription.ts @@ -0,0 +1,56 @@ +import { useEffect } from 'react' +import { ApiRoute, FetchState, useApiFetcher } from './fetch.js' + +export type ApiSubscriptionHook = FetchState & { + loading: boolean +} + +export interface SubscriptionOptions { + interval?: number +} + +/** + * A hook for subscribing to an API route. This hook will send regular GET requests to keep the data up to date. + * The route must be a nilpotent GET request. + * + * @param options Options for the subscription. + * @param route The route to subscribe to. + * @param args The arguments to pass to the route. + * @returns An object with the current state of the request. + */ +export function useApiSubscription ( + options: SubscriptionOptions, + route: ApiRoute, + ...args: Args +): ApiSubscriptionHook { + const fetcher = useApiFetcher() + + const { url, method, body } = route.request(...args) + + const { fetch, reset } = fetcher + useEffect(() => { + // This effect hook is called once initially, and then every time the arguments change. + // As such, we want to start with empty data and error. + reset() + // Prevent subscriptions to non-nilpotent routes. + if (method != null && method !== 'GET') { + return + } + let abortController: AbortController | undefined + function update (): void { + abortController?.abort() + abortController = new AbortController() + fetch({ url }, route, abortController.signal) + } + update() + const interval = setInterval(update, options.interval ?? 60_000) + return () => { + abortController?.abort() + clearInterval(interval) + } + }, [fetch, reset, options.interval, route, url, method, body]) + + const loading = fetcher.data == null && fetcher.error == null + + return { ...fetcher, loading } +} diff --git a/frontend/src/components/Annotation.tsx b/frontend/src/components/Annotation.tsx new file mode 100644 index 0000000..7463a43 --- /dev/null +++ b/frontend/src/components/Annotation.tsx @@ -0,0 +1,15 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core' +import { FunctionComponent, PropsWithChildren } from 'react' +import Icon from './Icon.js' + +export const Annotation: FunctionComponent> = (props) => { + return ( + + + {props.text} + + ) +} diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx new file mode 100644 index 0000000..d0efd85 --- /dev/null +++ b/frontend/src/components/Button.tsx @@ -0,0 +1,42 @@ +import { FunctionComponent, PropsWithChildren } from 'react' +import clsx from 'clsx' + +export const Button: FunctionComponent void + title?: string +}>> = (props) => { + if (props.as === 'a') { + return ( + + {props.children} + + ) + } + + return ( + + ) +} diff --git a/frontend/src/components/ColoredSkeleton.tsx b/frontend/src/components/ColoredSkeleton.tsx new file mode 100644 index 0000000..eec0359 --- /dev/null +++ b/frontend/src/components/ColoredSkeleton.tsx @@ -0,0 +1,12 @@ +import { FunctionComponent } from 'react' +import Skeleton from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' + +export const ColoredSkeleton: FunctionComponent<{ + className?: string + width?: string | number +}> = (props) => { + return ( + + ) +} diff --git a/frontend/src/components/ErrorMessage.tsx b/frontend/src/components/ErrorMessage.tsx new file mode 100644 index 0000000..b343d04 --- /dev/null +++ b/frontend/src/components/ErrorMessage.tsx @@ -0,0 +1,9 @@ +import { FunctionComponent, PropsWithChildren } from 'react' + +export const ErrorMessage: FunctionComponent = (props) => { + return ( +
+ {props.children} +
+ ) +} diff --git a/frontend/src/components/Heading.tsx b/frontend/src/components/Heading.tsx new file mode 100644 index 0000000..cec2c8f --- /dev/null +++ b/frontend/src/components/Heading.tsx @@ -0,0 +1,9 @@ +import { FunctionComponent, PropsWithChildren } from 'react' + +export const Heading: FunctionComponent = (props) => { + return ( +

+ {props.children} +

+ ) +} diff --git a/frontend/src/components/Icon.tsx b/frontend/src/components/Icon.tsx new file mode 100644 index 0000000..de80cf0 --- /dev/null +++ b/frontend/src/components/Icon.tsx @@ -0,0 +1,12 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { memo } from 'react' + +/** + * Use this component whenever you need an icon. + * + * This is simply a memoized (cached) version of FontAwesomeIcon for performance reasons. + * FontAwesomeIcon is coded very inefficiently and each icon takes 1ms or longer PER RENDER. + * With this component, the problem is reduced. + */ +const Icon = memo(FontAwesomeIcon) +export default Icon diff --git a/frontend/src/components/InfoCard.tsx b/frontend/src/components/InfoCard.tsx new file mode 100644 index 0000000..0f7166c --- /dev/null +++ b/frontend/src/components/InfoCard.tsx @@ -0,0 +1,13 @@ +import { FunctionComponent, PropsWithChildren } from 'react' +import { ColoredSkeleton } from './ColoredSkeleton.js' + +export const InfoCard: FunctionComponent> = (props) => { + return ( +
+
{props.title ?? }
+
{props.children ?? }
+
+ ) +} diff --git a/frontend/src/components/InputInfo.tsx b/frontend/src/components/InputInfo.tsx new file mode 100644 index 0000000..85839d7 --- /dev/null +++ b/frontend/src/components/InputInfo.tsx @@ -0,0 +1,9 @@ +import { FunctionComponent, PropsWithChildren } from 'react' + +export const InputInfo: FunctionComponent = (props) => { + return ( +
+ {props.children} +
+ ) +} diff --git a/frontend/src/components/JobPanel.tsx b/frontend/src/components/JobPanel.tsx new file mode 100644 index 0000000..5387452 --- /dev/null +++ b/frontend/src/components/JobPanel.tsx @@ -0,0 +1,47 @@ +import { FunctionComponent } from 'react' +import { StatusDot } from './StatusDot.js' +import { ColoredSkeleton } from './ColoredSkeleton.js' +import { Link } from 'react-router-dom' +import { DateTime } from 'luxon' +import { Annotation } from './Annotation.js' +import { faPaintRoller } from '@fortawesome/free-solid-svg-icons' + +export const JobPanel: FunctionComponent<{ + namespace: string | undefined + name: string | undefined + manual: boolean + status: 'success' | 'failure' | 'active' | undefined + startTime: string | undefined +}> = (props) => { + if (props.namespace == null || props.name == null) { + return ( +
+
+ +
+ +
+
+ +
+ ) + } + + return ( + +
+ + {props.namespace}/{props.name} +
+
+ {props.startTime != null ? DateTime.fromISO(props.startTime).toRelative() : } + {props.manual && ( + + )} +
+ + ) +} diff --git a/frontend/src/components/Label.tsx b/frontend/src/components/Label.tsx new file mode 100644 index 0000000..3415257 --- /dev/null +++ b/frontend/src/components/Label.tsx @@ -0,0 +1,12 @@ +import { FunctionComponent, PropsWithChildren } from 'react' + +export const Label: FunctionComponent> = (props) => { + return ( + + ) +} diff --git a/frontend/src/components/LogDialog.tsx b/frontend/src/components/LogDialog.tsx new file mode 100644 index 0000000..8615196 --- /dev/null +++ b/frontend/src/components/LogDialog.tsx @@ -0,0 +1,123 @@ +import { FunctionComponent, useLayoutEffect, useRef, useState } from 'react' +import { useApiSubscription } from '../api/subscription.js' +import { api } from '../api/api.js' +import { ToggleSwitch } from './ToggleSwitch.js' +import Icon from './Icon.js' +import { faCircleNotch, faXmark } from '@fortawesome/free-solid-svg-icons' +import { useLocalStorage } from '@uidotdev/usehooks' +import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react' + +const PRETTY_KEY = 'foreman.logdialog.pretty' + +export const LogDialog: FunctionComponent<{ + podNamespace: string + podName: string + podActive: boolean + onClose: () => void +}> = (props) => { + const [pretty, setPretty] = useLocalStorage(PRETTY_KEY, true) + const [follow, setFollow] = useState(true) + + return ( + +
+ + + + + Logs for pod: {`${props.podNamespace}/${props.podName}`} + + +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+ + setFollow(value)} + /> +
+
+
+ ) +} + +const LogDisplay: FunctionComponent<{ + podNamespace: string + podName: string + podActive: boolean + pretty: boolean + follow: boolean + onChangeFollow?: (follow: boolean) => void +}> = (props) => { + const { data: logs } = useApiSubscription({ + // Poll more frequently while the pod is running. Once it's done, there should be no further log output. + interval: props.podActive ? 5_000 : (60 * 60 * 1000) + }, api.podLogs, { + namespace: props.podNamespace, + name: props.podName + }, { + pretty: props.pretty + }) + + const logElement = useRef(null) + + useLayoutEffect(() => { + if (props.follow) { + logElement.current?.scrollTo({ + top: logElement.current?.scrollHeight + }) + } + }, [props.follow, logs]) + + if (logs == null) { + return ( +
+
+
+ + Loading... +
+
+
+ ) + } + + return ( +
{ + if (props.onChangeFollow != null && event.target instanceof HTMLDivElement) { + const { scrollTop, scrollHeight, clientHeight } = event.target + if (scrollTop + clientHeight >= scrollHeight - 10) { + props.onChangeFollow(true) + } else { + props.onChangeFollow(false) + } + } + }} + > + {logs} +
+ ) +} diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx new file mode 100644 index 0000000..a834b4f --- /dev/null +++ b/frontend/src/components/Navigation.tsx @@ -0,0 +1,125 @@ +import { FunctionComponent, PropsWithChildren } from 'react' +import { useApiSubscription } from '../api/subscription.js' +import { api } from '../api/api.js' +import { NavLink } from 'react-router-dom' +import clsx from 'clsx' +import { StatusDot } from './StatusDot.js' +import { DateTime } from 'luxon' +import { ColoredSkeleton } from './ColoredSkeleton.js' +import { faPaintRoller } from '@fortawesome/free-solid-svg-icons' +import { Annotation } from './Annotation.js' +import { useMobileNavigation } from '../util/navigation.js' + +const JOB_HISTORY_LIMIT = 3 + +export const Navigation: FunctionComponent<{ + onNavigate?: () => void +}> = (props) => { + const isMobileNavigation = useMobileNavigation() + + return ( + + ) +} + +const NavigationJobsSection: FunctionComponent<{ + onNavigate?: () => void +}> = (props) => { + const { loading, data: jobs } = useApiSubscription({ interval: 5_000 }, api.jobs) + + return ( + <> +
+ Past/Active jobs: +
+ + {!loading + ? jobs?.slice(0, JOB_HISTORY_LIMIT).map((job) => ( + + )) + : [...Array(JOB_HISTORY_LIMIT)].map((_, i) => ( + + )) + } + {!loading + ? ( + + View all jobs ({jobs?.length}) + + ) + : ( +
+ +
+ )} + + ) +} + +const NavigationItem: FunctionComponent void +}>> = (props) => { + return ( + clsx( + 'block mb-2 px-4 py-2 border-2 border-transparent rounded', + isActive ? 'bg-[#63656a]' : 'bg-[#43454a] hocus:border-[#fff4]' + )} + onClick={() => props.onNavigate?.()} + > + {props.children} + + ) +} + +const JobNavigationItem: FunctionComponent<{ + namespace: string | undefined + name: string | undefined + manual: boolean + status: 'success' | 'failure' | 'active' | undefined + startTime: string | undefined + onNavigate?: () => void +}> = (props) => { + if (props.namespace == null || props.name == null) { + return ( +
+
+ +
+ +
+
+ +
+ ) + } + + return ( + +
+ + {props.namespace}/{props.name} +
+
+ {props.startTime != null ? DateTime.fromISO(props.startTime).toRelative() : } + {props.manual && ( + + )} +
+
+ ) +} diff --git a/frontend/src/components/StatusDot.tsx b/frontend/src/components/StatusDot.tsx new file mode 100644 index 0000000..4ac6ba6 --- /dev/null +++ b/frontend/src/components/StatusDot.tsx @@ -0,0 +1,25 @@ +import { FunctionComponent } from 'react' +import clsx from 'clsx' + +export const StatusDot: FunctionComponent<{ + status: 'success' | 'failure' | 'active' | undefined +}> = (props) => { + if (props.status === 'active') { + return ( + + + + + ) + } + + return ( + + ) +} diff --git a/frontend/src/components/TextInput.tsx b/frontend/src/components/TextInput.tsx new file mode 100644 index 0000000..679082d --- /dev/null +++ b/frontend/src/components/TextInput.tsx @@ -0,0 +1,16 @@ +import { FunctionComponent } from 'react' + +export const TextInput: FunctionComponent<{ + type?: string + value: string + onChange: (value: string) => void +}> = (props) => { + return ( + props.onChange(event.target.value)} + /> + ) +} diff --git a/frontend/src/components/ToggleSwitch.tsx b/frontend/src/components/ToggleSwitch.tsx new file mode 100644 index 0000000..1de1829 --- /dev/null +++ b/frontend/src/components/ToggleSwitch.tsx @@ -0,0 +1,26 @@ +import { FunctionComponent } from 'react' +import { Switch } from '@headlessui/react' +import clsx from 'clsx' + +export const ToggleSwitch: FunctionComponent<{ + value: boolean + onChange: (value: boolean) => void +}> = (props) => { + return ( + + + + ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..3209e1e --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,24 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + position: relative; + margin: 0; + font-family: + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: #1e1f22; + color: #fff; +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..1960356 --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,24 @@ +import './index.css' +import '@fortawesome/fontawesome-svg-core/styles.css' +import { createRoot } from 'react-dom/client' +import { StrictMode } from 'react' +import { App } from './App.js' +import { Settings } from 'luxon' +import { config } from '@fortawesome/fontawesome-svg-core' + +// Disable injecting the CSS into the document since it is blocked by CSP. It is already built by Vite via an import. +config.autoAddCss = false + +// This app is in English, so use the en-US locale for date/time formatting. +Settings.defaultLocale = 'en-US' + +const container = document.getElementById('root') +if (container == null) { + throw new Error('missing root') +} + +createRoot(container).render( + + + +) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..7e0091d --- /dev/null +++ b/frontend/src/pages/Home.tsx @@ -0,0 +1,81 @@ +import { FunctionComponent } from 'react' +import { DateTime } from 'luxon' +import { useApiSubscription } from '../api/subscription.js' +import { api } from '../api/api.js' +import { Heading } from '../components/Heading.js' +import { InfoCard } from '../components/InfoCard.js' +import cronstrue from 'cronstrue' +import { JobPanel } from '../components/JobPanel.js' +import { ColoredSkeleton } from '../components/ColoredSkeleton.js' +import { Link } from 'react-router-dom' +import { useMobileNavigation } from '../util/navigation.js' + +const JOB_HISTORY_LIMIT = 3 + +function lowercaseFirstLetter (str: string): string { + // https://github.com/bradymholt/cRonstrue/issues/40 + return str.charAt(0).toLowerCase() + str.slice(1) +} + +export const Home: FunctionComponent = () => { + const { loading, data: cronJob } = useApiSubscription({ interval: 60_000 }, api.cronJob) + + const isMobileNavigation = useMobileNavigation() + + return ( +
+ + Overview + +
+ + {!loading && cronJob != null + ? ( + <> + {DateTime.fromISO(cronJob.nextScheduleTime).toLocaleString(DateTime.DATETIME_MED)}  + ({DateTime.fromISO(cronJob.nextScheduleTime).toRelative()}) + + ) + : null} + + + {!loading && cronJob?.schedule != null + ? `${cronJob.schedule} (${lowercaseFirstLetter(cronstrue.toString((cronJob.schedule)))})` + : null} + +
+ {isMobileNavigation && } +
+ ) +} + +const JobsSection: FunctionComponent = () => { + const { data: jobs, loading } = useApiSubscription({ interval: 5_000 }, api.jobs) + + return ( + <> + + Past/Active jobs + + {!loading + ? jobs?.slice(0, JOB_HISTORY_LIMIT).map((job) => ( + + )) + : [...Array(JOB_HISTORY_LIMIT)].map((_, i) => ( + + )) + } + {!loading + ? ( + + View all jobs ({jobs?.length}) + + ) + : ( +
+ +
+ )} + + ) +} diff --git a/frontend/src/pages/Job.tsx b/frontend/src/pages/Job.tsx new file mode 100644 index 0000000..19b3bc1 --- /dev/null +++ b/frontend/src/pages/Job.tsx @@ -0,0 +1,284 @@ +import { FunctionComponent, useCallback, useMemo, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { useApiSubscription } from '../api/subscription.js' +import { api } from '../api/api.js' +import { StatusDot } from '../components/StatusDot.js' +import { LogDialog } from '../components/LogDialog.js' +import { Heading } from '../components/Heading.js' +import { ColoredSkeleton } from '../components/ColoredSkeleton.js' +import type { JobPodsRoute, JobsItemRoute, PodProgressRoute } from 'backend/routes' +import { faClock, faDownload, faPaintRoller } from '@fortawesome/free-solid-svg-icons' +import Icon from '../components/Icon.js' +import { ErrorMessage } from '../components/ErrorMessage.js' +import { DateTime } from 'luxon' +import { Button } from '../components/Button.js' +import clsx from 'clsx' + +export const Job: FunctionComponent = () => { + const { namespace, name } = useParams() + if (namespace == null || name == null) { + // shouldn't happen - routes are configured to require these params + throw new Error() + } + + const { loading: jobLoading, data: job, error: jobError } = useApiSubscription({ interval: 5_000 }, api.job, { namespace, name }) + const { loading: podsLoading, data: pods, error: podsError } = useApiSubscription({ interval: 5_000 }, api.podsForJob, { namespace, name }) + + // The name of the pod currently shown in the dialog + const [dialogPodName, setDialogPodName] = useState(undefined) + + const dialogPod = pods?.find((pod) => pod.name === dialogPodName) + + return ( +
+ + {job != null ? Job: {job.namespace}/{job.name} : } + + {jobError != null && (Error loading job: {jobError.message})} +
+ {jobLoading && } + {job != null && } +
+ {podsError != null && (Error loading pods: {podsError.message})} +

+ {podsLoading && } + {pods != null && ( + {pods.length === 1 ? 'There is 1 associated pod' : `There are ${pods.length} associated pods`}: + )} +

+ {podsLoading && } + {pods?.map((pod) => ( + setDialogPodName(pod.name)} /> + ))} + {dialogPod != null && ( + setDialogPodName(undefined)} + /> + )} +
+ ) +} + +const JobStatus: FunctionComponent<{ + job: JobsItemRoute['Reply'] +}> = (props) => { + const navigate = useNavigate() + + const retry = useCallback(() => { + if (props.job == null) { + return undefined + } + const params = new URLSearchParams() + if (props.job.settings.repositoryScope != null) { + params.set('repositoryScope', props.job.settings.repositoryScope) + } + if (props.job.settings.debugLogging != null) { + params.set('debugLogging', props.job.settings.debugLogging.toString()) + } + navigate(`/trigger?${params.toString()}`) + }, [props.job, navigate]) + + const common = ( + <> + {props.job.startTime != null && ( +

+ Started: {DateTime.fromISO(props.job.startTime).toLocaleString(DateTime.DATETIME_MED)}  + ({DateTime.fromISO(props.job.startTime).toRelative()}) +

+ )} + {props.job.completionTime != null && ( +

+ Completed: {DateTime.fromISO(props.job.completionTime).toLocaleString(DateTime.DATETIME_MED)}  + ({DateTime.fromISO(props.job.completionTime).toRelative()}) +

+ )} + {props.job.settings.repositoryScope != null && ( +

+ Repository scope: {props.job.settings.repositoryScope} +

+ )} + {props.job.settings.debugLogging != null && ( +

+ Debug logging: {props.job.settings.debugLogging ? 'enabled' : 'disabled'} +

+ )} +
+ +
+ + ) + + if (!props.job.manual) { + return ( +
+

+ + This job was scheduled to run automatically. +

+ {common} +
+ ) + } + + return ( +
+

+ + {props.job.triggeredBy == null && 'This job was triggered manually by an unknown user.'} + {props.job.triggeredBy != null && `This job was triggered manually by ${props.job.triggeredBy.username} (${props.job.triggeredBy.strategy} login).`} +

+ {common} +
+ ) +} + +const Pod: FunctionComponent<{ + pod: JobPodsRoute['Reply'][number] + onViewLogs: () => void +}> = (props) => { + const { pod } = props + + const { loading, data: progress } = useApiSubscription({ + // Poll more frequently while the pod is running. Once it's done, there should be no more changes. + interval: pod.status === 'success' ? (60 * 60 * 1000) : 5_000 + }, api.podProgress, { + namespace: pod.namespace, + name: pod.name + }) + + // A value between 0 and 1 representing the progress of repositories + const progressValue = useMemo(() => { + if (progress == null || progress.length === 0) { + return 0 + } + let value = 0 + for (const item of progress ?? []) { + switch (item.state) { + case 'started': + value += 1 + break + case 'finished': + value += 2 + break + } + } + return value / (progress.length * 2) + }, [progress]) + + return ( +
+ {/* name and actions */} +
+
+ + {pod.namespace}/{pod.name} +
+
+ + + + +
+
+ {/* progress summary */} + {!loading && progress != null + ? ( + <> +
+
+
+
+ {progress.map((item) => ( + + ))} +
+ + ) + :
+ +
+ } +
+ ) +} + +const DummyPod: FunctionComponent = () => { + return ( +
+ {/* name and actions */} +
+
+ + +
+
+ +

+ +

+
+
+ {/* progress summary */} +
+ +
+
+ ) +} + +const ProgressItem: FunctionComponent<{ + item: PodProgressRoute['Reply'][number] | undefined +}> = ({ item }) => { + if (item == null) { + return ( + + ) + } + + return ( +
+ + {item.state} + + {item.repositoryUrl != null + ? ( + + {item.repository} + + ) + : item.repository + } +
+ ) +} diff --git a/frontend/src/pages/Jobs.tsx b/frontend/src/pages/Jobs.tsx new file mode 100644 index 0000000..5b3a429 --- /dev/null +++ b/frontend/src/pages/Jobs.tsx @@ -0,0 +1,25 @@ +import { FunctionComponent } from 'react' +import { useApiSubscription } from '../api/subscription.js' +import { api } from '../api/api.js' +import { Heading } from '../components/Heading.js' +import { JobPanel } from '../components/JobPanel.js' + +export const Jobs: FunctionComponent = () => { + const { loading, data: jobs } = useApiSubscription({ interval: 5_000 }, api.jobs) + + return ( +
+ + Job History + + {!loading + ? jobs?.map((job) => ( + + )) + : [...Array(8)].map((_, i) => ( + + )) + } +
+ ) +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..f73a520 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,115 @@ +import { FormEvent, FunctionComponent, PropsWithChildren, useCallback, useEffect, useState } from 'react' +import { Label } from '../components/Label.js' +import { TextInput } from '../components/TextInput.js' +import { Button } from '../components/Button.js' +import { useApiDispatch } from '../api/dispatch.js' +import { api } from '../api/api.js' +import { Heading } from '../components/Heading.js' +import { useSearchParams } from 'react-router-dom' +import { useApiSubscription } from '../api/subscription.js' +import { ErrorMessage } from '../components/ErrorMessage.js' + +export const Login: FunctionComponent = () => { + const [searchParams] = useSearchParams() + const error = searchParams.get('login_error') ?? undefined + + const authStrategies = useApiSubscription({ interval: 60_000 }, api.authStrategies) + + return ( +
+ + Login + + {authStrategies.data != null && ( +
+ {authStrategies.data.length === 0 && ( +
+ No login methods are available. +
+ )} + {authStrategies.data.includes('oidc') && } + {authStrategies.data.includes('local') && } +
+ )} +
+ ) +} + +const LoginOption: FunctionComponent> = (props) => { + return ( +
+

+ {props.title} +

+ {props.children} +
+ ) +} + +const OidcLogin: FunctionComponent<{ + hasError?: boolean +}> = (props) => { + return ( + +
+ {props.hasError === true && } + +
+
+ ) +} + +const LocalLogin: FunctionComponent<{ + hasError?: boolean +}> = (props) => { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + + const { dispatch: dispatchLocalLogin, data: localLoginResult, error: loginError, inProgress } = useApiDispatch(api.localLogin) + + const onSubmit = useCallback((event: FormEvent): void => { + event.preventDefault() + dispatchLocalLogin({ + username: username.trim(), + password + }) + }, [dispatchLocalLogin, username, password]) + + // reload page when login is successful + useEffect(() => { + if (localLoginResult != null) { + window.location.reload() + } + }, [localLoginResult]) + + const isValid = username.trim() !== '' && password !== '' + + return ( + +
+ {(loginError != null || props.hasError === true) && } + + + + +
+ ) +} + +const LoginFailureMessage: FunctionComponent = () => { + return ( + + Login failed. Please try again. + + ) +} diff --git a/frontend/src/pages/Trigger.tsx b/frontend/src/pages/Trigger.tsx new file mode 100644 index 0000000..6a216b8 --- /dev/null +++ b/frontend/src/pages/Trigger.tsx @@ -0,0 +1,75 @@ +import { FormEvent, FunctionComponent, useCallback, useEffect, useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' +import { useApiDispatch } from '../api/dispatch.js' +import { api, TriggerJobOptions } from '../api/api.js' +import { Heading } from '../components/Heading.js' +import { Label } from '../components/Label.js' +import { TextInput } from '../components/TextInput.js' +import { InputInfo } from '../components/InputInfo.js' +import { Button } from '../components/Button.js' +import { ToggleSwitch } from '../components/ToggleSwitch.js' + +export const Trigger: FunctionComponent = () => { + const navigate = useNavigate() + + const [repositoryScope, setRepositoryScope] = useState('') + const [debugLogging, setDebugLogging] = useState(false) + + const [searchParams] = useSearchParams() + const initialRepositoryScope = searchParams.get('repositoryScope') ?? '' + const initialDebugLogging = searchParams.get('debugLogging') === 'true' + useEffect(() => { + setRepositoryScope(initialRepositoryScope) + setDebugLogging(initialDebugLogging) + }, [initialRepositoryScope, initialDebugLogging]) + + // repositoryScope may not: start or end with a slash, contain two consecutive slashes, or contain invalid characters + const isValid = repositoryScope === '' || /^[a-z0-9-]+(\/[a-z0-9-]+)*$/i.test(repositoryScope) + + const { dispatch: dispatchTriggerJob, data: jobData, inProgress } = useApiDispatch(api.triggerJob) + + const onTrigger = useCallback((event: FormEvent): void => { + event.preventDefault() + const options: TriggerJobOptions = { + debugLogging + } + if (repositoryScope !== '') { + options.repositoryScope = repositoryScope + } + dispatchTriggerJob(options) + }, [dispatchTriggerJob, repositoryScope, debugLogging]) + + // redirect to job page when jobData is available + useEffect(() => { + if (jobData != null) { + navigate(`/jobs/${encodeURIComponent(jobData.namespace)}/${encodeURIComponent(jobData.name)}`) + } + }, [jobData, navigate]) + + return ( +
+ + Custom run + +
+ + + +
+
+ ) +} diff --git a/frontend/src/util/navigation.ts b/frontend/src/util/navigation.ts new file mode 100644 index 0000000..f94558d --- /dev/null +++ b/frontend/src/util/navigation.ts @@ -0,0 +1,10 @@ +import { useMediaQuery } from '@uidotdev/usehooks' + +/** + * A hook that returns whether the mobile navigation experience should be used. + */ +export function useMobileNavigation (): boolean { + // 768px is the breakpoint for Tailwind's "md" screen size. + // The negation (vs. using max-width) is intentional to match Tailwind when the screen is exactly 768px wide. + return !useMediaQuery('only screen and (min-width: 768px)') +} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..a165882 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,18 @@ +import plugin from 'tailwindcss/plugin' +import { Config } from 'tailwindcss' + +export default { + content: [ + './index.html', + './src/**/*.{js,ts,jsx,tsx}' + ], + theme: { + extend: {} + }, + plugins: [ + plugin(({ addVariant }) => { + // This enables e.g. `hocus:underline`, to avoid having to write `hover:underline focus:underline`. + addVariant('hocus', ['&:hover', '&:focus']) + }) + ] +} satisfies Config diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..b53758b --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/frontend/tsconfig.lint.json b/frontend/tsconfig.lint.json new file mode 100644 index 0000000..7d3a9f2 --- /dev/null +++ b/frontend/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src", + "vite.config.ts", + "tailwind.config.ts" + ] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..453290f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from 'tailwindcss' +import autoprefixer from 'autoprefixer' +// @ts-expect-error - no typings for vite-plugin-favicons-inject +import vitePluginFaviconsInject from 'vite-plugin-favicons-inject' + +export default defineConfig({ + plugins: [ + react(), + vitePluginFaviconsInject('public/favicon.svg', { + appName: 'Foreman', + appShortName: 'Foreman', + appDescription: 'Foreman - A Renovate web UI' + }) + ], + css: { + postcss: { + plugins: [ + tailwindcss(), + autoprefixer() + ] + } + } +}) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7619784 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5642 @@ +{ + "name": "@contane/foreman", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@contane/foreman", + "version": "0.0.0", + "license": "Apache-2.0", + "workspaces": [ + "backend", + "frontend" + ], + "dependencies": { + "@fastify/passport": "2.5.0", + "@fastify/secure-session": "7.5.1", + "@fastify/static": "7.0.4", + "@kubernetes/client-node": "0.21.0", + "cron-parser": "4.9.0", + "deepmerge": "4.3.1", + "dotenv": "16.4.5", + "fastify": "4.28.1", + "ms": "2.1.3", + "openid-client": "5.6.5", + "passport-local": "1.0.0", + "pino": "9.2.0", + "react-router-dom": "6.24.1", + "superstruct": "2.0.0", + "yaml": "2.4.5" + }, + "devDependencies": { + "@types/ms": "0.7.34", + "@types/node": "20.14.10", + "@types/passport-local": "1.0.38", + "typescript": "5.5.3" + }, + "engines": { + "node": ">=20", + "npm": ">=9" + } + }, + "backend": { + "version": "0.0.0", + "dependencies": { + "pino-pretty": "11.2.1", + "secure-json-parse": "2.7.0" + } + }, + "frontend": { + "version": "0.0.0", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@fortawesome/react-fontawesome": "0.2.2", + "@headlessui/react": "2.1.2", + "@uidotdev/usehooks": "2.4.1", + "clsx": "2.1.1", + "cronstrue": "2.50.0", + "luxon": "3.4.4", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-loading-skeleton": "3.4.0" + }, + "devDependencies": { + "@types/luxon": "3.4.2", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "@vitejs/plugin-react": "4.3.1", + "autoprefixer": "10.4.19", + "postcss": "8.4.39", + "tailwindcss": "3.4.4", + "vite": "5.3.3", + "vite-plugin-favicons-inject": "2.2.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", + "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/cookie": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-9.3.1.tgz", + "integrity": "sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==", + "dependencies": { + "cookie-signature": "^1.1.0", + "fastify-plugin": "^4.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/flash": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@fastify/flash/-/flash-5.2.0.tgz", + "integrity": "sha512-mVPXZmX8PgNHWhqasN5He/Cz68YSxXuZ732Uqd8HZ8Rjtkhk8aqNU6v54NFmxzACNI4dLtXP4yj1BogdaySyRw==", + "dependencies": { + "fastify-plugin": "^4.0.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@fastify/passport": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@fastify/passport/-/passport-2.5.0.tgz", + "integrity": "sha512-C7FFT4XMjjDFQDDDNZYfciDpCC/BcEy+PBI0RgSJTrRtrDhvDxbQXCiFV6ls5wnGFfz+fIkUYfYzRP2HPv+pBw==", + "dependencies": { + "@fastify/flash": "^5.0.0", + "fastify-plugin": "^4.0.0" + } + }, + "node_modules/@fastify/secure-session": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@fastify/secure-session/-/secure-session-7.5.1.tgz", + "integrity": "sha512-3xnTmlI4rhboyLtXtGDnrAXz8MLbrkwn9Owo4CZ2/GC9H3Eux0D5QWwKowxmn4VaFadj2wC67RoycFFi/46xaw==", + "dependencies": { + "@fastify/cookie": "^9.0.4", + "fastify-plugin": "^4.0.0", + "sodium-native": "^4.0.0" + }, + "bin": { + "secure-session": "genkey.js" + } + }, + "node_modules/@fastify/send": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", + "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "2.0.0", + "mime": "^3.0.0" + } + }, + "node_modules/@fastify/static": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz", + "integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==", + "dependencies": { + "@fastify/accept-negotiator": "^1.0.0", + "@fastify/send": "^2.0.0", + "content-disposition": "^0.5.3", + "fastify-plugin": "^4.0.0", + "fastq": "^1.17.0", + "glob": "^10.3.4" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", + "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==", + "dependencies": { + "@floating-ui/utils": "^0.2.4" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz", + "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.4" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.19.tgz", + "integrity": "sha512-Jk6zITdjjIvjO/VdQFvpRaD3qPwOHH6AoDHxjhpy+oK4KFgaSP871HYWUAPdnLmx1gQ+w/pB312co3tVml+BXA==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.1", + "@floating-ui/utils": "^0.2.4", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", + "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==" + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, + "node_modules/@headlessui/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.2.tgz", + "integrity": "sha512-Kb3hgk9gRNRcTZktBrKdHhF3xFhYkca1Rk6e1/im2ENf83dgN54orMW0uSKTXFnUpZOUFZ+wcY05LlipwgZIFQ==", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@tanstack/react-virtual": "^3.8.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", + "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", + "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", + "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", + "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", + "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", + "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", + "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", + "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", + "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", + "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", + "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", + "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.31", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", + "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", + "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", + "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", + "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.1.1" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", + "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", + "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kubernetes/client-node": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.21.0.tgz", + "integrity": "sha512-yYRbgMeyQbvZDHt/ZqsW3m4lRefzhbbJEuj8sVXM+bufKrgmzriA2oq7lWPH/k/LQIicAME9ixPUadTrxIF6dQ==", + "dependencies": { + "@types/js-yaml": "^4.0.1", + "@types/node": "^20.1.1", + "@types/request": "^2.47.1", + "@types/ws": "^8.5.3", + "byline": "^5.0.0", + "isomorphic-ws": "^5.0.0", + "js-yaml": "^4.1.0", + "jsonpath-plus": "^8.0.0", + "request": "^2.88.0", + "rfc4648": "^1.3.0", + "stream-buffers": "^3.0.2", + "tar": "^7.0.0", + "tslib": "^2.4.1", + "ws": "^8.11.0" + }, + "optionalDependencies": { + "openid-client": "^5.3.0" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "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, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.1.tgz", + "integrity": "sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ==", + "dependencies": { + "@react-aria/interactions": "^3.21.3", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.21.3", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.3.tgz", + "integrity": "sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==", + "dependencies": { + "@react-aria/ssr": "^3.9.4", + "@react-aria/utils": "^3.24.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz", + "integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.1.tgz", + "integrity": "sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==", + "dependencies": { + "@react-aria/ssr": "^3.9.4", + "@react-stately/utils": "^3.10.1", + "@react-types/shared": "^3.23.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.1.tgz", + "integrity": "sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/shared": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.1.tgz", + "integrity": "sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.1.tgz", + "integrity": "sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.8.1.tgz", + "integrity": "sha512-dP5a7giEM4BQWLJ7K07ToZv8rF51mzbrBMkf0scg1QNYuFx3utnPUBPUHdzaowZhIez1K2XS78amuzD+YGRA5Q==", + "dependencies": { + "@tanstack/virtual-core": "3.8.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.8.1.tgz", + "integrity": "sha512-uNtAwenT276M9QYCjTBoHZ8X3MUeCRoGK59zPi92hMIxdfS9AyHjkDWJ94WroDxnv48UE+hIeo21BU84jKc8aQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" + }, + "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, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/passport": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", + "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "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 + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@uidotdev/usehooks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/ajv": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "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==" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/avvio": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.2.tgz", + "integrity": "sha512-st8e519GWHa/azv8S87mcJvZs4WsgTBjOw/Ih1CP6u+8SZvcOeAYNG6JbsIrAUUJJ7JfmrnOkR8ipDS+u9SIRQ==", + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==" + }, + "node_modules/backend": { + "resolved": "backend", + "link": true + }, + "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==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001640", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", + "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", + "dev": true, + "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" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "engines": { + "node": ">=18" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "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==", + "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==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", + "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cronstrue": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.50.0.tgz", + "integrity": "sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==", + "bin": { + "cronstrue": "bin/cli.js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.818", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz", + "integrity": "sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "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==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "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.4" + }, + "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, + "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==" + }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/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==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "engines": { + "node": ">=6" + } + }, + "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==" + }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==" + }, + "node_modules/fastify": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.28.1.tgz", + "integrity": "sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/favicons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/favicons/-/favicons-7.2.0.tgz", + "integrity": "sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw==", + "dev": true, + "dependencies": { + "escape-html": "^1.0.3", + "sharp": "^0.33.1", + "xml2js": "^0.6.1" + }, + "engines": { + "node": ">=14.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, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.0.tgz", + "integrity": "sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/frontend": { + "resolved": "frontend", + "link": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/har-validator/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/har-validator/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==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "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==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/jackspeak": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.1.tgz", + "integrity": "sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "engines": { + "node": ">=10" + } + }, + "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==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonpath-plus": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-8.1.0.tgz", + "integrity": "sha512-qVTiuKztFGw0dGhYi3WNqvddx3/SHtyDT0xJaeyz4uP0d1tkpG+0y5uYQ4OcIo1TLAz3PE/qDOW9F0uDt3+CTw==", + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/light-my-request": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.13.0.tgz", + "integrity": "sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ==", + "dependencies": { + "cookie": "^0.6.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, + "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, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openid-client": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", + "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "dependencies": { + "jose": "^4.15.5", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.1.tgz", + "integrity": "sha512-9/8QXrtbGeMB6LxwQd4x1tIMnsmUxMvIH/qWGsccz6bt9Uln3S+sgAaqfQNhbGA8ufzs2fHuP/yqapGgP9Hh2g==", + "engines": { + "node": ">=18" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pino": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.2.0.tgz", + "integrity": "sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.2.1.tgz", + "integrity": "sha512-O05NuD9tkRasFRWVaF/uHLOvoRDFD7tb5VMertr78rbsYFjYp48Vg3477EshVAF5eZaEw+OpDl/tu+B0R5o+7g==", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "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" + } + ] + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-loading-skeleton": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz", + "integrity": "sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.1.tgz", + "integrity": "sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==", + "dependencies": { + "@remix-run/router": "1.17.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.1.tgz", + "integrity": "sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==", + "dependencies": { + "@remix-run/router": "1.17.1", + "react-router": "6.24.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfc4648": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz", + "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/rimraf": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.8.tgz", + "integrity": "sha512-XSh0V2/yNhDEi8HwdIefD8MLgs4LQXPag/nEJWs3YUc3Upn+UHa1GyIkEg9xSSNt7HnkO5FjTvmcRzgf+8UZuw==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "dependencies": { + "ret": "~0.4.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "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 + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/sharp": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", + "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.0" + }, + "engines": { + "libvips": ">=8.15.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.4", + "@img/sharp-darwin-x64": "0.33.4", + "@img/sharp-libvips-darwin-arm64": "1.0.2", + "@img/sharp-libvips-darwin-x64": "1.0.2", + "@img/sharp-libvips-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-arm64": "1.0.2", + "@img/sharp-libvips-linux-s390x": "1.0.2", + "@img/sharp-libvips-linux-x64": "1.0.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", + "@img/sharp-libvips-linuxmusl-x64": "1.0.2", + "@img/sharp-linux-arm": "0.33.4", + "@img/sharp-linux-arm64": "0.33.4", + "@img/sharp-linux-s390x": "0.33.4", + "@img/sharp-linux-x64": "0.33.4", + "@img/sharp-linuxmusl-arm64": "0.33.4", + "@img/sharp-linuxmusl-x64": "0.33.4", + "@img/sharp-wasm32": "0.33.4", + "@img/sharp-win32-ia32": "0.33.4", + "@img/sharp-win32-x64": "0.33.4" + } + }, + "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==", + "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==", + "engines": { + "node": ">=8" + } + }, + "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==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sodium-native": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.1.1.tgz", + "integrity": "sha512-LXkAfRd4FHtkQS4X6g+nRcVaN7mWVNepV06phIsC6+IZFvGh1voW5TNQiQp2twVaMf05gZqQjuS+uWLM6gHhNQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.8.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", + "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-buffers": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", + "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==" + }, + "node_modules/string-width-cjs/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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "engines": { + "node": ">=8" + } + }, + "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==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/superstruct": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.0.tgz", + "integrity": "sha512-l9HkF2HGmEc4cm0Mc3E08o/lhminlAdDDekqc+m1urG+SK6CL3e0TL0EJ8QyXkDwfS69rgI94GOXLD4qCD+1WA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, + "node_modules/tailwindcss": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", + "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, + "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==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "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" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.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==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", + "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-favicons-inject": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-favicons-inject/-/vite-plugin-favicons-inject-2.2.0.tgz", + "integrity": "sha512-MaqZE3IjwFffIED38yKcI5BKGkV039N6ilhPeOfW4TILzSKMBNd8Q0ehu0mwbl1emWH2tGcuwV3ZIzDKfb0vIQ==", + "dev": true, + "dependencies": { + "favicons": "^7.0.0-beta.4" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==" + }, + "node_modules/wrap-ansi-cjs/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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8dc29f5 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "@contane/foreman", + "version": "0.0.0", + "description": "A Kubernetes application for managing Renovate jobs.", + "author": "Contane", + "license": "Apache-2.0", + "private": true, + "type": "module", + "main": "dist/server.js", + "workspaces": [ + "backend", + "frontend" + ], + "scripts": { + "build": "npm run build --workspaces && node -e \"fs.rmSync('./dist',{force:true,recursive:true})\" && tsc", + "lint": "npm run lint --workspaces && tsc --noEmit -p tsconfig.lint.json", + "start": "node --disable-proto=delete dist/server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/contane/foreman.git" + }, + "bugs": { + "url": "https://github.com/contane/foreman/issues" + }, + "homepage": "https://github.com/contane/foreman#readme", + "engines": { + "node": ">=20", + "npm": ">=9" + }, + "devDependencies": { + "@types/ms": "0.7.34", + "@types/node": "20.14.10", + "@types/passport-local": "1.0.38", + "typescript": "5.5.3" + }, + "dependencies": { + "@fastify/passport": "2.5.0", + "@fastify/secure-session": "7.5.1", + "@fastify/static": "7.0.4", + "@kubernetes/client-node": "0.21.0", + "cron-parser": "4.9.0", + "deepmerge": "4.3.1", + "dotenv": "16.4.5", + "fastify": "4.28.1", + "ms": "2.1.3", + "openid-client": "5.6.5", + "passport-local": "1.0.0", + "pino": "9.2.0", + "react-router-dom": "6.24.1", + "superstruct": "2.0.0", + "yaml": "2.4.5" + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5db72dd --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..35b441f --- /dev/null +++ b/src/config.ts @@ -0,0 +1,139 @@ +import { + coerce, + defaulted, + enums, + Infer, + instance, + integer, + literal, + nonempty, + object, + optional, + refine, + string, + type, + union +} from 'superstruct' +import fs from 'node:fs' +import path from 'node:path' +import deepmerge from 'deepmerge' +import assert from 'node:assert' +import ms from 'ms' +import YAML from 'yaml' + +const httpUrlString = refine(nonempty(string()), 'url', (value) => { + try { + const url = new URL(value) + return url.protocol === 'http:' || url.protocol === 'https:' + } catch (e) { + return false + } +}) + +export const timeStringToMs = refine( + coerce(integer(), string(), (value) => ms(value)), + 'positive integer', + (value) => Number.isSafeInteger(value) && value > 0 +) + +const configSchema = object({ + kubeConfig: object({ + source: enums(['in-cluster', 'file']), + context: optional(nonempty(string())) + }), + cronJob: object({ + namespace: nonempty(string()), + name: nonempty(string()) + }), + cookies: object({ + // cookies.key is either undefined or a base64-encoded string, which is decoded to a Buffer + key: optional(coerce(instance(Buffer), nonempty(string()), (value) => Buffer.from(value, 'base64'))), + maxAge: timeStringToMs + }), + auth: object({ + // auth.local is either disabled or enabled with username and password + local: union([ + type({ enabled: literal(false) }), + object({ + enabled: literal(true), + username: defaulted(nonempty(string()), 'admin'), + password: nonempty(string()) + }) + ]), + // auth.oidc is either disabled or enabled with issuer, clientId and clientSecret + oidc: union([ + type({ enabled: literal(false) }), + object({ + enabled: literal(true), + issuer: httpUrlString, + clientId: nonempty(string()), + clientSecret: nonempty(string()), + publicUrl: httpUrlString + }) + ]) + }), + gitlab: object({ + host: optional(httpUrlString) + }) +}) + +export type Config = Infer + +export const defaultConfig: Config = Object.freeze({ + kubeConfig: Object.freeze({ + source: 'in-cluster', + context: undefined + }), + cronJob: Object.freeze({ + namespace: 'renovate', + name: 'renovate' + }), + cookies: Object.freeze({ + key: undefined, + maxAge: ms('24h') + }), + auth: Object.freeze({ + local: Object.freeze({ + enabled: false + }), + oidc: Object.freeze({ + enabled: false + }) + }), + gitlab: Object.freeze({}) +}) + +export async function readConfigDirectory (directory: string): Promise { + const entries = await fs.promises.readdir(directory, { withFileTypes: true }) + const configFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.yaml')) + + // sort by name, so that 01-foo.yaml is read before 02-bar.yaml + configFiles.sort((a, b) => a.name.localeCompare(b.name, 'en-US')) + + if (configFiles.length === 0) { + // ignore missing custom config, simply use default values + return defaultConfig + } + + const contents = await Promise.all(configFiles.map(async (configFile) => { + const stringContent = await fs.promises.readFile(path.join(directory, configFile.name), 'utf8') + return YAML.parse(stringContent) + })) + + return createConfig(...contents) +} + +export function createConfig (...configFiles: unknown[]): Config { + const values = configFiles.map((configFile) => { + assert.ok(configFile != null && typeof configFile === 'object' && !Array.isArray(configFile), 'config must be an object') + return configFile + }) + + const merged = deepmerge.all([defaultConfig, ...values], { + // arrays are overwritten completely, instead of joined + arrayMerge: (dst: any[], src: any[]) => src, + isMergeableObject: (value) => typeof value === 'object' && value != null && !Buffer.isBuffer(value) + }) + + return configSchema.create(merged) +} diff --git a/src/environment.ts b/src/environment.ts new file mode 100644 index 0000000..9921857 --- /dev/null +++ b/src/environment.ts @@ -0,0 +1,16 @@ +// load the .env file +import 'dotenv/config' + +function getEnvOrDefault (name: string, defaultValue: string): string { + const value = process.env[name] + return value == null || value === '' ? defaultValue : value +} + +export function getPort (): number { + const port = getEnvOrDefault('PORT', '8080') + const portNumber = Number.parseInt(port, 10) + if (!Number.isSafeInteger(portNumber) || portNumber < 0 || portNumber > 65535) { + throw new Error(`Invalid PORT: "${port}"`) + } + return portNumber +} diff --git a/src/handle-error.ts b/src/handle-error.ts new file mode 100644 index 0000000..bee8db7 --- /dev/null +++ b/src/handle-error.ts @@ -0,0 +1,33 @@ +import { FastifyError, FastifyReply } from 'fastify' +import { badRequest, internalServerError, payloadTooLarge, unsupportedMediaType } from 'backend' + +/** + * The error handling function that is used. This should be bound before registering any routes to handle their errors + * properly. + * + * @param error The error that occurred. + * @param reply The response object. + */ +export async function handleError (error: FastifyError, reply: FastifyReply): Promise { + // JSON input error (thrown during parsing of the request body) + if (error instanceof SyntaxError && error.statusCode != null && error.statusCode < 500) { + return await badRequest(reply) + } + + // Fastify content type parser errors + switch (error.code) { + case 'FST_ERR_CTP_BODY_TOO_LARGE': + return await payloadTooLarge(reply) + case 'FST_ERR_CTP_INVALID_TYPE': + case 'FST_ERR_CTP_EMPTY_TYPE': + case 'FST_ERR_CTP_INVALID_MEDIA_TYPE': + return await unsupportedMediaType(reply) + case 'FST_ERR_CTP_INVALID_CONTENT_LENGTH': + case 'FST_ERR_CTP_EMPTY_JSON_BODY': + return await badRequest(reply) + } + + // Internal server error + reply.log.error(error, 'request_error') + return await internalServerError(reply) +} diff --git a/src/kube-config.ts b/src/kube-config.ts new file mode 100644 index 0000000..a35fe44 --- /dev/null +++ b/src/kube-config.ts @@ -0,0 +1,47 @@ +import k8s from '@kubernetes/client-node' +import { Config } from './config.js' +import { BaseLogger } from 'pino' + +export function loadKubeConfig (log: BaseLogger, config: Config): k8s.KubeConfig { + const from = config.kubeConfig.source + log.info('kubeconfig_load', { from }) + + const kubeConfig = kubeConfigFromSource(from) + validateKubeConfig(kubeConfig) + + // set the current context if specified + const context: string | undefined = config.kubeConfig.context + if (context != null) { + log.info({ context }, 'kubeconfig_set_context') + if (kubeConfig.getContextObject(context) == null) { + throw new Error(`Unknown KubeConfig context: "${context}"`) + } + kubeConfig.setCurrentContext(context) + } + + log.info({ context: kubeConfig.getCurrentContext() satisfies string }, 'kubeconfig_loaded') + + return kubeConfig +} + +function kubeConfigFromSource (source: Config['kubeConfig']['source']): k8s.KubeConfig { + const kubeConfig = new k8s.KubeConfig() + + switch (source) { + case 'file': + kubeConfig.loadFromDefault() + return kubeConfig + case 'in-cluster': + kubeConfig.loadFromCluster() + return kubeConfig + } +} + +function validateKubeConfig (kubeConfig: k8s.KubeConfig): void { + if (kubeConfig.getCurrentCluster() == null) { + throw new Error('No current cluster in kubeconfig') + } + if (kubeConfig.getContexts().length === 0) { + throw new Error('No contexts in kubeconfig') + } +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..c82f28b --- /dev/null +++ b/src/server.ts @@ -0,0 +1,91 @@ +// this works because 'backend' is listed as a workspace in package.json +import { backend, notFound } from 'backend' + +import { fastifyStatic } from '@fastify/static' +import path from 'node:path' +import { fastify } from 'fastify' +import { Config, readConfigDirectory } from './config.js' +import { getPort } from './environment.js' +import { loadKubeConfig } from './kube-config.js' +import { StructError } from 'superstruct' +import { handleError } from './handle-error.js' +import pino from 'pino' + +const log = pino({ + level: 'info', + // do not log pid and hostname + base: undefined, + // use ISO strings for timestamps instead of milliseconds + timestamp: pino.stdTimeFunctions.isoTime, + // use string levels (e.g., "info") instead of level numbers (e.g., 30) + formatters: { + level: (label) => ({ level: label }), + log: (object) => { + if (object instanceof Error) { + return pino.stdSerializers.errWithCause(object) + } + return object + } + } +}) + +log.info('process_start') + +let config: Config +try { + config = await readConfigDirectory(path.join(process.cwd(), 'config')) +} catch (error) { + if (error instanceof StructError) { + // the value may contain secrets, so don't log it + error.value = undefined + error.branch = [] + } + log.fatal(error, 'config_error') + process.exit(1) +} + +try { + const port = getPort() + const kubeConfig = loadKubeConfig(log, config) + + const app = fastify({ + logger: log + }) + + app.setErrorHandler(async (error, req, reply) => await handleError(error, reply)) + + await app.register(fastifyStatic, { + root: path.join(process.cwd(), 'frontend/dist'), + index: 'index.html', + setHeaders: (res) => { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP + void res.setHeader('Content-Security-Policy', "default-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'") + // https://infosec.mozilla.org/guidelines/web_security#x-frame-options + void res.setHeader('X-Frame-Options', 'DENY') + // https://infosec.mozilla.org/guidelines/web_security#x-content-type-options + void res.setHeader('X-Content-Type-Options', 'nosniff') + // https://infosec.mozilla.org/guidelines/web_security#referrer-policy + void res.setHeader('Referrer-Policy', 'no-referrer') + } + }) + + await app.register(backend(kubeConfig, { + cronJob: config.cronJob, + cookies: config.cookies, + auth: config.auth, + gitlab: config.gitlab + }), { prefix: '/api' }) + + // Serve the frontend for all paths besides the API (SPA) + app.setNotFoundHandler(async (req, reply) => { + if (!req.url.startsWith('/api/') && (req.method === 'GET' || req.method === 'HEAD')) { + return await reply.sendFile('index.html') + } + return await notFound(reply) + }) + + await app.listen({ port, host: '::' }) +} catch (error) { + log.fatal(error, 'uncaught_error') + process.exit(1) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b56f3c6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "lib": ["ES2020"], + "module": "ES2022", + "target": "ES2022", + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "outDir": "./dist" + }, + "include": [ + "src" + ] +} diff --git a/tsconfig.lint.json b/tsconfig.lint.json new file mode 100644 index 0000000..f33590b --- /dev/null +++ b/tsconfig.lint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src", + "test" + ] +}