diff --git a/FACEAI_INTEGRATION_PLAN.md b/FACEAI_INTEGRATION_PLAN.md index b6f4e975..4e7f4622 100644 --- a/FACEAI_INTEGRATION_PLAN.md +++ b/FACEAI_INTEGRATION_PLAN.md @@ -9,9 +9,10 @@ The new app will: - start from the race photo view page - search only within the current race - accept a selfie upload -- show matched race photos with previews -- let the user open and download photos through the existing legacy download flow -- fall back to email delivery when the queue is long or processing is slow +- process the request inside FaceAI +- return the user to the original race page with the results filtered to only the matched images +- keep photo opening and downloading entirely on the legacy site +- use polling only in v1 if processing takes time ## What Exists Today @@ -31,11 +32,15 @@ The legacy site identity is held in the Java web session, not in PHP. That means Because of that, the recommended plan needs one of these two options: -1. Preferred: a very small server-side bridge on the legacy Java side, or at the reverse proxy level with app support, to mint a trusted handoff token for FaceAI. +1. Preferred: a very small server-side bridge on the legacy Java or JSP side, or at the reverse proxy level with app support, to mint a trusted handoff token for FaceAI. 2. Fallback: a separate login flow for FaceAI. If the requirement is strict single sign-on based on the current site session, option 1 is the only realistic path. +There is also an operational constraint from the implementation side: + +The bridge should be designed so it can be deployed without setting up a full local Java development environment and without recompiling the existing application locally. That makes a tiny JSP-based handoff endpoint or a minimal existing-controller extension preferable to adding new compiled Java modules. + ## Recommended Architecture Use three deployable parts: @@ -49,6 +54,7 @@ Use three deployable parts: - Keep authentication, membership checks, and download-credit subtraction as the source of truth. - Launch the FaceAI app from the race page. - Issue a short-lived signed handoff token that identifies the user and race context. +- Accept the final FaceAI match result and turn it into a legacy race-page filter. - Continue to serve original photo downloads so existing counters and permissions remain unchanged. ### 2. FaceAI app responsibilities @@ -58,7 +64,8 @@ Use three deployable parts: - Let the user upload a selfie. - Create a race-scoped search request. - Poll job status or show queued state. -- Render matched photos and route download/open actions back to legacy endpoints. +- Return a stable list of matched legacy photo identifiers. +- Redirect the user back to the legacy race page with a filter payload that reproduces the matched set. - Preserve the page the user came from and offer a one-click return. ### 3. Processing service responsibilities @@ -67,7 +74,7 @@ Use three deployable parts: - Queue requests and process them one by one. - Run the external face-recognition program. - Return match results with confidence and photo ids or file identifiers. -- Mark long-running jobs for async completion and email fallback. +- Return a completed result set usable by the legacy filter handoff. ## Authentication And Cookie Strategy @@ -96,6 +103,16 @@ Instead: This gives the new app shared-domain cookies while avoiding direct dependency on the Java session internals. +### Preferred bridge implementation shape + +Given the local environment constraint, the bridge should preferably be one of these: + +- a tiny JSP endpoint that reads the existing session beans and performs a redirect +- a minimal addition to an already-existing legacy action/controller endpoint +- a reverse-proxy-assisted signed redirect if the platform already supports auth subrequests + +Avoid a plan that requires introducing new compiled Java packages as the first step. + ## Access Check The handoff token should already include whether the feature is allowed. That check should be done on the legacy side where the real account state already exists. @@ -104,8 +121,6 @@ Minimal validation inputs: - logged-in user exists - account is active enough to use the feature -- race is eligible for FaceAI -- optional plan or quota flag for face search access To avoid unnecessary database reads, compute this from already-loaded session/account state when possible. Only hit the database if the existing session object does not contain enough information. @@ -115,14 +130,15 @@ The smallest practical change set on the legacy site is: ### Frontend change -Do not replace the dropdown in JSP markup first. +Remove the old `tipoPuntoFoto` select from the user flow and replace it with the FaceAI launch button. -Instead, update `www/_js/rus-ecom-240621.js` so that on the race page it: +The lowest-risk way to do that is to update `www/_js/rus-ecom-240621.js` so that on the race page it: - detects `#tipoPuntoFoto` -- hides or disables that select for eligible races +- removes that select from the rendered UI - inserts a `Face ID` button in the same area - builds the launch URL using the current race context and current page URL +- carries `raceId`, race description or slug, language, and exact `returnUrl` This avoids fragile JSP layout edits and keeps the change deployable as a single JS asset update. @@ -137,12 +153,23 @@ Add one minimal auth bridge endpoint on the legacy stack. It can be: That endpoint should: - read the current legacy session -- verify the user and access +- verify the user and active-membership access - generate the signed handoff token - redirect to FaceAI If this endpoint truly cannot be added, then single sign-on should be considered blocked and the plan should switch to a separate login flow. +### Legacy return filter change + +The old site needs one small return-integration path so FaceAI can send the user back to the race page showing only the matched images. + +That should be implemented as one of these: + +- a new legacy endpoint that accepts a signed FaceAI result token and loads the matched photo set into the request or session before rendering the race page +- an extension of the existing photo search flow so it can accept a FaceAI result id and fetch the matched photo ids server-side + +This is preferable to putting the matched ids directly in the browser URL, because the result set may be long and should remain tamper-resistant. + ## FaceAI App Structure The requested target folder is `faceai/`. It does not currently exist in this workspace, so this plan assumes it will be created as a new app. @@ -165,31 +192,47 @@ faceai/ ## FaceAI User Flow 1. User opens a race photo page on `www`. -2. The old `tipoPuntoFoto` dropdown is replaced in the browser by a `Face ID` button. +2. The old `tipoPuntoFoto` dropdown is removed from the visible UI and replaced by a `Face ID` button. 3. User clicks the button. 4. Legacy bridge validates session and redirects to FaceAI with signed context. 5. FaceAI shows a page styled like the old site, including a matching header and a clear `Back to race page` action. 6. User uploads a selfie. 7. FaceAI creates a search job with `userId`, `raceId`, `requestId`, and selfie file reference. -8. If the queue is short, FaceAI waits and then shows results. -9. If processing is long, FaceAI tells the user the request will complete by email and stores the result for later retrieval. -10. User opens a matched photo detail or download action. -11. That action goes back through the legacy photo view/download endpoints so the current account checks and photo-count subtraction still apply. +8. FaceAI polls until the processing job completes. +9. Once the result is ready, FaceAI redirects the browser back to the original race page on `www`. +10. The legacy site resolves the matched photo ids and renders the race page filtered to those photos only, similar in spirit to the existing pettorale-based flow. +11. User opens and downloads photos exactly as they do today, through the legacy site. ## Result And Download Strategy -Do not duplicate the download-credit logic in FaceAI. +Do not duplicate either the final photo listing view or the download-credit logic in FaceAI. Instead: -- FaceAI should store and display legacy photo identifiers, not its own download copies. -- When the user clicks a matched photo, FaceAI should open either: - - the existing legacy photo detail modal/page endpoint, or - - a dedicated legacy deep link for that photo -- When the user downloads, the request must end on the legacy side where `user.puoScaricareFoto()` and the existing decrement rules already live. +- FaceAI should store legacy photo identifiers, not its own download copies. +- FaceAI v1 should not become a second gallery UI for final results. +- When matching is complete, FaceAI should hand the result back to the legacy site. +- The legacy site should render the final matched-photo page using its existing race photo UI and existing photo modal/download endpoints. This keeps the business rule in one place and avoids mismatched counters. +### Recommended handoff model for match results + +Use a signed result reference instead of passing the whole match list in the browser. + +Recommended flow: + +1. FaceAI stores the match result under a `resultId`. +2. FaceAI redirects to something like `https://www.regalamiunsorriso.it/faceai-return?...` with: + - `resultId` + - `raceId` + - signed token +3. Legacy endpoint validates the token. +4. Legacy endpoint fetches the matched legacy photo ids from FaceAI or from a shared temporary store. +5. Legacy endpoint renders the normal race page constrained to that id set. + +This avoids URL-length issues and reduces tampering risk. + ## Matching Result Model The processing service should return at least: @@ -204,11 +247,12 @@ The processing service should return at least: Each match should contain: - `photoId` compatible with legacy photo endpoints -- `previewUrl` or enough file info to derive the thumbnail - `score` or confidence - `capturedAt` if known - `puntoFoto` or checkpoint info if available +For v1, `photoId` is the most important field. If the legacy page is the final renderer, thumbnails can remain a legacy concern after redirect. + Race scope is mandatory. The service must never search globally by default. ## Async Processing Design @@ -222,6 +266,7 @@ Use an API plus worker model. - `POST /api/searches` - `GET /api/searches/:id` - `GET /api/searches/:id/results` +- `GET /api/searches/:id/redirect` - `POST /api/searches/:id/cancel` optional ### Internal worker API or queue contract @@ -241,7 +286,7 @@ Output job: - match list - logs - processing duration -- email-required flag +- legacy-renderable result reference ### Queue choice @@ -253,23 +298,23 @@ Any of these are reasonable: For this use case, Redis plus BullMQ is the most pragmatic default. -## Email Fallback +## Polling And Timeout Strategy For V1 -If the job stays queued too long or exceeds a synchronous timeout: +V1 should use polling only and should not send email. -1. FaceAI stores the request in `queued` or `processing` state. -2. Worker completes later. -3. System emails the user with: - - race name - - request id - - summary of result count - - list of photo names or identifiers for the race, as requested - - optional direct link back to the FaceAI results page +Recommended behavior: + +1. FaceAI submits the job. +2. Browser polls `GET /api/searches/:id`. +3. While waiting, FaceAI shows a queue or processing state. +4. When complete, FaceAI redirects to the legacy filtered-result page. Recommended timeout split: -- up to 15 to 30 seconds: keep user on the page with polling -- beyond that: switch to email fallback and let the user leave +- up to 30 seconds: keep the user on the page with polling +- beyond 30 seconds: keep polling but show a clear long-running state and a manual retry or refresh path + +Email can be revisited in a later phase after the core handoff flow is stable. ## Database Usage @@ -283,7 +328,7 @@ Recommended storage responsibilities: - job status - uploaded selfie metadata - result references to legacy photo ids - - audit fields and email status + - audit fields Avoid copying full user profiles or photo business state into the FaceAI database. @@ -298,6 +343,8 @@ Recommended approach: - Show account actions based on FaceAI session state. - Add a prominent `Back to race results` link using the captured `returnUrl`. +For v1, the header should be a very close copy, not just a lightweight brand reference. The goal is that the user should feel they are still inside the same site family during the upload and waiting flow. + This is safer than trying to embed the old JSP header directly into a Node app. ## Security Requirements @@ -307,22 +354,24 @@ This is safer than trying to embed the old JSP header directly into a Node app. - Uploaded selfies should have a short retention period. - Face search results must be visible only to the requesting user. - Queue jobs must be race-scoped and tied to the authenticated user. -- Email contents should avoid exposing direct raw file paths. +- Result handoff back to legacy must be signed and must not trust raw photo ids coming from the browser. ## Rollout Plan ### Phase 1: spike and contracts -- Confirm whether a minimal legacy auth bridge endpoint is possible. +- Confirm that a minimal JSP or existing-controller auth bridge endpoint is possible. - Define the signed token payload. - Define the worker input and output contract. - Confirm which legacy photo id is stable enough to use in FaceAI results. +- Define how legacy will accept a FaceAI result reference and render a filtered race page. ### Phase 2: legacy launch integration -- Update `www/_js/rus-ecom-240621.js` to replace the dropdown with a FaceAI button in the browser. +- Update `www/_js/rus-ecom-240621.js` to remove the dropdown from the UI and insert the FaceAI button. - Add the legacy auth bridge endpoint. - Pass `raceId`, `lang`, and `returnUrl` into the FaceAI launch. +- Add the legacy return endpoint or result-aware race filter path. ### Phase 3: FaceAI app shell @@ -330,42 +379,43 @@ This is safer than trying to embed the old JSP header directly into a Node app. - Implement auth callback and FaceAI session cookie. - Build the legacy-style header and return navigation. - Add selfie upload UI and request status page. +- Implement polling-only job completion flow. ### Phase 4: processing service - Add queue and worker. - Integrate the external face-recognition program. -- Return matched legacy photo ids and previews. +- Return matched legacy photo ids and a stored result reference suitable for legacy rendering. -### Phase 5: download integration +### Phase 5: legacy filtered-results integration -- Deep-link results back to legacy photo view/download endpoints. +- Redirect results back to the legacy race page. +- Verify that the legacy page can render only the matched id set. - Verify that photo-credit subtraction still happens only on successful legacy downloads. -### Phase 6: async completion and email +### Phase 6: optional future enhancements -- Add timeout-based fallback. -- Send race-scoped result emails with photo names and a link back to FaceAI. +- Add email or offline completion flow if polling-only v1 proves insufficient. +- Add richer FaceAI-side previews only if needed after the legacy handoff works reliably. ## Open Questions To Resolve Early -1. Can the legacy site accept one minimal Java or JSP bridge endpoint for SSO handoff? -2. Which exact account rule should control FaceAI access: active membership only, extra entitlement, race flag, or download quota? -3. Which legacy endpoint is the best deep link for opening one photo from FaceAI results? -4. Is the existing session cookie already scoped to `.regalamiunsorriso.it`, or is it host-only today? -5. Should FaceAI results include only downloadable photos, or also visible-but-not-downloadable photos? -6. What is the acceptable selfie retention period for privacy compliance? -7. Should the email contain only photo names, or also signed result links? +1. Which existing legacy endpoint is the best place to implement the FaceAI return filter flow? +2. Should the return flow fetch matched photo ids directly from FaceAI, or from a shared short-lived store? +3. What is the acceptable selfie retention period for privacy compliance? +4. Should long-running polling survive page refresh via persisted request id in the FaceAI session? +5. Does the legacy race page need an explicit visual label that the current listing comes from FaceAI results? ## Recommended First Implementation For the first version, keep the scope strict: - launch from one race page only -- synchronous search if the queue is short -- email fallback if it exceeds the timeout -- result cards with preview plus `Open on Regalami un Sorriso` +- remove `tipoPuntoFoto` from the user-facing race search UI +- polling only, no email +- final results rendered on the legacy race page, not inside FaceAI - all downloads still served by the legacy site - one lightweight auth bridge only +- one lightweight return-filter bridge only This version gives the new experience without moving the fragile parts of the old platform. \ No newline at end of file diff --git a/faceai/.dockerignore b/faceai/.dockerignore new file mode 100644 index 00000000..9d5bfb19 --- /dev/null +++ b/faceai/.dockerignore @@ -0,0 +1,5 @@ +node_modules/ +apps/frontend/node_modules/ +apps/backend/node_modules/ +apps/frontend/dist/ +.env diff --git a/faceai/.env.example b/faceai/.env.example new file mode 100644 index 00000000..0888f410 --- /dev/null +++ b/faceai/.env.example @@ -0,0 +1,8 @@ +PORT=3001 +FACEAI_FRONTEND_URL=http://localhost:5173 +FACEAI_PUBLIC_BASE_URL=http://localhost:3001 +FACEAI_LEGACY_RETURN_URL=http://localhost:3001/dev/legacy/return +FACEAI_ENABLE_LOCAL_LEGACY_STATIC=1 +FACEAI_LOCAL_LEGACY_STATIC_ROOT=k:\various\regalamiunsorriso\www +FACEAI_SHARED_SECRET=change-me +FACEAI_SESSION_COOKIE=rus_faceai_session diff --git a/faceai/.gitignore b/faceai/.gitignore new file mode 100644 index 00000000..6466bba2 --- /dev/null +++ b/faceai/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +apps/frontend/dist/ +.env diff --git a/faceai/README.md b/faceai/README.md new file mode 100644 index 00000000..d9a2762a --- /dev/null +++ b/faceai/README.md @@ -0,0 +1,111 @@ +# FaceAI Scaffold + +This folder scaffolds the new FaceAI app described in the integration plan. + +It includes: + +- a Vue frontend for the FaceAI upload and polling flow +- a Node/Express backend for session exchange, mocked searches, and return handoff +- a local legacy simulator so the launch and return flow can be tested without the old Java site +- a Dockerized PHP Apache stack for exercising the real `www/faceai_handoff.php` and `www/faceai_return.php` bridge files + +## Structure + +```text +faceai/ + apps/ + backend/ + frontend/ + docker/ + Dockerfile +``` + +## What The Local Test Covers + +The local simulator exercises the exact flow the plan is aiming for: + +1. a legacy-like race page shows a `Face ID` button instead of `tipoPuntoFoto` +2. clicking it hits a mock legacy handoff endpoint +3. the backend signs a short-lived handoff token and redirects to the Vue app +4. the Vue app exchanges the token for its own FaceAI session cookie +5. the user uploads a selfie and starts a mocked race-scoped search +6. the frontend polls until the job completes +7. FaceAI requests a signed return URL +8. the browser is redirected back to a legacy-like filtered race page showing only the matched photos + +## Local Run + +From this folder: + +```bash +npm install +npm run dev +``` + +Then open: + +```text +http://localhost:3001/dev/legacy/race?raceId=101&lang=it +``` + +That page simulates the old site and launches the FaceAI app at `http://localhost:5173`. + +## Docker Run With PHP Simulator + +If you do not have PHP locally, use Docker instead: + +```bash +npm install +npm run build +docker compose up --build +``` + +The Docker stack reuses the local FaceAI workspace and only containerizes the runtime services. That means PHP is fully containerized, while the Node service runs inside Docker against the already-installed local workspace dependencies and the already-built frontend assets. + +This starts: + +- FaceAI app on `http://localhost:3001` +- PHP Apache serving `www` on `http://localhost:8080` + +For the end-to-end test through the PHP bridge, open: + +```text +http://localhost:8080/faceai_simulator.php?raceId=101&lang=it +``` + +That page loads the original race-page JavaScript from `www/_js/rus-ecom-240621.js`, lets the script replace the visible `tipoPuntoFoto` selector with the new `Face ID` button, and launches the real PHP handoff bridge at `www/faceai_handoff.php`. + +If you change frontend code and want Docker to serve the updated UI, rebuild first with: + +```bash +npm run build +``` + +## Environment + +Defaults are already set for local development, but these can be overridden: + +```text +PORT=3001 +FACEAI_FRONTEND_URL=http://localhost:5173 +FACEAI_PUBLIC_BASE_URL=http://localhost:3001 +FACEAI_LEGACY_RETURN_URL=http://localhost:3001/dev/legacy/return +FACEAI_SHARED_SECRET=change-me +FACEAI_SESSION_COOKIE=rus_faceai_session +``` + +If you want FaceAI to return through the new PHP bridge prepared under `www`, point `FACEAI_LEGACY_RETURN_URL` to that endpoint instead, for example `http://localhost/faceai_return.php` or the equivalent URL in your local PHP setup. + +In the provided Docker Compose stack, that wiring is already done with: + +```text +FACEAI_LEGACY_RETURN_URL=http://localhost:8080/faceai_return.php +``` + +## Notes + +- The backend currently uses in-memory stores and mocked search results. +- No database or real queue is wired yet. +- The local legacy simulator is intentionally backend-driven so the handoff can be tested without compiling the existing Java application. +- `www/faceai_simulator.php` exists only for local testing. It does not replace the actual JSP race page. +- The final legacy integration still needs a real signed identity source and a real return-filter implementation on the old site. diff --git a/faceai/apps/backend/package.json b/faceai/apps/backend/package.json new file mode 100644 index 00000000..a516e935 --- /dev/null +++ b/faceai/apps/backend/package.json @@ -0,0 +1,15 @@ +{ + "name": "@regalami/faceai-backend", + "private": true, + "type": "module", + "scripts": { + "dev": "node --watch src/server.js", + "build": "node -e \"console.log('backend build not required')\"", + "start": "node src/server.js" + }, + "dependencies": { + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "express": "^4.21.2" + } +} diff --git a/faceai/apps/backend/src/auth.js b/faceai/apps/backend/src/auth.js new file mode 100644 index 00000000..eb4a3284 --- /dev/null +++ b/faceai/apps/backend/src/auth.js @@ -0,0 +1,40 @@ +import crypto from 'node:crypto'; + +function base64UrlEncode(input) { + return Buffer.from(input).toString('base64url'); +} + +function base64UrlDecode(input) { + return Buffer.from(input, 'base64url').toString('utf8'); +} + +export function signPayload(payload, secret) { + const body = base64UrlEncode(JSON.stringify(payload)); + const signature = crypto.createHmac('sha256', secret).update(body).digest('base64url'); + return `${body}.${signature}`; +} + +export function verifySignedPayload(token, secret) { + if (!token || !token.includes('.')) { + throw new Error('Invalid token format'); + } + + const [body, signature] = token.split('.'); + const expected = crypto.createHmac('sha256', secret).update(body).digest('base64url'); + + if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { + throw new Error('Invalid token signature'); + } + + const payload = JSON.parse(base64UrlDecode(body)); + + if (payload.expiresAt && Date.now() > payload.expiresAt) { + throw new Error('Token expired'); + } + + return payload; +} + +export function randomId(prefix) { + return `${prefix}_${crypto.randomBytes(8).toString('hex')}`; +} diff --git a/faceai/apps/backend/src/config.js b/faceai/apps/backend/src/config.js new file mode 100644 index 00000000..8dcb1d23 --- /dev/null +++ b/faceai/apps/backend/src/config.js @@ -0,0 +1,18 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const defaultLocalLegacyRoot = path.resolve(__dirname, '../../../../www'); + +export const config = { + port: Number(process.env.PORT || 3001), + frontendUrl: process.env.FACEAI_FRONTEND_URL || 'http://localhost:5173', + publicBaseUrl: process.env.FACEAI_PUBLIC_BASE_URL || 'http://localhost:3001', + legacyReturnUrl: process.env.FACEAI_LEGACY_RETURN_URL || 'http://localhost:3001/dev/legacy/return', + enableLocalLegacyStatic: process.env.FACEAI_ENABLE_LOCAL_LEGACY_STATIC + ? process.env.FACEAI_ENABLE_LOCAL_LEGACY_STATIC === '1' + : process.env.NODE_ENV !== 'production', + localLegacyStaticRoot: process.env.FACEAI_LOCAL_LEGACY_STATIC_ROOT || defaultLocalLegacyRoot, + sharedSecret: process.env.FACEAI_SHARED_SECRET || 'change-me', + sessionCookieName: process.env.FACEAI_SESSION_COOKIE || 'rus_faceai_session' +}; diff --git a/faceai/apps/backend/src/server.js b/faceai/apps/backend/src/server.js new file mode 100644 index 00000000..6db1f221 --- /dev/null +++ b/faceai/apps/backend/src/server.js @@ -0,0 +1,356 @@ +import express from 'express'; +import cors from 'cors'; +import cookieParser from 'cookie-parser'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { config } from './config.js'; +import { signPayload, verifySignedPayload } from './auth.js'; +import { createSession, createSearch, completeSearch, getResult, getSearch, getSession, mockCatalog } from './store.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const frontendDist = path.resolve(__dirname, '../../frontend/dist'); +const app = express(); + +app.use(cookieParser()); +app.use(express.json()); +if (config.enableLocalLegacyStatic && fs.existsSync(config.localLegacyStaticRoot)) { + app.use('/legacy-static', express.static(config.localLegacyStaticRoot)); +} else { + app.use('/legacy-static', (req, res) => { + res.status(404).type('text/plain').send('Legacy static assets are not configured in this environment.'); + }); +} +app.use(cors({ + origin: config.frontendUrl, + credentials: true +})); + +function getFaceAiSession(req) { + const sessionId = req.cookies[config.sessionCookieName]; + return sessionId ? getSession(sessionId) : null; +} + +function requireSession(req, res, next) { + const session = getFaceAiSession(req); + if (!session) { + res.status(401).json({ error: 'Not authenticated with FaceAI' }); + return; + } + + req.faceaiSession = session; + next(); +} + +function issueHandoffToken({ raceId, raceSlug, lang, returnUrl }) { + const race = mockCatalog[raceId] || { id: raceId, slug: raceSlug || `race-${raceId}`, name: raceSlug || `Race ${raceId}` }; + + return signPayload({ + type: 'handoff', + user: { + id: 'legacy-user-1', + displayName: 'Mario Rossi', + email: 'mario.rossi@example.test', + membershipStatus: 'active' + }, + race: { + id: race.id, + slug: race.slug, + name: race.name + }, + lang: lang || 'it', + returnUrl, + expiresAt: Date.now() + 5 * 60 * 1000 + }, config.sharedSecret); +} + +function issueReturnToken(result) { + return signPayload({ + type: 'return', + resultId: result.id, + raceId: result.raceId, + userId: result.userId, + expiresAt: Date.now() + 5 * 60 * 1000 + }, config.sharedSecret); +} + +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function renderLegacyRacePage({ raceId, lang = 'it', result = null }) { + const race = mockCatalog[raceId] || { id: raceId, name: `Race ${raceId}`, slug: `race-${raceId}`, photos: [] }; + const returnUrl = `${config.publicBaseUrl}/dev/legacy/race?raceId=${encodeURIComponent(race.id)}&lang=${encodeURIComponent(lang)}`; + const photos = result ? result.matches : race.photos; + const banner = result + ? `
Vista filtrata da FaceAI. Trovate ${photos.length} foto per l'utente corrente.
` + : '
Pagina gara simulata per il test locale del handoff FaceAI.
'; + + const photoList = photos.length + ? photos.map((photo) => ` +
  • +
    ${escapeHtml(photo.thumb || photo.id)}
    +
    + ${escapeHtml(photo.label)} + ID foto: ${escapeHtml(photo.id)} + Punto foto: ${escapeHtml(photo.checkpoint || '-')} +
    +
  • + `).join('') + : '
  • Nessuna foto disponibile.
  • '; + + return ` + + + + + Legacy Race Simulator + + + +
    +
    Regalami un Sorriso ETS
    +
    Utente simulato: Mario Rossi
    +
    +
    +

    ${escapeHtml(race.name)}

    +
    + + + +
    + ${banner} +

    ${result ? 'La pagina mostra solo le foto restituite da FaceAI.' : 'In questa simulazione il vecchio select tipoPuntoFoto รจ sostituito dal pulsante Face ID.'}

    + +
    + + + `; +} + +app.get('/health', (req, res) => { + res.json({ ok: true }); +}); + +app.get('/dev/legacy/race', (req, res) => { + const raceId = String(req.query.raceId || '101'); + const lang = String(req.query.lang || 'it'); + res.type('html').send(renderLegacyRacePage({ raceId, lang })); +}); + +app.get('/dev/legacy/launch', (req, res) => { + const raceId = String(req.query.raceId || '101'); + const raceSlug = String(req.query.raceSlug || mockCatalog[raceId]?.slug || `race-${raceId}`); + const lang = String(req.query.lang || 'it'); + const returnUrl = String(req.query.returnUrl || `${config.publicBaseUrl}/dev/legacy/race?raceId=${encodeURIComponent(raceId)}&lang=${encodeURIComponent(lang)}`); + const token = issueHandoffToken({ raceId, raceSlug, lang, returnUrl }); + res.redirect(`${config.frontendUrl}/auth/callback?token=${encodeURIComponent(token)}`); +}); + +app.get('/dev/legacy/return', (req, res) => { + try { + const token = String(req.query.token || ''); + const payload = verifySignedPayload(token, config.sharedSecret); + if (payload.type !== 'return') { + throw new Error('Wrong token type'); + } + + const result = getResult(String(req.query.resultId || payload.resultId)); + if (!result || result.userId !== payload.userId) { + throw new Error('Result not found'); + } + + res.type('html').send(renderLegacyRacePage({ raceId: result.raceId, lang: result.lang || 'it', result })); + } catch (error) { + res.status(400).type('html').send(`

    Return handoff failed

    ${escapeHtml(error.message)}

    `); + } +}); + +app.post('/api/auth/exchange', (req, res) => { + try { + const { token } = req.body; + const payload = verifySignedPayload(token, config.sharedSecret); + if (payload.type !== 'handoff') { + throw new Error('Wrong token type'); + } + + const sessionId = createSession({ + user: payload.user, + race: payload.race, + lang: payload.lang, + returnUrl: payload.returnUrl, + access: { + faceAiAllowed: payload.user.membershipStatus === 'active' + } + }); + + res.cookie(config.sessionCookieName, sessionId, { + httpOnly: true, + sameSite: 'lax', + secure: false, + path: '/' + }); + + res.json({ + user: payload.user, + race: payload.race, + lang: payload.lang, + returnUrl: payload.returnUrl, + access: { + faceAiAllowed: true + } + }); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +app.get('/api/session', requireSession, (req, res) => { + res.json(req.faceaiSession); +}); + +app.post('/api/searches', requireSession, (req, res) => { + const raceId = String(req.body.raceId || req.faceaiSession.race.id); + const selfieName = String(req.body.selfieName || 'selfie.jpg'); + + const search = createSearch({ + raceId, + selfieName, + user: req.faceaiSession.user, + returnUrl: req.faceaiSession.returnUrl, + lang: req.faceaiSession.lang + }); + + setTimeout(() => { + completeSearch(search.id); + }, 3500); + + res.status(201).json({ + id: search.id, + status: search.status, + raceId: search.raceId, + selfieName: search.selfieName + }); +}); + +app.get('/api/searches/:id', requireSession, (req, res) => { + const search = getSearch(req.params.id); + if (!search || search.user.id !== req.faceaiSession.user.id) { + res.status(404).json({ error: 'Search not found' }); + return; + } + + res.json({ + id: search.id, + status: search.status, + raceId: search.raceId, + resultId: search.resultId, + createdAt: search.createdAt, + completedAt: search.completedAt, + matchCount: search.matches.length + }); +}); + +app.get('/api/searches/:id/redirect', requireSession, (req, res) => { + const search = getSearch(req.params.id); + if (!search || search.user.id !== req.faceaiSession.user.id) { + res.status(404).json({ error: 'Search not found' }); + return; + } + + if (search.status !== 'completed' || !search.resultId) { + res.status(409).json({ error: 'Search not completed yet' }); + return; + } + + const result = getResult(search.resultId); + const token = issueReturnToken(result); + + res.json({ + url: `${config.legacyReturnUrl}?resultId=${encodeURIComponent(result.id)}&token=${encodeURIComponent(token)}` + }); +}); + +app.get('/bridge/results/:id', (req, res) => { + try { + const token = String(req.query.token || ''); + const payload = verifySignedPayload(token, config.sharedSecret); + if (payload.type !== 'return') { + throw new Error('Wrong token type'); + } + + if (String(payload.resultId || '') !== String(req.params.id)) { + throw new Error('Result id mismatch'); + } + + const result = getResult(req.params.id); + if (!result || result.userId !== payload.userId) { + throw new Error('Result not found'); + } + + res.json({ + id: result.id, + raceId: result.raceId, + raceName: result.raceName, + userId: result.userId, + returnUrl: result.returnUrl, + lang: result.lang, + matches: result.matches + }); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +if (fs.existsSync(frontendDist)) { + app.use(express.static(frontendDist)); + app.get('*', (req, res, next) => { + if (req.path.startsWith('/api/') || req.path.startsWith('/dev/')) { + next(); + return; + } + res.sendFile(path.join(frontendDist, 'index.html')); + }); +} + +app.listen(config.port, () => { + console.log(`FaceAI backend listening on http://localhost:${config.port}`); +}); diff --git a/faceai/apps/backend/src/store.js b/faceai/apps/backend/src/store.js new file mode 100644 index 00000000..dd25fc4a --- /dev/null +++ b/faceai/apps/backend/src/store.js @@ -0,0 +1,110 @@ +import { randomId } from './auth.js'; + +export const mockCatalog = { + '101': { + id: '101', + slug: 'mezza-di-firenze', + name: 'Mezza di Firenze', + photos: [ + { id: 'f101-001', label: 'Arrivo 001', bib: '245', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-001.jpg' }, + { id: 'f101-002', label: 'Arrivo 002', bib: '245', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-002.jpg' }, + { id: 'f101-003', label: 'Ponte 003', bib: '245', checkpoint: 'Ponte', thumb: 'thumb-ponte-003.jpg' }, + { id: 'f101-004', label: 'Centro 004', bib: '245', checkpoint: 'Centro', thumb: 'thumb-centro-004.jpg' }, + { id: 'f101-005', label: 'Centro 005', bib: '812', checkpoint: 'Centro', thumb: 'thumb-centro-005.jpg' }, + { id: 'f101-006', label: 'Arrivo 006', bib: '812', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-006.jpg' }, + { id: 'f101-007', label: 'Ponte 007', bib: '391', checkpoint: 'Ponte', thumb: 'thumb-ponte-007.jpg' }, + { id: 'f101-008', label: 'Centro 008', bib: '391', checkpoint: 'Centro', thumb: 'thumb-centro-008.jpg' }, + { id: 'f101-009', label: 'Arrivo 009', bib: '128', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-009.jpg' }, + { id: 'f101-010', label: 'Lungarno 010', bib: '128', checkpoint: 'Lungarno', thumb: 'thumb-lungarno-010.jpg' }, + { id: 'f101-011', label: 'Piazza 011', bib: '560', checkpoint: 'Piazza', thumb: 'thumb-piazza-011.jpg' }, + { id: 'f101-012', label: 'Arrivo 012', bib: '560', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-012.jpg' } + ] + }, + '202': { + id: '202', + slug: 'trail-del-chianti', + name: 'Trail del Chianti', + photos: [ + { id: 'f202-001', label: 'Bosco 001', bib: '77', checkpoint: 'Bosco', thumb: 'thumb-bosco-001.jpg' }, + { id: 'f202-002', label: 'Salita 002', bib: '77', checkpoint: 'Salita', thumb: 'thumb-salita-002.jpg' }, + { id: 'f202-003', label: 'Arrivo 003', bib: '77', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-003.jpg' }, + { id: 'f202-004', label: 'Bosco 004', bib: '19', checkpoint: 'Bosco', thumb: 'thumb-bosco-004.jpg' } + ] + } +}; + +const sessions = new Map(); +const searches = new Map(); +const results = new Map(); + +export function createSession(session) { + const sessionId = randomId('sess'); + sessions.set(sessionId, { + ...session, + createdAt: Date.now() + }); + return sessionId; +} + +export function getSession(sessionId) { + return sessions.get(sessionId) || null; +} + +export function createSearch({ raceId, user, selfieName, returnUrl, lang }) { + const searchId = randomId('search'); + searches.set(searchId, { + id: searchId, + raceId, + user, + selfieName, + returnUrl, + lang, + status: 'processing', + createdAt: Date.now(), + completedAt: null, + resultId: null, + matches: [] + }); + return searches.get(searchId); +} + +export function getSearch(searchId) { + return searches.get(searchId) || null; +} + +export function completeSearch(searchId) { + const search = searches.get(searchId); + if (!search) { + return null; + } + + const race = mockCatalog[search.raceId]; + const matches = (race?.photos || []).slice(0, Math.min(4, race?.photos?.length || 0)); + const resultId = randomId('result'); + + results.set(resultId, { + id: resultId, + raceId: search.raceId, + raceName: race?.name || search.raceId, + userId: search.user.id, + returnUrl: search.returnUrl, + lang: search.lang, + matches, + createdAt: Date.now() + }); + + const completed = { + ...search, + status: 'completed', + completedAt: Date.now(), + resultId, + matches + }; + + searches.set(searchId, completed); + return completed; +} + +export function getResult(resultId) { + return results.get(resultId) || null; +} diff --git a/faceai/apps/frontend/index.html b/faceai/apps/frontend/index.html new file mode 100644 index 00000000..2d806b4d --- /dev/null +++ b/faceai/apps/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + FaceAI + + + +
    + + diff --git a/faceai/apps/frontend/package.json b/faceai/apps/frontend/package.json new file mode 100644 index 00000000..9b0e4595 --- /dev/null +++ b/faceai/apps/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "@regalami/faceai-frontend", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.1.0" + } +} diff --git a/faceai/apps/frontend/src/App.vue b/faceai/apps/frontend/src/App.vue new file mode 100644 index 00000000..98240aef --- /dev/null +++ b/faceai/apps/frontend/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/faceai/apps/frontend/src/components/LegacyHeader.vue b/faceai/apps/frontend/src/components/LegacyHeader.vue new file mode 100644 index 00000000..3963c337 --- /dev/null +++ b/faceai/apps/frontend/src/components/LegacyHeader.vue @@ -0,0 +1,57 @@ + + + diff --git a/faceai/apps/frontend/src/legacyAssets.js b/faceai/apps/frontend/src/legacyAssets.js new file mode 100644 index 00000000..07e8a100 --- /dev/null +++ b/faceai/apps/frontend/src/legacyAssets.js @@ -0,0 +1,26 @@ +const legacyAssetBaseUrl = (import.meta.env.VITE_LEGACY_ASSET_BASE_URL || '/legacy-static').replace(/\/$/, ''); + +export function legacyAsset(path) { + return `${legacyAssetBaseUrl}${path.startsWith('/') ? path : `/${path}`}`; +} + +export function injectLegacyStylesheets() { + const stylesheets = [ + legacyAsset('/vendor/bootstrap/css/bootstrap.min.css'), + legacyAsset('/css/font-awesome.min.css'), + 'https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i', + legacyAsset('/css/custom-style.css') + ]; + + stylesheets.forEach((href) => { + if (document.head.querySelector(`link[data-legacy-href="${href}"]`)) { + return; + } + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + link.dataset.legacyHref = href; + document.head.appendChild(link); + }); +} diff --git a/faceai/apps/frontend/src/main.js b/faceai/apps/frontend/src/main.js new file mode 100644 index 00000000..c16bc0fb --- /dev/null +++ b/faceai/apps/frontend/src/main.js @@ -0,0 +1,8 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import router from './router.js'; +import './styles.css'; +import { injectLegacyStylesheets } from './legacyAssets.js'; + +injectLegacyStylesheets(); +createApp(App).use(router).mount('#app'); diff --git a/faceai/apps/frontend/src/router.js b/faceai/apps/frontend/src/router.js new file mode 100644 index 00000000..56ed3bd8 --- /dev/null +++ b/faceai/apps/frontend/src/router.js @@ -0,0 +1,17 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import HomeView from './views/HomeView.vue'; +import HandoffCallbackView from './views/HandoffCallbackView.vue'; + +export default createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + component: HomeView + }, + { + path: '/auth/callback', + component: HandoffCallbackView + } + ] +}); diff --git a/faceai/apps/frontend/src/styles.css b/faceai/apps/frontend/src/styles.css new file mode 100644 index 00000000..98bc8be7 --- /dev/null +++ b/faceai/apps/frontend/src/styles.css @@ -0,0 +1,65 @@ +body { + background: #fff; +} + +.faceai-page { + min-height: calc(100vh - 120px); +} + +.faceai-form-shell { + padding: 0 0 12px; +} + +.faceai-action-row { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; +} + +.faceai-feedback { + background: #f8f9fa; + border-left: 5px solid #fe3d00; + padding: 16px 18px; +} + +.faceai-feedback .lead { + font-size: 1rem; +} + +.faceai-spinner-block { + display: inline-flex; + align-items: center; + gap: 12px; + font-weight: 500; + color: #5b4938; +} + +.faceai-spinner-block .spinner-border { + flex: 0 0 auto; +} + +.callback-shell { + min-height: calc(100vh - 120px); +} + +.callback-card { + width: 100%; + max-width: 620px; + background: #fff; + border: 1px solid #ddd; + padding: 24px; + margin: 40px auto; +} + +@media (max-width: 767px) { + .faceai-action-row { + display: block; + } + + .faceai-action-row .btn { + display: block; + width: 100%; + margin-bottom: 8px; + } +} diff --git a/faceai/apps/frontend/src/views/HandoffCallbackView.vue b/faceai/apps/frontend/src/views/HandoffCallbackView.vue new file mode 100644 index 00000000..42400b25 --- /dev/null +++ b/faceai/apps/frontend/src/views/HandoffCallbackView.vue @@ -0,0 +1,51 @@ + + + diff --git a/faceai/apps/frontend/src/views/HomeView.vue b/faceai/apps/frontend/src/views/HomeView.vue new file mode 100644 index 00000000..b9df54a5 --- /dev/null +++ b/faceai/apps/frontend/src/views/HomeView.vue @@ -0,0 +1,235 @@ + + + diff --git a/faceai/apps/frontend/vite.config.js b/faceai/apps/frontend/vite.config.js new file mode 100644 index 00000000..fd1e2d1d --- /dev/null +++ b/faceai/apps/frontend/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5173, + proxy: { + '/api': 'http://localhost:3001', + '/dev': 'http://localhost:3001' + } + } +}); diff --git a/faceai/docker-compose.yml b/faceai/docker-compose.yml new file mode 100644 index 00000000..114e4f2d --- /dev/null +++ b/faceai/docker-compose.yml @@ -0,0 +1,34 @@ +services: + faceai: + image: node:20-alpine + container_name: regalami-faceai + working_dir: /app + command: sh -c "npm run start --workspace @regalami/faceai-backend" + environment: + PORT: 3001 + FACEAI_FRONTEND_URL: http://localhost:3001 + FACEAI_PUBLIC_BASE_URL: http://localhost:3001 + FACEAI_LEGACY_RETURN_URL: http://localhost:8080/faceai_return.php + FACEAI_ENABLE_LOCAL_LEGACY_STATIC: 1 + FACEAI_LOCAL_LEGACY_STATIC_ROOT: /legacy-www + FACEAI_SHARED_SECRET: change-me + FACEAI_SESSION_COOKIE: rus_faceai_session + volumes: + - .:/app + - ../www:/legacy-www:ro + ports: + - "3001:3001" + + legacy-php: + image: php:8.3-apache + container_name: regalami-legacy-php + environment: + FACEAI_BACKEND_INTERNAL_URL: http://faceai:3001 + FACEAI_FRONTEND_URL: http://localhost:3001 + FACEAI_SHARED_SECRET: change-me + FACEAI_ALLOW_DEV_HANDOFF: 1 + FACEAI_IDENTITY_COOKIE: rus_faceai_identity + volumes: + - ../www:/var/www/html + ports: + - "8080:80" diff --git a/faceai/docker/Dockerfile b/faceai/docker/Dockerfile new file mode 100644 index 00000000..57879377 --- /dev/null +++ b/faceai/docker/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package.json ./ +COPY apps/frontend/package.json apps/frontend/package.json +COPY apps/backend/package.json apps/backend/package.json + +RUN npm install + +COPY . . + +RUN npm run build + +FROM node:20-alpine AS runtime + +WORKDIR /app + +COPY --from=build /app /app + +ENV NODE_ENV=production +EXPOSE 3001 + +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/faceai/package-lock.json b/faceai/package-lock.json new file mode 100644 index 00000000..3f0e2e71 --- /dev/null +++ b/faceai/package-lock.json @@ -0,0 +1,2535 @@ +{ + "name": "faceai", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "faceai", + "workspaces": [ + "apps/frontend", + "apps/backend" + ], + "devDependencies": { + "concurrently": "^9.1.2" + } + }, + "apps/backend": { + "name": "@regalami/faceai-backend", + "dependencies": { + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "express": "^4.21.2" + } + }, + "apps/frontend": { + "name": "@regalami/faceai-frontend", + "dependencies": { + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.1.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@regalami/faceai-backend": { + "resolved": "apps/backend", + "link": true + }, + "node_modules/@regalami/faceai-frontend": { + "resolved": "apps/frontend", + "link": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "vue": "3.5.32" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/faceai/package.json b/faceai/package.json new file mode 100644 index 00000000..7a2b1c19 --- /dev/null +++ b/faceai/package.json @@ -0,0 +1,18 @@ +{ + "name": "faceai", + "private": true, + "workspaces": [ + "apps/frontend", + "apps/backend" + ], + "scripts": { + "dev": "concurrently \"npm:dev:backend\" \"npm:dev:frontend\"", + "dev:backend": "npm run dev --workspace @regalami/faceai-backend", + "dev:frontend": "npm run dev --workspace @regalami/faceai-frontend", + "build": "npm run build --workspace @regalami/faceai-frontend && npm run build --workspace @regalami/faceai-backend", + "start": "npm run start --workspace @regalami/faceai-backend" + }, + "devDependencies": { + "concurrently": "^9.1.2" + } +} diff --git a/www/_js/rus-ecom-240621.js b/www/_js/rus-ecom-240621.js index e8adea56..5e82edf8 100644 --- a/www/_js/rus-ecom-240621.js +++ b/www/_js/rus-ecom-240621.js @@ -96,10 +96,89 @@ function searchGara() { /* PAGINA RICERCA FOTOCR */ /***************************************************/ /***************************************************/ +function getTipoPuntoFotoValue() { + var field = $("#tipoPuntoFoto"); + if (!field.length) { + return ""; + } + + return field.val() || ""; +} + +function getCurrentLangValue() { + var field = $("#lang"); + if (field.length && field.val()) { + return field.val(); + } + + return $("html").attr("lang") || "it"; +} + +function buildFaceAiLaunchUrl() { + var raceId = $("#id_gara").val() || ""; + var raceSlug = $("#garaDesc").val() || ""; + var raceName = $("h1.my-4").last().text().replace(/\s+/g, " ").trim(); + var lang = getCurrentLangValue(); + var handoffUrl = (window.faceAiSimulator && window.faceAiSimulator.handoffUrl) || "faceai_handoff.php"; + var returnUrl = (window.faceAiSimulator && window.faceAiSimulator.returnUrl) || window.location.href; + var query = [ + "raceId=" + encodeURIComponent(raceId), + "raceSlug=" + encodeURIComponent(raceSlug), + "raceName=" + encodeURIComponent(raceName), + "lang=" + encodeURIComponent(lang), + "returnUrl=" + encodeURIComponent(returnUrl) + ]; + + if (window.faceAiSimulator && window.faceAiSimulator.devUserId) { + query.push("devUserId=" + encodeURIComponent(window.faceAiSimulator.devUserId)); + } + if (window.faceAiSimulator && window.faceAiSimulator.devDisplayName) { + query.push("devDisplayName=" + encodeURIComponent(window.faceAiSimulator.devDisplayName)); + } + if (window.faceAiSimulator && window.faceAiSimulator.devEmail) { + query.push("devEmail=" + encodeURIComponent(window.faceAiSimulator.devEmail)); + } + if (window.faceAiSimulator && window.faceAiSimulator.devMembershipStatus) { + query.push("devMembershipStatus=" + encodeURIComponent(window.faceAiSimulator.devMembershipStatus)); + } + + return handoffUrl + "?" + query.join("&"); +} + +function launchFaceAi() { + $("body").addClass("loading"); + window.location.href = buildFaceAiLaunchUrl(); + return false; +} + +function initFaceAiRaceSearchButton() { + var select = $("#tipoPuntoFoto"); + if (!select.length || $("#faceaiLaunchButton").length) { + return; + } + + var inputGroup = select.closest(".input-group"); + var renderTarget = inputGroup.length ? inputGroup : select.parent(); + var currentValue = select.val() || ""; + + if (!renderTarget.length) { + return; + } + + select.off("change"); + select.remove(); + + if (!$("#tipoPuntoFoto").length) { + renderTarget.append(''); + } + + renderTarget.append(''); +} + function searching() { //gara%201_gara-1---2.html $("body").addClass("loading"); - theSvlt = $("#garaDesc").val() + "_gara-" + $("#id_gara").val() + "-" + $("#id_puntoFoto").val() + "-" + $("#tipoPuntoFoto").val() + "-" + $("#pageRow").val() + "-1-"+$("#pettorale").val()+"-"+$("#lang").val()+".html"; + theSvlt = $("#garaDesc").val() + "_gara-" + $("#id_gara").val() + "-" + $("#id_puntoFoto").val() + "-" + getTipoPuntoFotoValue() + "-" + $("#pageRow").val() + "-1-"+$("#pettorale").val()+"-"+getCurrentLangValue()+".html"; //alert(theSvlt); location.href = theSvlt; @@ -107,7 +186,7 @@ function searching() { function searchingTPF() { //gara%201_gara-1---2.html - theSvlt = $("#garaDesc").val() + "_gara-" + $("#id_gara").val() + "--" + $("#tipoPuntoFoto").val() + "-" + $("#pageRow").val() + "-1.html"; + theSvlt = $("#garaDesc").val() + "_gara-" + $("#id_gara").val() + "--" + getTipoPuntoFotoValue() + "-" + $("#pageRow").val() + "-1.html"; //alert(theSvlt); location.href = theSvlt; @@ -288,7 +367,7 @@ function goPage() if(parseFloat(pnGo)<= parseFloat(pn)) { - theSvlt = $("#garaDesc").val() + "_gara-" + $("#id_gara").val() + "-" + $("#id_puntoFoto").val() + "-" + $("#tipoPuntoFoto").val() + "-" + $("#pageRow").val() + "-"+pnGo+".html"; + theSvlt = $("#garaDesc").val() + "_gara-" + $("#id_gara").val() + "-" + $("#id_puntoFoto").val() + "-" + getTipoPuntoFotoValue() + "-" + $("#pageRow").val() + "-"+pnGo+".html"; //alert(theSvlt); location.href = theSvlt; } @@ -296,6 +375,10 @@ function goPage() alert('Errore!!'); } +$(function() { + initFaceAiRaceSearchButton(); +}); + diff --git a/www/faceai_config.php b/www/faceai_config.php new file mode 100644 index 00000000..c5ad0f98 --- /dev/null +++ b/www/faceai_config.php @@ -0,0 +1,185 @@ + rtrim(faceai_env('FACEAI_FRONTEND_URL', 'http://localhost:5173'), '/'), + 'backend_internal_url' => rtrim(faceai_env('FACEAI_BACKEND_INTERNAL_URL', 'http://localhost:3001'), '/'), + 'shared_secret' => (string) faceai_env('FACEAI_SHARED_SECRET', 'change-me'), + 'allow_dev_handoff' => faceai_env('FACEAI_ALLOW_DEV_HANDOFF', '1') === '1', + 'identity_cookie' => (string) faceai_env('FACEAI_IDENTITY_COOKIE', 'rus_faceai_identity'), + 'return_forward_url' => rtrim((string) faceai_env('FACEAI_RETURN_FORWARD_URL', ''), '/') + ); + + return $config; +} + +function faceai_base64url_encode($value) +{ + return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); +} + +function faceai_base64url_decode($value) +{ + $padding = strlen($value) % 4; + if ($padding > 0) { + $value .= str_repeat('=', 4 - $padding); + } + + return base64_decode(strtr($value, '-_', '+/')); +} + +function faceai_sign_payload(array $payload, $secret) +{ + $body = faceai_base64url_encode(json_encode($payload)); + $signature = hash_hmac('sha256', $body, $secret, true); + return $body . '.' . faceai_base64url_encode($signature); +} + +function faceai_verify_payload($token, $secret) +{ + if (!is_string($token) || strpos($token, '.') === false) { + throw new RuntimeException('Invalid token format.'); + } + + list($body, $signature) = explode('.', $token, 2); + $expected = faceai_base64url_encode(hash_hmac('sha256', $body, $secret, true)); + + if (!hash_equals($expected, $signature)) { + throw new RuntimeException('Invalid token signature.'); + } + + $decoded = faceai_base64url_decode($body); + $payload = json_decode($decoded, true); + + if (!is_array($payload)) { + throw new RuntimeException('Invalid token payload.'); + } + + if (isset($payload['expiresAt']) && (int) $payload['expiresAt'] < (int) round(microtime(true) * 1000)) { + throw new RuntimeException('Token expired.'); + } + + return $payload; +} + +function faceai_build_url($baseUrl, array $params) +{ + return $baseUrl . (strpos($baseUrl, '?') === false ? '?' : '&') . http_build_query($params); +} + +function faceai_request_value($key, $default = '') +{ + if (!isset($_GET[$key])) { + return $default; + } + + if (is_array($_GET[$key])) { + return $default; + } + + return trim((string) $_GET[$key]); +} + +function faceai_html($value) +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +function faceai_resolve_identity(array $config) +{ + if (!empty($_COOKIE[$config['identity_cookie']])) { + $payload = faceai_verify_payload($_COOKIE[$config['identity_cookie']], $config['shared_secret']); + if (($payload['type'] ?? '') !== 'legacy-identity') { + throw new RuntimeException('Unexpected identity cookie payload.'); + } + + return array( + 'id' => (string) ($payload['userId'] ?? ''), + 'displayName' => (string) ($payload['displayName'] ?? ''), + 'email' => (string) ($payload['email'] ?? ''), + 'membershipStatus' => (string) ($payload['membershipStatus'] ?? 'inactive') + ); + } + + if ($config['allow_dev_handoff']) { + $userId = faceai_request_value('devUserId'); + if ($userId !== '') { + return array( + 'id' => $userId, + 'displayName' => faceai_request_value('devDisplayName', 'Local Test User'), + 'email' => faceai_request_value('devEmail', 'local.test@example.invalid'), + 'membershipStatus' => faceai_request_value('devMembershipStatus', 'active') + ); + } + } + + return null; +} + +function faceai_render_message_page($title, $message, array $details = array(), $statusCode = 400) +{ + http_response_code($statusCode); + header('Content-Type: text/html; charset=UTF-8'); + + echo ''; + echo '' . faceai_html($title) . ''; + echo ''; + echo '
    '; + echo '

    ' . faceai_html($title) . '

    '; + echo '

    ' . faceai_html($message) . '

    '; + + if (!empty($details)) { + echo ''; + } + + echo '
    '; + exit; +} + +function faceai_fetch_json($url) +{ + $context = stream_context_create(array( + 'http' => array( + 'ignore_errors' => true, + 'timeout' => 10 + ) + )); + + $response = @file_get_contents($url, false, $context); + if ($response === false) { + throw new RuntimeException('Unable to fetch remote FaceAI data.'); + } + + $statusCode = 0; + if (!empty($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $matches)) { + $statusCode = (int) $matches[1]; + } + + $payload = json_decode($response, true); + if (!is_array($payload)) { + throw new RuntimeException('FaceAI returned invalid JSON.'); + } + + if ($statusCode >= 400) { + throw new RuntimeException($payload['error'] ?? ('FaceAI bridge request failed with status ' . $statusCode . '.')); + } + + return $payload; +} diff --git a/www/faceai_handoff.php b/www/faceai_handoff.php new file mode 100644 index 00000000..73cbe10e --- /dev/null +++ b/www/faceai_handoff.php @@ -0,0 +1,76 @@ + 'handoff', + 'user' => array( + 'id' => $identity['id'], + 'displayName' => $identity['displayName'], + 'email' => $identity['email'], + 'membershipStatus' => $identity['membershipStatus'] + ), + 'race' => array( + 'id' => $raceId, + 'slug' => $raceSlug !== '' ? $raceSlug : $raceId, + 'name' => $raceName !== '' ? $raceName : $raceId + ), + 'lang' => $lang, + 'returnUrl' => $returnUrl, + 'expiresAt' => ((int) round(microtime(true) * 1000)) + (5 * 60 * 1000) + ); + + $token = faceai_sign_payload($payload, $config['shared_secret']); + $targetUrl = faceai_build_url($config['frontend_url'] . '/auth/callback', array('token' => $token)); + + header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); + header('Pragma: no-cache'); + header('Location: ' . $targetUrl, true, 302); + exit; +} catch (Throwable $error) { + faceai_render_message_page('Errore handoff FaceAI', $error->getMessage(), array(), 500); +} diff --git a/www/faceai_return.php b/www/faceai_return.php new file mode 100644 index 00000000..d897e0df --- /dev/null +++ b/www/faceai_return.php @@ -0,0 +1,56 @@ + $resultId, + 'token' => $token + )), true, 302); + exit; + } + + $bridgeUrl = faceai_build_url($config['backend_internal_url'] . '/bridge/results/' . rawurlencode($resultId), array( + 'token' => $token + )); + $result = faceai_fetch_json($bridgeUrl); + + faceai_sim_render_page(array( + 'raceId' => (string) ($result['raceId'] ?? ($payload['raceId'] ?? '')), + 'lang' => (string) ($result['lang'] ?? 'it'), + 'raceSlug' => (string) ($result['raceId'] ?? ($payload['raceId'] ?? '')), + 'raceName' => (string) ($result['raceName'] ?? ('Race ' . ($payload['raceId'] ?? ''))), + 'returnUrl' => (string) ($result['returnUrl'] ?? 'faceai_simulator.php'), + 'banner' => 'Vista filtrata da FaceAI. Sono state trovate ' . count($result['matches'] ?? array()) . ' foto corrispondenti per l utente corrente.', + 'totalLabel' => count($result['matches'] ?? array()) . ' foto da FaceAI', + 'photos' => is_array($result['matches'] ?? null) ? $result['matches'] : array(), + 'showSimulatorBootstrap' => false + )); +} catch (Throwable $error) { + faceai_render_message_page('Errore return FaceAI', $error->getMessage(), array(), 500); +} diff --git a/www/faceai_simulator.php b/www/faceai_simulator.php new file mode 100644 index 00000000..cc027c17 --- /dev/null +++ b/www/faceai_simulator.php @@ -0,0 +1,35 @@ + 'f101-001', 'thumb' => 'thumb-arrivo-001.jpg', 'label' => 'Arrivo 001', 'checkpoint' => 'Arrivo'), + array('id' => 'f101-002', 'thumb' => 'thumb-arrivo-002.jpg', 'label' => 'Arrivo 002', 'checkpoint' => 'Arrivo'), + array('id' => 'f101-003', 'thumb' => 'thumb-ponte-003.jpg', 'label' => 'Ponte 003', 'checkpoint' => 'Ponte'), + array('id' => 'f101-004', 'thumb' => 'thumb-centro-004.jpg', 'label' => 'Centro 004', 'checkpoint' => 'Centro'), + array('id' => 'f101-005', 'thumb' => 'thumb-centro-005.jpg', 'label' => 'Centro 005', 'checkpoint' => 'Centro'), + array('id' => 'f101-006', 'thumb' => 'thumb-arrivo-006.jpg', 'label' => 'Arrivo 006', 'checkpoint' => 'Arrivo'), + array('id' => 'f101-007', 'thumb' => 'thumb-ponte-007.jpg', 'label' => 'Ponte 007', 'checkpoint' => 'Ponte'), + array('id' => 'f101-008', 'thumb' => 'thumb-centro-008.jpg', 'label' => 'Centro 008', 'checkpoint' => 'Centro'), + array('id' => 'f101-009', 'thumb' => 'thumb-arrivo-009.jpg', 'label' => 'Arrivo 009', 'checkpoint' => 'Arrivo'), + array('id' => 'f101-010', 'thumb' => 'thumb-lungarno-010.jpg', 'label' => 'Lungarno 010', 'checkpoint' => 'Lungarno'), + array('id' => 'f101-011', 'thumb' => 'thumb-piazza-011.jpg', 'label' => 'Piazza 011', 'checkpoint' => 'Piazza'), + array('id' => 'f101-012', 'thumb' => 'thumb-arrivo-012.jpg', 'label' => 'Arrivo 012', 'checkpoint' => 'Arrivo') +); + +faceai_sim_render_page(array( + 'raceId' => $raceId, + 'lang' => $lang, + 'raceSlug' => $raceSlug, + 'raceName' => $raceName, + 'returnUrl' => $returnUrl, + 'banner' => 'Questa pagina PHP simula il punto di ingresso del sito legacy. Il vecchio select con ID tipoPuntoFoto viene rimosso dal JavaScript originale e sostituito dal pulsante Face ID.', + 'totalLabel' => count($photos) . ' foto demo', + 'photos' => $photos, + 'showSimulatorBootstrap' => true +)); diff --git a/www/faceai_simulator_view.php b/www/faceai_simulator_view.php new file mode 100644 index 00000000..8ebccf3d --- /dev/null +++ b/www/faceai_simulator_view.php @@ -0,0 +1,184 @@ + + + + + +FaceAI Legacy Simulator + + + + + + + + + +
    +
    +
    +

    +
    +
    + Gara +
    +
    + +
    + +
    + + + + + + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + +
    + + + + + + + + + +