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
+
+
+
+
+
+ ${escapeHtml(race.name)}
+
+ Punti Foto
+
+ -- Punti Foto --
+ Arrivo
+ Centro
+ Ponte
+
+ Face ID
+
+ ${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 @@
+
+
+
+
+
+
+
+
Face ID
+
+
+ Validazione handoff legacy e creazione della sessione FaceAI in corso...
+
+
{{ errorMessage }}
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
Face ID
+
+
+
+
+
+
+
+
+
+
{{ session ? session.user.displayName : 'Sessione FaceAI' }}
+
+
+
{{ session ? session.race.name : 'Upload selfie' }}
+
+
+
{{ activeSearch ? activeSearch.status : 'ready' }}
+
+
+
+
+
+
+
{{ statusLabel }}
+
+
+ {{ busyLabel }}
+
+
Match trovati: {{ activeSearch.matchCount }}
+
Reindirizzamento alla pagina legacy filtrata in corso...
+
{{ errorMessage }}
+
+
+
+
+
+
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(' Face ID ');
+}
+
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 '';
+ foreach ($details as $detail) {
+ echo '' . faceai_html($detail) . ' ';
+ }
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ID foto:
+
Punto foto:
+
+
+
+
+
+
+
+
+
+
+
+
+
+