Spaces:
Running
Running
Deploy combined Node and Python backend in one Docker Space
Browse files- .dockerignore +11 -5
- .env.example +11 -2
- .gitignore +6 -0
- Dockerfile +19 -7
- README.md +95 -10
- app.py +9 -5
- package-lock.json +1749 -0
- package.json +30 -0
- src/analysis.ts +84 -0
- src/auth.ts +54 -0
- src/config.ts +42 -0
- src/server.ts +494 -0
- src/subscriptions.ts +37 -0
- src/supabase.ts +53 -0
- start.py +55 -0
- tsconfig.json +14 -0
.dockerignore
CHANGED
|
@@ -1,5 +1,11 @@
|
|
| 1 |
-
.
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
.
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Include only Docker build inputs; credentials and local artifacts stay out.
|
| 2 |
+
*
|
| 3 |
+
!Dockerfile
|
| 4 |
+
!package.json
|
| 5 |
+
!package-lock.json
|
| 6 |
+
!tsconfig.json
|
| 7 |
+
!src/
|
| 8 |
+
!src/**
|
| 9 |
+
!requirements.txt
|
| 10 |
+
!app.py
|
| 11 |
+
!start.py
|
.env.example
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
CORE_REPO_ID=your-account/sporalize-core
|
| 2 |
STORAGE_REPO_ID=your-account/sporalize-application-storage
|
| 3 |
HF_TOKEN=
|
| 4 |
-
|
| 5 |
-
|
|
|
|
| 1 |
+
PORT=4000
|
| 2 |
+
TRUST_PROXY_HOPS=1
|
| 3 |
+
NODE_ENV=development
|
| 4 |
+
FRONTEND_URL=http://localhost:8080
|
| 5 |
+
BACKEND_URL=http://localhost:4000
|
| 6 |
+
ANALYSIS_PROXY_SECRET=replace-with-a-random-secret-of-at-least-32-characters
|
| 7 |
+
SUPABASE_URL=https://your-project.supabase.co
|
| 8 |
+
SUPABASE_ANON_KEY=your-publishable-or-anon-key
|
| 9 |
+
SUPABASE_SERVICE_ROLE_KEY=your-service-role-or-secret-key
|
| 10 |
CORE_REPO_ID=your-account/sporalize-core
|
| 11 |
STORAGE_REPO_ID=your-account/sporalize-application-storage
|
| 12 |
HF_TOKEN=
|
| 13 |
+
# CORE_REVISION=optional-pinned-revision
|
| 14 |
+
# Internal Node/AI URLs are set automatically by start.py.
|
.gitignore
CHANGED
|
@@ -1,3 +1,9 @@
|
|
| 1 |
.env
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
__pycache__/
|
|
|
|
| 3 |
.venv/
|
|
|
|
|
|
| 1 |
.env
|
| 2 |
+
.env.*
|
| 3 |
+
!.env.example
|
| 4 |
+
node_modules/
|
| 5 |
+
dist/
|
| 6 |
__pycache__/
|
| 7 |
+
*.py[cod]
|
| 8 |
.venv/
|
| 9 |
+
*.log
|
Dockerfile
CHANGED
|
@@ -1,11 +1,23 @@
|
|
| 1 |
-
FROM
|
| 2 |
-
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 HF_HOME=/home/appuser/.cache/huggingface
|
| 3 |
-
RUN apt-get update && apt-get install -y --no-install-recommends libglib2.0-0 libgl1 && rm -rf /var/lib/apt/lists/*
|
| 4 |
-
RUN useradd --create-home --uid 1000 appuser
|
| 5 |
WORKDIR /app
|
| 6 |
-
COPY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
-
COPY --chown=appuser:appuser app
|
|
|
|
|
|
|
| 9 |
USER appuser
|
| 10 |
EXPOSE 7860
|
| 11 |
-
CMD ["
|
|
|
|
| 1 |
+
FROM node:22-bookworm-slim AS node-build
|
|
|
|
|
|
|
|
|
|
| 2 |
WORKDIR /app
|
| 3 |
+
COPY package*.json ./
|
| 4 |
+
RUN npm ci
|
| 5 |
+
COPY tsconfig.json ./
|
| 6 |
+
COPY src ./src
|
| 7 |
+
RUN npm run build && npm prune --omit=dev
|
| 8 |
+
|
| 9 |
+
FROM python:3.10-slim-bookworm
|
| 10 |
+
ENV NODE_ENV=production PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 HF_HOME=/home/appuser/.cache/huggingface PORT=7860
|
| 11 |
+
RUN apt-get update && apt-get install -y --no-install-recommends libglib2.0-0 libgl1 \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 13 |
+
&& useradd --create-home --uid 1000 appuser
|
| 14 |
+
COPY --from=node-build /usr/local/bin/node /usr/local/bin/node
|
| 15 |
+
WORKDIR /app
|
| 16 |
+
COPY requirements.txt ./
|
| 17 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 18 |
+
COPY --from=node-build --chown=appuser:appuser /app/dist ./dist
|
| 19 |
+
COPY --from=node-build --chown=appuser:appuser /app/node_modules ./node_modules
|
| 20 |
+
COPY --chown=appuser:appuser package.json app.py start.py ./
|
| 21 |
USER appuser
|
| 22 |
EXPOSE 7860
|
| 23 |
+
CMD ["python", "start.py"]
|
README.md
CHANGED
|
@@ -1,21 +1,106 @@
|
|
| 1 |
-
|
| 2 |
-
title: Sporalize
|
| 3 |
sdk: docker
|
| 4 |
app_port: 7860
|
| 5 |
---
|
| 6 |
|
| 7 |
-
# Sporalize
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
The
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Sporalize Backend
|
| 3 |
sdk: docker
|
| 4 |
app_port: 7860
|
| 5 |
---
|
| 6 |
|
| 7 |
+
# Sporalize Backend
|
| 8 |
+
|
| 9 |
+
One Docker service runs Express for authentication, subscriptions and Supabase access, and FastAPI for video analysis. Node exposes port 7860 in the Space; Python binds only to 127.0.0.1:8000. `start.py` supervises both processes and stops the service if either exits. Python calls Node over loopback; `BACKEND_URL` remains the public origin for authentication redirects and media links.
|
| 10 |
+
|
| 11 |
+
## Local development
|
| 12 |
+
|
| 13 |
+
From this directory:
|
| 14 |
+
|
| 15 |
+
```powershell
|
| 16 |
+
Copy-Item .env.example .env
|
| 17 |
+
npm ci
|
| 18 |
+
npm run build
|
| 19 |
+
python -m pip install -r requirements.txt
|
| 20 |
+
npm start
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
Fill in `.env` before starting. Both processes share this file. The default local public port is 4000. Rebuild TypeScript after editing Node code. `npm run dev` starts only Node; use `npm start` for the full application.
|
| 24 |
+
|
| 25 |
+
## Hugging Face deployment
|
| 26 |
+
|
| 27 |
+
Upload the contents of `Backend/` as the root of a Docker Space, including this README and Dockerfile. Exclude `.env`, `node_modules`, and local build/cache directories. The Space downloads Core from its private dataset repository; local development uses the root `Core/` directory.
|
| 28 |
+
|
| 29 |
+
Set these Space variables/secrets from `.env.example`:
|
| 30 |
+
|
| 31 |
+
- `PORT=7860`, `NODE_ENV=production`, `TRUST_PROXY_HOPS=1` (adjust to the actual trusted proxy chain).
|
| 32 |
+
- `FRONTEND_URL`: public HTTPS Cloudflare frontend origin.
|
| 33 |
+
- `BACKEND_URL`: public HTTPS origin of this Space.
|
| 34 |
+
- `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`.
|
| 35 |
+
- `CORE_REPO_ID`, `STORAGE_REPO_ID`, `HF_TOKEN`; optionally `CORE_REVISION`.
|
| 36 |
+
- `ANALYSIS_PROXY_SECRET`: a random secret with at least 32 characters.
|
| 37 |
+
|
| 38 |
+
Store tokens, the service-role key, and proxy secret as Space secrets. `HF_TOKEN` requires read access to Core and write access to the private storage dataset. Use a dedicated storage dataset for this application. `start.py` sets `ANALYSIS_API_URL` and `BACKEND_INTERNAL_URL` automatically; do not configure a separate public AI URL.
|
| 39 |
+
|
| 40 |
+
For a local Docker check, build with `docker build -t sporalize-backend .`, then run `docker run --rm --env-file .env -e PORT=7860 -p 7860:7860 sporalize-backend`. The `.env` NODE_ENV setting controls whether production HTTPS checks apply.
|
| 41 |
+
|
| 42 |
+
Node health: `GET /health`. AI health is internal at `http://127.0.0.1:8000/healthz` and becomes available after model initialization. Node health alone does not indicate model readiness. Video uploads go through Node directly to Python without Vercel. Validate hosting timeouts and representative large uploads before release.
|
| 43 |
+
|
| 44 |
+
Run one container with one Python worker: analysis jobs and retry payloads are in memory and are lost on restart. Temporary local files are ephemeral. Completed videos and `results.json` are stored in the private HF dataset; Node stores session metadata and frame indices in Supabase. Model constants are in `app.py`.
|
| 45 |
+
|
| 46 |
+
Apply `../Database/migrations/20261016_node_analysis.sql` before analyses. It assumes the legacy actions table is empty. Secret-protected `/internal/analysis` routes authorize sessions and persist completion/failure.
|
| 47 |
+
|
| 48 |
+
## Supabase dashboard setup
|
| 49 |
+
|
| 50 |
+
1. Authentication → Providers → Email: enable it and enable **Confirm email**.
|
| 51 |
+
2. Authentication → Providers → Google: enable it, add the Google client ID/secret, and use Supabase's displayed callback URL in Google Cloud.
|
| 52 |
+
3. Authentication → URL Configuration:
|
| 53 |
+
- Site URL: `http://localhost:8080` during development.
|
| 54 |
+
- Redirect URLs: `http://localhost:4000/api/auth/callback` and the production equivalent.
|
| 55 |
+
|
| 56 |
+
## Brevo email verification
|
| 57 |
+
|
| 58 |
+
Brevo is configured as Supabase Auth's SMTP provider; the Express API does not send verification messages directly.
|
| 59 |
+
|
| 60 |
+
1. Create a Brevo account and verify the sender email/domain you will use.
|
| 61 |
+
2. In Brevo open **Transactional → Settings → SMTP & API → SMTP** and create an SMTP key.
|
| 62 |
+
3. In Supabase open **Authentication → Emails → SMTP Settings**, enable custom SMTP, and enter:
|
| 63 |
+
- Host: `smtp-relay.brevo.com`
|
| 64 |
+
- Port: `587`
|
| 65 |
+
- Username: the Brevo SMTP login shown on the SMTP page (not the account email unless they match)
|
| 66 |
+
- Password: the Brevo SMTP key (not the account password)
|
| 67 |
+
- Sender email: the verified Brevo sender
|
| 68 |
+
- Sender name: `Sporalize`
|
| 69 |
+
4. Keep **Confirm email** enabled under Authentication → Providers → Email.
|
| 70 |
+
5. In **Authentication → Email Templates → Confirm signup**, set the confirmation link to the backend's absolute URL (replace `https://your-api-domain` with the deployed API origin):
|
| 71 |
+
|
| 72 |
+
```html
|
| 73 |
+
<h2>Confirm your Sporalize account</h2>
|
| 74 |
+
<p><a href="https://your-api-domain/api/auth/confirm?token_hash={{ .TokenHash }}">Confirm email</a></p>
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
Set **Site URL** to the frontend origin. Keep the backend callback URL in **Authentication → URL Configuration → Redirect URLs** for Google sign-in, and add its production equivalent. The email template links directly to the backend confirmation endpoint, which verifies the one-time token and creates the application's secure session cookies.
|
| 78 |
+
|
| 79 |
+
Brevo credentials stay in Supabase SMTP settings and are not added to the browser or committed to `.env`.
|
| 80 |
+
|
| 81 |
+
## Routes
|
| 82 |
|
| 83 |
+
- `POST /api/auth/signup` — `{ fname, lname, email, password, academy? }`; sends Supabase's confirmation email when confirmation is enabled.
|
| 84 |
+
- `POST /api/auth/login` — `{ email, password }`.
|
| 85 |
+
- `GET /api/auth/google?redirect=/labs` — starts Google OAuth.
|
| 86 |
+
- `GET /api/auth/callback` — exchanges the Supabase code and sets HttpOnly cookies.
|
| 87 |
+
- `GET /api/auth/confirm?token_hash=...` — verifies the signup email and sets HttpOnly cookies.
|
| 88 |
+
- `GET /api/auth/me` — verifies the session and refreshes it when necessary.
|
| 89 |
+
- `POST /api/auth/logout` — clears the session cookies.
|
| 90 |
+
- `GET /api/auth/profile` and `PATCH /api/auth/profile` — read and update the signed-in user's profile.
|
| 91 |
+
- `POST /api/auth/password` — change or set the signed-in user's password.
|
| 92 |
+
- `DELETE /api/auth/account` — delete the signed-in user's Supabase Auth account and profile row.
|
| 93 |
|
| 94 |
+
To allow profiles with no selected role, apply `Database/migrations/20261015_user_role_optional.sql` to the Supabase database once. New accounts start with a `NULL` role and academy; users can complete both from `/labs/profile`.
|
| 95 |
|
| 96 |
+
## Subscription activation flow
|
| 97 |
|
| 98 |
+
The access gate already reads `users_subscriptions` and only unlocks Sporalize Labs while `startdate` and `enddate` include today.
|
| 99 |
|
| 100 |
+
When a payment provider is added, the server-side webhook flow should:
|
| 101 |
|
| 102 |
+
1. Verify the provider signature and confirm the payment is paid.
|
| 103 |
+
2. Resolve the authenticated Sporalize user ID and the selected `subscriptions.subscription_id` from trusted checkout metadata.
|
| 104 |
+
3. Call `assignPaidSubscription` from `src/subscriptions.ts` with the paid period dates.
|
| 105 |
|
| 106 |
+
That helper upserts the user's single current plan in `users_subscriptions`. The existing access check then opens Labs, session details, and analysis automatically. The browser return page must not assign access by itself. Store provider payment references/idempotency records when the provider integration is implemented.
|
app.py
CHANGED
|
@@ -47,7 +47,7 @@ def hub():
|
|
| 47 |
def backend(path, user_id, method="GET", body=None):
|
| 48 |
for attempt in range(3):
|
| 49 |
try:
|
| 50 |
-
response = httpx.request(method, f'{os.environ["
|
| 51 |
json=body, timeout=60, headers={"x-sporalize-proxy-secret": os.environ["ANALYSIS_PROXY_SECRET"], "x-sporalize-user-id": user_id})
|
| 52 |
response.raise_for_status()
|
| 53 |
return response.json() if response.content else None
|
|
@@ -59,9 +59,13 @@ def backend(path, user_id, method="GET", body=None):
|
|
| 59 |
|
| 60 |
@lru_cache(maxsize=1)
|
| 61 |
def runtime():
|
| 62 |
-
root = Path(
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
spec = importlib.util.spec_from_file_location("sporalize_core", root / "pipeline.py")
|
| 66 |
module = importlib.util.module_from_spec(spec)
|
| 67 |
spec.loader.exec_module(module)
|
|
@@ -98,7 +102,7 @@ def runtime():
|
|
| 98 |
|
| 99 |
@asynccontextmanager
|
| 100 |
async def lifespan(app):
|
| 101 |
-
for name in ("
|
| 102 |
if not os.environ.get(name):
|
| 103 |
raise RuntimeError(f"{name} is required")
|
| 104 |
if len(os.environ.get("ANALYSIS_PROXY_SECRET", "")) < 32:
|
|
|
|
| 47 |
def backend(path, user_id, method="GET", body=None):
|
| 48 |
for attempt in range(3):
|
| 49 |
try:
|
| 50 |
+
response = httpx.request(method, f'{os.environ["BACKEND_INTERNAL_URL"].rstrip("/")}/internal/analysis{path}',
|
| 51 |
json=body, timeout=60, headers={"x-sporalize-proxy-secret": os.environ["ANALYSIS_PROXY_SECRET"], "x-sporalize-user-id": user_id})
|
| 52 |
response.raise_for_status()
|
| 53 |
return response.json() if response.content else None
|
|
|
|
| 59 |
|
| 60 |
@lru_cache(maxsize=1)
|
| 61 |
def runtime():
|
| 62 |
+
root = Path(__file__).resolve().parent.parent / "Core"
|
| 63 |
+
if not (root / "pipeline.py").is_file():
|
| 64 |
+
if not os.environ.get("CORE_REPO_ID"):
|
| 65 |
+
raise RuntimeError("CORE_REPO_ID is required when Core is not included locally")
|
| 66 |
+
root = Path(snapshot_download(repo_id=os.environ["CORE_REPO_ID"], repo_type="dataset",
|
| 67 |
+
revision=os.environ.get("CORE_REVISION"), token=os.environ["HF_TOKEN"],
|
| 68 |
+
allow_patterns=["pipeline.py", "Weights/*"]))
|
| 69 |
spec = importlib.util.spec_from_file_location("sporalize_core", root / "pipeline.py")
|
| 70 |
module = importlib.util.module_from_spec(spec)
|
| 71 |
spec.loader.exec_module(module)
|
|
|
|
| 102 |
|
| 103 |
@asynccontextmanager
|
| 104 |
async def lifespan(app):
|
| 105 |
+
for name in ("STORAGE_REPO_ID", "HF_TOKEN", "BACKEND_INTERNAL_URL"):
|
| 106 |
if not os.environ.get(name):
|
| 107 |
raise RuntimeError(f"{name} is required")
|
| 108 |
if len(os.environ.get("ANALYSIS_PROXY_SECRET", "")) < 32:
|
package-lock.json
ADDED
|
@@ -0,0 +1,1749 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "sporalize-backend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "sporalize-backend",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@supabase/supabase-js": "^2.57.0",
|
| 12 |
+
"bcryptjs": "^3.0.2",
|
| 13 |
+
"cookie-parser": "^1.4.7",
|
| 14 |
+
"cors": "^2.8.5",
|
| 15 |
+
"dotenv": "^16.6.1",
|
| 16 |
+
"express": "^5.1.0",
|
| 17 |
+
"express-rate-limit": "^8.1.0",
|
| 18 |
+
"helmet": "^8.1.0",
|
| 19 |
+
"zod": "^4.1.5"
|
| 20 |
+
},
|
| 21 |
+
"devDependencies": {
|
| 22 |
+
"@types/cookie-parser": "^1.4.9",
|
| 23 |
+
"@types/cors": "^2.8.19",
|
| 24 |
+
"@types/express": "^5.0.3",
|
| 25 |
+
"@types/node": "^22.16.5",
|
| 26 |
+
"tsx": "^4.20.5",
|
| 27 |
+
"typescript": "^5.8.3"
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 31 |
+
"version": "0.28.2",
|
| 32 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
| 33 |
+
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
| 34 |
+
"cpu": [
|
| 35 |
+
"ppc64"
|
| 36 |
+
],
|
| 37 |
+
"dev": true,
|
| 38 |
+
"license": "MIT",
|
| 39 |
+
"optional": true,
|
| 40 |
+
"os": [
|
| 41 |
+
"aix"
|
| 42 |
+
],
|
| 43 |
+
"engines": {
|
| 44 |
+
"node": ">=18"
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
"node_modules/@esbuild/android-arm": {
|
| 48 |
+
"version": "0.28.2",
|
| 49 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
| 50 |
+
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
| 51 |
+
"cpu": [
|
| 52 |
+
"arm"
|
| 53 |
+
],
|
| 54 |
+
"dev": true,
|
| 55 |
+
"license": "MIT",
|
| 56 |
+
"optional": true,
|
| 57 |
+
"os": [
|
| 58 |
+
"android"
|
| 59 |
+
],
|
| 60 |
+
"engines": {
|
| 61 |
+
"node": ">=18"
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"node_modules/@esbuild/android-arm64": {
|
| 65 |
+
"version": "0.28.2",
|
| 66 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
| 67 |
+
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
| 68 |
+
"cpu": [
|
| 69 |
+
"arm64"
|
| 70 |
+
],
|
| 71 |
+
"dev": true,
|
| 72 |
+
"license": "MIT",
|
| 73 |
+
"optional": true,
|
| 74 |
+
"os": [
|
| 75 |
+
"android"
|
| 76 |
+
],
|
| 77 |
+
"engines": {
|
| 78 |
+
"node": ">=18"
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
"node_modules/@esbuild/android-x64": {
|
| 82 |
+
"version": "0.28.2",
|
| 83 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
| 84 |
+
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
| 85 |
+
"cpu": [
|
| 86 |
+
"x64"
|
| 87 |
+
],
|
| 88 |
+
"dev": true,
|
| 89 |
+
"license": "MIT",
|
| 90 |
+
"optional": true,
|
| 91 |
+
"os": [
|
| 92 |
+
"android"
|
| 93 |
+
],
|
| 94 |
+
"engines": {
|
| 95 |
+
"node": ">=18"
|
| 96 |
+
}
|
| 97 |
+
},
|
| 98 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 99 |
+
"version": "0.28.2",
|
| 100 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
| 101 |
+
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
| 102 |
+
"cpu": [
|
| 103 |
+
"arm64"
|
| 104 |
+
],
|
| 105 |
+
"dev": true,
|
| 106 |
+
"license": "MIT",
|
| 107 |
+
"optional": true,
|
| 108 |
+
"os": [
|
| 109 |
+
"darwin"
|
| 110 |
+
],
|
| 111 |
+
"engines": {
|
| 112 |
+
"node": ">=18"
|
| 113 |
+
}
|
| 114 |
+
},
|
| 115 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 116 |
+
"version": "0.28.2",
|
| 117 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
| 118 |
+
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
| 119 |
+
"cpu": [
|
| 120 |
+
"x64"
|
| 121 |
+
],
|
| 122 |
+
"dev": true,
|
| 123 |
+
"license": "MIT",
|
| 124 |
+
"optional": true,
|
| 125 |
+
"os": [
|
| 126 |
+
"darwin"
|
| 127 |
+
],
|
| 128 |
+
"engines": {
|
| 129 |
+
"node": ">=18"
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 133 |
+
"version": "0.28.2",
|
| 134 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
| 135 |
+
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
| 136 |
+
"cpu": [
|
| 137 |
+
"arm64"
|
| 138 |
+
],
|
| 139 |
+
"dev": true,
|
| 140 |
+
"license": "MIT",
|
| 141 |
+
"optional": true,
|
| 142 |
+
"os": [
|
| 143 |
+
"freebsd"
|
| 144 |
+
],
|
| 145 |
+
"engines": {
|
| 146 |
+
"node": ">=18"
|
| 147 |
+
}
|
| 148 |
+
},
|
| 149 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 150 |
+
"version": "0.28.2",
|
| 151 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
| 152 |
+
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
| 153 |
+
"cpu": [
|
| 154 |
+
"x64"
|
| 155 |
+
],
|
| 156 |
+
"dev": true,
|
| 157 |
+
"license": "MIT",
|
| 158 |
+
"optional": true,
|
| 159 |
+
"os": [
|
| 160 |
+
"freebsd"
|
| 161 |
+
],
|
| 162 |
+
"engines": {
|
| 163 |
+
"node": ">=18"
|
| 164 |
+
}
|
| 165 |
+
},
|
| 166 |
+
"node_modules/@esbuild/linux-arm": {
|
| 167 |
+
"version": "0.28.2",
|
| 168 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
| 169 |
+
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
| 170 |
+
"cpu": [
|
| 171 |
+
"arm"
|
| 172 |
+
],
|
| 173 |
+
"dev": true,
|
| 174 |
+
"license": "MIT",
|
| 175 |
+
"optional": true,
|
| 176 |
+
"os": [
|
| 177 |
+
"linux"
|
| 178 |
+
],
|
| 179 |
+
"engines": {
|
| 180 |
+
"node": ">=18"
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 184 |
+
"version": "0.28.2",
|
| 185 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
| 186 |
+
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
| 187 |
+
"cpu": [
|
| 188 |
+
"arm64"
|
| 189 |
+
],
|
| 190 |
+
"dev": true,
|
| 191 |
+
"license": "MIT",
|
| 192 |
+
"optional": true,
|
| 193 |
+
"os": [
|
| 194 |
+
"linux"
|
| 195 |
+
],
|
| 196 |
+
"engines": {
|
| 197 |
+
"node": ">=18"
|
| 198 |
+
}
|
| 199 |
+
},
|
| 200 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 201 |
+
"version": "0.28.2",
|
| 202 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
| 203 |
+
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
| 204 |
+
"cpu": [
|
| 205 |
+
"ia32"
|
| 206 |
+
],
|
| 207 |
+
"dev": true,
|
| 208 |
+
"license": "MIT",
|
| 209 |
+
"optional": true,
|
| 210 |
+
"os": [
|
| 211 |
+
"linux"
|
| 212 |
+
],
|
| 213 |
+
"engines": {
|
| 214 |
+
"node": ">=18"
|
| 215 |
+
}
|
| 216 |
+
},
|
| 217 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 218 |
+
"version": "0.28.2",
|
| 219 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
| 220 |
+
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
| 221 |
+
"cpu": [
|
| 222 |
+
"loong64"
|
| 223 |
+
],
|
| 224 |
+
"dev": true,
|
| 225 |
+
"license": "MIT",
|
| 226 |
+
"optional": true,
|
| 227 |
+
"os": [
|
| 228 |
+
"linux"
|
| 229 |
+
],
|
| 230 |
+
"engines": {
|
| 231 |
+
"node": ">=18"
|
| 232 |
+
}
|
| 233 |
+
},
|
| 234 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 235 |
+
"version": "0.28.2",
|
| 236 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
| 237 |
+
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
| 238 |
+
"cpu": [
|
| 239 |
+
"mips64el"
|
| 240 |
+
],
|
| 241 |
+
"dev": true,
|
| 242 |
+
"license": "MIT",
|
| 243 |
+
"optional": true,
|
| 244 |
+
"os": [
|
| 245 |
+
"linux"
|
| 246 |
+
],
|
| 247 |
+
"engines": {
|
| 248 |
+
"node": ">=18"
|
| 249 |
+
}
|
| 250 |
+
},
|
| 251 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 252 |
+
"version": "0.28.2",
|
| 253 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
| 254 |
+
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
| 255 |
+
"cpu": [
|
| 256 |
+
"ppc64"
|
| 257 |
+
],
|
| 258 |
+
"dev": true,
|
| 259 |
+
"license": "MIT",
|
| 260 |
+
"optional": true,
|
| 261 |
+
"os": [
|
| 262 |
+
"linux"
|
| 263 |
+
],
|
| 264 |
+
"engines": {
|
| 265 |
+
"node": ">=18"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 269 |
+
"version": "0.28.2",
|
| 270 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
| 271 |
+
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
| 272 |
+
"cpu": [
|
| 273 |
+
"riscv64"
|
| 274 |
+
],
|
| 275 |
+
"dev": true,
|
| 276 |
+
"license": "MIT",
|
| 277 |
+
"optional": true,
|
| 278 |
+
"os": [
|
| 279 |
+
"linux"
|
| 280 |
+
],
|
| 281 |
+
"engines": {
|
| 282 |
+
"node": ">=18"
|
| 283 |
+
}
|
| 284 |
+
},
|
| 285 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 286 |
+
"version": "0.28.2",
|
| 287 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
| 288 |
+
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
| 289 |
+
"cpu": [
|
| 290 |
+
"s390x"
|
| 291 |
+
],
|
| 292 |
+
"dev": true,
|
| 293 |
+
"license": "MIT",
|
| 294 |
+
"optional": true,
|
| 295 |
+
"os": [
|
| 296 |
+
"linux"
|
| 297 |
+
],
|
| 298 |
+
"engines": {
|
| 299 |
+
"node": ">=18"
|
| 300 |
+
}
|
| 301 |
+
},
|
| 302 |
+
"node_modules/@esbuild/linux-x64": {
|
| 303 |
+
"version": "0.28.2",
|
| 304 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
| 305 |
+
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
| 306 |
+
"cpu": [
|
| 307 |
+
"x64"
|
| 308 |
+
],
|
| 309 |
+
"dev": true,
|
| 310 |
+
"license": "MIT",
|
| 311 |
+
"optional": true,
|
| 312 |
+
"os": [
|
| 313 |
+
"linux"
|
| 314 |
+
],
|
| 315 |
+
"engines": {
|
| 316 |
+
"node": ">=18"
|
| 317 |
+
}
|
| 318 |
+
},
|
| 319 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 320 |
+
"version": "0.28.2",
|
| 321 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
| 322 |
+
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
| 323 |
+
"cpu": [
|
| 324 |
+
"arm64"
|
| 325 |
+
],
|
| 326 |
+
"dev": true,
|
| 327 |
+
"license": "MIT",
|
| 328 |
+
"optional": true,
|
| 329 |
+
"os": [
|
| 330 |
+
"netbsd"
|
| 331 |
+
],
|
| 332 |
+
"engines": {
|
| 333 |
+
"node": ">=18"
|
| 334 |
+
}
|
| 335 |
+
},
|
| 336 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 337 |
+
"version": "0.28.2",
|
| 338 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
| 339 |
+
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
| 340 |
+
"cpu": [
|
| 341 |
+
"x64"
|
| 342 |
+
],
|
| 343 |
+
"dev": true,
|
| 344 |
+
"license": "MIT",
|
| 345 |
+
"optional": true,
|
| 346 |
+
"os": [
|
| 347 |
+
"netbsd"
|
| 348 |
+
],
|
| 349 |
+
"engines": {
|
| 350 |
+
"node": ">=18"
|
| 351 |
+
}
|
| 352 |
+
},
|
| 353 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 354 |
+
"version": "0.28.2",
|
| 355 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
| 356 |
+
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
| 357 |
+
"cpu": [
|
| 358 |
+
"arm64"
|
| 359 |
+
],
|
| 360 |
+
"dev": true,
|
| 361 |
+
"license": "MIT",
|
| 362 |
+
"optional": true,
|
| 363 |
+
"os": [
|
| 364 |
+
"openbsd"
|
| 365 |
+
],
|
| 366 |
+
"engines": {
|
| 367 |
+
"node": ">=18"
|
| 368 |
+
}
|
| 369 |
+
},
|
| 370 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 371 |
+
"version": "0.28.2",
|
| 372 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
| 373 |
+
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
| 374 |
+
"cpu": [
|
| 375 |
+
"x64"
|
| 376 |
+
],
|
| 377 |
+
"dev": true,
|
| 378 |
+
"license": "MIT",
|
| 379 |
+
"optional": true,
|
| 380 |
+
"os": [
|
| 381 |
+
"openbsd"
|
| 382 |
+
],
|
| 383 |
+
"engines": {
|
| 384 |
+
"node": ">=18"
|
| 385 |
+
}
|
| 386 |
+
},
|
| 387 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 388 |
+
"version": "0.28.2",
|
| 389 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
| 390 |
+
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
| 391 |
+
"cpu": [
|
| 392 |
+
"arm64"
|
| 393 |
+
],
|
| 394 |
+
"dev": true,
|
| 395 |
+
"license": "MIT",
|
| 396 |
+
"optional": true,
|
| 397 |
+
"os": [
|
| 398 |
+
"openharmony"
|
| 399 |
+
],
|
| 400 |
+
"engines": {
|
| 401 |
+
"node": ">=18"
|
| 402 |
+
}
|
| 403 |
+
},
|
| 404 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 405 |
+
"version": "0.28.2",
|
| 406 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
| 407 |
+
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
| 408 |
+
"cpu": [
|
| 409 |
+
"x64"
|
| 410 |
+
],
|
| 411 |
+
"dev": true,
|
| 412 |
+
"license": "MIT",
|
| 413 |
+
"optional": true,
|
| 414 |
+
"os": [
|
| 415 |
+
"sunos"
|
| 416 |
+
],
|
| 417 |
+
"engines": {
|
| 418 |
+
"node": ">=18"
|
| 419 |
+
}
|
| 420 |
+
},
|
| 421 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 422 |
+
"version": "0.28.2",
|
| 423 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
| 424 |
+
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
| 425 |
+
"cpu": [
|
| 426 |
+
"arm64"
|
| 427 |
+
],
|
| 428 |
+
"dev": true,
|
| 429 |
+
"license": "MIT",
|
| 430 |
+
"optional": true,
|
| 431 |
+
"os": [
|
| 432 |
+
"win32"
|
| 433 |
+
],
|
| 434 |
+
"engines": {
|
| 435 |
+
"node": ">=18"
|
| 436 |
+
}
|
| 437 |
+
},
|
| 438 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 439 |
+
"version": "0.28.2",
|
| 440 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
| 441 |
+
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
| 442 |
+
"cpu": [
|
| 443 |
+
"ia32"
|
| 444 |
+
],
|
| 445 |
+
"dev": true,
|
| 446 |
+
"license": "MIT",
|
| 447 |
+
"optional": true,
|
| 448 |
+
"os": [
|
| 449 |
+
"win32"
|
| 450 |
+
],
|
| 451 |
+
"engines": {
|
| 452 |
+
"node": ">=18"
|
| 453 |
+
}
|
| 454 |
+
},
|
| 455 |
+
"node_modules/@esbuild/win32-x64": {
|
| 456 |
+
"version": "0.28.2",
|
| 457 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
| 458 |
+
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
| 459 |
+
"cpu": [
|
| 460 |
+
"x64"
|
| 461 |
+
],
|
| 462 |
+
"dev": true,
|
| 463 |
+
"license": "MIT",
|
| 464 |
+
"optional": true,
|
| 465 |
+
"os": [
|
| 466 |
+
"win32"
|
| 467 |
+
],
|
| 468 |
+
"engines": {
|
| 469 |
+
"node": ">=18"
|
| 470 |
+
}
|
| 471 |
+
},
|
| 472 |
+
"node_modules/@supabase/auth-js": {
|
| 473 |
+
"version": "2.118.0",
|
| 474 |
+
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.118.0.tgz",
|
| 475 |
+
"integrity": "sha512-xWfPXyhS2KDSfAneMtI6p/tBnbdiz/5vaRKXEqUfzfC9WCniKr2Ds5o5G7RIGig0bom4z6wUY9zaV07WDVTfnQ==",
|
| 476 |
+
"license": "MIT",
|
| 477 |
+
"dependencies": {
|
| 478 |
+
"tslib": "2.8.1"
|
| 479 |
+
},
|
| 480 |
+
"engines": {
|
| 481 |
+
"node": ">=22.0.0"
|
| 482 |
+
}
|
| 483 |
+
},
|
| 484 |
+
"node_modules/@supabase/functions-js": {
|
| 485 |
+
"version": "2.118.0",
|
| 486 |
+
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.118.0.tgz",
|
| 487 |
+
"integrity": "sha512-4ofletPa/noKAU6KHeqsd3FztGzRPUEJRey1MKY0o+PIHaVePqRPShIXtl37gcVyIWaFM+zNg6r9zh0uUSFOSA==",
|
| 488 |
+
"license": "MIT",
|
| 489 |
+
"dependencies": {
|
| 490 |
+
"tslib": "2.8.1"
|
| 491 |
+
},
|
| 492 |
+
"engines": {
|
| 493 |
+
"node": ">=22.0.0"
|
| 494 |
+
}
|
| 495 |
+
},
|
| 496 |
+
"node_modules/@supabase/phoenix": {
|
| 497 |
+
"version": "0.4.5",
|
| 498 |
+
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz",
|
| 499 |
+
"integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==",
|
| 500 |
+
"license": "MIT"
|
| 501 |
+
},
|
| 502 |
+
"node_modules/@supabase/postgrest-js": {
|
| 503 |
+
"version": "2.118.0",
|
| 504 |
+
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.118.0.tgz",
|
| 505 |
+
"integrity": "sha512-JXy+37KPApHsyotaQACovv/Jy9E22rfiWLRuT+2UjoEAcAIelDFIrsF/FY47Dy19mO7lI0Tq+zDCX8oJ2Ozfiw==",
|
| 506 |
+
"license": "MIT",
|
| 507 |
+
"dependencies": {
|
| 508 |
+
"tslib": "2.8.1"
|
| 509 |
+
},
|
| 510 |
+
"engines": {
|
| 511 |
+
"node": ">=22.0.0"
|
| 512 |
+
}
|
| 513 |
+
},
|
| 514 |
+
"node_modules/@supabase/realtime-js": {
|
| 515 |
+
"version": "2.118.0",
|
| 516 |
+
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.118.0.tgz",
|
| 517 |
+
"integrity": "sha512-pzCIEWjqD5/xqEZQerpt+q9rsgYkcrkgCJgf0W2ULqC4fYUeB/p9CFa3t9vn+IVh6dMN9r4ZCE262lZvUC+Bfw==",
|
| 518 |
+
"license": "MIT",
|
| 519 |
+
"dependencies": {
|
| 520 |
+
"@supabase/phoenix": "0.4.5",
|
| 521 |
+
"tslib": "2.8.1"
|
| 522 |
+
},
|
| 523 |
+
"engines": {
|
| 524 |
+
"node": ">=22.0.0"
|
| 525 |
+
}
|
| 526 |
+
},
|
| 527 |
+
"node_modules/@supabase/storage-js": {
|
| 528 |
+
"version": "2.118.0",
|
| 529 |
+
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.118.0.tgz",
|
| 530 |
+
"integrity": "sha512-5MNkKt2Moy7Jud/sPX7/28BwdDxZMCVHGJT00b8op3KRa9LKSjOAYU5IcfGWIstaasAqY8GkbA6oI3IRDgB1YQ==",
|
| 531 |
+
"license": "MIT",
|
| 532 |
+
"dependencies": {
|
| 533 |
+
"iceberg-js": "^0.8.1",
|
| 534 |
+
"tslib": "2.8.1"
|
| 535 |
+
},
|
| 536 |
+
"engines": {
|
| 537 |
+
"node": ">=22.0.0"
|
| 538 |
+
}
|
| 539 |
+
},
|
| 540 |
+
"node_modules/@supabase/supabase-js": {
|
| 541 |
+
"version": "2.118.0",
|
| 542 |
+
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.118.0.tgz",
|
| 543 |
+
"integrity": "sha512-FxI8UXMVC2UG7IkeiI8fuQZqq2oI3JeSuN8MPIC2RLVaoEAxKS0nqE6kii9Ewpq8f9gNPM6git1Y8f0vN/QLFw==",
|
| 544 |
+
"license": "MIT",
|
| 545 |
+
"dependencies": {
|
| 546 |
+
"@supabase/auth-js": "2.118.0",
|
| 547 |
+
"@supabase/functions-js": "2.118.0",
|
| 548 |
+
"@supabase/postgrest-js": "2.118.0",
|
| 549 |
+
"@supabase/realtime-js": "2.118.0",
|
| 550 |
+
"@supabase/storage-js": "2.118.0"
|
| 551 |
+
},
|
| 552 |
+
"engines": {
|
| 553 |
+
"node": ">=22.0.0"
|
| 554 |
+
},
|
| 555 |
+
"peerDependencies": {
|
| 556 |
+
"@opentelemetry/api": ">=1.0.0"
|
| 557 |
+
},
|
| 558 |
+
"peerDependenciesMeta": {
|
| 559 |
+
"@opentelemetry/api": {
|
| 560 |
+
"optional": true
|
| 561 |
+
}
|
| 562 |
+
}
|
| 563 |
+
},
|
| 564 |
+
"node_modules/@types/body-parser": {
|
| 565 |
+
"version": "1.19.6",
|
| 566 |
+
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
| 567 |
+
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
| 568 |
+
"dev": true,
|
| 569 |
+
"license": "MIT",
|
| 570 |
+
"dependencies": {
|
| 571 |
+
"@types/connect": "*",
|
| 572 |
+
"@types/node": "*"
|
| 573 |
+
}
|
| 574 |
+
},
|
| 575 |
+
"node_modules/@types/connect": {
|
| 576 |
+
"version": "3.4.38",
|
| 577 |
+
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
| 578 |
+
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
| 579 |
+
"dev": true,
|
| 580 |
+
"license": "MIT",
|
| 581 |
+
"dependencies": {
|
| 582 |
+
"@types/node": "*"
|
| 583 |
+
}
|
| 584 |
+
},
|
| 585 |
+
"node_modules/@types/cookie-parser": {
|
| 586 |
+
"version": "1.4.10",
|
| 587 |
+
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
|
| 588 |
+
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
|
| 589 |
+
"dev": true,
|
| 590 |
+
"license": "MIT",
|
| 591 |
+
"peerDependencies": {
|
| 592 |
+
"@types/express": "*"
|
| 593 |
+
}
|
| 594 |
+
},
|
| 595 |
+
"node_modules/@types/cors": {
|
| 596 |
+
"version": "2.8.19",
|
| 597 |
+
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
| 598 |
+
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
| 599 |
+
"dev": true,
|
| 600 |
+
"license": "MIT",
|
| 601 |
+
"dependencies": {
|
| 602 |
+
"@types/node": "*"
|
| 603 |
+
}
|
| 604 |
+
},
|
| 605 |
+
"node_modules/@types/express": {
|
| 606 |
+
"version": "5.0.6",
|
| 607 |
+
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
|
| 608 |
+
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
| 609 |
+
"dev": true,
|
| 610 |
+
"license": "MIT",
|
| 611 |
+
"dependencies": {
|
| 612 |
+
"@types/body-parser": "*",
|
| 613 |
+
"@types/express-serve-static-core": "^5.0.0",
|
| 614 |
+
"@types/serve-static": "^2"
|
| 615 |
+
}
|
| 616 |
+
},
|
| 617 |
+
"node_modules/@types/express-serve-static-core": {
|
| 618 |
+
"version": "5.1.3",
|
| 619 |
+
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz",
|
| 620 |
+
"integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==",
|
| 621 |
+
"dev": true,
|
| 622 |
+
"license": "MIT",
|
| 623 |
+
"dependencies": {
|
| 624 |
+
"@types/node": "*",
|
| 625 |
+
"@types/qs": "*",
|
| 626 |
+
"@types/range-parser": "*",
|
| 627 |
+
"@types/send": "*"
|
| 628 |
+
}
|
| 629 |
+
},
|
| 630 |
+
"node_modules/@types/http-errors": {
|
| 631 |
+
"version": "2.0.5",
|
| 632 |
+
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
| 633 |
+
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
| 634 |
+
"dev": true,
|
| 635 |
+
"license": "MIT"
|
| 636 |
+
},
|
| 637 |
+
"node_modules/@types/node": {
|
| 638 |
+
"version": "22.20.5",
|
| 639 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
|
| 640 |
+
"integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
|
| 641 |
+
"dev": true,
|
| 642 |
+
"license": "MIT",
|
| 643 |
+
"dependencies": {
|
| 644 |
+
"undici-types": "~6.21.0"
|
| 645 |
+
}
|
| 646 |
+
},
|
| 647 |
+
"node_modules/@types/qs": {
|
| 648 |
+
"version": "6.15.1",
|
| 649 |
+
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
|
| 650 |
+
"integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
|
| 651 |
+
"dev": true,
|
| 652 |
+
"license": "MIT"
|
| 653 |
+
},
|
| 654 |
+
"node_modules/@types/range-parser": {
|
| 655 |
+
"version": "1.2.7",
|
| 656 |
+
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
| 657 |
+
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
| 658 |
+
"dev": true,
|
| 659 |
+
"license": "MIT"
|
| 660 |
+
},
|
| 661 |
+
"node_modules/@types/send": {
|
| 662 |
+
"version": "1.2.1",
|
| 663 |
+
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
| 664 |
+
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
| 665 |
+
"dev": true,
|
| 666 |
+
"license": "MIT",
|
| 667 |
+
"dependencies": {
|
| 668 |
+
"@types/node": "*"
|
| 669 |
+
}
|
| 670 |
+
},
|
| 671 |
+
"node_modules/@types/serve-static": {
|
| 672 |
+
"version": "2.2.0",
|
| 673 |
+
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
|
| 674 |
+
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
|
| 675 |
+
"dev": true,
|
| 676 |
+
"license": "MIT",
|
| 677 |
+
"dependencies": {
|
| 678 |
+
"@types/http-errors": "*",
|
| 679 |
+
"@types/node": "*"
|
| 680 |
+
}
|
| 681 |
+
},
|
| 682 |
+
"node_modules/accepts": {
|
| 683 |
+
"version": "2.0.0",
|
| 684 |
+
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
| 685 |
+
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
| 686 |
+
"license": "MIT",
|
| 687 |
+
"dependencies": {
|
| 688 |
+
"mime-types": "^3.0.0",
|
| 689 |
+
"negotiator": "^1.0.0"
|
| 690 |
+
},
|
| 691 |
+
"engines": {
|
| 692 |
+
"node": ">= 0.6"
|
| 693 |
+
}
|
| 694 |
+
},
|
| 695 |
+
"node_modules/bcryptjs": {
|
| 696 |
+
"version": "3.0.3",
|
| 697 |
+
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
| 698 |
+
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
| 699 |
+
"license": "BSD-3-Clause",
|
| 700 |
+
"bin": {
|
| 701 |
+
"bcrypt": "bin/bcrypt"
|
| 702 |
+
}
|
| 703 |
+
},
|
| 704 |
+
"node_modules/body-parser": {
|
| 705 |
+
"version": "2.3.0",
|
| 706 |
+
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
| 707 |
+
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
| 708 |
+
"license": "MIT",
|
| 709 |
+
"dependencies": {
|
| 710 |
+
"bytes": "^3.1.2",
|
| 711 |
+
"content-type": "^2.0.0",
|
| 712 |
+
"debug": "^4.4.3",
|
| 713 |
+
"http-errors": "^2.0.1",
|
| 714 |
+
"iconv-lite": "^0.7.2",
|
| 715 |
+
"on-finished": "^2.4.1",
|
| 716 |
+
"qs": "^6.15.2",
|
| 717 |
+
"raw-body": "^3.0.2",
|
| 718 |
+
"type-is": "^2.1.0"
|
| 719 |
+
},
|
| 720 |
+
"engines": {
|
| 721 |
+
"node": ">=18"
|
| 722 |
+
},
|
| 723 |
+
"funding": {
|
| 724 |
+
"type": "opencollective",
|
| 725 |
+
"url": "https://opencollective.com/express"
|
| 726 |
+
}
|
| 727 |
+
},
|
| 728 |
+
"node_modules/bytes": {
|
| 729 |
+
"version": "3.1.2",
|
| 730 |
+
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
| 731 |
+
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
| 732 |
+
"license": "MIT",
|
| 733 |
+
"engines": {
|
| 734 |
+
"node": ">= 0.8"
|
| 735 |
+
}
|
| 736 |
+
},
|
| 737 |
+
"node_modules/call-bind-apply-helpers": {
|
| 738 |
+
"version": "1.0.2",
|
| 739 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 740 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 741 |
+
"license": "MIT",
|
| 742 |
+
"dependencies": {
|
| 743 |
+
"es-errors": "^1.3.0",
|
| 744 |
+
"function-bind": "^1.1.2"
|
| 745 |
+
},
|
| 746 |
+
"engines": {
|
| 747 |
+
"node": ">= 0.4"
|
| 748 |
+
}
|
| 749 |
+
},
|
| 750 |
+
"node_modules/call-bound": {
|
| 751 |
+
"version": "1.0.4",
|
| 752 |
+
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
| 753 |
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
| 754 |
+
"license": "MIT",
|
| 755 |
+
"dependencies": {
|
| 756 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 757 |
+
"get-intrinsic": "^1.3.0"
|
| 758 |
+
},
|
| 759 |
+
"engines": {
|
| 760 |
+
"node": ">= 0.4"
|
| 761 |
+
},
|
| 762 |
+
"funding": {
|
| 763 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 764 |
+
}
|
| 765 |
+
},
|
| 766 |
+
"node_modules/content-disposition": {
|
| 767 |
+
"version": "2.0.1",
|
| 768 |
+
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
|
| 769 |
+
"integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==",
|
| 770 |
+
"license": "MIT",
|
| 771 |
+
"engines": {
|
| 772 |
+
"node": ">=18"
|
| 773 |
+
},
|
| 774 |
+
"funding": {
|
| 775 |
+
"type": "opencollective",
|
| 776 |
+
"url": "https://opencollective.com/express"
|
| 777 |
+
}
|
| 778 |
+
},
|
| 779 |
+
"node_modules/content-type": {
|
| 780 |
+
"version": "2.1.0",
|
| 781 |
+
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
| 782 |
+
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
| 783 |
+
"license": "MIT",
|
| 784 |
+
"engines": {
|
| 785 |
+
"node": ">=18"
|
| 786 |
+
},
|
| 787 |
+
"funding": {
|
| 788 |
+
"type": "opencollective",
|
| 789 |
+
"url": "https://opencollective.com/express"
|
| 790 |
+
}
|
| 791 |
+
},
|
| 792 |
+
"node_modules/cookie": {
|
| 793 |
+
"version": "0.7.2",
|
| 794 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
| 795 |
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
| 796 |
+
"license": "MIT",
|
| 797 |
+
"engines": {
|
| 798 |
+
"node": ">= 0.6"
|
| 799 |
+
}
|
| 800 |
+
},
|
| 801 |
+
"node_modules/cookie-parser": {
|
| 802 |
+
"version": "1.4.7",
|
| 803 |
+
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
| 804 |
+
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
| 805 |
+
"license": "MIT",
|
| 806 |
+
"dependencies": {
|
| 807 |
+
"cookie": "0.7.2",
|
| 808 |
+
"cookie-signature": "1.0.6"
|
| 809 |
+
},
|
| 810 |
+
"engines": {
|
| 811 |
+
"node": ">= 0.8.0"
|
| 812 |
+
}
|
| 813 |
+
},
|
| 814 |
+
"node_modules/cookie-signature": {
|
| 815 |
+
"version": "1.0.6",
|
| 816 |
+
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
| 817 |
+
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
| 818 |
+
"license": "MIT"
|
| 819 |
+
},
|
| 820 |
+
"node_modules/cors": {
|
| 821 |
+
"version": "2.8.6",
|
| 822 |
+
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
| 823 |
+
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
| 824 |
+
"license": "MIT",
|
| 825 |
+
"dependencies": {
|
| 826 |
+
"object-assign": "^4",
|
| 827 |
+
"vary": "^1"
|
| 828 |
+
},
|
| 829 |
+
"engines": {
|
| 830 |
+
"node": ">= 0.10"
|
| 831 |
+
},
|
| 832 |
+
"funding": {
|
| 833 |
+
"type": "opencollective",
|
| 834 |
+
"url": "https://opencollective.com/express"
|
| 835 |
+
}
|
| 836 |
+
},
|
| 837 |
+
"node_modules/debug": {
|
| 838 |
+
"version": "4.4.3",
|
| 839 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 840 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 841 |
+
"license": "MIT",
|
| 842 |
+
"dependencies": {
|
| 843 |
+
"ms": "^2.1.3"
|
| 844 |
+
},
|
| 845 |
+
"engines": {
|
| 846 |
+
"node": ">=6.0"
|
| 847 |
+
},
|
| 848 |
+
"peerDependenciesMeta": {
|
| 849 |
+
"supports-color": {
|
| 850 |
+
"optional": true
|
| 851 |
+
}
|
| 852 |
+
}
|
| 853 |
+
},
|
| 854 |
+
"node_modules/depd": {
|
| 855 |
+
"version": "2.0.0",
|
| 856 |
+
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
| 857 |
+
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
| 858 |
+
"license": "MIT",
|
| 859 |
+
"engines": {
|
| 860 |
+
"node": ">= 0.8"
|
| 861 |
+
}
|
| 862 |
+
},
|
| 863 |
+
"node_modules/dotenv": {
|
| 864 |
+
"version": "16.6.1",
|
| 865 |
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
| 866 |
+
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
| 867 |
+
"license": "BSD-2-Clause",
|
| 868 |
+
"engines": {
|
| 869 |
+
"node": ">=12"
|
| 870 |
+
},
|
| 871 |
+
"funding": {
|
| 872 |
+
"url": "https://dotenvx.com"
|
| 873 |
+
}
|
| 874 |
+
},
|
| 875 |
+
"node_modules/dunder-proto": {
|
| 876 |
+
"version": "1.0.1",
|
| 877 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 878 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 879 |
+
"license": "MIT",
|
| 880 |
+
"dependencies": {
|
| 881 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 882 |
+
"es-errors": "^1.3.0",
|
| 883 |
+
"gopd": "^1.2.0"
|
| 884 |
+
},
|
| 885 |
+
"engines": {
|
| 886 |
+
"node": ">= 0.4"
|
| 887 |
+
}
|
| 888 |
+
},
|
| 889 |
+
"node_modules/ee-first": {
|
| 890 |
+
"version": "1.1.1",
|
| 891 |
+
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
| 892 |
+
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
| 893 |
+
"license": "MIT"
|
| 894 |
+
},
|
| 895 |
+
"node_modules/encodeurl": {
|
| 896 |
+
"version": "2.0.0",
|
| 897 |
+
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
| 898 |
+
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
| 899 |
+
"license": "MIT",
|
| 900 |
+
"engines": {
|
| 901 |
+
"node": ">= 0.8"
|
| 902 |
+
}
|
| 903 |
+
},
|
| 904 |
+
"node_modules/es-define-property": {
|
| 905 |
+
"version": "1.0.1",
|
| 906 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 907 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 908 |
+
"license": "MIT",
|
| 909 |
+
"engines": {
|
| 910 |
+
"node": ">= 0.4"
|
| 911 |
+
}
|
| 912 |
+
},
|
| 913 |
+
"node_modules/es-errors": {
|
| 914 |
+
"version": "1.3.0",
|
| 915 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 916 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 917 |
+
"license": "MIT",
|
| 918 |
+
"engines": {
|
| 919 |
+
"node": ">= 0.4"
|
| 920 |
+
}
|
| 921 |
+
},
|
| 922 |
+
"node_modules/es-object-atoms": {
|
| 923 |
+
"version": "1.1.2",
|
| 924 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
| 925 |
+
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
| 926 |
+
"license": "MIT",
|
| 927 |
+
"dependencies": {
|
| 928 |
+
"es-errors": "^1.3.0"
|
| 929 |
+
},
|
| 930 |
+
"engines": {
|
| 931 |
+
"node": ">= 0.4"
|
| 932 |
+
}
|
| 933 |
+
},
|
| 934 |
+
"node_modules/esbuild": {
|
| 935 |
+
"version": "0.28.2",
|
| 936 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
| 937 |
+
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
| 938 |
+
"dev": true,
|
| 939 |
+
"hasInstallScript": true,
|
| 940 |
+
"license": "MIT",
|
| 941 |
+
"bin": {
|
| 942 |
+
"esbuild": "bin/esbuild"
|
| 943 |
+
},
|
| 944 |
+
"engines": {
|
| 945 |
+
"node": ">=18"
|
| 946 |
+
},
|
| 947 |
+
"optionalDependencies": {
|
| 948 |
+
"@esbuild/aix-ppc64": "0.28.2",
|
| 949 |
+
"@esbuild/android-arm": "0.28.2",
|
| 950 |
+
"@esbuild/android-arm64": "0.28.2",
|
| 951 |
+
"@esbuild/android-x64": "0.28.2",
|
| 952 |
+
"@esbuild/darwin-arm64": "0.28.2",
|
| 953 |
+
"@esbuild/darwin-x64": "0.28.2",
|
| 954 |
+
"@esbuild/freebsd-arm64": "0.28.2",
|
| 955 |
+
"@esbuild/freebsd-x64": "0.28.2",
|
| 956 |
+
"@esbuild/linux-arm": "0.28.2",
|
| 957 |
+
"@esbuild/linux-arm64": "0.28.2",
|
| 958 |
+
"@esbuild/linux-ia32": "0.28.2",
|
| 959 |
+
"@esbuild/linux-loong64": "0.28.2",
|
| 960 |
+
"@esbuild/linux-mips64el": "0.28.2",
|
| 961 |
+
"@esbuild/linux-ppc64": "0.28.2",
|
| 962 |
+
"@esbuild/linux-riscv64": "0.28.2",
|
| 963 |
+
"@esbuild/linux-s390x": "0.28.2",
|
| 964 |
+
"@esbuild/linux-x64": "0.28.2",
|
| 965 |
+
"@esbuild/netbsd-arm64": "0.28.2",
|
| 966 |
+
"@esbuild/netbsd-x64": "0.28.2",
|
| 967 |
+
"@esbuild/openbsd-arm64": "0.28.2",
|
| 968 |
+
"@esbuild/openbsd-x64": "0.28.2",
|
| 969 |
+
"@esbuild/openharmony-arm64": "0.28.2",
|
| 970 |
+
"@esbuild/sunos-x64": "0.28.2",
|
| 971 |
+
"@esbuild/win32-arm64": "0.28.2",
|
| 972 |
+
"@esbuild/win32-ia32": "0.28.2",
|
| 973 |
+
"@esbuild/win32-x64": "0.28.2"
|
| 974 |
+
}
|
| 975 |
+
},
|
| 976 |
+
"node_modules/escape-html": {
|
| 977 |
+
"version": "1.0.3",
|
| 978 |
+
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
| 979 |
+
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
| 980 |
+
"license": "MIT"
|
| 981 |
+
},
|
| 982 |
+
"node_modules/etag": {
|
| 983 |
+
"version": "1.8.1",
|
| 984 |
+
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
| 985 |
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
| 986 |
+
"license": "MIT",
|
| 987 |
+
"engines": {
|
| 988 |
+
"node": ">= 0.6"
|
| 989 |
+
}
|
| 990 |
+
},
|
| 991 |
+
"node_modules/express": {
|
| 992 |
+
"version": "5.3.0",
|
| 993 |
+
"resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz",
|
| 994 |
+
"integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==",
|
| 995 |
+
"license": "MIT",
|
| 996 |
+
"dependencies": {
|
| 997 |
+
"accepts": "^2.0.0",
|
| 998 |
+
"body-parser": "^2.3.0",
|
| 999 |
+
"content-disposition": "^2.0.1",
|
| 1000 |
+
"content-type": "^2.0.0",
|
| 1001 |
+
"cookie": "^0.7.1",
|
| 1002 |
+
"cookie-signature": "^1.2.1",
|
| 1003 |
+
"debug": "^4.4.0",
|
| 1004 |
+
"depd": "^2.0.0",
|
| 1005 |
+
"encodeurl": "^2.0.0",
|
| 1006 |
+
"escape-html": "^1.0.3",
|
| 1007 |
+
"etag": "^1.8.1",
|
| 1008 |
+
"finalhandler": "^2.1.0",
|
| 1009 |
+
"fresh": "^2.0.0",
|
| 1010 |
+
"http-errors": "^2.0.0",
|
| 1011 |
+
"merge-descriptors": "^2.0.0",
|
| 1012 |
+
"mime-types": "^3.0.0",
|
| 1013 |
+
"on-finished": "^2.4.1",
|
| 1014 |
+
"once": "^1.4.0",
|
| 1015 |
+
"parseurl": "^1.3.3",
|
| 1016 |
+
"proxy-addr": "^2.0.8",
|
| 1017 |
+
"qs": "^6.16.0",
|
| 1018 |
+
"range-parser": "^1.2.1",
|
| 1019 |
+
"router": "^2.2.0",
|
| 1020 |
+
"send": "^1.1.0",
|
| 1021 |
+
"serve-static": "^2.2.0",
|
| 1022 |
+
"statuses": "^2.0.1",
|
| 1023 |
+
"type-is": "^2.1.0",
|
| 1024 |
+
"vary": "^1.1.2"
|
| 1025 |
+
},
|
| 1026 |
+
"engines": {
|
| 1027 |
+
"node": ">= 18"
|
| 1028 |
+
},
|
| 1029 |
+
"funding": {
|
| 1030 |
+
"type": "opencollective",
|
| 1031 |
+
"url": "https://opencollective.com/express"
|
| 1032 |
+
}
|
| 1033 |
+
},
|
| 1034 |
+
"node_modules/express-rate-limit": {
|
| 1035 |
+
"version": "8.7.1",
|
| 1036 |
+
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz",
|
| 1037 |
+
"integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==",
|
| 1038 |
+
"license": "MIT",
|
| 1039 |
+
"dependencies": {
|
| 1040 |
+
"debug": "^4.4.3",
|
| 1041 |
+
"ip-address": "^10.2.0"
|
| 1042 |
+
},
|
| 1043 |
+
"engines": {
|
| 1044 |
+
"node": ">= 16"
|
| 1045 |
+
},
|
| 1046 |
+
"funding": {
|
| 1047 |
+
"url": "https://github.com/sponsors/express-rate-limit"
|
| 1048 |
+
},
|
| 1049 |
+
"peerDependencies": {
|
| 1050 |
+
"express": ">= 4.11"
|
| 1051 |
+
}
|
| 1052 |
+
},
|
| 1053 |
+
"node_modules/express/node_modules/cookie-signature": {
|
| 1054 |
+
"version": "1.2.2",
|
| 1055 |
+
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
| 1056 |
+
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
| 1057 |
+
"license": "MIT",
|
| 1058 |
+
"engines": {
|
| 1059 |
+
"node": ">=6.6.0"
|
| 1060 |
+
}
|
| 1061 |
+
},
|
| 1062 |
+
"node_modules/finalhandler": {
|
| 1063 |
+
"version": "2.1.1",
|
| 1064 |
+
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
| 1065 |
+
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
| 1066 |
+
"license": "MIT",
|
| 1067 |
+
"dependencies": {
|
| 1068 |
+
"debug": "^4.4.0",
|
| 1069 |
+
"encodeurl": "^2.0.0",
|
| 1070 |
+
"escape-html": "^1.0.3",
|
| 1071 |
+
"on-finished": "^2.4.1",
|
| 1072 |
+
"parseurl": "^1.3.3",
|
| 1073 |
+
"statuses": "^2.0.1"
|
| 1074 |
+
},
|
| 1075 |
+
"engines": {
|
| 1076 |
+
"node": ">= 18.0.0"
|
| 1077 |
+
},
|
| 1078 |
+
"funding": {
|
| 1079 |
+
"type": "opencollective",
|
| 1080 |
+
"url": "https://opencollective.com/express"
|
| 1081 |
+
}
|
| 1082 |
+
},
|
| 1083 |
+
"node_modules/forwarded": {
|
| 1084 |
+
"version": "0.2.0",
|
| 1085 |
+
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
| 1086 |
+
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
| 1087 |
+
"license": "MIT",
|
| 1088 |
+
"engines": {
|
| 1089 |
+
"node": ">= 0.6"
|
| 1090 |
+
}
|
| 1091 |
+
},
|
| 1092 |
+
"node_modules/fresh": {
|
| 1093 |
+
"version": "2.0.0",
|
| 1094 |
+
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
| 1095 |
+
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
| 1096 |
+
"license": "MIT",
|
| 1097 |
+
"engines": {
|
| 1098 |
+
"node": ">= 0.8"
|
| 1099 |
+
}
|
| 1100 |
+
},
|
| 1101 |
+
"node_modules/fsevents": {
|
| 1102 |
+
"version": "2.3.3",
|
| 1103 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1104 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1105 |
+
"dev": true,
|
| 1106 |
+
"hasInstallScript": true,
|
| 1107 |
+
"license": "MIT",
|
| 1108 |
+
"optional": true,
|
| 1109 |
+
"os": [
|
| 1110 |
+
"darwin"
|
| 1111 |
+
],
|
| 1112 |
+
"engines": {
|
| 1113 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1114 |
+
}
|
| 1115 |
+
},
|
| 1116 |
+
"node_modules/function-bind": {
|
| 1117 |
+
"version": "1.1.2",
|
| 1118 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 1119 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 1120 |
+
"license": "MIT",
|
| 1121 |
+
"funding": {
|
| 1122 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1123 |
+
}
|
| 1124 |
+
},
|
| 1125 |
+
"node_modules/get-intrinsic": {
|
| 1126 |
+
"version": "1.3.0",
|
| 1127 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 1128 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 1129 |
+
"license": "MIT",
|
| 1130 |
+
"dependencies": {
|
| 1131 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 1132 |
+
"es-define-property": "^1.0.1",
|
| 1133 |
+
"es-errors": "^1.3.0",
|
| 1134 |
+
"es-object-atoms": "^1.1.1",
|
| 1135 |
+
"function-bind": "^1.1.2",
|
| 1136 |
+
"get-proto": "^1.0.1",
|
| 1137 |
+
"gopd": "^1.2.0",
|
| 1138 |
+
"has-symbols": "^1.1.0",
|
| 1139 |
+
"hasown": "^2.0.2",
|
| 1140 |
+
"math-intrinsics": "^1.1.0"
|
| 1141 |
+
},
|
| 1142 |
+
"engines": {
|
| 1143 |
+
"node": ">= 0.4"
|
| 1144 |
+
},
|
| 1145 |
+
"funding": {
|
| 1146 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1147 |
+
}
|
| 1148 |
+
},
|
| 1149 |
+
"node_modules/get-proto": {
|
| 1150 |
+
"version": "1.0.1",
|
| 1151 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 1152 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 1153 |
+
"license": "MIT",
|
| 1154 |
+
"dependencies": {
|
| 1155 |
+
"dunder-proto": "^1.0.1",
|
| 1156 |
+
"es-object-atoms": "^1.0.0"
|
| 1157 |
+
},
|
| 1158 |
+
"engines": {
|
| 1159 |
+
"node": ">= 0.4"
|
| 1160 |
+
}
|
| 1161 |
+
},
|
| 1162 |
+
"node_modules/gopd": {
|
| 1163 |
+
"version": "1.2.0",
|
| 1164 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 1165 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 1166 |
+
"license": "MIT",
|
| 1167 |
+
"engines": {
|
| 1168 |
+
"node": ">= 0.4"
|
| 1169 |
+
},
|
| 1170 |
+
"funding": {
|
| 1171 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1172 |
+
}
|
| 1173 |
+
},
|
| 1174 |
+
"node_modules/has-symbols": {
|
| 1175 |
+
"version": "1.1.0",
|
| 1176 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 1177 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 1178 |
+
"license": "MIT",
|
| 1179 |
+
"engines": {
|
| 1180 |
+
"node": ">= 0.4"
|
| 1181 |
+
},
|
| 1182 |
+
"funding": {
|
| 1183 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1184 |
+
}
|
| 1185 |
+
},
|
| 1186 |
+
"node_modules/hasown": {
|
| 1187 |
+
"version": "2.0.4",
|
| 1188 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
| 1189 |
+
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
| 1190 |
+
"license": "MIT",
|
| 1191 |
+
"dependencies": {
|
| 1192 |
+
"function-bind": "^1.1.2"
|
| 1193 |
+
},
|
| 1194 |
+
"engines": {
|
| 1195 |
+
"node": ">= 0.4"
|
| 1196 |
+
}
|
| 1197 |
+
},
|
| 1198 |
+
"node_modules/helmet": {
|
| 1199 |
+
"version": "8.3.0",
|
| 1200 |
+
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
|
| 1201 |
+
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
|
| 1202 |
+
"license": "MIT",
|
| 1203 |
+
"engines": {
|
| 1204 |
+
"node": ">=18.0.0"
|
| 1205 |
+
},
|
| 1206 |
+
"funding": {
|
| 1207 |
+
"url": "https://github.com/sponsors/EvanHahn"
|
| 1208 |
+
}
|
| 1209 |
+
},
|
| 1210 |
+
"node_modules/http-errors": {
|
| 1211 |
+
"version": "2.0.1",
|
| 1212 |
+
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
| 1213 |
+
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
| 1214 |
+
"license": "MIT",
|
| 1215 |
+
"dependencies": {
|
| 1216 |
+
"depd": "~2.0.0",
|
| 1217 |
+
"inherits": "~2.0.4",
|
| 1218 |
+
"setprototypeof": "~1.2.0",
|
| 1219 |
+
"statuses": "~2.0.2",
|
| 1220 |
+
"toidentifier": "~1.0.1"
|
| 1221 |
+
},
|
| 1222 |
+
"engines": {
|
| 1223 |
+
"node": ">= 0.8"
|
| 1224 |
+
},
|
| 1225 |
+
"funding": {
|
| 1226 |
+
"type": "opencollective",
|
| 1227 |
+
"url": "https://opencollective.com/express"
|
| 1228 |
+
}
|
| 1229 |
+
},
|
| 1230 |
+
"node_modules/iceberg-js": {
|
| 1231 |
+
"version": "0.8.1",
|
| 1232 |
+
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
| 1233 |
+
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
| 1234 |
+
"license": "MIT",
|
| 1235 |
+
"engines": {
|
| 1236 |
+
"node": ">=20.0.0"
|
| 1237 |
+
}
|
| 1238 |
+
},
|
| 1239 |
+
"node_modules/iconv-lite": {
|
| 1240 |
+
"version": "0.7.3",
|
| 1241 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
| 1242 |
+
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
| 1243 |
+
"license": "MIT",
|
| 1244 |
+
"dependencies": {
|
| 1245 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
| 1246 |
+
},
|
| 1247 |
+
"engines": {
|
| 1248 |
+
"node": ">=0.10.0"
|
| 1249 |
+
},
|
| 1250 |
+
"funding": {
|
| 1251 |
+
"type": "opencollective",
|
| 1252 |
+
"url": "https://opencollective.com/express"
|
| 1253 |
+
}
|
| 1254 |
+
},
|
| 1255 |
+
"node_modules/inherits": {
|
| 1256 |
+
"version": "2.0.4",
|
| 1257 |
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 1258 |
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 1259 |
+
"license": "ISC"
|
| 1260 |
+
},
|
| 1261 |
+
"node_modules/ip-address": {
|
| 1262 |
+
"version": "10.7.4",
|
| 1263 |
+
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz",
|
| 1264 |
+
"integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==",
|
| 1265 |
+
"license": "MIT",
|
| 1266 |
+
"engines": {
|
| 1267 |
+
"node": ">= 12"
|
| 1268 |
+
}
|
| 1269 |
+
},
|
| 1270 |
+
"node_modules/ipaddr.js": {
|
| 1271 |
+
"version": "1.9.1",
|
| 1272 |
+
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
| 1273 |
+
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
| 1274 |
+
"license": "MIT",
|
| 1275 |
+
"engines": {
|
| 1276 |
+
"node": ">= 0.10"
|
| 1277 |
+
}
|
| 1278 |
+
},
|
| 1279 |
+
"node_modules/is-promise": {
|
| 1280 |
+
"version": "4.0.0",
|
| 1281 |
+
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
| 1282 |
+
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
| 1283 |
+
"license": "MIT"
|
| 1284 |
+
},
|
| 1285 |
+
"node_modules/math-intrinsics": {
|
| 1286 |
+
"version": "1.1.0",
|
| 1287 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 1288 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 1289 |
+
"license": "MIT",
|
| 1290 |
+
"engines": {
|
| 1291 |
+
"node": ">= 0.4"
|
| 1292 |
+
}
|
| 1293 |
+
},
|
| 1294 |
+
"node_modules/media-typer": {
|
| 1295 |
+
"version": "1.1.1",
|
| 1296 |
+
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
|
| 1297 |
+
"integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
|
| 1298 |
+
"license": "MIT",
|
| 1299 |
+
"engines": {
|
| 1300 |
+
"node": ">= 0.8"
|
| 1301 |
+
},
|
| 1302 |
+
"funding": {
|
| 1303 |
+
"type": "opencollective",
|
| 1304 |
+
"url": "https://opencollective.com/express"
|
| 1305 |
+
}
|
| 1306 |
+
},
|
| 1307 |
+
"node_modules/merge-descriptors": {
|
| 1308 |
+
"version": "2.0.0",
|
| 1309 |
+
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
| 1310 |
+
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
| 1311 |
+
"license": "MIT",
|
| 1312 |
+
"engines": {
|
| 1313 |
+
"node": ">=18"
|
| 1314 |
+
},
|
| 1315 |
+
"funding": {
|
| 1316 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1317 |
+
}
|
| 1318 |
+
},
|
| 1319 |
+
"node_modules/mime-db": {
|
| 1320 |
+
"version": "1.54.0",
|
| 1321 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
| 1322 |
+
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
| 1323 |
+
"license": "MIT",
|
| 1324 |
+
"engines": {
|
| 1325 |
+
"node": ">= 0.6"
|
| 1326 |
+
}
|
| 1327 |
+
},
|
| 1328 |
+
"node_modules/mime-types": {
|
| 1329 |
+
"version": "3.0.2",
|
| 1330 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
| 1331 |
+
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
| 1332 |
+
"license": "MIT",
|
| 1333 |
+
"dependencies": {
|
| 1334 |
+
"mime-db": "^1.54.0"
|
| 1335 |
+
},
|
| 1336 |
+
"engines": {
|
| 1337 |
+
"node": ">=18"
|
| 1338 |
+
},
|
| 1339 |
+
"funding": {
|
| 1340 |
+
"type": "opencollective",
|
| 1341 |
+
"url": "https://opencollective.com/express"
|
| 1342 |
+
}
|
| 1343 |
+
},
|
| 1344 |
+
"node_modules/ms": {
|
| 1345 |
+
"version": "2.1.3",
|
| 1346 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1347 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1348 |
+
"license": "MIT"
|
| 1349 |
+
},
|
| 1350 |
+
"node_modules/negotiator": {
|
| 1351 |
+
"version": "1.1.0",
|
| 1352 |
+
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
|
| 1353 |
+
"integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
|
| 1354 |
+
"license": "MIT",
|
| 1355 |
+
"dependencies": {
|
| 1356 |
+
"content-type": "^2.1.0"
|
| 1357 |
+
},
|
| 1358 |
+
"engines": {
|
| 1359 |
+
"node": ">=18"
|
| 1360 |
+
},
|
| 1361 |
+
"funding": {
|
| 1362 |
+
"type": "opencollective",
|
| 1363 |
+
"url": "https://opencollective.com/express"
|
| 1364 |
+
}
|
| 1365 |
+
},
|
| 1366 |
+
"node_modules/object-assign": {
|
| 1367 |
+
"version": "4.1.1",
|
| 1368 |
+
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
| 1369 |
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
| 1370 |
+
"license": "MIT",
|
| 1371 |
+
"engines": {
|
| 1372 |
+
"node": ">=0.10.0"
|
| 1373 |
+
}
|
| 1374 |
+
},
|
| 1375 |
+
"node_modules/object-inspect": {
|
| 1376 |
+
"version": "1.13.4",
|
| 1377 |
+
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
| 1378 |
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
| 1379 |
+
"license": "MIT",
|
| 1380 |
+
"engines": {
|
| 1381 |
+
"node": ">= 0.4"
|
| 1382 |
+
},
|
| 1383 |
+
"funding": {
|
| 1384 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1385 |
+
}
|
| 1386 |
+
},
|
| 1387 |
+
"node_modules/on-finished": {
|
| 1388 |
+
"version": "2.4.1",
|
| 1389 |
+
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
| 1390 |
+
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
| 1391 |
+
"license": "MIT",
|
| 1392 |
+
"dependencies": {
|
| 1393 |
+
"ee-first": "1.1.1"
|
| 1394 |
+
},
|
| 1395 |
+
"engines": {
|
| 1396 |
+
"node": ">= 0.8"
|
| 1397 |
+
}
|
| 1398 |
+
},
|
| 1399 |
+
"node_modules/once": {
|
| 1400 |
+
"version": "1.4.0",
|
| 1401 |
+
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
| 1402 |
+
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
| 1403 |
+
"license": "ISC",
|
| 1404 |
+
"dependencies": {
|
| 1405 |
+
"wrappy": "1"
|
| 1406 |
+
}
|
| 1407 |
+
},
|
| 1408 |
+
"node_modules/parseurl": {
|
| 1409 |
+
"version": "1.3.3",
|
| 1410 |
+
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
| 1411 |
+
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
| 1412 |
+
"license": "MIT",
|
| 1413 |
+
"engines": {
|
| 1414 |
+
"node": ">= 0.8"
|
| 1415 |
+
}
|
| 1416 |
+
},
|
| 1417 |
+
"node_modules/path-to-regexp": {
|
| 1418 |
+
"version": "8.4.2",
|
| 1419 |
+
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
| 1420 |
+
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
| 1421 |
+
"license": "MIT",
|
| 1422 |
+
"funding": {
|
| 1423 |
+
"type": "opencollective",
|
| 1424 |
+
"url": "https://opencollective.com/express"
|
| 1425 |
+
}
|
| 1426 |
+
},
|
| 1427 |
+
"node_modules/proxy-addr": {
|
| 1428 |
+
"version": "2.0.8",
|
| 1429 |
+
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
|
| 1430 |
+
"integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
|
| 1431 |
+
"license": "MIT",
|
| 1432 |
+
"dependencies": {
|
| 1433 |
+
"forwarded": "0.2.0",
|
| 1434 |
+
"ipaddr.js": "1.9.1"
|
| 1435 |
+
},
|
| 1436 |
+
"engines": {
|
| 1437 |
+
"node": ">= 0.10"
|
| 1438 |
+
},
|
| 1439 |
+
"funding": {
|
| 1440 |
+
"type": "opencollective",
|
| 1441 |
+
"url": "https://opencollective.com/express"
|
| 1442 |
+
}
|
| 1443 |
+
},
|
| 1444 |
+
"node_modules/qs": {
|
| 1445 |
+
"version": "6.16.1",
|
| 1446 |
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz",
|
| 1447 |
+
"integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==",
|
| 1448 |
+
"license": "BSD-3-Clause",
|
| 1449 |
+
"dependencies": {
|
| 1450 |
+
"es-define-property": "^1.0.1",
|
| 1451 |
+
"side-channel": "^1.1.1"
|
| 1452 |
+
},
|
| 1453 |
+
"engines": {
|
| 1454 |
+
"node": ">=0.6"
|
| 1455 |
+
},
|
| 1456 |
+
"funding": {
|
| 1457 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1458 |
+
}
|
| 1459 |
+
},
|
| 1460 |
+
"node_modules/range-parser": {
|
| 1461 |
+
"version": "1.3.0",
|
| 1462 |
+
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
|
| 1463 |
+
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
|
| 1464 |
+
"license": "MIT",
|
| 1465 |
+
"engines": {
|
| 1466 |
+
"node": ">= 0.6"
|
| 1467 |
+
},
|
| 1468 |
+
"funding": {
|
| 1469 |
+
"type": "opencollective",
|
| 1470 |
+
"url": "https://opencollective.com/express"
|
| 1471 |
+
}
|
| 1472 |
+
},
|
| 1473 |
+
"node_modules/raw-body": {
|
| 1474 |
+
"version": "3.0.2",
|
| 1475 |
+
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
| 1476 |
+
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
| 1477 |
+
"license": "MIT",
|
| 1478 |
+
"dependencies": {
|
| 1479 |
+
"bytes": "~3.1.2",
|
| 1480 |
+
"http-errors": "~2.0.1",
|
| 1481 |
+
"iconv-lite": "~0.7.0",
|
| 1482 |
+
"unpipe": "~1.0.0"
|
| 1483 |
+
},
|
| 1484 |
+
"engines": {
|
| 1485 |
+
"node": ">= 0.10"
|
| 1486 |
+
}
|
| 1487 |
+
},
|
| 1488 |
+
"node_modules/router": {
|
| 1489 |
+
"version": "2.2.0",
|
| 1490 |
+
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
| 1491 |
+
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
| 1492 |
+
"license": "MIT",
|
| 1493 |
+
"dependencies": {
|
| 1494 |
+
"debug": "^4.4.0",
|
| 1495 |
+
"depd": "^2.0.0",
|
| 1496 |
+
"is-promise": "^4.0.0",
|
| 1497 |
+
"parseurl": "^1.3.3",
|
| 1498 |
+
"path-to-regexp": "^8.0.0"
|
| 1499 |
+
},
|
| 1500 |
+
"engines": {
|
| 1501 |
+
"node": ">= 18"
|
| 1502 |
+
}
|
| 1503 |
+
},
|
| 1504 |
+
"node_modules/safer-buffer": {
|
| 1505 |
+
"version": "2.1.2",
|
| 1506 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 1507 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 1508 |
+
"license": "MIT"
|
| 1509 |
+
},
|
| 1510 |
+
"node_modules/send": {
|
| 1511 |
+
"version": "1.2.1",
|
| 1512 |
+
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
| 1513 |
+
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
| 1514 |
+
"license": "MIT",
|
| 1515 |
+
"dependencies": {
|
| 1516 |
+
"debug": "^4.4.3",
|
| 1517 |
+
"encodeurl": "^2.0.0",
|
| 1518 |
+
"escape-html": "^1.0.3",
|
| 1519 |
+
"etag": "^1.8.1",
|
| 1520 |
+
"fresh": "^2.0.0",
|
| 1521 |
+
"http-errors": "^2.0.1",
|
| 1522 |
+
"mime-types": "^3.0.2",
|
| 1523 |
+
"ms": "^2.1.3",
|
| 1524 |
+
"on-finished": "^2.4.1",
|
| 1525 |
+
"range-parser": "^1.2.1",
|
| 1526 |
+
"statuses": "^2.0.2"
|
| 1527 |
+
},
|
| 1528 |
+
"engines": {
|
| 1529 |
+
"node": ">= 18"
|
| 1530 |
+
},
|
| 1531 |
+
"funding": {
|
| 1532 |
+
"type": "opencollective",
|
| 1533 |
+
"url": "https://opencollective.com/express"
|
| 1534 |
+
}
|
| 1535 |
+
},
|
| 1536 |
+
"node_modules/serve-static": {
|
| 1537 |
+
"version": "2.2.1",
|
| 1538 |
+
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
| 1539 |
+
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
| 1540 |
+
"license": "MIT",
|
| 1541 |
+
"dependencies": {
|
| 1542 |
+
"encodeurl": "^2.0.0",
|
| 1543 |
+
"escape-html": "^1.0.3",
|
| 1544 |
+
"parseurl": "^1.3.3",
|
| 1545 |
+
"send": "^1.2.0"
|
| 1546 |
+
},
|
| 1547 |
+
"engines": {
|
| 1548 |
+
"node": ">= 18"
|
| 1549 |
+
},
|
| 1550 |
+
"funding": {
|
| 1551 |
+
"type": "opencollective",
|
| 1552 |
+
"url": "https://opencollective.com/express"
|
| 1553 |
+
}
|
| 1554 |
+
},
|
| 1555 |
+
"node_modules/setprototypeof": {
|
| 1556 |
+
"version": "1.2.0",
|
| 1557 |
+
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
| 1558 |
+
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
| 1559 |
+
"license": "ISC"
|
| 1560 |
+
},
|
| 1561 |
+
"node_modules/side-channel": {
|
| 1562 |
+
"version": "1.1.1",
|
| 1563 |
+
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
| 1564 |
+
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
| 1565 |
+
"license": "MIT",
|
| 1566 |
+
"dependencies": {
|
| 1567 |
+
"es-errors": "^1.3.0",
|
| 1568 |
+
"object-inspect": "^1.13.4",
|
| 1569 |
+
"side-channel-list": "^1.0.1",
|
| 1570 |
+
"side-channel-map": "^1.0.1",
|
| 1571 |
+
"side-channel-weakmap": "^1.0.2"
|
| 1572 |
+
},
|
| 1573 |
+
"engines": {
|
| 1574 |
+
"node": ">= 0.4"
|
| 1575 |
+
},
|
| 1576 |
+
"funding": {
|
| 1577 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1578 |
+
}
|
| 1579 |
+
},
|
| 1580 |
+
"node_modules/side-channel-list": {
|
| 1581 |
+
"version": "1.0.1",
|
| 1582 |
+
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
| 1583 |
+
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
| 1584 |
+
"license": "MIT",
|
| 1585 |
+
"dependencies": {
|
| 1586 |
+
"es-errors": "^1.3.0",
|
| 1587 |
+
"object-inspect": "^1.13.4"
|
| 1588 |
+
},
|
| 1589 |
+
"engines": {
|
| 1590 |
+
"node": ">= 0.4"
|
| 1591 |
+
},
|
| 1592 |
+
"funding": {
|
| 1593 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1594 |
+
}
|
| 1595 |
+
},
|
| 1596 |
+
"node_modules/side-channel-map": {
|
| 1597 |
+
"version": "1.0.1",
|
| 1598 |
+
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
| 1599 |
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
| 1600 |
+
"license": "MIT",
|
| 1601 |
+
"dependencies": {
|
| 1602 |
+
"call-bound": "^1.0.2",
|
| 1603 |
+
"es-errors": "^1.3.0",
|
| 1604 |
+
"get-intrinsic": "^1.2.5",
|
| 1605 |
+
"object-inspect": "^1.13.3"
|
| 1606 |
+
},
|
| 1607 |
+
"engines": {
|
| 1608 |
+
"node": ">= 0.4"
|
| 1609 |
+
},
|
| 1610 |
+
"funding": {
|
| 1611 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1612 |
+
}
|
| 1613 |
+
},
|
| 1614 |
+
"node_modules/side-channel-weakmap": {
|
| 1615 |
+
"version": "1.0.2",
|
| 1616 |
+
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
| 1617 |
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
| 1618 |
+
"license": "MIT",
|
| 1619 |
+
"dependencies": {
|
| 1620 |
+
"call-bound": "^1.0.2",
|
| 1621 |
+
"es-errors": "^1.3.0",
|
| 1622 |
+
"get-intrinsic": "^1.2.5",
|
| 1623 |
+
"object-inspect": "^1.13.3",
|
| 1624 |
+
"side-channel-map": "^1.0.1"
|
| 1625 |
+
},
|
| 1626 |
+
"engines": {
|
| 1627 |
+
"node": ">= 0.4"
|
| 1628 |
+
},
|
| 1629 |
+
"funding": {
|
| 1630 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1631 |
+
}
|
| 1632 |
+
},
|
| 1633 |
+
"node_modules/statuses": {
|
| 1634 |
+
"version": "2.0.2",
|
| 1635 |
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
| 1636 |
+
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
| 1637 |
+
"license": "MIT",
|
| 1638 |
+
"engines": {
|
| 1639 |
+
"node": ">= 0.8"
|
| 1640 |
+
}
|
| 1641 |
+
},
|
| 1642 |
+
"node_modules/toidentifier": {
|
| 1643 |
+
"version": "1.0.1",
|
| 1644 |
+
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
| 1645 |
+
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
| 1646 |
+
"license": "MIT",
|
| 1647 |
+
"engines": {
|
| 1648 |
+
"node": ">=0.6"
|
| 1649 |
+
}
|
| 1650 |
+
},
|
| 1651 |
+
"node_modules/tslib": {
|
| 1652 |
+
"version": "2.8.1",
|
| 1653 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
| 1654 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 1655 |
+
"license": "0BSD"
|
| 1656 |
+
},
|
| 1657 |
+
"node_modules/tsx": {
|
| 1658 |
+
"version": "4.23.15",
|
| 1659 |
+
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
|
| 1660 |
+
"integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
|
| 1661 |
+
"dev": true,
|
| 1662 |
+
"license": "MIT",
|
| 1663 |
+
"dependencies": {
|
| 1664 |
+
"esbuild": "~0.28.0"
|
| 1665 |
+
},
|
| 1666 |
+
"bin": {
|
| 1667 |
+
"tsx": "dist/cli.mjs"
|
| 1668 |
+
},
|
| 1669 |
+
"engines": {
|
| 1670 |
+
"node": ">=18.0.0"
|
| 1671 |
+
},
|
| 1672 |
+
"optionalDependencies": {
|
| 1673 |
+
"fsevents": "~2.3.3"
|
| 1674 |
+
}
|
| 1675 |
+
},
|
| 1676 |
+
"node_modules/type-is": {
|
| 1677 |
+
"version": "2.1.0",
|
| 1678 |
+
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
| 1679 |
+
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
| 1680 |
+
"license": "MIT",
|
| 1681 |
+
"dependencies": {
|
| 1682 |
+
"content-type": "^2.0.0",
|
| 1683 |
+
"media-typer": "^1.1.0",
|
| 1684 |
+
"mime-types": "^3.0.0"
|
| 1685 |
+
},
|
| 1686 |
+
"engines": {
|
| 1687 |
+
"node": ">= 18"
|
| 1688 |
+
},
|
| 1689 |
+
"funding": {
|
| 1690 |
+
"type": "opencollective",
|
| 1691 |
+
"url": "https://opencollective.com/express"
|
| 1692 |
+
}
|
| 1693 |
+
},
|
| 1694 |
+
"node_modules/typescript": {
|
| 1695 |
+
"version": "5.9.3",
|
| 1696 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 1697 |
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 1698 |
+
"dev": true,
|
| 1699 |
+
"license": "Apache-2.0",
|
| 1700 |
+
"bin": {
|
| 1701 |
+
"tsc": "bin/tsc",
|
| 1702 |
+
"tsserver": "bin/tsserver"
|
| 1703 |
+
},
|
| 1704 |
+
"engines": {
|
| 1705 |
+
"node": ">=14.17"
|
| 1706 |
+
}
|
| 1707 |
+
},
|
| 1708 |
+
"node_modules/undici-types": {
|
| 1709 |
+
"version": "6.21.0",
|
| 1710 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 1711 |
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
| 1712 |
+
"dev": true,
|
| 1713 |
+
"license": "MIT"
|
| 1714 |
+
},
|
| 1715 |
+
"node_modules/unpipe": {
|
| 1716 |
+
"version": "1.0.0",
|
| 1717 |
+
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
| 1718 |
+
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
| 1719 |
+
"license": "MIT",
|
| 1720 |
+
"engines": {
|
| 1721 |
+
"node": ">= 0.8"
|
| 1722 |
+
}
|
| 1723 |
+
},
|
| 1724 |
+
"node_modules/vary": {
|
| 1725 |
+
"version": "1.1.2",
|
| 1726 |
+
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
| 1727 |
+
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
| 1728 |
+
"license": "MIT",
|
| 1729 |
+
"engines": {
|
| 1730 |
+
"node": ">= 0.8"
|
| 1731 |
+
}
|
| 1732 |
+
},
|
| 1733 |
+
"node_modules/wrappy": {
|
| 1734 |
+
"version": "1.0.2",
|
| 1735 |
+
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
| 1736 |
+
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
| 1737 |
+
"license": "ISC"
|
| 1738 |
+
},
|
| 1739 |
+
"node_modules/zod": {
|
| 1740 |
+
"version": "4.6.7",
|
| 1741 |
+
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
|
| 1742 |
+
"integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
|
| 1743 |
+
"license": "MIT",
|
| 1744 |
+
"funding": {
|
| 1745 |
+
"url": "https://github.com/sponsors/colinhacks"
|
| 1746 |
+
}
|
| 1747 |
+
}
|
| 1748 |
+
}
|
| 1749 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "sporalize-backend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "tsx watch src/server.ts",
|
| 8 |
+
"build": "tsc -p tsconfig.json",
|
| 9 |
+
"start": "python start.py"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@supabase/supabase-js": "^2.57.0",
|
| 13 |
+
"bcryptjs": "^3.0.2",
|
| 14 |
+
"cookie-parser": "^1.4.7",
|
| 15 |
+
"cors": "^2.8.5",
|
| 16 |
+
"dotenv": "^16.6.1",
|
| 17 |
+
"express": "^5.1.0",
|
| 18 |
+
"express-rate-limit": "^8.1.0",
|
| 19 |
+
"helmet": "^8.1.0",
|
| 20 |
+
"zod": "^4.1.5"
|
| 21 |
+
},
|
| 22 |
+
"devDependencies": {
|
| 23 |
+
"@types/cookie-parser": "^1.4.9",
|
| 24 |
+
"@types/cors": "^2.8.19",
|
| 25 |
+
"@types/express": "^5.0.3",
|
| 26 |
+
"@types/node": "^22.16.5",
|
| 27 |
+
"tsx": "^4.20.5",
|
| 28 |
+
"typescript": "^5.8.3"
|
| 29 |
+
}
|
| 30 |
+
}
|
src/analysis.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createHash, timingSafeEqual } from "node:crypto";
|
| 2 |
+
import express from "express";
|
| 3 |
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
| 4 |
+
import { z } from "zod";
|
| 5 |
+
|
| 6 |
+
export function stableId(value: string) {
|
| 7 |
+
const hex = createHash("sha256").update(value).digest("hex");
|
| 8 |
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const beginSchema = z.object({ clientId: z.uuid(), playerId: z.string().trim().min(1).max(100), name: z.string().trim().max(150) });
|
| 12 |
+
const reportSchema = z.object({
|
| 13 |
+
index: z.number().int().nonnegative(), action: z.string().min(1).max(50),
|
| 14 |
+
start_frame: z.number().int().nonnegative().optional(), end_frame: z.number().int().nonnegative().optional(),
|
| 15 |
+
error: z.string().max(2000).optional(),
|
| 16 |
+
});
|
| 17 |
+
export const completionSchema = z.discriminatedUnion("status", [
|
| 18 |
+
z.object({ status: z.literal("completed"), path: z.string(), actions: z.array(reportSchema).max(5000) }),
|
| 19 |
+
z.object({ status: z.enum(["failed", "cancelled"]) }),
|
| 20 |
+
]);
|
| 21 |
+
|
| 22 |
+
export function actionRows(sessionId: string, reports: z.infer<typeof reportSchema>[]) {
|
| 23 |
+
if (new Set(reports.map((report) => report.index)).size !== reports.length) throw new Error("Duplicate action index");
|
| 24 |
+
return reports.filter((report) => report.error === undefined).map((report) => {
|
| 25 |
+
const { start_frame: startFrame, end_frame: endFrame } = report;
|
| 26 |
+
if (startFrame === undefined || endFrame === undefined || endFrame < startFrame) throw new Error("Invalid action interval");
|
| 27 |
+
return { action_id: stableId(`${sessionId}:${report.index}`), type: report.action,
|
| 28 |
+
start_frame: startFrame, end_frame: endFrame };
|
| 29 |
+
});
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export function analysisRouter(db: SupabaseClient, secret: string) {
|
| 33 |
+
const router = express.Router();
|
| 34 |
+
router.use((req, res, next) => {
|
| 35 |
+
const supplied = Buffer.from(req.header("x-sporalize-proxy-secret") ?? "");
|
| 36 |
+
const expected = Buffer.from(secret);
|
| 37 |
+
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) return res.sendStatus(403);
|
| 38 |
+
const user = z.uuid().safeParse(req.header("x-sporalize-user-id"));
|
| 39 |
+
if (!user.success) return res.sendStatus(403);
|
| 40 |
+
res.locals.userId = user.data;
|
| 41 |
+
next();
|
| 42 |
+
});
|
| 43 |
+
router.use(express.json({ limit: "2mb" }));
|
| 44 |
+
router.post("/", async (req, res, next) => {
|
| 45 |
+
try {
|
| 46 |
+
const input = beginSchema.parse(req.body);
|
| 47 |
+
const playerId = z.uuid().safeParse(input.playerId);
|
| 48 |
+
const { data, error } = await db.rpc("begin_analysis", {
|
| 49 |
+
p_session_id: input.clientId, p_user_id: res.locals.userId,
|
| 50 |
+
p_player_id: playerId.success ? playerId.data : stableId(`${res.locals.userId}:player:${input.playerId}`),
|
| 51 |
+
p_reference: playerId.success ? null : input.playerId,
|
| 52 |
+
p_name: input.name || `Session ${input.clientId.slice(0, 8)}`,
|
| 53 |
+
});
|
| 54 |
+
if (error) return res.status(error.code === "P0001" ? 409 : 503).json({ error: "Unable to prepare analysis session" });
|
| 55 |
+
return res.json(data);
|
| 56 |
+
} catch (error) { next(error); }
|
| 57 |
+
});
|
| 58 |
+
router.get("/:sessionId", async (req, res, next) => {
|
| 59 |
+
try {
|
| 60 |
+
const sessionId = z.uuid().parse(req.params.sessionId);
|
| 61 |
+
const { data, error } = await db.from("sessions").select("session_id,player_id,path,status,name,created_at")
|
| 62 |
+
.eq("session_id", sessionId).eq("user_id", res.locals.userId).maybeSingle();
|
| 63 |
+
if (error) return res.status(503).json({ error: "Unable to load session" });
|
| 64 |
+
return data ? res.json(data) : res.sendStatus(404);
|
| 65 |
+
} catch (error) { next(error); }
|
| 66 |
+
});
|
| 67 |
+
router.put("/:sessionId", async (req, res, next) => {
|
| 68 |
+
try {
|
| 69 |
+
const sessionId = z.uuid().parse(req.params.sessionId);
|
| 70 |
+
const input = completionSchema.parse(req.body);
|
| 71 |
+
if (input.status === "completed" && input.path !== `sessions/${sessionId}`) return res.sendStatus(400);
|
| 72 |
+
let actions;
|
| 73 |
+
try { actions = input.status === "completed" ? actionRows(sessionId, input.actions) : []; }
|
| 74 |
+
catch { return res.status(400).json({ error: "Invalid action details" }); }
|
| 75 |
+
const { error } = await db.rpc("finish_analysis", {
|
| 76 |
+
p_session_id: sessionId, p_user_id: res.locals.userId, p_status: input.status,
|
| 77 |
+
p_path: input.status === "completed" ? input.path : null, p_actions: actions,
|
| 78 |
+
});
|
| 79 |
+
if (error) return res.status(error.code === "P0001" ? 409 : 503).json({ error: "Unable to save analysis" });
|
| 80 |
+
return res.json({ ok: true });
|
| 81 |
+
} catch (error) { next(error); }
|
| 82 |
+
});
|
| 83 |
+
return router;
|
| 84 |
+
}
|
src/auth.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Request, Response } from "express";
|
| 2 |
+
import type { Session, User } from "@supabase/supabase-js";
|
| 3 |
+
import { env, isProduction } from "./config.js";
|
| 4 |
+
import { supabaseAuth } from "./supabase.js";
|
| 5 |
+
|
| 6 |
+
const accessCookie = "sp_access";
|
| 7 |
+
const refreshCookie = "sp_refresh";
|
| 8 |
+
const sameSite = isProduction ? "none" as const : "lax" as const;
|
| 9 |
+
const cookieOptions = { httpOnly: true, secure: isProduction, sameSite, path: "/" };
|
| 10 |
+
|
| 11 |
+
export function setSessionCookies(res: Response, session: Session) {
|
| 12 |
+
res.cookie(accessCookie, session.access_token, { ...cookieOptions, maxAge: Math.max(session.expires_in * 1000, 60_000) });
|
| 13 |
+
res.cookie(refreshCookie, session.refresh_token, { ...cookieOptions, maxAge: 1000 * 60 * 60 * 24 * 30 });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function clearSessionCookies(res: Response) {
|
| 17 |
+
res.clearCookie(accessCookie, cookieOptions);
|
| 18 |
+
res.clearCookie(refreshCookie, cookieOptions);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function authenticatedUser(req: Request, res: Response): Promise<{ user: User; session?: Session } | null> {
|
| 22 |
+
const accessToken = req.cookies?.[accessCookie] as string | undefined;
|
| 23 |
+
const refreshToken = req.cookies?.[refreshCookie] as string | undefined;
|
| 24 |
+
if (!accessToken && !refreshToken) return null;
|
| 25 |
+
|
| 26 |
+
if (accessToken) {
|
| 27 |
+
const { data, error } = await supabaseAuth.auth.getUser(accessToken);
|
| 28 |
+
if (data.user && !error) return { user: data.user };
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
if (!refreshToken) {
|
| 32 |
+
clearSessionCookies(res);
|
| 33 |
+
return null;
|
| 34 |
+
}
|
| 35 |
+
const { data, error } = await supabaseAuth.auth.refreshSession({ refresh_token: refreshToken });
|
| 36 |
+
if (error || !data.user || !data.session) {
|
| 37 |
+
clearSessionCookies(res);
|
| 38 |
+
return null;
|
| 39 |
+
}
|
| 40 |
+
setSessionCookies(res, data.session);
|
| 41 |
+
return { user: data.user, session: data.session };
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export function safeRedirect(path: unknown) {
|
| 45 |
+
const frontend = new URL(env.FRONTEND_URL);
|
| 46 |
+
const fallback = new URL("/labs", frontend).toString();
|
| 47 |
+
if (typeof path !== "string" || !path.startsWith("/")) return fallback;
|
| 48 |
+
try {
|
| 49 |
+
const target = new URL(path, frontend);
|
| 50 |
+
return target.origin === frontend.origin ? target.toString() : fallback;
|
| 51 |
+
} catch {
|
| 52 |
+
return fallback;
|
| 53 |
+
}
|
| 54 |
+
}
|
src/config.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import "dotenv/config";
|
| 2 |
+
import { z } from "zod";
|
| 3 |
+
|
| 4 |
+
const envSchema = z.object({
|
| 5 |
+
PORT: z.coerce.number().int().min(1).max(65535).default(4000),
|
| 6 |
+
TRUST_PROXY_HOPS: z.coerce.number().int().min(0).max(5).default(1),
|
| 7 |
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
| 8 |
+
FRONTEND_URL: z.string().url(),
|
| 9 |
+
BACKEND_URL: z.string().url(),
|
| 10 |
+
ANALYSIS_API_URL: z.string().url().default("http://127.0.0.1:8000"),
|
| 11 |
+
ANALYSIS_PROXY_SECRET: z.string().min(32),
|
| 12 |
+
SUPABASE_URL: z.string().url(),
|
| 13 |
+
SUPABASE_ANON_KEY: z.string().min(1),
|
| 14 |
+
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
const parsed = envSchema.safeParse(process.env);
|
| 18 |
+
if (!parsed.success) {
|
| 19 |
+
console.error("Invalid environment configuration:", parsed.error.flatten().fieldErrors);
|
| 20 |
+
process.exit(1);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export const env = parsed.data;
|
| 24 |
+
export const isProduction = env.NODE_ENV === "production";
|
| 25 |
+
|
| 26 |
+
if (isProduction) {
|
| 27 |
+
for (const name of ["FRONTEND_URL", "BACKEND_URL", "SUPABASE_URL"] as const) {
|
| 28 |
+
const url = new URL(env[name]);
|
| 29 |
+
if (url.protocol !== "https:" || ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
| 30 |
+
console.error(`${name} must use a public HTTPS URL in production`);
|
| 31 |
+
process.exit(1);
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
// Trusted identity headers must stay inside the combined backend.
|
| 38 |
+
const analysisUrl = new URL(env.ANALYSIS_API_URL);
|
| 39 |
+
if (analysisUrl.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(analysisUrl.hostname)) {
|
| 40 |
+
console.error("ANALYSIS_API_URL must use HTTP on loopback");
|
| 41 |
+
process.exit(1);
|
| 42 |
+
}
|
src/server.ts
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express, { type NextFunction, type Request, type Response } from "express";
|
| 2 |
+
import cors from "cors";
|
| 3 |
+
import cookieParser from "cookie-parser";
|
| 4 |
+
import helmet from "helmet";
|
| 5 |
+
import rateLimit from "express-rate-limit";
|
| 6 |
+
import { Readable } from "node:stream";
|
| 7 |
+
import { pipeline } from "node:stream/promises";
|
| 8 |
+
import { z } from "zod";
|
| 9 |
+
import { env } from "./config.js";
|
| 10 |
+
import { analysisRouter } from "./analysis.js";
|
| 11 |
+
import { authenticatedUser, clearSessionCookies, safeRedirect, setSessionCookies } from "./auth.js";
|
| 12 |
+
import { hashPassword, supabaseAdmin, supabaseAuth, syncProfile, userHasPassword, userPayload } from "./supabase.js";
|
| 13 |
+
|
| 14 |
+
const app = express();
|
| 15 |
+
app.disable("x-powered-by");
|
| 16 |
+
app.set("trust proxy", env.TRUST_PROXY_HOPS);
|
| 17 |
+
app.use(helmet());
|
| 18 |
+
app.use(cors({
|
| 19 |
+
origin: env.FRONTEND_URL,
|
| 20 |
+
credentials: true,
|
| 21 |
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
| 22 |
+
}));
|
| 23 |
+
app.use("/internal/analysis", analysisRouter(supabaseAdmin, env.ANALYSIS_PROXY_SECRET));
|
| 24 |
+
app.use(express.json({ limit: "32kb" }));
|
| 25 |
+
app.use(cookieParser());
|
| 26 |
+
app.use((req, res, next) => {
|
| 27 |
+
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) return next();
|
| 28 |
+
if (req.header("origin") !== new URL(env.FRONTEND_URL).origin) return res.sendStatus(403);
|
| 29 |
+
return next();
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 25, standardHeaders: "draft-8", legacyHeaders: false });
|
| 33 |
+
const signupSchema = z.object({ fname: z.string().trim().min(1).max(100), lname: z.string().trim().min(1).max(100), email: z.string().trim().toLowerCase().email().max(255), password: z.string().min(8).max(128), academy: z.string().trim().max(150).optional() });
|
| 34 |
+
const loginSchema = z.object({ email: z.string().trim().toLowerCase().email(), password: z.string().min(1).max(128) });
|
| 35 |
+
const profileRoles = ["football_player", "coach", "club_manager", "academy_manager", "scout", "performance_analyst", "sports_director", "other"] as const;
|
| 36 |
+
const profileSchema = z.object({
|
| 37 |
+
fname: z.string().trim().min(1).max(100),
|
| 38 |
+
lname: z.string().trim().min(1).max(100),
|
| 39 |
+
academy: z.string().trim().max(150).transform((value) => value || null).nullable(),
|
| 40 |
+
role: z.enum(profileRoles).nullable(),
|
| 41 |
+
});
|
| 42 |
+
const passwordSchema = z.object({ currentPassword: z.string().max(128).optional(), newPassword: z.string().min(8).max(128) });
|
| 43 |
+
|
| 44 |
+
async function activeSubscriptionFor(userId: string) {
|
| 45 |
+
const today = new Date().toISOString().slice(0, 10);
|
| 46 |
+
const { data: assignment, error } = await supabaseAdmin.from("users_subscriptions")
|
| 47 |
+
.select("subscription_id,startdate,enddate").eq("user_id", userId).maybeSingle();
|
| 48 |
+
if (error) throw new Error(`Unable to check subscription: ${error.message}`);
|
| 49 |
+
if (!assignment?.subscription_id || assignment.startdate > today || assignment.enddate < today) return null;
|
| 50 |
+
|
| 51 |
+
const { data: plan, error: planError } = await supabaseAdmin.from("subscriptions")
|
| 52 |
+
.select("subscription_id,plan,price").eq("subscription_id", assignment.subscription_id).maybeSingle();
|
| 53 |
+
if (planError) throw new Error(`Unable to load subscription plan: ${planError.message}`);
|
| 54 |
+
return plan ? { ...plan, startdate: assignment.startdate, enddate: assignment.enddate } : null;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
async function requireActiveSubscription(req: Request, res: Response) {
|
| 58 |
+
const current = await authenticatedUser(req, res);
|
| 59 |
+
if (!current) {
|
| 60 |
+
res.status(401).json({ error: "Authentication required." });
|
| 61 |
+
return null;
|
| 62 |
+
}
|
| 63 |
+
if (!await activeSubscriptionFor(current.user.id)) {
|
| 64 |
+
res.status(402).json({ error: "An active subscription is required to access Sporalize Labs.", code: "subscription_required" });
|
| 65 |
+
return null;
|
| 66 |
+
}
|
| 67 |
+
return current;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
app.get("/health", (_req, res) => res.json({ ok: true }));
|
| 71 |
+
|
| 72 |
+
app.get("/api/subscriptions", async (_req, res, next) => {
|
| 73 |
+
try {
|
| 74 |
+
const { data, error } = await supabaseAdmin.from("subscriptions")
|
| 75 |
+
.select("subscription_id,plan,price")
|
| 76 |
+
.order("price", { ascending: true });
|
| 77 |
+
if (error) throw new Error(`Unable to load subscription plans: ${error.message}`);
|
| 78 |
+
const plans = (data ?? []).map((row) => ({
|
| 79 |
+
id: row.plan.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-"),
|
| 80 |
+
subscriptionId: row.subscription_id,
|
| 81 |
+
name: row.plan,
|
| 82 |
+
monthlyPrice: Number(row.price),
|
| 83 |
+
}));
|
| 84 |
+
return res.json({ plans });
|
| 85 |
+
} catch (error) { next(error); }
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
app.get("/api/subscription/access", async (req, res, next) => {
|
| 89 |
+
try {
|
| 90 |
+
const current = await authenticatedUser(req, res);
|
| 91 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 92 |
+
const subscription = await activeSubscriptionFor(current.user.id);
|
| 93 |
+
return res.json({ active: Boolean(subscription), subscription });
|
| 94 |
+
} catch (error) { next(error); }
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
// Players and sessions are currently a shared workspace: all signed-in users
|
| 98 |
+
// can read the common catalogue. Revisit these read scopes if per-user data is
|
| 99 |
+
// introduced later. The service-role client bypasses RLS, so keep this explicit.
|
| 100 |
+
app.get("/api/players-overview", async (req, res, next) => {
|
| 101 |
+
try {
|
| 102 |
+
const current = await authenticatedUser(req, res);
|
| 103 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 104 |
+
|
| 105 |
+
const { data: rows, error } = await supabaseAdmin.from("players")
|
| 106 |
+
.select("player_id,user_id,fname,lname,bdate,height,weight,position,club")
|
| 107 |
+
.order("fname", { ascending: true });
|
| 108 |
+
if (error) throw new Error(`Unable to load players: ${error.message}`);
|
| 109 |
+
|
| 110 |
+
const playerIds = (rows ?? []).map((row) => row.player_id);
|
| 111 |
+
const { data: sessions, error: sessionsError } = playerIds.length
|
| 112 |
+
? await supabaseAdmin.from("sessions").select("session_id,player_id,name,created_at")
|
| 113 |
+
.in("player_id", playerIds).order("created_at", { ascending: false })
|
| 114 |
+
: { data: [], error: null };
|
| 115 |
+
if (sessionsError) throw new Error(`Unable to load player sessions: ${sessionsError.message}`);
|
| 116 |
+
|
| 117 |
+
const sessionIds = (sessions ?? []).map((row) => row.session_id);
|
| 118 |
+
const { data: actions, error: actionsError } = sessionIds.length
|
| 119 |
+
? await supabaseAdmin.from("actions").select("action_id,session_id,type")
|
| 120 |
+
.in("session_id", sessionIds)
|
| 121 |
+
: { data: [], error: null };
|
| 122 |
+
if (actionsError) throw new Error(`Unable to load player actions: ${actionsError.message}`);
|
| 123 |
+
|
| 124 |
+
const actionIds = (actions ?? []).map((row) => row.action_id);
|
| 125 |
+
const { data: annotations, error: annotationsError } = actionIds.length
|
| 126 |
+
? await supabaseAdmin.from("annotations").select("action_id,score").in("action_id", actionIds)
|
| 127 |
+
: { data: [], error: null };
|
| 128 |
+
if (annotationsError) throw new Error(`Unable to load action scores: ${annotationsError.message}`);
|
| 129 |
+
|
| 130 |
+
const scoresByAction = new Map((annotations ?? []).map((row) => [row.action_id, row.score == null ? null : Number(row.score)]));
|
| 131 |
+
const sessionsByPlayer = new Map<string, typeof sessions>();
|
| 132 |
+
for (const session of sessions ?? []) sessionsByPlayer.set(session.player_id, [...(sessionsByPlayer.get(session.player_id) ?? []), session]);
|
| 133 |
+
const actionsBySession = new Map<string, typeof actions>();
|
| 134 |
+
for (const action of actions ?? []) actionsBySession.set(action.session_id, [...(actionsBySession.get(action.session_id) ?? []), action]);
|
| 135 |
+
|
| 136 |
+
const players = (rows ?? []).map((row) => {
|
| 137 |
+
const playerSessions = sessionsByPlayer.get(row.player_id) ?? [];
|
| 138 |
+
const playerActions = playerSessions.flatMap((session) => actionsBySession.get(session.session_id) ?? []);
|
| 139 |
+
const scored = playerActions.map((action) => scoresByAction.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
|
| 140 |
+
const byAction = [...new Set(playerActions.map((action) => action.type))].map((type) => {
|
| 141 |
+
const matching = playerActions.filter((action) => action.type === type);
|
| 142 |
+
const values = matching.map((action) => scoresByAction.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
|
| 143 |
+
return { axis: type, score: values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null, samples: values.length };
|
| 144 |
+
});
|
| 145 |
+
const playerScoreSessions = playerSessions.map((session) => {
|
| 146 |
+
const sessionActions = actionsBySession.get(session.session_id) ?? [];
|
| 147 |
+
const values = sessionActions.map((action) => scoresByAction.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
|
| 148 |
+
return { sessionId: session.session_id, createdAt: Date.parse(session.created_at) || 0, score: values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null, actionCount: sessionActions.length, scoredCount: values.length };
|
| 149 |
+
});
|
| 150 |
+
return {
|
| 151 |
+
playerId: row.player_id,
|
| 152 |
+
name: [row.fname, row.lname].filter(Boolean).join(" "),
|
| 153 |
+
age: row.bdate ? String(Math.max(0, new Date().getFullYear() - new Date(row.bdate).getFullYear())) : "",
|
| 154 |
+
heightCm: row.height == null ? "" : String(row.height),
|
| 155 |
+
weightKg: row.weight == null ? "" : String(row.weight),
|
| 156 |
+
club: row.club ?? "", position: row.position ?? "", dominantFoot: "", notes: "", avatarPath: "",
|
| 157 |
+
createdAt: "", updatedAt: "", hasProfile: true,
|
| 158 |
+
sessionCount: playerSessions.length, actionCount: playerActions.length,
|
| 159 |
+
lastSessionAt: playerScoreSessions[0]?.createdAt ?? 0,
|
| 160 |
+
scores: {
|
| 161 |
+
playerId: row.player_id, rubricVersion: "database", totalActions: playerActions.length,
|
| 162 |
+
scoredActions: scored.length, byAction, byQuality: [],
|
| 163 |
+
overall: scored.length ? scored.reduce((sum, value) => sum + value, 0) / scored.length : null,
|
| 164 |
+
sessions: playerScoreSessions,
|
| 165 |
+
},
|
| 166 |
+
};
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
return res.json({ players });
|
| 170 |
+
} catch (error) { next(error); }
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
app.get("/api/players-overview/:playerId", async (req, res, next) => {
|
| 174 |
+
try {
|
| 175 |
+
const current = await authenticatedUser(req, res);
|
| 176 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 177 |
+
const playerId = z.string().uuid().safeParse(req.params.playerId);
|
| 178 |
+
if (!playerId.success) return res.status(404).json({ error: "Player not found." });
|
| 179 |
+
const { data, error } = await supabaseAdmin.from("players").select("player_id")
|
| 180 |
+
.eq("player_id", playerId.data).maybeSingle();
|
| 181 |
+
if (error) throw new Error(`Unable to load player: ${error.message}`);
|
| 182 |
+
if (!data) return res.status(404).json({ error: "Player not found." });
|
| 183 |
+
|
| 184 |
+
const { data: overview, error: overviewError } = await supabaseAdmin.from("players")
|
| 185 |
+
.select("player_id,user_id,fname,lname,bdate,height,weight,position,club")
|
| 186 |
+
.order("fname", { ascending: true });
|
| 187 |
+
if (overviewError) throw new Error(`Unable to load players: ${overviewError.message}`);
|
| 188 |
+
const { data: sessions, error: sessionsError } = await supabaseAdmin.from("sessions")
|
| 189 |
+
.select("session_id,player_id,name,created_at");
|
| 190 |
+
if (sessionsError) throw new Error(`Unable to load sessions: ${sessionsError.message}`);
|
| 191 |
+
const { data: actions, error: actionsError } = (sessions ?? []).length
|
| 192 |
+
? await supabaseAdmin.from("actions").select("action_id,session_id,type").in("session_id", sessions!.map((row) => row.session_id))
|
| 193 |
+
: { data: [], error: null };
|
| 194 |
+
if (actionsError) throw new Error(`Unable to load actions: ${actionsError.message}`);
|
| 195 |
+
const actionIds = (actions ?? []).map((row) => row.action_id);
|
| 196 |
+
const { data: annotations, error: annotationsError } = actionIds.length
|
| 197 |
+
? await supabaseAdmin.from("annotations").select("action_id,score").in("action_id", actionIds)
|
| 198 |
+
: { data: [], error: null };
|
| 199 |
+
if (annotationsError) throw new Error(`Unable to load action scores: ${annotationsError.message}`);
|
| 200 |
+
|
| 201 |
+
const scoreMap = new Map((annotations ?? []).map((row) => [row.action_id, row.score == null ? null : Number(row.score)]));
|
| 202 |
+
const ownSessions = (sessions ?? []).filter((row) => row.player_id === playerId.data);
|
| 203 |
+
const ownActions = (actions ?? []).filter((row) => ownSessions.some((session) => session.session_id === row.session_id));
|
| 204 |
+
const ownScores = ownActions.map((row) => scoreMap.get(row.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
|
| 205 |
+
const byAction = [...new Set(ownActions.map((row) => row.type))].map((type) => {
|
| 206 |
+
const matching = ownActions.filter((row) => row.type === type);
|
| 207 |
+
const values = matching.map((row) => scoreMap.get(row.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
|
| 208 |
+
return { axis: type, score: values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null, samples: values.length };
|
| 209 |
+
});
|
| 210 |
+
const player = overview?.find((row) => row.player_id === playerId.data);
|
| 211 |
+
if (!player) return res.status(404).json({ error: "Player not found." });
|
| 212 |
+
const sessionsForPlayer = ownSessions.map((row) => {
|
| 213 |
+
const sessionActions = ownActions.filter((action) => action.session_id === row.session_id);
|
| 214 |
+
const values = sessionActions.map((action) => scoreMap.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
|
| 215 |
+
return { sessionId: row.session_id, createdAt: Date.parse(row.created_at) || 0, score: values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null, actionCount: sessionActions.length, scoredCount: values.length };
|
| 216 |
+
});
|
| 217 |
+
const { data: peers, error: peersError } = await supabaseAdmin.from("players").select("player_id");
|
| 218 |
+
if (peersError) throw new Error(`Unable to load roster: ${peersError.message}`);
|
| 219 |
+
const squadAverage = { byQuality: [], byAction };
|
| 220 |
+
return res.json({
|
| 221 |
+
playerId: player.player_id, name: [player.fname, player.lname].filter(Boolean).join(" "),
|
| 222 |
+
age: player.bdate ? String(Math.max(0, new Date().getFullYear() - new Date(player.bdate).getFullYear())) : "",
|
| 223 |
+
heightCm: player.height == null ? "" : String(player.height), weightKg: player.weight == null ? "" : String(player.weight),
|
| 224 |
+
club: player.club ?? "", position: player.position ?? "", dominantFoot: "", notes: "", avatarPath: "",
|
| 225 |
+
createdAt: "", updatedAt: "", hasProfile: true, sessionCount: ownSessions.length, actionCount: ownActions.length,
|
| 226 |
+
lastSessionAt: sessionsForPlayer[0]?.createdAt ?? 0,
|
| 227 |
+
scores: { playerId: player.player_id, rubricVersion: "database", totalActions: ownActions.length, scoredActions: ownScores.length, byAction, byQuality: [], overall: ownScores.length ? ownScores.reduce((sum, value) => sum + value, 0) / ownScores.length : null, sessions: sessionsForPlayer },
|
| 228 |
+
squadAverage, rank: null, playerCount: peers?.length ?? 0,
|
| 229 |
+
});
|
| 230 |
+
} catch (error) { next(error); }
|
| 231 |
+
});
|
| 232 |
+
|
| 233 |
+
app.get("/api/sessions-overview", async (req, res, next) => {
|
| 234 |
+
try {
|
| 235 |
+
const current = await authenticatedUser(req, res);
|
| 236 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 237 |
+
|
| 238 |
+
const { data: sessions, error } = await supabaseAdmin.from("sessions")
|
| 239 |
+
.select("session_id,player_id,name,path,created_at,status")
|
| 240 |
+
.order("created_at", { ascending: false });
|
| 241 |
+
if (error) throw new Error(`Unable to load sessions: ${error.message}`);
|
| 242 |
+
const playerIds = [...new Set((sessions ?? []).flatMap((row) => row.player_id ? [row.player_id] : []))];
|
| 243 |
+
const { data: players, error: playersError } = playerIds.length
|
| 244 |
+
? await supabaseAdmin.from("players").select("player_id,fname,lname").in("player_id", playerIds)
|
| 245 |
+
: { data: [], error: null };
|
| 246 |
+
if (playersError) throw new Error(`Unable to load session players: ${playersError.message}`);
|
| 247 |
+
const sessionIds = (sessions ?? []).map((row) => row.session_id);
|
| 248 |
+
const { data: actions, error: actionsError } = sessionIds.length
|
| 249 |
+
? await supabaseAdmin.from("actions").select("session_id,type").in("session_id", sessionIds)
|
| 250 |
+
: { data: [], error: null };
|
| 251 |
+
if (actionsError) throw new Error(`Unable to load session actions: ${actionsError.message}`);
|
| 252 |
+
const playerNames = new Map((players ?? []).map((row) => [row.player_id, [row.fname, row.lname].filter(Boolean).join(" ")]));
|
| 253 |
+
const breakdownBySession = new Map<string, Record<string, number>>();
|
| 254 |
+
for (const action of actions ?? []) {
|
| 255 |
+
const breakdown = breakdownBySession.get(action.session_id) ?? {};
|
| 256 |
+
breakdown[action.type] = (breakdown[action.type] ?? 0) + 1;
|
| 257 |
+
breakdownBySession.set(action.session_id, breakdown);
|
| 258 |
+
}
|
| 259 |
+
return res.json({ sessions: (sessions ?? []).map((row) => {
|
| 260 |
+
const actionBreakdown = breakdownBySession.get(row.session_id) ?? {};
|
| 261 |
+
return {
|
| 262 |
+
sessionId: row.session_id, playerId: row.player_id ?? "", playerName: row.player_id ? playerNames.get(row.player_id) ?? "" : "",
|
| 263 |
+
name: row.name, createdAt: Date.parse(row.created_at) || 0,
|
| 264 |
+
actionCount: Object.values(actionBreakdown).reduce((sum, count) => sum + count, 0),
|
| 265 |
+
cameraCount: 0, actionBreakdown, path: row.path, status: row.status,
|
| 266 |
+
};
|
| 267 |
+
}) });
|
| 268 |
+
} catch (error) { next(error); }
|
| 269 |
+
});
|
| 270 |
+
|
| 271 |
+
// Session deletion is handled by the AI service through the proxy below so
|
| 272 |
+
// the session folder and database records are deleted together.
|
| 273 |
+
|
| 274 |
+
app.post("/api/auth/signup", authLimiter, async (req, res, next) => {
|
| 275 |
+
try {
|
| 276 |
+
const input = signupSchema.parse(req.body);
|
| 277 |
+
const passwordHash = await hashPassword(input.password);
|
| 278 |
+
const { data, error } = await supabaseAuth.auth.signUp({ email: input.email, password: input.password, options: { data: { fname: input.fname, lname: input.lname, academy: input.academy ?? null } } });
|
| 279 |
+
if (error) {
|
| 280 |
+
console.error("Supabase signup failed", { status: error.status, code: error.code, message: error.message });
|
| 281 |
+
return res.status(400).json({ error: env.NODE_ENV === "development" ? error.message : "Unable to create the account with these details." });
|
| 282 |
+
}
|
| 283 |
+
if (data.user?.identities?.length) await syncProfile(data.user, { ...input, passwordHash });
|
| 284 |
+
if (data.user && data.session) {
|
| 285 |
+
setSessionCookies(res, data.session);
|
| 286 |
+
return res.status(201).json({ user: userPayload(data.user), requiresEmailVerification: false });
|
| 287 |
+
}
|
| 288 |
+
return res.status(201).json({ user: data.user ? userPayload(data.user) : null, requiresEmailVerification: true });
|
| 289 |
+
} catch (error) { next(error); }
|
| 290 |
+
});
|
| 291 |
+
|
| 292 |
+
app.post("/api/auth/login", authLimiter, async (req, res, next) => {
|
| 293 |
+
try {
|
| 294 |
+
const input = loginSchema.parse(req.body);
|
| 295 |
+
const { data, error } = await supabaseAuth.auth.signInWithPassword(input);
|
| 296 |
+
if (error || !data.user || !data.session) return res.status(401).json({ error: "Invalid email or password." });
|
| 297 |
+
await syncProfile(data.user);
|
| 298 |
+
setSessionCookies(res, data.session);
|
| 299 |
+
return res.json({ user: userPayload(data.user) });
|
| 300 |
+
} catch (error) { next(error); }
|
| 301 |
+
});
|
| 302 |
+
|
| 303 |
+
app.get("/api/auth/google", authLimiter, async (req, res, next) => {
|
| 304 |
+
try {
|
| 305 |
+
const redirect = typeof req.query.redirect === "string" ? req.query.redirect : "/labs";
|
| 306 |
+
const { data, error } = await supabaseAuth.auth.signInWithOAuth({ provider: "google", options: { redirectTo: `${env.BACKEND_URL}/api/auth/callback?next=${encodeURIComponent(redirect)}` } });
|
| 307 |
+
if (error || !data.url) return res.status(502).json({ error: "Google sign-in is currently unavailable." });
|
| 308 |
+
return res.redirect(data.url);
|
| 309 |
+
} catch (error) { next(error); }
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
app.get("/api/auth/callback", async (req, res, next) => {
|
| 313 |
+
try {
|
| 314 |
+
const code = typeof req.query.code === "string" ? req.query.code : null;
|
| 315 |
+
// Explicitly terminate any fragment from an unexpected implicit-flow
|
| 316 |
+
// response so access tokens cannot be inherited by this redirect URL.
|
| 317 |
+
if (!code) return res.redirect(`${env.FRONTEND_URL}/login?error=oauth_callback_failed#`);
|
| 318 |
+
const { data, error } = await supabaseAuth.auth.exchangeCodeForSession(code);
|
| 319 |
+
if (error || !data.user || !data.session) return res.redirect(`${env.FRONTEND_URL}/login?error=oauth_callback_failed`);
|
| 320 |
+
await syncProfile(data.user);
|
| 321 |
+
setSessionCookies(res, data.session);
|
| 322 |
+
return res.redirect(safeRedirect(req.query.next));
|
| 323 |
+
} catch (error) { next(error); }
|
| 324 |
+
});
|
| 325 |
+
|
| 326 |
+
// Supabase confirmation emails can use {{ .TokenHash }} to deliver the one-time
|
| 327 |
+
// token directly to this server. This lets us establish the same HttpOnly
|
| 328 |
+
// cookie session used by password and Google sign-in.
|
| 329 |
+
app.get("/api/auth/confirm", async (req, res, next) => {
|
| 330 |
+
try {
|
| 331 |
+
const tokenHash = typeof req.query.token_hash === "string" ? req.query.token_hash : "";
|
| 332 |
+
if (!tokenHash) return res.redirect(`${env.FRONTEND_URL}/login?error=email_confirmation_failed`);
|
| 333 |
+
|
| 334 |
+
const { data, error } = await supabaseAuth.auth.verifyOtp({ token_hash: tokenHash, type: "email" });
|
| 335 |
+
if (error || !data.user || !data.session) return res.redirect(`${env.FRONTEND_URL}/login?error=email_confirmation_failed`);
|
| 336 |
+
|
| 337 |
+
await syncProfile(data.user);
|
| 338 |
+
setSessionCookies(res, data.session);
|
| 339 |
+
return res.redirect(`${env.FRONTEND_URL}/labs`);
|
| 340 |
+
} catch (error) { next(error); }
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
app.post("/api/auth/logout", async (req, res, next) => {
|
| 344 |
+
try {
|
| 345 |
+
clearSessionCookies(res);
|
| 346 |
+
return res.status(204).send();
|
| 347 |
+
} catch (error) { next(error); }
|
| 348 |
+
});
|
| 349 |
+
|
| 350 |
+
app.get("/api/auth/me", async (req, res, next) => {
|
| 351 |
+
try {
|
| 352 |
+
const current = await authenticatedUser(req, res);
|
| 353 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 354 |
+
return res.json({ user: userPayload(current.user) });
|
| 355 |
+
} catch (error) { next(error); }
|
| 356 |
+
});
|
| 357 |
+
|
| 358 |
+
app.get("/api/auth/profile", async (req, res, next) => {
|
| 359 |
+
try {
|
| 360 |
+
const current = await authenticatedUser(req, res);
|
| 361 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 362 |
+
await syncProfile(current.user);
|
| 363 |
+
const { data, error } = await supabaseAdmin.from("users")
|
| 364 |
+
.select("fname,lname,email,academy,role")
|
| 365 |
+
.eq("user_id", current.user.id)
|
| 366 |
+
.single();
|
| 367 |
+
if (error || !data) throw new Error(`Unable to load user profile: ${error?.message ?? "Profile not found"}`);
|
| 368 |
+
const hasPassword = userHasPassword(current.user);
|
| 369 |
+
return res.json({ profile: { ...data, emailVerified: Boolean(current.user.email_confirmed_at), hasPassword } });
|
| 370 |
+
} catch (error) { next(error); }
|
| 371 |
+
});
|
| 372 |
+
|
| 373 |
+
app.patch("/api/auth/profile", async (req, res, next) => {
|
| 374 |
+
try {
|
| 375 |
+
const current = await authenticatedUser(req, res);
|
| 376 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 377 |
+
const input = profileSchema.parse(req.body);
|
| 378 |
+
const profile = { ...input, academy: input.academy?.trim() || null };
|
| 379 |
+
const { data, error } = await supabaseAdmin.from("users")
|
| 380 |
+
.update(profile)
|
| 381 |
+
.eq("user_id", current.user.id)
|
| 382 |
+
.select("fname,lname,email,academy,role")
|
| 383 |
+
.single();
|
| 384 |
+
if (error || !data) throw new Error(`Unable to save user profile: ${error?.message ?? "Profile not found"}`);
|
| 385 |
+
|
| 386 |
+
// Keep auth metadata useful for integrations while the users row remains canonical.
|
| 387 |
+
const { error: metadataError } = await supabaseAdmin.auth.admin.updateUserById(current.user.id, {
|
| 388 |
+
user_metadata: { ...current.user.user_metadata, ...profile },
|
| 389 |
+
});
|
| 390 |
+
if (metadataError) console.error("Unable to sync profile metadata", { code: metadataError.code, message: metadataError.message });
|
| 391 |
+
return res.json({ profile: { ...data, emailVerified: Boolean(current.user.email_confirmed_at), hasPassword: userHasPassword(current.user) } });
|
| 392 |
+
} catch (error) { next(error); }
|
| 393 |
+
});
|
| 394 |
+
|
| 395 |
+
app.post("/api/auth/password", async (req, res, next) => {
|
| 396 |
+
try {
|
| 397 |
+
const current = await authenticatedUser(req, res);
|
| 398 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 399 |
+
const input = passwordSchema.parse(req.body);
|
| 400 |
+
const hasPassword = userHasPassword(current.user);
|
| 401 |
+
if (hasPassword) {
|
| 402 |
+
if (!input.currentPassword) return res.status(400).json({ error: "Enter your current password." });
|
| 403 |
+
const { error: verifyError } = await supabaseAuth.auth.signInWithPassword({ email: current.user.email ?? "", password: input.currentPassword });
|
| 404 |
+
if (verifyError) return res.status(400).json({ error: "Your current password is incorrect." });
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
const { error } = await supabaseAdmin.auth.admin.updateUserById(current.user.id, {
|
| 408 |
+
password: input.newPassword,
|
| 409 |
+
app_metadata: { ...current.user.app_metadata, has_password: true },
|
| 410 |
+
});
|
| 411 |
+
if (error) return res.status(400).json({ error: "Unable to update the password. Please try again." });
|
| 412 |
+
const passwordHash = await hashPassword(input.newPassword);
|
| 413 |
+
const { error: profileError } = await supabaseAdmin.from("users").update({ password_hash: passwordHash }).eq("user_id", current.user.id);
|
| 414 |
+
if (profileError) throw new Error(`Password updated but profile hash sync failed: ${profileError.message}`);
|
| 415 |
+
return res.json({ ok: true });
|
| 416 |
+
} catch (error) { next(error); }
|
| 417 |
+
});
|
| 418 |
+
|
| 419 |
+
app.delete("/api/auth/account", async (req, res, next) => {
|
| 420 |
+
try {
|
| 421 |
+
const current = await authenticatedUser(req, res);
|
| 422 |
+
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 423 |
+
if (req.body?.confirmation !== "DELETE") return res.status(400).json({ error: "Confirm account deletion by typing DELETE." });
|
| 424 |
+
const { error: authError } = await supabaseAdmin.auth.admin.deleteUser(current.user.id);
|
| 425 |
+
if (authError) return res.status(500).json({ error: "Unable to delete your account. Please try again." });
|
| 426 |
+
const { error: profileError } = await supabaseAdmin.from("users").delete().eq("user_id", current.user.id);
|
| 427 |
+
clearSessionCookies(res);
|
| 428 |
+
if (profileError) throw new Error(`Account deleted but profile cleanup failed: ${profileError.message}`);
|
| 429 |
+
return res.status(204).send();
|
| 430 |
+
} catch (error) { next(error); }
|
| 431 |
+
});
|
| 432 |
+
|
| 433 |
+
async function proxyAnalysisRequest(req: Request, res: Response) {
|
| 434 |
+
const current = await requireActiveSubscription(req, res);
|
| 435 |
+
if (!current) return;
|
| 436 |
+
|
| 437 |
+
const target = new URL(req.originalUrl, env.ANALYSIS_API_URL);
|
| 438 |
+
const headers = new Headers();
|
| 439 |
+
headers.set("x-sporalize-proxy-secret", env.ANALYSIS_PROXY_SECRET);
|
| 440 |
+
// Derived from the verified cookie session; never trust a browser-supplied ID.
|
| 441 |
+
headers.set("x-sporalize-user-id", current.user.id);
|
| 442 |
+
headers.set("x-forwarded-host", new URL(env.BACKEND_URL).host);
|
| 443 |
+
headers.set("x-forwarded-proto", new URL(env.BACKEND_URL).protocol.replace(":", ""));
|
| 444 |
+
for (const name of ["accept", "content-type", "range", "if-range"]) {
|
| 445 |
+
const value = req.header(name);
|
| 446 |
+
if (value) headers.set(name, value);
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
| 450 |
+
const body = !hasBody ? undefined : req.is("application/json")
|
| 451 |
+
? JSON.stringify(req.body ?? {})
|
| 452 |
+
: req as unknown as BodyInit;
|
| 453 |
+
const init: RequestInit & { duplex?: "half" } = {
|
| 454 |
+
method: req.method,
|
| 455 |
+
headers,
|
| 456 |
+
body,
|
| 457 |
+
...(body && typeof body !== "string" ? { duplex: "half" as const } : {}),
|
| 458 |
+
};
|
| 459 |
+
|
| 460 |
+
const upstream = await fetch(target, init);
|
| 461 |
+
res.status(upstream.status);
|
| 462 |
+
res.set("Cross-Origin-Resource-Policy", "cross-origin");
|
| 463 |
+
for (const name of ["content-type", "content-disposition", "cache-control", "accept-ranges", "content-range", "last-modified"]) {
|
| 464 |
+
const value = upstream.headers.get(name);
|
| 465 |
+
if (value) res.set(name, value);
|
| 466 |
+
}
|
| 467 |
+
if (!upstream.body) return res.end();
|
| 468 |
+
await pipeline(Readable.fromWeb(upstream.body as import("node:stream/web").ReadableStream), res);
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
const analysisProxy = (req: Request, res: Response, next: NextFunction) => {
|
| 472 |
+
void proxyAnalysisRequest(req, res).catch((error: unknown) => {
|
| 473 |
+
if (res.headersSent) return res.destroy(error instanceof Error ? error : undefined);
|
| 474 |
+
console.error("Analysis API proxy failed", error);
|
| 475 |
+
return res.status(502).json({ error: "The analysis service is unavailable." });
|
| 476 |
+
});
|
| 477 |
+
};
|
| 478 |
+
|
| 479 |
+
// All analysis API traffic and stored media pass through this subscription
|
| 480 |
+
// gate. Auth and subscription status endpoints remain available without a plan.
|
| 481 |
+
app.use("/api", (req, res, next) => {
|
| 482 |
+
if (req.originalUrl.startsWith("/api/auth/") || req.originalUrl === "/api/subscriptions" || req.originalUrl === "/api/subscription/access" || req.originalUrl === "/api/public-config") return next();
|
| 483 |
+
return analysisProxy(req, res, next);
|
| 484 |
+
});
|
| 485 |
+
app.use("/storage", analysisProxy);
|
| 486 |
+
|
| 487 |
+
app.use((_req, res) => res.status(404).json({ error: "Route not found." }));
|
| 488 |
+
app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
| 489 |
+
console.error(error);
|
| 490 |
+
if (error instanceof z.ZodError) return res.status(400).json({ error: "Please check the submitted fields." });
|
| 491 |
+
return res.status(500).json({ error: "Something went wrong. Please try again." });
|
| 492 |
+
});
|
| 493 |
+
|
| 494 |
+
app.listen(env.PORT, () => console.log(`Sporalize API listening on http://localhost:${env.PORT}`));
|
src/subscriptions.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { supabaseAdmin } from "./supabase.js";
|
| 2 |
+
|
| 3 |
+
type AssignSubscriptionInput = {
|
| 4 |
+
userId: string;
|
| 5 |
+
subscriptionId: string;
|
| 6 |
+
startDate: string;
|
| 7 |
+
endDate: string;
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Persist a paid subscription after a payment provider's webhook has been
|
| 12 |
+
* authenticated and its payment has been confirmed. Never call this directly
|
| 13 |
+
* from a browser request.
|
| 14 |
+
*/
|
| 15 |
+
export async function assignPaidSubscription(input: AssignSubscriptionInput) {
|
| 16 |
+
const { userId, subscriptionId, startDate, endDate } = input;
|
| 17 |
+
const isoDate = /^\d{4}-\d{2}-\d{2}$/;
|
| 18 |
+
if (!isoDate.test(startDate) || !isoDate.test(endDate) || endDate < startDate) {
|
| 19 |
+
throw new Error("Invalid subscription period.");
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const { data: plan, error: planError } = await supabaseAdmin
|
| 23 |
+
.from("subscriptions")
|
| 24 |
+
.select("subscription_id")
|
| 25 |
+
.eq("subscription_id", subscriptionId)
|
| 26 |
+
.maybeSingle();
|
| 27 |
+
if (planError) throw new Error(`Unable to validate subscription plan: ${planError.message}`);
|
| 28 |
+
if (!plan) throw new Error("Subscription plan was not found.");
|
| 29 |
+
|
| 30 |
+
const { data, error } = await supabaseAdmin
|
| 31 |
+
.from("users_subscriptions")
|
| 32 |
+
.upsert({ user_id: userId, subscription_id: subscriptionId, startdate: startDate, enddate: endDate }, { onConflict: "user_id" })
|
| 33 |
+
.select("user_id,subscription_id,startdate,enddate")
|
| 34 |
+
.single();
|
| 35 |
+
if (error) throw new Error(`Unable to assign paid subscription: ${error.message}`);
|
| 36 |
+
return data;
|
| 37 |
+
}
|
src/supabase.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createClient, type User } from "@supabase/supabase-js";
|
| 2 |
+
import bcrypt from "bcryptjs";
|
| 3 |
+
import { randomBytes } from "node:crypto";
|
| 4 |
+
import { env } from "./config.js";
|
| 5 |
+
|
| 6 |
+
export const supabaseAuth = createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY, {
|
| 7 |
+
// OAuth returns a one-time code for the backend callback to exchange, rather
|
| 8 |
+
// than placing session tokens in the browser URL fragment.
|
| 9 |
+
auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false, flowType: "pkce" },
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
export const supabaseAdmin = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY, {
|
| 13 |
+
auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false },
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
export async function hashPassword(password: string) {
|
| 17 |
+
return bcrypt.hash(password, 12);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export function userHasPassword(user: User) {
|
| 21 |
+
return user.app_metadata?.has_password === true || Boolean(user.identities?.some((identity) => identity.provider === "email"));
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
async function randomPasswordHash() {
|
| 25 |
+
return bcrypt.hash(randomBytes(32).toString("hex"), 12);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export async function syncProfile(user: User, profile?: { fname?: string; lname?: string; academy?: string | null; role?: string | null; passwordHash?: string }) {
|
| 29 |
+
const metadata = user.user_metadata ?? {};
|
| 30 |
+
const { data: existing, error: readError } = await supabaseAdmin.from("users").select("user_id").eq("user_id", user.id).maybeSingle();
|
| 31 |
+
if (readError) throw new Error(`Unable to read user profile: ${readError.message}`);
|
| 32 |
+
if (existing) {
|
| 33 |
+
// The profile table is the canonical source for editable profile fields.
|
| 34 |
+
// Auth callbacks should only keep the email in sync, never reset user edits.
|
| 35 |
+
const { error } = await supabaseAdmin.from("users").update({ email: user.email }).eq("user_id", user.id);
|
| 36 |
+
if (error) throw new Error(`Unable to update user profile: ${error.message}`);
|
| 37 |
+
return;
|
| 38 |
+
}
|
| 39 |
+
const fields = {
|
| 40 |
+
user_id: user.id,
|
| 41 |
+
fname: profile?.fname ?? metadata.fname ?? metadata.full_name?.split(" ")[0] ?? "User",
|
| 42 |
+
lname: profile?.lname ?? metadata.lname ?? metadata.full_name?.split(" ").slice(1).join(" ") ?? "User",
|
| 43 |
+
email: user.email,
|
| 44 |
+
academy: profile?.academy ?? metadata.academy ?? null,
|
| 45 |
+
role: profile?.role ?? metadata.role ?? null,
|
| 46 |
+
};
|
| 47 |
+
const { error } = await supabaseAdmin.from("users").insert({ ...fields, password_hash: profile?.passwordHash ?? await randomPasswordHash() });
|
| 48 |
+
if (error) throw new Error(`Unable to create user profile: ${error.message}`);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export function userPayload(user: User) {
|
| 52 |
+
return { id: user.id, email: user.email, emailVerified: Boolean(user.email_confirmed_at), metadata: user.user_metadata ?? {} };
|
| 53 |
+
}
|
start.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run the public Node API and private Python worker as one service."""
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import signal
|
| 5 |
+
import subprocess
|
| 6 |
+
import sys
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
root = Path(__file__).resolve().parent
|
| 14 |
+
load_dotenv(root / ".env")
|
| 15 |
+
port = int(os.environ.get("PORT", "4000"))
|
| 16 |
+
if not 1 <= port <= 65535 or port == 8000:
|
| 17 |
+
raise ValueError("PORT must be between 1 and 65535 and differ from AI port 8000")
|
| 18 |
+
env = dict(os.environ, PORT=str(port),
|
| 19 |
+
ANALYSIS_API_URL="http://127.0.0.1:8000",
|
| 20 |
+
BACKEND_INTERNAL_URL=f"http://127.0.0.1:{port}")
|
| 21 |
+
children = []
|
| 22 |
+
stopping = False
|
| 23 |
+
|
| 24 |
+
def stop(signum, frame):
|
| 25 |
+
nonlocal stopping
|
| 26 |
+
stopping = True
|
| 27 |
+
|
| 28 |
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
| 29 |
+
signal.signal(sig, stop)
|
| 30 |
+
try:
|
| 31 |
+
children.append(subprocess.Popen([sys.executable, "-m", "uvicorn", "app:app",
|
| 32 |
+
"--host", "127.0.0.1", "--port", "8000", "--workers", "1"], cwd=root, env=env))
|
| 33 |
+
children.append(subprocess.Popen(["node", "dist/server.js"], cwd=root, env=env))
|
| 34 |
+
while not stopping:
|
| 35 |
+
for child in children:
|
| 36 |
+
if child.poll() is not None:
|
| 37 |
+
print(f"Backend process {child.pid} exited ({child.returncode}); stopping service", flush=True)
|
| 38 |
+
return child.returncode or 1
|
| 39 |
+
time.sleep(0.25)
|
| 40 |
+
return 0
|
| 41 |
+
finally:
|
| 42 |
+
for child in children:
|
| 43 |
+
if child.poll() is None:
|
| 44 |
+
child.terminate()
|
| 45 |
+
deadline = time.monotonic() + 15
|
| 46 |
+
for child in children:
|
| 47 |
+
try:
|
| 48 |
+
child.wait(timeout=max(0, deadline - time.monotonic()))
|
| 49 |
+
except subprocess.TimeoutExpired:
|
| 50 |
+
child.kill()
|
| 51 |
+
child.wait()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
sys.exit(main())
|
tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"module": "NodeNext",
|
| 5 |
+
"moduleResolution": "NodeNext",
|
| 6 |
+
"outDir": "dist",
|
| 7 |
+
"rootDir": "src",
|
| 8 |
+
"strict": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"skipLibCheck": true,
|
| 11 |
+
"forceConsistentCasingInFileNames": true
|
| 12 |
+
},
|
| 13 |
+
"include": ["src"]
|
| 14 |
+
}
|