commit cae1a4647ba23932d3ef473c0c7744e7dad30abd Author: Marco Gallegos Date: Sat Dec 13 13:08:31 2025 -0600 feat: Initial release of Formbricks Vanity Server diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5d3e547 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.env +.git +.gitignore +README.md +DEVELOPMENT_PLAN.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b1f5c73 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +PORT=3011 +FORMBRICKS_ENV_ID=your_environment_id_here +FORMBRICKS_SDK_URL=https://your-formbricks-instance.com +FORMBRICKS_API_KEY=your_api_key_here +BASE_DOMAIN=https://your-vanity-server-domain.com +ADMIN_API_TOKEN=your_admin_token_here +SQLITE_DB_PATH= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0065240 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Dependencies +/node_modules + +# Environment variables +.env + +# Logs +server.log +data/*.db diff --git a/COOLIFY.md b/COOLIFY.md new file mode 100644 index 0000000..f3414a8 --- /dev/null +++ b/COOLIFY.md @@ -0,0 +1,179 @@ +# Formbricks Vanity Server - Coolify Deployment Guide + +## Configuración para Coolify en VPS + +### Información del Deployment + +- **Dominio**: `your-vanity-server.com` (configurable) +- **Formbricks Instance**: `your-formbricks-instance.com` (configurable) +- **Puerto**: 3011 + +## Pasos para Desplegar en Coolify + +### 1. Publicar Imagen en Docker Hub + +```bash +# Login a Docker Hub +docker login + +# Construir la imagen +docker build -t your-dockerhub-username/formbricks-vanity-server:latest . + +# Publicar a Docker Hub +docker push your-dockerhub-username/formbricks-vanity-server:latest +``` + +### 2. Configurar en Coolify + +1. **Crear Nuevo Recurso** + + - Ve a tu proyecto en Coolify + - Click en "Add New Resource" + - Selecciona "Docker Compose" + +2. **Configurar Docker Compose** + + - Pega el contenido del archivo `docker-compose.yml` + - O usa la imagen directamente desde Docker Hub + +3. **Variables de Entorno** + + En Coolify, configura estas variables de entorno: + + ``` + FORMBRICKS_API_KEY=fbk_6QpdF1eC0E9umr9HjWUBaTxO_ispeHZYd-dI_EK9m2Q + ADMIN_API_TOKEN=9HiRr6K0Hfp2I4RgoLLsXr + FORMBRICKS_ENV_ID=cmbgr9ipo000ls201jpy12fbi,cmbgr9ipk000gs201rcukyfr7 + ``` + + > ⚠️ **Importante**: No incluyas `FORMBRICKS_SDK_URL` ni `BASE_DOMAIN` en las variables de entorno de Coolify, ya que están hardcodeadas en el docker-compose.yml + +4. **Configurar Dominio** + + - En Coolify, ve a "Domains" + - Agrega: `forms.soul23.cloud` + - Coolify configurará automáticamente SSL con Let's Encrypt + +5. **Configurar Red** + + - Asegúrate de que el servicio esté en la red `coolify` + - Esto ya está configurado en el `docker-compose.yml` + +6. **Volumen para Persistencia** + - El volumen `formbricks_data` se crea automáticamente + - Los datos de SQLite se guardarán en `/app/data` + +### 3. Desplegar + +1. Click en "Deploy" en Coolify +2. Espera a que la imagen se descargue y el contenedor inicie +3. Verifica los logs en Coolify + +### 4. Verificar Deployment + +Una vez desplegado, verifica: + +- **Admin UI**: `https://forms.soul23.cloud/admin` +- **Ejemplo de encuesta**: `https://forms.soul23.cloud/socias/Contratos` + - Debe redirigir a: `https://feedback.soul23.cloud/s/k40zfrs2r62ifbgavpumemlc` + +## Configuración Inicial + +### 1. Acceder al Admin UI + +``` +URL: https://forms.soul23.cloud/admin +Token: 9HiRr6K0Hfp2I4RgoLLsXr +``` + +### 2. Configurar Aliases + +1. Ve al Admin UI +2. Configura los aliases para tus proyectos: + - `socias` → Environment `cmbgr9ipo000ls201jpy12fbi` + - `vanity` → Environment `cmbgr6u7s0009s201i45xtbtv` + +### 3. Usar las Encuestas + +Tus encuestas estarán disponibles en: + +- `https://forms.soul23.cloud/{alias}/{nombre-encuesta}` + +Ejemplos: + +- `https://forms.soul23.cloud/socias/Contratos` (redirige a Formbricks) +- `https://forms.soul23.cloud/vanity/test` (embebida) + +## Actualizar la Aplicación + +Para actualizar a una nueva versión: + +```bash +# 1. Construir nueva imagen +docker build -t your-dockerhub-username/formbricks-vanity-server:v1.1.0 . + +# 2. Publicar +docker push your-dockerhub-username/formbricks-vanity-server:v1.1.0 + +# 3. En Coolify, actualiza la imagen en docker-compose.yml +# 4. Click en "Redeploy" +``` + +## Troubleshooting + +### Ver Logs en Coolify + +1. Ve a tu servicio en Coolify +2. Click en "Logs" +3. Verifica que el servidor inicie correctamente + +### Problemas Comunes + +**Error de conexión a Formbricks** + +- Verifica que `FORMBRICKS_API_KEY` sea correcta +- Verifica que `feedback.soul23.cloud` sea accesible desde el VPS + +**Base de datos no persiste** + +- Verifica que el volumen `formbricks_data` esté montado +- En Coolify, ve a "Volumes" y verifica que exista + +**SSL no funciona** + +- Coolify maneja SSL automáticamente +- Verifica que el dominio `forms.soul23.cloud` apunte a tu VPS +- Espera unos minutos para que Let's Encrypt emita el certificado + +## Backup de la Base de Datos + +Para hacer backup de la base de datos SQLite: + +```bash +# Conectarse al contenedor +docker exec -it formbricks-vanity sh + +# Copiar la base de datos +cp /app/data/survey_mappings.db /tmp/backup.db + +# Salir del contenedor +exit + +# Copiar desde el contenedor al host +docker cp formbricks-vanity:/tmp/backup.db ./backup-$(date +%Y%m%d).db +``` + +## Monitoreo + +Coolify proporciona métricas automáticas. Puedes ver: + +- CPU usage +- Memory usage +- Network traffic +- Container status + +## Recursos Adicionales + +- **Documentación de Coolify**: https://coolify.io/docs +- **Docker Hub**: Publica tu imagen para fácil deployment +- **Health Checks**: Coolify monitorea automáticamente la salud del contenedor diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..c4d4651 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,138 @@ +# Formbricks Vanity Server - Docker Deployment Guide + +## Quick Start + +### 1. Build the Docker Image + +```bash +docker build -t your-dockerhub-username/formbricks-vanity-server:latest . +``` + +### 2. Run the Container + +```bash +docker run -d \ + -p 3011:3011 \ + -e FORMBRICKS_SDK_URL=https://your-formbricks-instance.com \ + -e FORMBRICKS_API_KEY=your_api_key_here \ + -e ADMIN_API_TOKEN=your_admin_token_here \ + -v $(pwd)/data:/app/data \ + --name formbricks-vanity \ + your-dockerhub-username/formbricks-vanity-server:latest +``` + +### 3. Using Docker Compose (Recommended) + +Create a `docker-compose.yml` file: + +```yaml +version: "3.8" + +services: + formbricks-vanity: + image: your-dockerhub-username/formbricks-vanity-server:latest + container_name: formbricks-vanity + ports: + - "3011:3011" + environment: + - PORT=3011 + - FORMBRICKS_SDK_URL=https://your-formbricks-instance.com + - FORMBRICKS_API_KEY=your_api_key_here + - ADMIN_API_TOKEN=your_admin_token_here + - FORMBRICKS_ENV_ID=your_environment_id + - BASE_DOMAIN=https://your-formbricks-instance.com + volumes: + - ./data:/app/data + restart: unless-stopped +``` + +Then run: + +```bash +docker-compose up -d +``` + +## Environment Variables + +| Variable | Required | Description | +| -------------------- | -------- | ----------------------------------------------------- | +| `PORT` | No | Server port (default: 3011) | +| `FORMBRICKS_SDK_URL` | Yes | Your Formbricks instance URL | +| `FORMBRICKS_API_KEY` | Yes | Formbricks API key | +| `ADMIN_API_TOKEN` | Yes | Token for admin UI access | +| `FORMBRICKS_ENV_ID` | No | Environment ID (optional, for backward compatibility) | +| `BASE_DOMAIN` | No | Base domain for the application | +| `SQLITE_DB_PATH` | No | Custom SQLite database path | + +## Publishing to Docker Hub + +### 1. Login to Docker Hub + +```bash +docker login +``` + +### 2. Tag Your Image + +```bash +docker tag formbricks-vanity-server:latest your-dockerhub-username/formbricks-vanity-server:latest +docker tag formbricks-vanity-server:latest your-dockerhub-username/formbricks-vanity-server:v1.0.0 +``` + +### 3. Push to Docker Hub + +```bash +docker push your-dockerhub-username/formbricks-vanity-server:latest +docker push your-dockerhub-username/formbricks-vanity-server:v1.0.0 +``` + +## Data Persistence + +The SQLite database is stored in `/app/data` inside the container. Make sure to mount a volume to persist data: + +```bash +-v $(pwd)/data:/app/data +``` + +## Accessing the Application + +- **Surveys**: `http://localhost:3011/{alias}/{survey_name}` +- **Admin UI**: `http://localhost:3011/admin` + +## Health Check + +Add a health check to your docker-compose.yml: + +```yaml +healthcheck: + test: + [ + "CMD", + "wget", + "--quiet", + "--tries=1", + "--spider", + "http://localhost:3011/admin", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s +``` + +## Troubleshooting + +### Container won't start + +- Check logs: `docker logs formbricks-vanity` +- Verify environment variables are set correctly +- Ensure the data directory has proper permissions + +### Database issues + +- The database is created automatically on first run +- If you need to reset, stop the container and delete the `data` directory + +### Port conflicts + +- Change the host port mapping: `-p 8080:3011` (maps host port 8080 to container port 3011) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c44b08a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Use an official Node.js runtime as a parent image +FROM node:20-alpine + +# Set the working directory in the container +WORKDIR /app + +# Copy package.json and package-lock.json to leverage Docker cache +COPY package*.json ./ + +# Install app dependencies +RUN npm install --only=production + +# Bundle app source +COPY . . + +# Your app binds to port 3011 +EXPOSE 3011 + +# Define the command to run your app +CMD ["node", "src/server.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa2286a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Marco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..29516c1 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +
+ +![Formbricks Vanity Server](./assets/banner.png) + +# Formbricks Vanity Server + +**Servidor de URLs personalizadas para encuestas de Formbricks con gestión inteligente de redirecciones** + +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](https://www.docker.com/) +[![Coolify](https://img.shields.io/badge/Coolify-Compatible-6C47FF?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiAxMkwxMiAyMkwyMiAxMkwxMiAyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+)](https://coolify.io/) +[![Node.js](https://img.shields.io/badge/Node.js-20-339933?logo=node.js&logoColor=white)](https://nodejs.org/) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) + +[Características](#características) • +[Instalación](#instalación-rápida) • +[Deployment](#deployment) • +[Documentación](#documentación) + +
+ +--- + +## 🎯 Características + +- **🔗 URLs Personalizadas**: Crea URLs amigables para tus encuestas de Formbricks +- **🎨 Admin UI**: Interfaz visual para gestionar aliases de proyectos +- **🔀 Redirección Inteligente**: + - Encuestas tipo "link" → Redirigen automáticamente a Formbricks + - Encuestas tipo "app" → Se embeben en tu servidor +- **🏢 Multi-Proyecto**: Soporte para múltiples ambientes de Formbricks +- **🐳 Docker Ready**: Listo para desplegar en Coolify, Docker, o cualquier plataforma de contenedores +- **💾 Persistencia**: Base de datos SQLite con sincronización automática desde Formbricks API + +## 🚀 Instalación Rápida + +### Opción 1: Docker (Recomendado) + +```bash +docker run -d \ + -p 3011:3011 \ + -e FORMBRICKS_SDK_URL=https://your-formbricks-instance.com \ + -e FORMBRICKS_API_KEY=your_api_key \ + -e ADMIN_API_TOKEN=your_admin_token \ + -e BASE_DOMAIN=https://your-vanity-server.com \ + -v ./data:/app/data \ + --name formbricks-vanity \ + marcogll/soul23_form_mgr:latest +``` + +### Opción 2: Desarrollo Local + +```bash +# Clonar repositorio +git clone https://github.com/your-username/formbricks-vanity-server.git +cd formbricks-vanity-server + +# Instalar dependencias +npm install + +# Configurar variables de entorno +cp .env.example .env +# Editar .env con tus credenciales + +# Iniciar servidor +npm start +``` + +## 📦 Deployment + +### Coolify (VPS) + +Deployment en un solo click con Coolify. Ver [COOLIFY.md](./COOLIFY.md) para instrucciones detalladas. + +**Configuración:** + +- **Dominio**: `forms.soul23.cloud` +- **Puerto**: 3011 +- **SSL**: Automático con Let's Encrypt + +### Docker Hub + +```bash +# Construir imagen +docker build -t your-username/formbricks-vanity-server:latest . + +# Publicar a Docker Hub +docker push your-username/formbricks-vanity-server:latest +``` + +## 🎮 Uso + +### 1. Configurar Admin UI + +Accede a `https://your-vanity-server.com/admin` (o `http://localhost:3011/admin` en local) + +1. Ingresa tu Admin Token +2. Configura aliases para tus proyectos: + - `socias` → Environment de Formbricks + - `vanity` → Otro environment + +### 2. Acceder a Encuestas + +Tus encuestas estarán disponibles en: + +``` +https://your-vanity-server.com/{alias}/{nombre-encuesta} +``` + +**Ejemplos:** + +- `https://your-vanity-server.com/socias/Contratos` → Redirige a Formbricks +- `https://your-vanity-server.com/vanity/test` → Embebida en el servidor + +## ⚙️ Configuración + +### Variables de Entorno + +| Variable | Requerida | Descripción | +| -------------------- | --------- | ----------------------------------- | +| `FORMBRICKS_SDK_URL` | ✅ | URL de tu instancia de Formbricks | +| `FORMBRICKS_API_KEY` | ✅ | API Key de Formbricks | +| `ADMIN_API_TOKEN` | ✅ | Token para acceder al Admin UI | +| `PORT` | ❌ | Puerto del servidor (default: 3011) | +| `FORMBRICKS_ENV_ID` | ❌ | ID de ambiente (opcional) | + +Ver [.env.example](./.env.example) para más detalles. + +## 📚 Documentación + +- **[COOLIFY.md](./COOLIFY.md)** - Guía completa de deployment en Coolify +- **[DOCKER.md](./DOCKER.md)** - Guía general de Docker y Docker Compose +- **[.env.example](./.env.example)** - Plantilla de variables de entorno + +## 🛠️ Tecnologías + +
+ +| Tecnología | Uso | +| ---------------------------------------------------------------------------------------- | ---------------- | +| ![Node.js](https://img.shields.io/badge/Node.js-20-339933?logo=node.js&logoColor=white) | Runtime | +| ![Express](https://img.shields.io/badge/Express-4.x-000000?logo=express&logoColor=white) | Framework Web | +| ![SQLite](https://img.shields.io/badge/SQLite-3-003B57?logo=sqlite&logoColor=white) | Base de Datos | +| ![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white) | Containerización | + +
+ +## 🏗️ Arquitectura + +``` +┌─────────────────┐ +│ forms.soul23 │ ← URLs Personalizadas +│ .cloud │ +└────────┬────────┘ + │ + ├─ /socias/Contratos ──→ 302 Redirect ──→ feedback.soul23.cloud/s/{id} + │ (Encuestas tipo "link") + │ + └─ /vanity/test ──→ Embedded Survey + (Encuestas tipo "app") +``` + +## 🤝 Contribuir + +Las contribuciones son bienvenidas. Por favor: + +1. Fork el proyecto +2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`) +3. Commit tus cambios (`git commit -m 'Add some AmazingFeature'`) +4. Push a la rama (`git push origin feature/AmazingFeature`) +5. Abre un Pull Request + +## 📝 Licencia + +Este proyecto está bajo la Licencia MIT. Ver [LICENSE](LICENSE) para más detalles. + +## 🙏 Agradecimientos + +- [Formbricks](https://formbricks.com/) - Plataforma de encuestas open-source +- [Coolify](https://coolify.io/) - Plataforma de deployment self-hosted + +--- + +
+ +**Hecho con ❤️ para la comunidad de Formbricks** + +[⬆ Volver arriba](#formbricks-vanity-server) + +
diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 0000000..22d8572 Binary files /dev/null and b/assets/banner.png differ diff --git a/assets/banner_link.png b/assets/banner_link.png new file mode 100644 index 0000000..0631858 Binary files /dev/null and b/assets/banner_link.png differ diff --git a/data/survey_mappings.db-shm b/data/survey_mappings.db-shm new file mode 100644 index 0000000..28f666b Binary files /dev/null and b/data/survey_mappings.db-shm differ diff --git a/data/survey_mappings.db-wal b/data/survey_mappings.db-wal new file mode 100644 index 0000000..981b147 Binary files /dev/null and b/data/survey_mappings.db-wal differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ab4d0fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.8" + +services: + formbricks-vanity: + image: your-dockerhub-username/formbricks-vanity-server:latest + container_name: formbricks-vanity + ports: + - "3011:3011" + environment: + - PORT=3011 + - FORMBRICKS_SDK_URL=${FORMBRICKS_SDK_URL} + - FORMBRICKS_API_KEY=${FORMBRICKS_API_KEY} + - ADMIN_API_TOKEN=${ADMIN_API_TOKEN} + - FORMBRICKS_ENV_ID=${FORMBRICKS_ENV_ID} + - BASE_DOMAIN=${BASE_DOMAIN} + - SQLITE_DB_PATH= + volumes: + - formbricks_data:/app/data + restart: unless-stopped + networks: + - coolify + +volumes: + formbricks_data: + driver: local + +networks: + coolify: + external: true diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6b9ef53 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1469 @@ +{ + "name": "forms_server_vanity", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.13.2", + "better-sqlite3": "^8.7.0", + "dotenv": "^17.2.3", + "ejs": "^3.1.10", + "express": "^5.2.1" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.7.0.tgz", + "integrity": "sha512-99jZU4le+f3G6aIl6PmmV0cxUIWqKieHxsiF7G34CVFiE+/UabpYqkU0NJIkY/96mQKikHeBjtR27vFfs5JpEw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2e0cd6 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "scripts": { + "start": "node src/server.js" + }, + "dependencies": { + "axios": "^1.13.2", + "better-sqlite3": "^8.7.0", + "dotenv": "^17.2.3", + "ejs": "^3.1.10", + "express": "^5.2.1" + } +} diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..f20e3c7 --- /dev/null +++ b/src/db.js @@ -0,0 +1,42 @@ +const fs = require("fs"); +const path = require("path"); +const Database = require("better-sqlite3"); + +const defaultDbPath = path.join(__dirname, "..", "data", "survey_mappings.db"); +const dbPath = process.env.SQLITE_DB_PATH || defaultDbPath; + +fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + +const db = new Database(dbPath); +db.pragma("journal_mode = WAL"); + +db.exec(` + CREATE TABLE IF NOT EXISTS survey_mappings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project TEXT NOT NULL, + partner TEXT NOT NULL, + survey_name TEXT NOT NULL, + survey_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(project, partner, survey_name) + ); + + CREATE TABLE IF NOT EXISTS environment_aliases ( + environment_id TEXT PRIMARY KEY, + alias TEXT UNIQUE, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS surveys ( + id TEXT PRIMARY KEY, + environment_id TEXT NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL DEFAULT 'link', + custom_slug TEXT, + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); +`); + +module.exports = db; diff --git a/src/middleware/adminAuth.js b/src/middleware/adminAuth.js new file mode 100644 index 0000000..142d4ab --- /dev/null +++ b/src/middleware/adminAuth.js @@ -0,0 +1,15 @@ +function ensureAdminToken(req, res, next) { + const expectedToken = process.env.ADMIN_API_TOKEN; + if (!expectedToken) { + return res.status(503).json({ error: 'Admin token is not configured.' }); + } + + const providedToken = req.header('x-admin-token'); + if (providedToken !== expectedToken) { + return res.status(403).json({ error: 'Forbidden' }); + } + + next(); +} + +module.exports = ensureAdminToken; diff --git a/src/routes/admin.js b/src/routes/admin.js new file mode 100644 index 0000000..84a471a --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,64 @@ +const express = require("express"); +const router = express.Router(); +const ensureAdminToken = require("../middleware/adminAuth"); +const { + getAllEnvironments, + updateEnvironmentAlias, + getSurveysByEnvironment, + updateSurveySlug, +} = require("../services/formbricks"); + +router.use(ensureAdminToken); + +router.get("/environments", (req, res) => { + try { + const environments = getAllEnvironments(); + res.json({ environments }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.put("/environments/:id/alias", (req, res) => { + const { id } = req.params; + const { alias } = req.body; + + if (!alias) { + return res.status(400).json({ error: "Alias is required" }); + } + + try { + updateEnvironmentAlias(id, alias); + res.json({ status: "ok" }); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +router.get("/environments/:id/surveys", (req, res) => { + const { id } = req.params; + try { + const surveys = getSurveysByEnvironment(id); + res.json({ surveys }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.put("/surveys/:id/slug", (req, res) => { + const { id } = req.params; + const { slug } = req.body; + + try { + const success = updateSurveySlug(id, slug); + if (success) { + res.json({ status: "ok" }); + } else { + res.status(404).json({ error: "Survey not found" }); + } + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/src/routes/surveys.js b/src/routes/surveys.js new file mode 100644 index 0000000..f50a37d --- /dev/null +++ b/src/routes/surveys.js @@ -0,0 +1,36 @@ +const express = require("express"); +const router = express.Router(); +const { getSurveyIdByAlias } = require("../services/formbricks"); + +router.get("/:root/:survey", (req, res) => { + const { root, survey } = req.params; + console.log(`Requesting survey: root=${root}, survey=${survey}`); + const result = getSurveyIdByAlias(root, survey); + + if (result) { + const { surveyId, environmentId, type } = result; + console.log( + `Found surveyId: ${surveyId}, environmentId: ${environmentId}, type: ${type}` + ); + + // Redirect link surveys to Formbricks + if (type === "link") { + const redirectUrl = `${process.env.FORMBRICKS_SDK_URL}/s/${surveyId}`; + console.log(`Redirecting to: ${redirectUrl}`); + return res.redirect(redirectUrl); + } + + // Embed app surveys + res.render("survey", { + title: `${root} - ${survey}`, + surveyId: surveyId, + formbricksSdkUrl: process.env.FORMBRICKS_SDK_URL, + formbricksEnvId: environmentId, + }); + } else { + console.log("Survey not found"); + res.status(404).send("Survey not found"); + } +}); + +module.exports = router; diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..1412f2e --- /dev/null +++ b/src/server.js @@ -0,0 +1,65 @@ +require('dotenv').config(); +const express = require('express'); +const path = require('path'); +const surveyRoutes = require('./routes/surveys'); +const adminRoutes = require('./routes/admin'); +const { refreshSurveyCache } = require('./services/formbricks'); + +const app = express(); +const PORT = process.env.PORT || 3011; + +// Template Engine Setup +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Health check endpoint +app.get('/health', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); + +// Root landing page +app.get('/', (req, res) => { + res.render('index', { + title: 'Formbricks Survey Portal' + }); +}); + +// Admin UI +app.get('/admin', (req, res) => { + res.render('admin', { + title: 'Admin - Formbricks Vanity' + }); +}); + +// Admin API routes +app.use('/api/mappings', adminRoutes); + +// Main survey routes (catch-all for vanity URLs) +app.use('/', surveyRoutes); + +// Handle 404 for any other route +app.use((req, res, next) => { + res.status(404).send('Sorry, that page does not exist.'); +}); + +// Global error handler +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); + +// Initialize the survey cache at startup +refreshSurveyCache() + .then(() => { + app.listen(PORT, () => { + console.log(`Server is running at http://localhost:${PORT}`); + }); + }) + .catch(error => { + console.error('Failed to initialize Formbricks survey cache. Please check API key and connection.', error); + process.exit(1); // Exit if we can't load the initial surveys + }); diff --git a/src/services/formbricks.js b/src/services/formbricks.js new file mode 100644 index 0000000..cea439b --- /dev/null +++ b/src/services/formbricks.js @@ -0,0 +1,174 @@ +const axios = require("axios"); +const db = require("../db"); + +// --- Prepared Statements --- + +const upsertEnvironmentStmt = db.prepare(` + INSERT INTO environment_aliases (environment_id) + VALUES (?) + ON CONFLICT(environment_id) DO NOTHING +`); + +const upsertSurveyStmt = db.prepare(` + INSERT INTO surveys (id, environment_id, name, type, updated_at) + VALUES (@id, @environmentId, @name, @type, datetime('now')) + ON CONFLICT(id) DO UPDATE SET + environment_id = excluded.environment_id, + name = excluded.name, + type = excluded.type, + updated_at = datetime('now') +`); + +const selectAliasStmt = db.prepare(` + SELECT alias FROM environment_aliases WHERE environment_id = ? +`); + +const selectEnvByAliasStmt = db.prepare(` + SELECT environment_id FROM environment_aliases WHERE alias = ? +`); + +const selectSurveyStmt = db.prepare(` + SELECT id, type, custom_slug FROM surveys WHERE environment_id = ? AND (name = ? OR custom_slug = ?) +`); + +const selectAllEnvsStmt = db.prepare(` + SELECT * FROM environment_aliases ORDER BY created_at DESC +`); + +const updateAliasStmt = db.prepare(` + UPDATE environment_aliases SET alias = ?, updated_at = datetime('now') WHERE environment_id = ? +`); + +const selectSurveysByEnvStmt = db.prepare(` + SELECT id, name, type, custom_slug FROM surveys WHERE environment_id = ? ORDER BY name ASC +`); + +// --- Helper Functions --- + +async function fetchSurveysFromAPI() { + if (!process.env.FORMBRICKS_API_KEY) { + throw new Error( + "FORMBRICKS_API_KEY is not defined in environment variables." + ); + } + if (!process.env.FORMBRICKS_SDK_URL) { + throw new Error( + "FORMBRICKS_SDK_URL is not defined in environment variables." + ); + } + + try { + const response = await axios.get( + `${process.env.FORMBRICKS_SDK_URL}/api/v1/management/surveys`, + { + headers: { + "x-api-key": process.env.FORMBRICKS_API_KEY, + }, + } + ); + + return Array.isArray(response.data?.data) ? response.data.data : []; + } catch (error) { + console.error( + "Failed to fetch surveys from Formbricks API:", + error.message + ); + throw new Error("Could not fetch surveys from Formbricks API."); + } +} + +async function refreshSurveyCache() { + try { + console.log("Fetching surveys from Formbricks API..."); + const surveys = await fetchSurveysFromAPI(); + let synced = 0; + + const dbTransaction = db.transaction((surveys) => { + for (const survey of surveys) { + if (!survey?.id || !survey?.environmentId) { + continue; + } + + // Ensure environment exists + upsertEnvironmentStmt.run(survey.environmentId); + + // Upsert survey + upsertSurveyStmt.run({ + id: survey.id, + environmentId: survey.environmentId, + name: survey.name || survey.id, + type: survey.type || "link", + }); + + synced += 1; + } + }); + + dbTransaction(surveys); + console.log(`Successfully synced ${synced} surveys into the database.`); + } catch (error) { + console.error("Failed to refresh survey cache:", error.message); + } +} + +// --- Exported Functions --- + +function getAllEnvironments() { + return selectAllEnvsStmt.all(); +} + +function updateEnvironmentAlias(environmentId, alias) { + try { + const result = updateAliasStmt.run(alias, environmentId); + return result.changes > 0; + } catch (error) { + if (error.code === "SQLITE_CONSTRAINT_UNIQUE") { + throw new Error("Alias already in use"); + } + throw error; + } +} + +function getSurveysByEnvironment(environmentId) { + return selectSurveysByEnvStmt.all(environmentId); +} + +function getSurveyIdByAlias(rootAlias, surveyName) { + const env = selectEnvByAliasStmt.get(rootAlias); + if (!env) return null; + + const survey = selectSurveyStmt.get( + env.environment_id, + surveyName, + surveyName + ); + return survey + ? { + surveyId: survey.id, + environmentId: env.environment_id, + type: survey.type, + } + : null; +} + +function updateSurveySlug(surveyId, customSlug) { + try { + const stmt = db.prepare(` + UPDATE surveys SET custom_slug = ?, updated_at = datetime('now') WHERE id = ? + `); + const result = stmt.run(customSlug || null, surveyId); + return result.changes > 0; + } catch (error) { + throw error; + } +} + +module.exports = { + refreshSurveyCache, + fetchSurveysFromAPI, + getAllEnvironments, + updateEnvironmentAlias, + getSurveysByEnvironment, + getSurveyIdByAlias, + updateSurveySlug, +}; diff --git a/src/views/admin.ejs b/src/views/admin.ejs new file mode 100644 index 0000000..8f3e7af --- /dev/null +++ b/src/views/admin.ejs @@ -0,0 +1,463 @@ + + + + + + <%= title %> + + + + + + +
+

Admin Authentication

+

Enter your Admin Token to manage surveys.

+
+ +
+ +
+ + + + + + diff --git a/src/views/index.ejs b/src/views/index.ejs new file mode 100644 index 0000000..79841d2 --- /dev/null +++ b/src/views/index.ejs @@ -0,0 +1,103 @@ + + + + + + <%= title %> + + + +
+
Formbricks Vanity
+

Surveys under your own brand

+

+ This service proxies Formbricks surveys behind clean, semantic URLs and renders the + official experience within your own domain. No manual mapping files, no redirect + chains — just friendly slugs that everyone on your team can memorise. +

+ +
+
+

Friendly URLs

+

+ Every survey is addressable via /:project/:partner/:survey. The slug + maps transparently to the Formbricks survey ID. +

+
+
+

Automatic Sync

+

+ The server fetches your Formbricks catalog at startup and keeps the SQLite-backed + registry in sync without manual edits. +

+
+
+

Admin controls

+

+ A protected API lets you inspect and manage the current mappings, so your ops team + can add or edit surveys without touching the CLI. +

+
+
+
+ + diff --git a/src/views/survey.ejs b/src/views/survey.ejs new file mode 100644 index 0000000..7e007db --- /dev/null +++ b/src/views/survey.ejs @@ -0,0 +1,40 @@ + + + + + + <%= title %> + + + + + +