First
Some checks failed
Build All Docker Images / changes (push) Has been cancelled
Build and Push App Docker Image / build (push) Has been cancelled
Build and Push Node Docker Image / build (push) Has been cancelled
Test and Lint / test-app (push) Has been cancelled
Test and Lint / test-node (push) Has been cancelled
Test and Lint / lint-dockerfiles (push) Has been cancelled
Test and Lint / security-scan (push) Has been cancelled
Build All Docker Images / build-app (push) Has been cancelled
Build All Docker Images / build-node (push) Has been cancelled
Build All Docker Images / summary (push) Has been cancelled
Some checks failed
Build All Docker Images / changes (push) Has been cancelled
Build and Push App Docker Image / build (push) Has been cancelled
Build and Push Node Docker Image / build (push) Has been cancelled
Test and Lint / test-app (push) Has been cancelled
Test and Lint / test-node (push) Has been cancelled
Test and Lint / lint-dockerfiles (push) Has been cancelled
Test and Lint / security-scan (push) Has been cancelled
Build All Docker Images / build-app (push) Has been cancelled
Build All Docker Images / build-node (push) Has been cancelled
Build All Docker Images / summary (push) Has been cancelled
This commit is contained in:
commit
4169337dd0
68 changed files with 8726 additions and 0 deletions
145
.forgejo/workflows/build-all.yml
Normal file
145
.forgejo/workflows/build-all.yml
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
name: Build All Docker Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
app: ${{ steps.changes.outputs.app }}
|
||||||
|
node: ${{ steps.changes.outputs.node }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
uses: dorny/paths-filter@v2
|
||||||
|
id: changes
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
app:
|
||||||
|
- 'app/**'
|
||||||
|
- '.forgejo/workflows/build-all.yml'
|
||||||
|
node:
|
||||||
|
- 'node/**'
|
||||||
|
- '.forgejo/workflows/build-all.yml'
|
||||||
|
|
||||||
|
build-app:
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.app == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for App
|
||||||
|
id: meta-app
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ github.repository }}/frp-manager-app
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push App Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./app
|
||||||
|
file: ./app/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta-app.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-app.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=app
|
||||||
|
cache-to: type=gha,mode=max,scope=app
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
build-node:
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.node == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Node
|
||||||
|
id: meta-node
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ github.repository }}/home-server-agent
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Node Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./node
|
||||||
|
file: ./node/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta-node.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-node.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=node
|
||||||
|
cache-to: type=gha,mode=max,scope=node
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
summary:
|
||||||
|
needs: [build-app, build-node]
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### App Build: ${{ needs.build-app.result }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Node Build: ${{ needs.build-node.result }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${{ github.event_name }}" == "release" ]; then
|
||||||
|
echo "### Release Images:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- App: \`${{ env.REGISTRY }}/${{ github.repository }}/frp-manager-app:${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Node: \`${{ env.REGISTRY }}/${{ github.repository }}/home-server-agent:${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
69
.forgejo/workflows/build-app.yml
Normal file
69
.forgejo/workflows/build-app.yml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
name: Build and Push App Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
paths:
|
||||||
|
- 'app/**'
|
||||||
|
- '.forgejo/workflows/build-app.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'app/**'
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}/frp-manager-app
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./app
|
||||||
|
file: ./app/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
- name: Update Docker Compose with new image
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
run: |
|
||||||
|
echo "Built and pushed image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}"
|
||||||
|
echo "Update your docker-compose.yml to use: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}"
|
||||||
69
.forgejo/workflows/build-node.yml
Normal file
69
.forgejo/workflows/build-node.yml
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
name: Build and Push Node Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
paths:
|
||||||
|
- 'node/**'
|
||||||
|
- '.forgejo/workflows/build-node.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'node/**'
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}/home-server-agent
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./node
|
||||||
|
file: ./node/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
- name: Update Docker Compose with new image
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
run: |
|
||||||
|
echo "Built and pushed image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}"
|
||||||
|
echo "Update your docker-compose.yml to use: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}"
|
||||||
157
.forgejo/workflows/deploy.yml
Normal file
157
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: 'Environment to deploy to'
|
||||||
|
required: true
|
||||||
|
default: 'staging'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ github.event.inputs.environment || 'production' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set environment variables
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "release" ]; then
|
||||||
|
echo "DEPLOY_ENV=production" >> $GITHUB_ENV
|
||||||
|
echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "DEPLOY_ENV=${{ github.event.inputs.environment }}" >> $GITHUB_ENV
|
||||||
|
echo "IMAGE_TAG=latest" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create deployment package
|
||||||
|
run: |
|
||||||
|
mkdir -p deployment
|
||||||
|
|
||||||
|
# Copy docker-compose files
|
||||||
|
cp app/docker-compose.yml deployment/docker-compose-app.yml
|
||||||
|
cp node/docker-compose.yml deployment/docker-compose-node.yml
|
||||||
|
|
||||||
|
# Copy environment templates
|
||||||
|
cp app/.env.example deployment/.env.app.example
|
||||||
|
cp node/.env.example deployment/.env.node.example
|
||||||
|
|
||||||
|
# Create deployment script
|
||||||
|
cat > deployment/deploy.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REGISTRY="${{ env.REGISTRY }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
TAG="${{ env.IMAGE_TAG }}"
|
||||||
|
|
||||||
|
echo "Deploying FRP Manager to ${{ env.DEPLOY_ENV }}"
|
||||||
|
echo "Using images: $REGISTRY/$REPO/frp-manager-app:$TAG and $REGISTRY/$REPO/home-server-agent:$TAG"
|
||||||
|
|
||||||
|
# Pull latest images
|
||||||
|
docker pull $REGISTRY/$REPO/frp-manager-app:$TAG
|
||||||
|
docker pull $REGISTRY/$REPO/home-server-agent:$TAG
|
||||||
|
|
||||||
|
# Update docker-compose files with new image references
|
||||||
|
sed -i "s|build: \.|image: $REGISTRY/$REPO/frp-manager-app:$TAG|g" docker-compose-app.yml
|
||||||
|
sed -i "s|build: \.|image: $REGISTRY/$REPO/home-server-agent:$TAG|g" docker-compose-node.yml
|
||||||
|
|
||||||
|
echo "Deployment package ready!"
|
||||||
|
echo "1. Configure .env files based on .env.*.example"
|
||||||
|
echo "2. Run: docker-compose -f docker-compose-app.yml up -d"
|
||||||
|
echo "3. Run: docker-compose -f docker-compose-node.yml up -d (on home server)"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x deployment/deploy.sh
|
||||||
|
|
||||||
|
- name: Create deployment documentation
|
||||||
|
run: |
|
||||||
|
cat > deployment/README.md << 'EOF'
|
||||||
|
# FRP Manager Deployment Package
|
||||||
|
|
||||||
|
This package contains everything needed to deploy the FRP Manager application.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `docker-compose-app.yml` - App deployment configuration
|
||||||
|
- `docker-compose-node.yml` - Node deployment configuration
|
||||||
|
- `.env.app.example` - App environment template
|
||||||
|
- `.env.node.example` - Node environment template
|
||||||
|
- `deploy.sh` - Deployment script
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. App Server (VPS)
|
||||||
|
```bash
|
||||||
|
# Configure environment
|
||||||
|
cp .env.app.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
docker-compose -f docker-compose-app.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Home Server
|
||||||
|
```bash
|
||||||
|
# Configure environment
|
||||||
|
cp .env.node.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
docker-compose -f docker-compose-node.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
See the `.env.*.example` files for required configuration.
|
||||||
|
|
||||||
|
**Important**: Set matching tokens for `NODE_TOKEN` (app) and `API_TOKEN` (node).
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Check app: `http://your-vps:3000`
|
||||||
|
2. Check node: `http://your-home-server:3001/health`
|
||||||
|
3. Test integration via the "Push to Node" button in the web interface
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
- App Image: `${{ env.REGISTRY }}/${{ github.repository }}/frp-manager-app:${{ env.IMAGE_TAG }}`
|
||||||
|
- Node Image: `${{ env.REGISTRY }}/${{ github.repository }}/home-server-agent:${{ env.IMAGE_TAG }}`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Package deployment artifacts
|
||||||
|
run: |
|
||||||
|
tar -czf frp-manager-deployment-${{ env.IMAGE_TAG }}.tar.gz -C deployment .
|
||||||
|
|
||||||
|
- name: Upload deployment package
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frp-manager-deployment-${{ env.DEPLOY_ENV }}-${{ env.IMAGE_TAG }}
|
||||||
|
path: frp-manager-deployment-${{ env.IMAGE_TAG }}.tar.gz
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
- name: Create deployment summary
|
||||||
|
run: |
|
||||||
|
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Environment:** ${{ env.DEPLOY_ENV }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Version:** ${{ env.IMAGE_TAG }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Images Built:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- App: \`${{ env.REGISTRY }}/${{ github.repository }}/frp-manager-app:${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Node: \`${{ env.REGISTRY }}/${{ github.repository }}/home-server-agent:${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Deployment Package:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Download the deployment artifact and follow the README.md instructions." >> $GITHUB_STEP_SUMMARY
|
||||||
104
.forgejo/workflows/test.yml
Normal file
104
.forgejo/workflows/test.yml
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
name: Test and Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-app:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./app
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: './app/package-lock.json'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Run tests (if available)
|
||||||
|
run: npm test --if-present
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
test-node:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./node
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: bunx tsc --noEmit
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Run tests (if available)
|
||||||
|
run: bun test --if-present
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
lint-dockerfiles:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Lint App Dockerfile
|
||||||
|
uses: hadolint/hadolint-action@v3.1.0
|
||||||
|
with:
|
||||||
|
dockerfile: ./app/Dockerfile
|
||||||
|
failure-threshold: warning
|
||||||
|
|
||||||
|
- name: Lint Node Dockerfile
|
||||||
|
uses: hadolint/hadolint-action@v3.1.0
|
||||||
|
with:
|
||||||
|
dockerfile: ./node/Dockerfile
|
||||||
|
failure-threshold: warning
|
||||||
|
|
||||||
|
security-scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Trivy vulnerability scanner
|
||||||
|
uses: aquasecurity/trivy-action@master
|
||||||
|
with:
|
||||||
|
scan-type: 'fs'
|
||||||
|
scan-ref: '.'
|
||||||
|
format: 'sarif'
|
||||||
|
output: 'trivy-results.sarif'
|
||||||
|
|
||||||
|
- name: Upload Trivy scan results
|
||||||
|
uses: github/codeql-action/upload-sarif@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: 'trivy-results.sarif'
|
||||||
297
CI_CD_DOCUMENTATION.md
Normal file
297
CI_CD_DOCUMENTATION.md
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
# Forgejo Actions CI/CD Documentation
|
||||||
|
|
||||||
|
This document describes the Forgejo Actions workflows for building, testing, and deploying the FRP Manager application.
|
||||||
|
|
||||||
|
## Workflow Overview
|
||||||
|
|
||||||
|
The CI/CD pipeline consists of several automated workflows:
|
||||||
|
|
||||||
|
### 1. **Test and Lint** (`.forgejo/workflows/test.yml`)
|
||||||
|
- **Triggers**: Push to `main`/`develop`, Pull Requests
|
||||||
|
- **Purpose**: Code quality, type checking, and security scanning
|
||||||
|
- **Jobs**:
|
||||||
|
- `test-app`: TypeScript compilation and build testing for the app
|
||||||
|
- `test-node`: TypeScript compilation and build testing for the node
|
||||||
|
- `lint-dockerfiles`: Dockerfile linting with hadolint
|
||||||
|
- `security-scan`: Vulnerability scanning with Trivy
|
||||||
|
|
||||||
|
### 2. **Build App** (`.forgejo/workflows/build-app.yml`)
|
||||||
|
- **Triggers**: Changes to `app/` directory, releases
|
||||||
|
- **Purpose**: Build and push the FRP Manager app Docker image
|
||||||
|
- **Features**: Multi-platform builds (amd64, arm64), caching
|
||||||
|
|
||||||
|
### 3. **Build Node** (`.forgejo/workflows/build-node.yml`)
|
||||||
|
- **Triggers**: Changes to `node/` directory, releases
|
||||||
|
- **Purpose**: Build and push the Home Server Agent Docker image
|
||||||
|
- **Features**: Multi-platform builds (amd64, arm64), caching
|
||||||
|
|
||||||
|
### 4. **Build All** (`.forgejo/workflows/build-all.yml`)
|
||||||
|
- **Triggers**: Push to branches, PRs, releases, manual dispatch
|
||||||
|
- **Purpose**: Intelligent building of both images with change detection
|
||||||
|
- **Features**:
|
||||||
|
- Path-based change detection
|
||||||
|
- Parallel builds when both projects change
|
||||||
|
- Build summary generation
|
||||||
|
|
||||||
|
### 5. **Deploy** (`.forgejo/workflows/deploy.yml`)
|
||||||
|
- **Triggers**: Releases, manual dispatch
|
||||||
|
- **Purpose**: Create deployment packages for production
|
||||||
|
- **Features**:
|
||||||
|
- Environment-specific deployments
|
||||||
|
- Automated deployment package creation
|
||||||
|
- Documentation generation
|
||||||
|
|
||||||
|
## Container Registry
|
||||||
|
|
||||||
|
Images are published to GitHub Container Registry (GHCR):
|
||||||
|
- **App**: `ghcr.io/YOUR_USERNAME/YOUR_REPO/frp-manager-app`
|
||||||
|
- **Node**: `ghcr.io/YOUR_USERNAME/YOUR_REPO/home-server-agent`
|
||||||
|
|
||||||
|
### Image Tags
|
||||||
|
|
||||||
|
- `latest` - Latest build from main branch
|
||||||
|
- `main` - Latest main branch build
|
||||||
|
- `develop` - Latest develop branch build
|
||||||
|
- `v1.0.0` - Specific version tags (on releases)
|
||||||
|
- `1.0` - Major.minor tags (on releases)
|
||||||
|
|
||||||
|
## Setup Requirements
|
||||||
|
|
||||||
|
### 1. Repository Secrets
|
||||||
|
|
||||||
|
The workflows require these secrets to be configured in your Forgejo repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container registry access (automatically provided by Forgejo/GitHub)
|
||||||
|
GITHUB_TOKEN # Automatic, no setup needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Repository Settings
|
||||||
|
|
||||||
|
1. **Enable Actions**: Ensure Forgejo Actions are enabled in repository settings
|
||||||
|
2. **Container Registry**: Enable package publishing permissions
|
||||||
|
3. **Branch Protection**: Consider protecting `main` branch with required status checks
|
||||||
|
|
||||||
|
### 3. Runner Requirements
|
||||||
|
|
||||||
|
Ensure your Forgejo instance has runners configured with:
|
||||||
|
- Docker support
|
||||||
|
- Multi-platform build capabilities (for arm64 builds)
|
||||||
|
- Sufficient storage for image caching
|
||||||
|
|
||||||
|
## Workflow Triggers
|
||||||
|
|
||||||
|
### Automatic Triggers
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Push to main/develop branches
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
|
||||||
|
# Pull requests to main
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
# Release creation
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
|
||||||
|
# Path-based triggers (Build All workflow)
|
||||||
|
paths:
|
||||||
|
- 'app/**'
|
||||||
|
- 'node/**'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Triggers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Manual workflow dispatch (via UI or API)
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: 'Environment to deploy to'
|
||||||
|
required: true
|
||||||
|
default: 'staging'
|
||||||
|
type: choice
|
||||||
|
options: [ staging, production ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Process
|
||||||
|
|
||||||
|
### 1. **Code Quality Checks**
|
||||||
|
```bash
|
||||||
|
# TypeScript compilation
|
||||||
|
npx tsc --noEmit # App
|
||||||
|
bunx tsc --noEmit # Node
|
||||||
|
|
||||||
|
# Application builds
|
||||||
|
npm run build # App
|
||||||
|
bun run build # Node
|
||||||
|
|
||||||
|
# Dockerfile linting
|
||||||
|
hadolint Dockerfile
|
||||||
|
|
||||||
|
# Security scanning
|
||||||
|
trivy fs .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Docker Image Building**
|
||||||
|
```bash
|
||||||
|
# Multi-platform builds
|
||||||
|
docker buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
--push \
|
||||||
|
--tag ghcr.io/user/repo/app:latest \
|
||||||
|
./app
|
||||||
|
|
||||||
|
# With caching
|
||||||
|
--cache-from type=gha \
|
||||||
|
--cache-to type=gha,mode=max
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Image Publishing**
|
||||||
|
- Images are automatically pushed to GHCR
|
||||||
|
- Tags are generated based on branch/release
|
||||||
|
- Metadata labels are added for tracking
|
||||||
|
|
||||||
|
## Deployment Process
|
||||||
|
|
||||||
|
### 1. **Automated Deployment Package**
|
||||||
|
When a release is created or deployment is manually triggered:
|
||||||
|
|
||||||
|
1. **Image Building**: Latest images are built and pushed
|
||||||
|
2. **Package Creation**: Deployment artifacts are created
|
||||||
|
3. **Documentation**: Deployment instructions are generated
|
||||||
|
4. **Artifact Upload**: Package is uploaded for download
|
||||||
|
|
||||||
|
### 2. **Deployment Package Contents**
|
||||||
|
```
|
||||||
|
frp-manager-deployment-v1.0.0.tar.gz
|
||||||
|
├── docker-compose-app.yml # App deployment config
|
||||||
|
├── docker-compose-node.yml # Node deployment config
|
||||||
|
├── .env.app.example # App environment template
|
||||||
|
├── .env.node.example # Node environment template
|
||||||
|
├── deploy.sh # Deployment script
|
||||||
|
└── README.md # Deployment instructions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Manual Deployment Steps**
|
||||||
|
|
||||||
|
1. **Download Package**: Get the deployment artifact from the workflow run
|
||||||
|
2. **Extract**: `tar -xzf frp-manager-deployment-v1.0.0.tar.gz`
|
||||||
|
3. **Configure**:
|
||||||
|
```bash
|
||||||
|
cp .env.app.example .env # Configure app environment
|
||||||
|
cp .env.node.example .env # Configure node environment
|
||||||
|
```
|
||||||
|
4. **Deploy App** (VPS):
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose-app.yml up -d
|
||||||
|
```
|
||||||
|
5. **Deploy Node** (Home Server):
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose-node.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Troubleshooting
|
||||||
|
|
||||||
|
### 1. **Workflow Status**
|
||||||
|
- Check workflow runs in the Actions tab
|
||||||
|
- View build logs for debugging
|
||||||
|
- Monitor image registry for published images
|
||||||
|
|
||||||
|
### 2. **Common Issues**
|
||||||
|
|
||||||
|
**Build Failures**:
|
||||||
|
```bash
|
||||||
|
# Check TypeScript errors
|
||||||
|
npx tsc --noEmit
|
||||||
|
|
||||||
|
# Check Docker build context
|
||||||
|
docker build --no-cache ./app
|
||||||
|
```
|
||||||
|
|
||||||
|
**Image Push Failures**:
|
||||||
|
- Verify GITHUB_TOKEN permissions
|
||||||
|
- Check registry connectivity
|
||||||
|
- Ensure package publishing is enabled
|
||||||
|
|
||||||
|
**Deployment Issues**:
|
||||||
|
- Verify environment variables
|
||||||
|
- Check image availability in registry
|
||||||
|
- Validate Docker Compose syntax
|
||||||
|
|
||||||
|
### 3. **Local Testing**
|
||||||
|
|
||||||
|
Test workflows locally before pushing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test app build
|
||||||
|
cd app && npm run build
|
||||||
|
|
||||||
|
# Test node build
|
||||||
|
cd node && bun run build
|
||||||
|
|
||||||
|
# Test Docker builds
|
||||||
|
docker build -t test-app ./app
|
||||||
|
docker build -t test-node ./node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### 1. **Modify Triggers**
|
||||||
|
Edit workflow files to change when builds occur:
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop, feature/* ] # Add feature branches
|
||||||
|
paths:
|
||||||
|
- 'app/**'
|
||||||
|
- 'docs/**' # Add documentation changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Add Tests**
|
||||||
|
Extend test workflows with actual test suites:
|
||||||
|
```yaml
|
||||||
|
- name: Run unit tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
run: npm run test:integration
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Custom Deployment**
|
||||||
|
Add deployment to your specific infrastructure:
|
||||||
|
```yaml
|
||||||
|
- name: Deploy to Kubernetes
|
||||||
|
run: kubectl apply -f k8s/
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
run: |
|
||||||
|
scp deployment.tar.gz user@server:/tmp/
|
||||||
|
ssh user@server 'cd /tmp && ./deploy.sh'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. **Branch Strategy**
|
||||||
|
- Use `develop` for integration testing
|
||||||
|
- Use `main` for production-ready code
|
||||||
|
- Create releases for versioned deployments
|
||||||
|
|
||||||
|
### 2. **Image Management**
|
||||||
|
- Tag releases with semantic versions
|
||||||
|
- Use `latest` tag sparingly
|
||||||
|
- Clean up old images periodically
|
||||||
|
|
||||||
|
### 3. **Security**
|
||||||
|
- Regularly update base images
|
||||||
|
- Monitor security scan results
|
||||||
|
- Use specific version tags in production
|
||||||
|
|
||||||
|
### 4. **Performance**
|
||||||
|
- Leverage build caching
|
||||||
|
- Use multi-stage Docker builds
|
||||||
|
- Optimize image sizes
|
||||||
|
|
||||||
|
This CI/CD setup provides a robust foundation for building, testing, and deploying the FRP Manager application with proper automation and quality gates.
|
||||||
229
NODE_INTEGRATION.md
Normal file
229
NODE_INTEGRATION.md
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
# FRP Manager - Node Integration Setup
|
||||||
|
|
||||||
|
This guide explains how to set up the integration between the FRP Manager app and the home server node.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The integration allows the FRP Manager app (running on your VPS) to:
|
||||||
|
- Query the home server node for status
|
||||||
|
- Send updated frpc.toml configurations
|
||||||
|
- Restart the FRP client remotely
|
||||||
|
- Monitor node connectivity
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Environment Configuration
|
||||||
|
|
||||||
|
#### App (.env file)
|
||||||
|
```bash
|
||||||
|
# Copy from .env.example and configure these variables:
|
||||||
|
|
||||||
|
# FRP Server Configuration
|
||||||
|
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||||
|
FRPC_SERVER_PORT=7000
|
||||||
|
FRPC_TOKEN=your-secret-token
|
||||||
|
|
||||||
|
# Node Integration
|
||||||
|
NODE_URL=http://your-home-server-ip:3001
|
||||||
|
NODE_TOKEN=your-node-secret-token
|
||||||
|
NODE_TIMEOUT=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Node (.env file)
|
||||||
|
```bash
|
||||||
|
# Copy from .env.example and configure these variables:
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
API_TOKEN=your-node-secret-token
|
||||||
|
|
||||||
|
# FRP Configuration
|
||||||
|
FRPC_CONFIG_PATH=/app/data/frpc.toml
|
||||||
|
FRPC_CONTAINER_NAME=frpc
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Security Considerations
|
||||||
|
|
||||||
|
**Important**: Use the same token for `NODE_TOKEN` in the app and `API_TOKEN` in the node for authentication.
|
||||||
|
|
||||||
|
- Generate a strong, random token (e.g., 32+ character string)
|
||||||
|
- Keep tokens secure and never commit them to version control
|
||||||
|
- Use HTTPS in production for encrypted communication
|
||||||
|
|
||||||
|
### 3. Network Setup
|
||||||
|
|
||||||
|
#### Port Configuration
|
||||||
|
- **App**: Runs on port 3000 (configurable)
|
||||||
|
- **Node**: Runs on port 3001 (configurable)
|
||||||
|
|
||||||
|
#### Firewall Rules
|
||||||
|
Ensure the node port (3001) is accessible from your VPS:
|
||||||
|
```bash
|
||||||
|
# On your home server (if using UFW)
|
||||||
|
sudo ufw allow 3001/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Network Access
|
||||||
|
- The app needs HTTP/HTTPS access to the node
|
||||||
|
- Consider using a VPN or port forwarding if the node is behind NAT
|
||||||
|
- For production, use HTTPS with proper certificates
|
||||||
|
|
||||||
|
### 4. Docker Deployment
|
||||||
|
|
||||||
|
#### Starting the App (VPS)
|
||||||
|
```bash
|
||||||
|
cd app/
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Starting the Node (Home Server)
|
||||||
|
```bash
|
||||||
|
cd node/
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. API Endpoints
|
||||||
|
|
||||||
|
#### Node Endpoints (Protected by API_TOKEN)
|
||||||
|
- `GET /api/status` - Get server status
|
||||||
|
- `GET /api/frpc/status` - Get frpc container status
|
||||||
|
- `POST /api/frpc/update-config` - Update frpc configuration
|
||||||
|
- `POST /api/frpc/restart` - Restart frpc container
|
||||||
|
- `POST /api/frpc/push-and-restart` - Update config and restart in one call
|
||||||
|
|
||||||
|
#### App Endpoints (Node Integration)
|
||||||
|
- `GET /api/node/status` - Get node status through app
|
||||||
|
- `GET /api/node/connection` - Get node connection info
|
||||||
|
- `POST /api/node/push-config` - Push current config to node
|
||||||
|
- `POST /api/node/restart-frpc` - Restart frpc on node
|
||||||
|
- `POST /api/node/push-and-restart` - Push config and restart frpc on node
|
||||||
|
|
||||||
|
### 6. Frontend Features
|
||||||
|
|
||||||
|
#### Dashboard
|
||||||
|
- Live node status indicator
|
||||||
|
- Connection monitoring
|
||||||
|
- Last connection time tracking
|
||||||
|
|
||||||
|
#### Tunnel Manager
|
||||||
|
- "Push to Node" button for deploying configurations
|
||||||
|
- Real-time node connectivity status
|
||||||
|
- Error handling and user feedback
|
||||||
|
|
||||||
|
### 7. Testing the Integration
|
||||||
|
|
||||||
|
#### 1. Verify Node Connectivity
|
||||||
|
```bash
|
||||||
|
# From your VPS, test the node endpoint
|
||||||
|
curl -H "Authorization: Bearer your-node-secret-token" \
|
||||||
|
http://your-home-server-ip:3001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Test Configuration Push
|
||||||
|
1. Create/modify tunnels in the app
|
||||||
|
2. Click "Push to Node" button
|
||||||
|
3. Verify configuration updated on the node
|
||||||
|
4. Check that frpc restarted successfully
|
||||||
|
|
||||||
|
#### 3. Monitor Logs
|
||||||
|
```bash
|
||||||
|
# App logs
|
||||||
|
docker logs frp-manager
|
||||||
|
|
||||||
|
# Node logs
|
||||||
|
docker logs home-server-agent
|
||||||
|
|
||||||
|
# FRP client logs
|
||||||
|
docker logs frpc
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Troubleshooting
|
||||||
|
|
||||||
|
#### Common Issues
|
||||||
|
|
||||||
|
**"Node client not configured"**
|
||||||
|
- Check that `NODE_URL` and `NODE_TOKEN` are set in app environment
|
||||||
|
- Verify environment variables are loaded correctly
|
||||||
|
|
||||||
|
**"Failed to connect to node"**
|
||||||
|
- Verify node is running and accessible
|
||||||
|
- Check firewall rules
|
||||||
|
- Ensure correct IP address and port
|
||||||
|
- Verify token authentication
|
||||||
|
|
||||||
|
**"frpc container not found"**
|
||||||
|
- Ensure frpc container exists with the correct name
|
||||||
|
- Check `FRPC_CONTAINER_NAME` environment variable
|
||||||
|
- Verify Docker is accessible from the node
|
||||||
|
|
||||||
|
#### Debugging Steps
|
||||||
|
|
||||||
|
1. **Check Environment Variables**
|
||||||
|
```bash
|
||||||
|
# In app container
|
||||||
|
docker exec frp-manager env | grep NODE_
|
||||||
|
|
||||||
|
# In node container
|
||||||
|
docker exec home-server-agent env | grep API_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test Direct API Calls**
|
||||||
|
```bash
|
||||||
|
# Test node health endpoint (no auth required)
|
||||||
|
curl http://your-home-server-ip:3001/health
|
||||||
|
|
||||||
|
# Test authenticated endpoint
|
||||||
|
curl -H "Authorization: Bearer your-token" \
|
||||||
|
http://your-home-server-ip:3001/api/status
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check Container Status**
|
||||||
|
```bash
|
||||||
|
# On home server
|
||||||
|
docker ps | grep frpc
|
||||||
|
docker logs frpc
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Production Considerations
|
||||||
|
|
||||||
|
#### Security
|
||||||
|
- Use HTTPS with valid certificates
|
||||||
|
- Implement IP whitelisting if possible
|
||||||
|
- Regular token rotation
|
||||||
|
- Monitor authentication logs
|
||||||
|
|
||||||
|
#### Monitoring
|
||||||
|
- Set up health checks for both app and node
|
||||||
|
- Monitor node connectivity from app
|
||||||
|
- Log all configuration changes
|
||||||
|
- Set up alerts for connection failures
|
||||||
|
|
||||||
|
#### Backup
|
||||||
|
- Backup frpc configurations
|
||||||
|
- Backup tunnel database
|
||||||
|
- Document recovery procedures
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Deploying New Tunnel Configuration
|
||||||
|
|
||||||
|
1. **Add/Edit Tunnels**: Use the web interface to create or modify tunnel configurations
|
||||||
|
2. **Push to Node**: Click the "Push to Node" button in the Tunnel Manager
|
||||||
|
3. **Verify**: Check the dashboard for node status and tunnel activity
|
||||||
|
|
||||||
|
### Manual Configuration Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Push configuration via API
|
||||||
|
curl -X POST -H "Authorization: Bearer app-token" \
|
||||||
|
http://your-vps:3000/api/node/push-config
|
||||||
|
|
||||||
|
# Restart frpc on node
|
||||||
|
curl -X POST -H "Authorization: Bearer app-token" \
|
||||||
|
http://your-vps:3000/api/node/restart-frpc
|
||||||
|
```
|
||||||
|
|
||||||
|
This integration provides a seamless way to manage FRP configurations across your infrastructure while maintaining security and monitoring capabilities.
|
||||||
6
app/.env
Normal file
6
app/.env
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Environment variables for FRP configuration
|
||||||
|
FRPC_SERVER_ADDR=127.0.0.1
|
||||||
|
FRPC_SERVER_PORT=7000
|
||||||
|
FRPC_TOKEN=your-secret-token
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
22
app/.env.example
Normal file
22
app/.env.example
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Environment variables for FRP configuration
|
||||||
|
# Copy this file to .env and update the values
|
||||||
|
|
||||||
|
# FRP Server Configuration
|
||||||
|
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||||
|
FRPC_SERVER_PORT=7000
|
||||||
|
FRPC_TOKEN=your-secret-token
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Database Configuration (SQLite is used by default)
|
||||||
|
# DATABASE_PATH=./data/tunnels.db
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Node Integration Configuration
|
||||||
|
NODE_URL=http://your-home-server:3001
|
||||||
|
NODE_TOKEN=your-node-secret-token
|
||||||
|
NODE_TIMEOUT=5000
|
||||||
24
app/.gitignore
vendored
Normal file
24
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
336
app/DEPLOYMENT.md
Normal file
336
app/DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
# Deployment Guide
|
||||||
|
|
||||||
|
This guide covers different deployment scenarios for the FRP Manager application.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
1. [Docker Compose Deployment (Recommended)](#docker-compose-deployment)
|
||||||
|
2. [Manual Deployment](#manual-deployment)
|
||||||
|
3. [Development Setup](#development-setup)
|
||||||
|
4. [Production Configuration](#production-configuration)
|
||||||
|
5. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
## Docker Compose Deployment
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Access to your VPS/server running FRP server
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. **Clone and Setup**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd frp-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Environment**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` with your FRP server details:
|
||||||
|
```env
|
||||||
|
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||||
|
FRPC_SERVER_PORT=7000
|
||||||
|
FRPC_TOKEN=your-secret-token
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Deploy**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access Application**
|
||||||
|
- Web Interface: http://localhost:3000
|
||||||
|
- API: http://localhost:3000/api
|
||||||
|
- Health Check: http://localhost:3000/health
|
||||||
|
|
||||||
|
### Docker Compose Configuration
|
||||||
|
|
||||||
|
The `docker-compose.yml` includes:
|
||||||
|
- **app**: Main application container (Express API + React frontend)
|
||||||
|
- **frpc**: FRP client container for tunnel management
|
||||||
|
|
||||||
|
Key volumes:
|
||||||
|
- `./data:/app/data` - Database and configuration files
|
||||||
|
- `./logs:/app/logs` - Application logs
|
||||||
|
- `/var/run/docker.sock:/var/run/docker.sock` - Docker socket for container management
|
||||||
|
|
||||||
|
### Updating the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest changes
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Rebuild and restart
|
||||||
|
docker-compose down
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Deployment
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js 18+ installed
|
||||||
|
- Docker (for FRPC container)
|
||||||
|
- SQLite3
|
||||||
|
|
||||||
|
### Installation Steps
|
||||||
|
|
||||||
|
1. **Install Dependencies**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build Application**
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure Environment**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Create Directories**
|
||||||
|
```bash
|
||||||
|
mkdir -p data logs
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Start Application**
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Production
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Process Management
|
||||||
|
|
||||||
|
For production deployment, use a process manager like PM2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install PM2
|
||||||
|
npm install -g pm2
|
||||||
|
|
||||||
|
# Start application with PM2
|
||||||
|
pm2 start npm --name "frp-manager" -- start
|
||||||
|
|
||||||
|
# Save PM2 configuration
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
# Setup auto-restart on reboot
|
||||||
|
pm2 startup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reverse Proxy Setup
|
||||||
|
|
||||||
|
Example Nginx configuration:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. **Install Dependencies**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Environment File**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start Development Server**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Features
|
||||||
|
|
||||||
|
- **Hot Reload**: Both frontend and backend auto-reload on changes
|
||||||
|
- **Debug Logging**: Enhanced logging in development mode
|
||||||
|
- **React DevTools**: Included React Query DevTools
|
||||||
|
- **Error Handling**: Detailed error messages in development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── src/
|
||||||
|
│ ├── client/ # React frontend
|
||||||
|
│ │ ├── api/ # API client and types
|
||||||
|
│ │ ├── components/ # Reusable React components
|
||||||
|
│ │ ├── pages/ # Page components
|
||||||
|
│ │ └── App.tsx # Main App component
|
||||||
|
│ └── server/ # Express backend
|
||||||
|
│ ├── database.ts # SQLite database operations
|
||||||
|
│ ├── frpc-manager.ts # FRPC container management
|
||||||
|
│ ├── logger.ts # Winston logging configuration
|
||||||
|
│ ├── main.ts # Express server setup
|
||||||
|
│ ├── routes.ts # API route definitions
|
||||||
|
│ └── types.ts # TypeScript type definitions
|
||||||
|
├── public/ # Static assets
|
||||||
|
├── data/ # Database and FRPC config
|
||||||
|
├── logs/ # Application logs
|
||||||
|
├── Dockerfile # Docker container definition
|
||||||
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
|
└── package.json # Project dependencies and scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# FRP Server Configuration
|
||||||
|
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||||
|
FRPC_SERVER_PORT=7000
|
||||||
|
FRPC_TOKEN=your-secret-token
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Security (optional)
|
||||||
|
CORS_ORIGIN=https://your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
1. **Authentication**: Consider adding authentication for production use
|
||||||
|
2. **CORS**: Configure appropriate CORS settings
|
||||||
|
3. **HTTPS**: Use HTTPS in production
|
||||||
|
4. **Firewall**: Restrict access to necessary ports only
|
||||||
|
5. **Docker Security**: Run containers with non-root users
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
The application includes:
|
||||||
|
- **Health Check Endpoint**: `/health`
|
||||||
|
- **Structured Logging**: JSON logs with Winston
|
||||||
|
- **Error Tracking**: Comprehensive error logging
|
||||||
|
- **Service Status**: Built-in service monitoring
|
||||||
|
|
||||||
|
### Backup Strategy
|
||||||
|
|
||||||
|
Important data to backup:
|
||||||
|
- Database: `./data/tunnels.db`
|
||||||
|
- Configuration: `./data/frpc.toml`
|
||||||
|
- Environment: `.env`
|
||||||
|
- Logs: `./logs/` (optional)
|
||||||
|
|
||||||
|
Example backup script:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
BACKUP_DIR="./backups/$(date +%Y%m%d_%H%M%S)"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
cp -r ./data "$BACKUP_DIR/"
|
||||||
|
cp .env "$BACKUP_DIR/"
|
||||||
|
tar -czf "$BACKUP_DIR.tar.gz" "$BACKUP_DIR"
|
||||||
|
rm -rf "$BACKUP_DIR"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Application Won't Start
|
||||||
|
- Check Node.js version (requires 18+)
|
||||||
|
- Verify all dependencies are installed
|
||||||
|
- Check port availability (default: 3000)
|
||||||
|
- Review logs in `./logs/error.log`
|
||||||
|
|
||||||
|
#### Database Issues
|
||||||
|
- Ensure `./data` directory exists and is writable
|
||||||
|
- Check SQLite permissions
|
||||||
|
- Verify database file integrity
|
||||||
|
|
||||||
|
#### FRPC Container Issues
|
||||||
|
- Verify Docker is running
|
||||||
|
- Check FRPC container logs: `docker logs frpc`
|
||||||
|
- Ensure FRPC configuration is valid
|
||||||
|
- Verify server connectivity
|
||||||
|
|
||||||
|
#### API Errors
|
||||||
|
- Check server logs for detailed error messages
|
||||||
|
- Verify API endpoints are accessible
|
||||||
|
- Check CORS configuration for frontend issues
|
||||||
|
|
||||||
|
### Health Check Script
|
||||||
|
|
||||||
|
Use the provided health check script to verify all services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
./health-check.sh
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
powershell -ExecutionPolicy Bypass -File "health-check.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Analysis
|
||||||
|
|
||||||
|
Application logs are stored in `./logs/`:
|
||||||
|
- `combined.log`: All application logs
|
||||||
|
- `error.log`: Error logs only
|
||||||
|
|
||||||
|
Example log analysis:
|
||||||
|
```bash
|
||||||
|
# View recent errors
|
||||||
|
tail -f ./logs/error.log
|
||||||
|
|
||||||
|
# Search for specific errors
|
||||||
|
grep "Failed to" ./logs/combined.log
|
||||||
|
|
||||||
|
# View API requests
|
||||||
|
grep "API Request" ./logs/combined.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
For additional support:
|
||||||
|
1. Check the [README.md](README.md) file
|
||||||
|
2. Review application logs
|
||||||
|
3. Use the health check script
|
||||||
|
4. Open an issue on the repository
|
||||||
|
|
||||||
|
### Performance Tuning
|
||||||
|
|
||||||
|
For high-traffic deployments:
|
||||||
|
1. Use a reverse proxy (Nginx, Apache)
|
||||||
|
2. Enable HTTP/2
|
||||||
|
3. Implement caching strategies
|
||||||
|
4. Monitor resource usage
|
||||||
|
5. Scale horizontally if needed
|
||||||
|
|
||||||
|
### Updates and Maintenance
|
||||||
|
|
||||||
|
Regular maintenance tasks:
|
||||||
|
- Update dependencies: `npm update`
|
||||||
|
- Backup database regularly
|
||||||
|
- Monitor disk space for logs
|
||||||
|
- Review and rotate logs
|
||||||
|
- Update Docker images: `docker-compose pull`
|
||||||
29
app/Dockerfile
Normal file
29
app/Dockerfile
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Use Node.js 18 LTS
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Create directories for data and logs
|
||||||
|
RUN mkdir -p data logs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Install Docker CLI to manage frpc container
|
||||||
|
RUN apk add --no-cache docker-cli
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
270
app/README.md
Normal file
270
app/README.md
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
# FRP Manager
|
||||||
|
|
||||||
|
A fullstack application for managing FRP (Fast Reverse Proxy) tunnel configurations with a React frontend and Express backend. Now with **Node Integration** for remote management of home server FRP clients.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 **Web Dashboard**: Modern React-based interface for managing tunnels
|
||||||
|
- 🔧 **Tunnel Management**: Create, edit, delete, and toggle tunnel configurations
|
||||||
|
- 📊 **Real-time Status**: Monitor tunnel status and FRPC service health
|
||||||
|
- 🏠 **Node Integration**: Remote control of home server FRP clients
|
||||||
|
- <20> **Push to Node**: Deploy configurations to remote nodes with one click
|
||||||
|
- 🌐 **Live Monitoring**: Real-time node connectivity and status tracking
|
||||||
|
- <20>🐳 **Docker Support**: Complete Docker Compose setup for easy deployment
|
||||||
|
- 📝 **Auto Configuration**: Automatically generates FRPC configuration from active tunnels
|
||||||
|
- 🗄️ **SQLite Database**: Persistent storage for tunnel configurations
|
||||||
|
- 📋 **Service Logs**: View FRPC service logs directly from the web interface
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- React 19
|
||||||
|
- TypeScript
|
||||||
|
- React Router
|
||||||
|
- TanStack Query (React Query)
|
||||||
|
- Lucide React Icons
|
||||||
|
- React Hot Toast
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Node.js
|
||||||
|
- Express
|
||||||
|
- TypeScript
|
||||||
|
- SQLite (better-sqlite3)
|
||||||
|
- Zod validation
|
||||||
|
- Winston logging
|
||||||
|
- Axios (for node communication)
|
||||||
|
|
||||||
|
**Infrastructure:**
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- FRPC (Fast Reverse Proxy Client)
|
||||||
|
|
||||||
|
## New: Node Integration
|
||||||
|
|
||||||
|
The FRP Manager now supports remote management of home server nodes. This allows you to:
|
||||||
|
|
||||||
|
- **Query Status**: Get real-time status from your home server node
|
||||||
|
- **Push Configs**: Send updated frpc.toml configurations remotely
|
||||||
|
- **Restart Services**: Restart FRP client on the home server
|
||||||
|
- **Monitor Connectivity**: Live status indicators and connection tracking
|
||||||
|
|
||||||
|
### Setup Node Integration
|
||||||
|
|
||||||
|
1. **Configure the app** with node connection details in `.env`:
|
||||||
|
```bash
|
||||||
|
NODE_URL=http://your-home-server:3001
|
||||||
|
NODE_TOKEN=your-node-secret-token
|
||||||
|
NODE_TIMEOUT=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set up the home server node** (see `../node/` directory)
|
||||||
|
|
||||||
|
3. **Use the web interface** to push configurations with the "Push to Node" button
|
||||||
|
|
||||||
|
For detailed setup instructions, see [NODE_INTEGRATION.md](../NODE_INTEGRATION.md).
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Using Docker Compose (Recommended)
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd frp-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create environment configuration:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Edit `.env` with your FRP server details:
|
||||||
|
```bash
|
||||||
|
FRPC_SERVER_ADDR=your-vps-ip-address
|
||||||
|
FRPC_SERVER_PORT=7000
|
||||||
|
FRPC_TOKEN=your-secret-token
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Start the application:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Access the web interface at `http://localhost:3000`
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Set up environment variables:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Access the application at `http://localhost:3000`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `FRPC_SERVER_ADDR` | FRP server IP address | `your-vps-ip` |
|
||||||
|
| `FRPC_SERVER_PORT` | FRP server port | `7000` |
|
||||||
|
| `FRPC_TOKEN` | FRP authentication token | - |
|
||||||
|
| `NODE_ENV` | Node environment | `development` |
|
||||||
|
| `PORT` | Application port | `3000` |
|
||||||
|
|
||||||
|
### Tunnel Configuration
|
||||||
|
|
||||||
|
Each tunnel requires:
|
||||||
|
- **Name**: Descriptive name (e.g., "Minecraft Server")
|
||||||
|
- **Protocol**: TCP or UDP
|
||||||
|
- **Local IP**: IP address of the service on your network
|
||||||
|
- **Local Port**: Port of the service on your network
|
||||||
|
- **Remote Port**: Port to expose on the FRP server
|
||||||
|
- **Enabled**: Whether the tunnel is active
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Adding a Tunnel
|
||||||
|
|
||||||
|
1. Navigate to the "Tunnels" page
|
||||||
|
2. Click "Add Tunnel"
|
||||||
|
3. Fill in the tunnel details:
|
||||||
|
- Name: "Minecraft Server"
|
||||||
|
- Protocol: TCP
|
||||||
|
- Local IP: 192.168.1.100
|
||||||
|
- Local Port: 25565
|
||||||
|
- Remote Port: 25565
|
||||||
|
- Enabled: ✓
|
||||||
|
4. Click "Create"
|
||||||
|
|
||||||
|
### Managing Tunnels
|
||||||
|
|
||||||
|
- **Edit**: Click the edit button to modify tunnel settings
|
||||||
|
- **Toggle**: Use the power button to enable/disable tunnels
|
||||||
|
- **Delete**: Remove tunnels you no longer need
|
||||||
|
- **Status**: View real-time tunnel status on the dashboard
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
|
Use the "Server Status" page to:
|
||||||
|
- Start/Stop/Restart the FRPC service
|
||||||
|
- Regenerate FRPC configuration
|
||||||
|
- View service logs
|
||||||
|
- Monitor system health
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Tunnels
|
||||||
|
- `GET /api/tunnels` - Get all tunnels
|
||||||
|
- `POST /api/tunnels` - Create new tunnel
|
||||||
|
- `GET /api/tunnels/:id` - Get tunnel by ID
|
||||||
|
- `PUT /api/tunnels/:id` - Update tunnel
|
||||||
|
- `DELETE /api/tunnels/:id` - Delete tunnel
|
||||||
|
- `GET /api/tunnels/:id/status` - Get tunnel status
|
||||||
|
|
||||||
|
### FRPC Service
|
||||||
|
- `GET /api/frpc/status` - Get service status
|
||||||
|
- `POST /api/frpc/start` - Start service
|
||||||
|
- `POST /api/frpc/stop` - Stop service
|
||||||
|
- `POST /api/frpc/restart` - Restart service
|
||||||
|
- `POST /api/frpc/regenerate` - Regenerate configuration
|
||||||
|
- `GET /api/frpc/logs` - Get service logs
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── src/
|
||||||
|
│ ├── client/ # React frontend
|
||||||
|
│ │ ├── api/ # API client
|
||||||
|
│ │ ├── components/ # React components
|
||||||
|
│ │ ├── pages/ # Page components
|
||||||
|
│ │ └── App.tsx # Main App component
|
||||||
|
│ └── server/ # Express backend
|
||||||
|
│ ├── database.ts # Database layer
|
||||||
|
│ ├── frpc-manager.ts # FRPC management
|
||||||
|
│ ├── logger.ts # Logging utilities
|
||||||
|
│ ├── main.ts # Server entry point
|
||||||
|
│ ├── routes.ts # API routes
|
||||||
|
│ └── types.ts # TypeScript types
|
||||||
|
├── public/ # Static assets
|
||||||
|
├── data/ # Database and config files
|
||||||
|
├── logs/ # Application logs
|
||||||
|
└── docker-compose.yml # Docker configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start development server
|
||||||
|
|
||||||
|
# Production
|
||||||
|
npm run build # Build application
|
||||||
|
npm start # Start production server
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose up # Start all services
|
||||||
|
docker-compose down # Stop all services
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**FRPC service won't start:**
|
||||||
|
- Check if the server address and port are correct
|
||||||
|
- Verify the authentication token
|
||||||
|
- Ensure the FRPC container has the updated configuration
|
||||||
|
|
||||||
|
**Tunnels show as inactive:**
|
||||||
|
- Verify the local service is running
|
||||||
|
- Check firewall settings
|
||||||
|
- Ensure the local IP and port are correct
|
||||||
|
|
||||||
|
**Database errors:**
|
||||||
|
- Check if the `data` directory exists and is writable
|
||||||
|
- Verify SQLite permissions
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
Application logs are stored in the `logs` directory:
|
||||||
|
- `combined.log`: All application logs
|
||||||
|
- `error.log`: Error logs only
|
||||||
|
|
||||||
|
FRPC logs can be viewed through the web interface or directly:
|
||||||
|
```bash
|
||||||
|
docker logs frpc
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see LICENSE file for details.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
1. Check the troubleshooting section
|
||||||
|
2. Review the logs for error messages
|
||||||
|
3. Open an issue on the repository
|
||||||
862
app/bun.lock
Normal file
862
app/bun.lock
Normal file
|
|
@ -0,0 +1,862 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "arc-frp",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.13.4",
|
||||||
|
"@tanstack/react-query-devtools": "^5.13.5",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"better-sqlite3": "^9.2.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"react-router-dom": "^6.20.1",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
|
"tsx": "^4.19.3",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"vite-express": "*",
|
||||||
|
"winston": "^3.11.0",
|
||||||
|
"zod": "^3.22.4",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.8",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/node": "^22.15.2",
|
||||||
|
"@types/react": "^19.1.2",
|
||||||
|
"@types/react-dom": "^19.1.2",
|
||||||
|
"@types/uuid": "^9.0.7",
|
||||||
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
"nodemon": "^3.1.10",
|
||||||
|
"vite": "^6.3.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
|
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
|
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
|
||||||
|
|
||||||
|
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
|
"@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
|
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
|
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
|
"@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.28.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg=="],
|
||||||
|
|
||||||
|
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||||
|
|
||||||
|
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
|
||||||
|
|
||||||
|
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||||
|
|
||||||
|
"@npmcli/fs": ["@npmcli/fs@1.1.1", "", { "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" } }, "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ=="],
|
||||||
|
|
||||||
|
"@npmcli/move-file": ["@npmcli/move-file@1.1.2", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg=="],
|
||||||
|
|
||||||
|
"@remix-run/router": ["@remix-run/router@1.23.0", "", {}, "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="],
|
||||||
|
|
||||||
|
"@tanstack/query-core": ["@tanstack/query-core@5.81.5", "", {}, "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q=="],
|
||||||
|
|
||||||
|
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.81.2", "", {}, "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg=="],
|
||||||
|
|
||||||
|
"@tanstack/react-query": ["@tanstack/react-query@5.81.5", "", { "dependencies": { "@tanstack/query-core": "5.81.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw=="],
|
||||||
|
|
||||||
|
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.81.5", "", { "dependencies": { "@tanstack/query-devtools": "5.81.2" }, "peerDependencies": { "@tanstack/react-query": "^5.81.5", "react": "^18 || ^19" } }, "sha512-lCGMu4RX0uGnlrlLeSckBfnW/UV+KMlTBVqa97cwK7Z2ED5JKnZRSjNXwoma6sQBTJrcULvzgx2K6jEPvNUpDw=="],
|
||||||
|
|
||||||
|
"@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="],
|
||||||
|
|
||||||
|
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||||
|
|
||||||
|
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||||
|
|
||||||
|
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||||
|
|
||||||
|
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||||
|
|
||||||
|
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
|
||||||
|
|
||||||
|
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||||
|
|
||||||
|
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||||
|
|
||||||
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
|
||||||
|
|
||||||
|
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="],
|
||||||
|
|
||||||
|
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||||
|
|
||||||
|
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="],
|
||||||
|
|
||||||
|
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||||
|
|
||||||
|
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
|
||||||
|
|
||||||
|
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
|
||||||
|
|
||||||
|
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
|
||||||
|
|
||||||
|
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||||
|
|
||||||
|
"@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="],
|
||||||
|
|
||||||
|
"abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
|
||||||
|
|
||||||
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
|
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||||
|
|
||||||
|
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||||
|
|
||||||
|
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||||
|
|
||||||
|
"aproba": ["aproba@2.0.0", "", {}, "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="],
|
||||||
|
|
||||||
|
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
|
||||||
|
|
||||||
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"better-sqlite3": ["better-sqlite3@9.6.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ=="],
|
||||||
|
|
||||||
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
|
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||||
|
|
||||||
|
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||||
|
|
||||||
|
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
|
||||||
|
|
||||||
|
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||||
|
|
||||||
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
|
"cacache": ["cacache@15.3.0", "", { "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "glob": "^7.1.4", "infer-owner": "^1.0.4", "lru-cache": "^6.0.0", "minipass": "^3.1.1", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.2", "mkdirp": "^1.0.3", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^8.0.1", "tar": "^6.0.2", "unique-filename": "^1.1.1" } }, "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
|
|
||||||
|
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
||||||
|
|
||||||
|
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
|
||||||
|
|
||||||
|
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||||
|
|
||||||
|
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||||
|
|
||||||
|
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
|
||||||
|
|
||||||
|
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
|
||||||
|
|
||||||
|
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
||||||
|
|
||||||
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||||
|
|
||||||
|
"cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||||
|
|
||||||
|
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||||
|
|
||||||
|
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
|
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
|
||||||
|
|
||||||
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
|
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.179", "", {}, "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||||
|
|
||||||
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
|
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
|
||||||
|
|
||||||
|
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||||
|
|
||||||
|
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||||
|
|
||||||
|
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
|
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||||
|
|
||||||
|
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||||
|
|
||||||
|
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.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" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||||
|
|
||||||
|
"express-static-gzip": ["express-static-gzip@2.2.0", "", { "dependencies": { "parseurl": "^1.3.3", "serve-static": "^1.16.2" } }, "sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||||
|
|
||||||
|
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||||
|
|
||||||
|
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"finalhandler": ["finalhandler@2.1.0", "", { "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" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
||||||
|
|
||||||
|
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.3", "", { "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" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="],
|
||||||
|
|
||||||
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
|
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||||
|
|
||||||
|
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
|
||||||
|
|
||||||
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="],
|
||||||
|
|
||||||
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||||
|
|
||||||
|
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||||
|
|
||||||
|
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"goober": ["goober@2.1.16", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
|
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||||
|
|
||||||
|
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||||
|
|
||||||
|
"http-proxy-agent": ["http-proxy-agent@4.0.1", "", { "dependencies": { "@tootallnate/once": "1", "agent-base": "6", "debug": "4" } }, "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg=="],
|
||||||
|
|
||||||
|
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||||
|
|
||||||
|
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="],
|
||||||
|
|
||||||
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
|
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||||
|
|
||||||
|
"infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="],
|
||||||
|
|
||||||
|
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||||
|
|
||||||
|
"ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="],
|
||||||
|
|
||||||
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
|
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||||
|
|
||||||
|
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
|
"is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],
|
||||||
|
|
||||||
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
|
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||||
|
|
||||||
|
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="],
|
||||||
|
|
||||||
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||||
|
|
||||||
|
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@0.294.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA=="],
|
||||||
|
|
||||||
|
"make-fetch-happen": ["make-fetch-happen@9.1.0", "", { "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^6.0.0", "minipass": "^3.1.3", "minipass-collect": "^1.0.2", "minipass-fetch": "^1.3.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.2", "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" } }, "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||||
|
|
||||||
|
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||||
|
|
||||||
|
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||||
|
|
||||||
|
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
|
||||||
|
|
||||||
|
"minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="],
|
||||||
|
|
||||||
|
"minipass-fetch": ["minipass-fetch@1.4.1", "", { "dependencies": { "minipass": "^3.1.0", "minipass-sized": "^1.0.3", "minizlib": "^2.0.0" }, "optionalDependencies": { "encoding": "^0.1.12" } }, "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw=="],
|
||||||
|
|
||||||
|
"minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="],
|
||||||
|
|
||||||
|
"minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
|
||||||
|
|
||||||
|
"minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
|
||||||
|
|
||||||
|
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
|
||||||
|
|
||||||
|
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||||
|
|
||||||
|
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||||
|
|
||||||
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
|
"node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="],
|
||||||
|
|
||||||
|
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||||
|
|
||||||
|
"node-gyp": ["node-gyp@8.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^9.1.0", "nopt": "^5.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||||
|
|
||||||
|
"nodemon": ["nodemon@3.1.10", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw=="],
|
||||||
|
|
||||||
|
"nopt": ["nopt@5.0.0", "", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="],
|
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
|
"npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
|
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
||||||
|
|
||||||
|
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
|
||||||
|
|
||||||
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
||||||
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"prebuild-install": ["prebuild-install@7.1.3", "", { "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" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||||
|
|
||||||
|
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
|
||||||
|
|
||||||
|
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
|
||||||
|
|
||||||
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
|
"pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="],
|
||||||
|
|
||||||
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||||
|
|
||||||
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
|
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
|
||||||
|
|
||||||
|
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||||
|
|
||||||
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||||
|
|
||||||
|
"react-hot-toast": ["react-hot-toast@2.5.2", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw=="],
|
||||||
|
|
||||||
|
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||||
|
|
||||||
|
"react-router": ["react-router@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ=="],
|
||||||
|
|
||||||
|
"react-router-dom": ["react-router-dom@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0", "react-router": "6.30.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw=="],
|
||||||
|
|
||||||
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||||
|
|
||||||
|
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="],
|
||||||
|
|
||||||
|
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
|
"send": ["send@1.2.0", "", { "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" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
||||||
|
|
||||||
|
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
||||||
|
|
||||||
|
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
||||||
|
|
||||||
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "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" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||||
|
|
||||||
|
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
|
||||||
|
|
||||||
|
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||||
|
|
||||||
|
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
|
||||||
|
|
||||||
|
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||||
|
|
||||||
|
"socks": ["socks@2.8.5", "", { "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww=="],
|
||||||
|
|
||||||
|
"socks-proxy-agent": ["socks-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
|
||||||
|
|
||||||
|
"sqlite3": ["sqlite3@5.1.7", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { "node-gyp": "8.x" } }, "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog=="],
|
||||||
|
|
||||||
|
"ssri": ["ssri@8.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ=="],
|
||||||
|
|
||||||
|
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||||
|
|
||||||
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||||
|
|
||||||
|
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
|
||||||
|
|
||||||
|
"tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
|
||||||
|
|
||||||
|
"tar-stream": ["tar-stream@2.2.0", "", { "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" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||||
|
|
||||||
|
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
|
"touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="],
|
||||||
|
|
||||||
|
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
||||||
|
|
||||||
|
"tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="],
|
||||||
|
|
||||||
|
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||||
|
|
||||||
|
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
|
"undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"unique-filename": ["unique-filename@1.1.1", "", { "dependencies": { "unique-slug": "^2.0.0" } }, "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ=="],
|
||||||
|
|
||||||
|
"unique-slug": ["unique-slug@2.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w=="],
|
||||||
|
|
||||||
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||||
|
|
||||||
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
|
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||||
|
|
||||||
|
"vite-express": ["vite-express@0.21.1", "", { "dependencies": { "express-static-gzip": "^2.2.0", "picocolors": "^1.1.1" } }, "sha512-/dz1syfdKfWwcNRSl9wxZQmH7dImrvxNR9TptbpYGqrlawWFD+USzbLR1ytWei8XJpDPDRUgOoT8dEIf/vviyQ=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
|
||||||
|
|
||||||
|
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
||||||
|
|
||||||
|
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
|
"zod": ["zod@3.25.71", "", {}, "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q=="],
|
||||||
|
|
||||||
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"cacache/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
|
|
||||||
|
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"express-static-gzip/serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
|
||||||
|
|
||||||
|
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||||
|
|
||||||
|
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"make-fetch-happen/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
|
|
||||||
|
"make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"make-fetch-happen/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
|
||||||
|
|
||||||
|
"minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||||
|
|
||||||
|
"express-static-gzip/serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||||
|
|
||||||
|
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"express-static-gzip/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"express-static-gzip/serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||||
|
|
||||||
|
"express-static-gzip/serve-static/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||||
|
|
||||||
|
"express-static-gzip/serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||||
|
|
||||||
|
"express-static-gzip/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/data/frpc.toml
Normal file
15
app/data/frpc.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[common]
|
||||||
|
server_addr = "127.0.0.1"
|
||||||
|
server_port = 7000
|
||||||
|
token = "your-secret-token"
|
||||||
|
|
||||||
|
# Example tunnel configuration
|
||||||
|
# This file will be automatically generated by the FRP Manager
|
||||||
|
# based on your tunnel configurations in the web interface
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# [minecraft-server]
|
||||||
|
# type = "tcp"
|
||||||
|
# local_ip = "192.168.1.100"
|
||||||
|
# local_port = 25565
|
||||||
|
# remote_port = 25565
|
||||||
BIN
app/data/tunnels.db
Normal file
BIN
app/data/tunnels.db
Normal file
Binary file not shown.
28
app/docker-compose.override.yml
Normal file
28
app/docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- FRPC_SERVER_ADDR=127.0.0.1
|
||||||
|
- FRPC_SERVER_PORT=7000
|
||||||
|
- FRPC_TOKEN=your-secret-token
|
||||||
|
depends_on:
|
||||||
|
- frpc
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frpc:
|
||||||
|
image: snowdreamtech/frpc:latest
|
||||||
|
container_name: frpc
|
||||||
|
volumes:
|
||||||
|
- ./data/frpc.toml:/etc/frp/frpc.toml:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
# Use host network for easier local development
|
||||||
|
network_mode: "host"
|
||||||
39
app/docker-compose.yml
Normal file
39
app/docker-compose.yml
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Main application (Express API + React frontend)
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: frp-manager
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- FRPC_SERVER_ADDR=${FRPC_SERVER_ADDR:-your-vps-ip}
|
||||||
|
- FRPC_SERVER_PORT=${FRPC_SERVER_PORT:-7000}
|
||||||
|
- FRPC_TOKEN=${FRPC_TOKEN}
|
||||||
|
- NODE_URL=${NODE_URL}
|
||||||
|
- NODE_TOKEN=${NODE_TOKEN}
|
||||||
|
- NODE_TIMEOUT=${NODE_TIMEOUT:-5000}
|
||||||
|
depends_on:
|
||||||
|
- frpc
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# FRPC client container
|
||||||
|
frpc:
|
||||||
|
image: snowdreamtech/frpc:latest
|
||||||
|
container_name: frpc
|
||||||
|
volumes:
|
||||||
|
- ./data/frpc.toml:/etc/frp/frpc.toml
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: "host"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
logs:
|
||||||
69
app/health-check.ps1
Normal file
69
app/health-check.ps1
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Health check script for FRP Manager (PowerShell)
|
||||||
|
# This script checks if all services are running correctly
|
||||||
|
|
||||||
|
Write-Host "🔍 FRP Manager Health Check" -ForegroundColor Cyan
|
||||||
|
Write-Host "==========================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Check if main application is running
|
||||||
|
Write-Host "📡 Checking main application..." -ForegroundColor Yellow
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -Uri "http://localhost:3000/health" -UseBasicParsing -TimeoutSec 5
|
||||||
|
if ($response.StatusCode -eq 200) {
|
||||||
|
Write-Host "✅ Main application is running" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ Main application returned status code: $($response.StatusCode)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ Main application is not accessible: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if API is responding
|
||||||
|
Write-Host "🔌 Checking API endpoints..." -ForegroundColor Yellow
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -Uri "http://localhost:3000/api/tunnels" -UseBasicParsing -TimeoutSec 5
|
||||||
|
if ($response.StatusCode -eq 200) {
|
||||||
|
Write-Host "✅ API is responding" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ API returned status code: $($response.StatusCode)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ API is not responding: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if FRPC container is running (if Docker is available)
|
||||||
|
if (Get-Command docker -ErrorAction SilentlyContinue) {
|
||||||
|
Write-Host "🐳 Checking FRPC container..." -ForegroundColor Yellow
|
||||||
|
$dockerPs = docker ps 2>$null
|
||||||
|
if ($dockerPs -and ($dockerPs | Select-String "frpc")) {
|
||||||
|
Write-Host "✅ FRPC container is running" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ FRPC container is not running (this is expected in development)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ Docker is not available, skipping FRPC container check" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if database is accessible
|
||||||
|
Write-Host "🗄️ Checking database..." -ForegroundColor Yellow
|
||||||
|
if (Test-Path "./data/tunnels.db") {
|
||||||
|
Write-Host "✅ Database file exists" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "ℹ️ Database file doesn't exist yet (will be created on first use)" -ForegroundColor Blue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if configuration directory exists
|
||||||
|
Write-Host "📁 Checking configuration directory..." -ForegroundColor Yellow
|
||||||
|
if (Test-Path "./data") {
|
||||||
|
Write-Host "✅ Configuration directory exists" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ Configuration directory is missing" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "🎉 Health check completed successfully!" -ForegroundColor Green
|
||||||
|
Write-Host "👉 Open http://localhost:3000 to access the FRP Manager" -ForegroundColor Cyan
|
||||||
58
app/health-check.sh
Normal file
58
app/health-check.sh
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Health check script for FRP Manager
|
||||||
|
# This script checks if all services are running correctly
|
||||||
|
|
||||||
|
echo "🔍 FRP Manager Health Check"
|
||||||
|
echo "=========================="
|
||||||
|
|
||||||
|
# Check if main application is running
|
||||||
|
echo "📡 Checking main application..."
|
||||||
|
if curl -s http://localhost:3000/health > /dev/null; then
|
||||||
|
echo "✅ Main application is running"
|
||||||
|
else
|
||||||
|
echo "❌ Main application is not accessible"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if API is responding
|
||||||
|
echo "🔌 Checking API endpoints..."
|
||||||
|
if curl -s http://localhost:3000/api/tunnels > /dev/null; then
|
||||||
|
echo "✅ API is responding"
|
||||||
|
else
|
||||||
|
echo "❌ API is not responding"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if FRPC container is running (if Docker is available)
|
||||||
|
if command -v docker &> /dev/null; then
|
||||||
|
echo "🐳 Checking FRPC container..."
|
||||||
|
if docker ps | grep -q frpc; then
|
||||||
|
echo "✅ FRPC container is running"
|
||||||
|
else
|
||||||
|
echo "⚠️ FRPC container is not running (this is expected in development)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Docker is not available, skipping FRPC container check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if database is accessible
|
||||||
|
echo "🗄️ Checking database..."
|
||||||
|
if [ -f "./data/tunnels.db" ]; then
|
||||||
|
echo "✅ Database file exists"
|
||||||
|
else
|
||||||
|
echo "ℹ️ Database file doesn't exist yet (will be created on first use)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if configuration directory exists
|
||||||
|
echo "📁 Checking configuration directory..."
|
||||||
|
if [ -d "./data" ]; then
|
||||||
|
echo "✅ Configuration directory exists"
|
||||||
|
else
|
||||||
|
echo "❌ Configuration directory is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Health check completed successfully!"
|
||||||
|
echo "👉 Open http://localhost:3000 to access the FRP Manager"
|
||||||
13
app/index.html
Normal file
13
app/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/client/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
app/package.json
Normal file
44
app/package.json
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "arc-frp",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon -w src/server -x tsx src/server/main.ts",
|
||||||
|
"start": "cross-env NODE_ENV=production tsx src/server/main.ts",
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"tsx": "^4.19.3",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite-express": "*",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
|
"better-sqlite3": "^9.2.2",
|
||||||
|
"zod": "^3.22.4",
|
||||||
|
"winston": "^3.11.0",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"react-router-dom": "^6.20.1",
|
||||||
|
"@tanstack/react-query": "^5.13.4",
|
||||||
|
"@tanstack/react-query-devtools": "^5.13.5",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"lucide-react": "^0.294.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/node": "^22.15.2",
|
||||||
|
"@types/react": "^19.1.2",
|
||||||
|
"@types/react-dom": "^19.1.2",
|
||||||
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
"nodemon": "^3.1.10",
|
||||||
|
"vite": "^6.3.3",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/better-sqlite3": "^7.6.8",
|
||||||
|
"@types/uuid": "^9.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
app/public/vite.svg
Normal file
1
app/public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
922
app/src/client/App.css
Normal file
922
app/src/client/App.css
Normal file
|
|
@ -0,0 +1,922 @@
|
||||||
|
/* Base styles */
|
||||||
|
:root {
|
||||||
|
--primary: #0066cc;
|
||||||
|
--primary-hover: #0052a3;
|
||||||
|
--secondary: #6c757d;
|
||||||
|
--success: #28a745;
|
||||||
|
--danger: #dc3545;
|
||||||
|
--warning: #ffc107;
|
||||||
|
--info: #17a2b8;
|
||||||
|
--light: #f8f9fa;
|
||||||
|
--dark: #343a40;
|
||||||
|
--border: #dee2e6;
|
||||||
|
--bg-primary: #ffffff;
|
||||||
|
--bg-secondary: #f8f9fa;
|
||||||
|
--text-primary: #212529;
|
||||||
|
--text-secondary: #6c757d;
|
||||||
|
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.navbar {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-icon {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover,
|
||||||
|
.nav-link.active {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
border-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--secondary);
|
||||||
|
border-color: var(--secondary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background-color: var(--success);
|
||||||
|
border-color: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--danger);
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background-color: var(--warning);
|
||||||
|
border-color: var(--warning);
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover:not(:disabled) {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard */
|
||||||
|
.dashboard {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header p {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content h3 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-description {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sections {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-info h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-info p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tunnel Manager */
|
||||||
|
.tunnel-manager {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-indicator {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-indicator.online {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-indicator.offline {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinning {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnels-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-card {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-title h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-error {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background-color: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-active {
|
||||||
|
background-color: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-inactive {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badges */
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
background-color: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inactive {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form overlay */
|
||||||
|
.form-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-form {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Status */
|
||||||
|
.server-status {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-section {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-section h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-section {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-section h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logs */
|
||||||
|
.logs-section {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-select {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
background-color: var(--dark);
|
||||||
|
color: var(--light);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System info */
|
||||||
|
.system-info {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-info h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-header h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-content p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-detail {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes */
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-green-500 {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-500 {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-yellow-500 {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-blue-500 {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.navbar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sections {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-controls {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnels-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-modal {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/src/client/App.tsx
Normal file
51
app/src/client/App.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
import Navbar from './components/Navbar';
|
||||||
|
import Dashboard from './pages/Dashboard';
|
||||||
|
import TunnelManager from './pages/TunnelManager';
|
||||||
|
import ServerStatus from './pages/ServerStatus';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: 2,
|
||||||
|
staleTime: 5 * 1000, // 5 seconds
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Router>
|
||||||
|
<div className="App">
|
||||||
|
<Navbar />
|
||||||
|
<main className="main-content">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
<Route path="/tunnels" element={<TunnelManager />} />
|
||||||
|
<Route path="/status" element={<ServerStatus />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
<Toaster
|
||||||
|
position="top-right"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 4000,
|
||||||
|
style: {
|
||||||
|
background: '#363636',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
199
app/src/client/api/client.ts
Normal file
199
app/src/client/api/client.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface TunnelConfig {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
protocol: 'TCP' | 'UDP';
|
||||||
|
localIp: string;
|
||||||
|
localPort: number;
|
||||||
|
remotePort: number;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TunnelStatus {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
lastChecked: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrpcStatus {
|
||||||
|
running: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrpcLogs {
|
||||||
|
logs: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeStatus {
|
||||||
|
status: string;
|
||||||
|
timestamp: string;
|
||||||
|
uptime?: number;
|
||||||
|
memory?: {
|
||||||
|
used: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
cpu?: {
|
||||||
|
usage: number;
|
||||||
|
};
|
||||||
|
connection?: {
|
||||||
|
url: string;
|
||||||
|
isOnline: boolean;
|
||||||
|
lastConnectionTime: Date | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeConnection {
|
||||||
|
url: string;
|
||||||
|
isOnline: boolean;
|
||||||
|
lastConnectionTime: Date | null;
|
||||||
|
isReachable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor for logging
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('API Request Error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor for error handling
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
console.error('API Response Error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const tunnelApi = {
|
||||||
|
// Get all tunnels
|
||||||
|
getAllTunnels: async (): Promise<TunnelConfig[]> => {
|
||||||
|
const response = await apiClient.get('/tunnels');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get tunnel by ID
|
||||||
|
getTunnelById: async (id: string): Promise<TunnelConfig> => {
|
||||||
|
const response = await apiClient.get(`/tunnels/${id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create new tunnel
|
||||||
|
createTunnel: async (tunnel: Omit<TunnelConfig, 'id' | 'createdAt' | 'updatedAt'>): Promise<TunnelConfig> => {
|
||||||
|
const response = await apiClient.post('/tunnels', tunnel);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update tunnel
|
||||||
|
updateTunnel: async (id: string, tunnel: Partial<TunnelConfig>): Promise<TunnelConfig> => {
|
||||||
|
const response = await apiClient.put(`/tunnels/${id}`, tunnel);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete tunnel
|
||||||
|
deleteTunnel: async (id: string): Promise<void> => {
|
||||||
|
await apiClient.delete(`/tunnels/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get tunnel status
|
||||||
|
getTunnelStatus: async (id: string): Promise<TunnelStatus> => {
|
||||||
|
const response = await apiClient.get(`/tunnels/${id}/status`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get all tunnel statuses
|
||||||
|
getAllTunnelStatuses: async (): Promise<TunnelStatus[]> => {
|
||||||
|
const response = await apiClient.get('/tunnels-status');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const frpcApi = {
|
||||||
|
// Get frpc status
|
||||||
|
getStatus: async (): Promise<FrpcStatus> => {
|
||||||
|
const response = await apiClient.get('/frpc/status');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Control frpc service
|
||||||
|
start: async (): Promise<{ message: string }> => {
|
||||||
|
const response = await apiClient.post('/frpc/start');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: async (): Promise<{ message: string }> => {
|
||||||
|
const response = await apiClient.post('/frpc/stop');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
restart: async (): Promise<{ message: string }> => {
|
||||||
|
const response = await apiClient.post('/frpc/restart');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
regenerate: async (): Promise<{ message: string }> => {
|
||||||
|
const response = await apiClient.post('/frpc/regenerate');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get frpc logs
|
||||||
|
getLogs: async (lines: number = 50): Promise<FrpcLogs> => {
|
||||||
|
const response = await apiClient.get(`/frpc/logs?lines=${lines}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nodeApi = {
|
||||||
|
// Get node status
|
||||||
|
getStatus: async (): Promise<NodeStatus> => {
|
||||||
|
const response = await apiClient.get('/node/status');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get node connection info
|
||||||
|
getConnection: async (): Promise<NodeConnection> => {
|
||||||
|
const response = await apiClient.get('/node/connection');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Push configuration to node
|
||||||
|
pushConfig: async (): Promise<{ message: string; tunnelCount: number; nodeResponse: any }> => {
|
||||||
|
const response = await apiClient.post('/node/push-config');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Restart frpc on node
|
||||||
|
restartFrpc: async (): Promise<{ message: string; nodeResponse: any }> => {
|
||||||
|
const response = await apiClient.post('/node/restart-frpc');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Push config and restart frpc on node
|
||||||
|
pushAndRestart: async (): Promise<{
|
||||||
|
message: string;
|
||||||
|
tunnelCount: number;
|
||||||
|
configResponse: any;
|
||||||
|
restartResponse: any
|
||||||
|
}> => {
|
||||||
|
const response = await apiClient.post('/node/push-and-restart');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default apiClient;
|
||||||
1
app/src/client/assets/react.svg
Normal file
1
app/src/client/assets/react.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4 KiB |
43
app/src/client/components/Navbar.tsx
Normal file
43
app/src/client/components/Navbar.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { Server, Settings, Activity } from 'lucide-react';
|
||||||
|
|
||||||
|
const Navbar: React.FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isActive = (path: string) => location.pathname === path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="navbar">
|
||||||
|
<div className="navbar-brand">
|
||||||
|
<Server className="navbar-icon" />
|
||||||
|
<span>FRP Manager</span>
|
||||||
|
</div>
|
||||||
|
<div className="navbar-nav">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className={`nav-link ${isActive('/') ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
<Activity size={18} />
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/tunnels"
|
||||||
|
className={`nav-link ${isActive('/tunnels') ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
<Settings size={18} />
|
||||||
|
Tunnels
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/status"
|
||||||
|
className={`nav-link ${isActive('/status') ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
<Server size={18} />
|
||||||
|
Server Status
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
232
app/src/client/components/TunnelForm.tsx
Normal file
232
app/src/client/components/TunnelForm.tsx
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { tunnelApi, TunnelConfig } from '../api/client';
|
||||||
|
import { X, Save, Loader2 } from 'lucide-react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
interface TunnelFormProps {
|
||||||
|
tunnel?: TunnelConfig | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TunnelForm: React.FC<TunnelFormProps> = ({ tunnel, onClose, onSubmit }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const isEditing = Boolean(tunnel);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: tunnel?.name || '',
|
||||||
|
protocol: tunnel?.protocol || 'TCP' as 'TCP' | 'UDP',
|
||||||
|
localIp: tunnel?.localIp || '127.0.0.1',
|
||||||
|
localPort: tunnel?.localPort || 8080,
|
||||||
|
remotePort: tunnel?.remotePort || 8080,
|
||||||
|
enabled: tunnel?.enabled ?? true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: tunnelApi.createTunnel,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||||
|
toast.success('Tunnel created successfully');
|
||||||
|
onSubmit();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Create error:', error);
|
||||||
|
toast.error('Failed to create tunnel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ id, updates }: { id: string; updates: Partial<TunnelConfig> }) =>
|
||||||
|
tunnelApi.updateTunnel(id, updates),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||||
|
toast.success('Tunnel updated successfully');
|
||||||
|
onSubmit();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Update error:', error);
|
||||||
|
toast.error('Failed to update tunnel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
toast.error('Please enter a name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.localPort < 1 || formData.localPort > 65535) {
|
||||||
|
toast.error('Local port must be between 1 and 65535');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.remotePort < 1 || formData.remotePort > 65535) {
|
||||||
|
toast.error('Remote port must be between 1 and 65535');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing && tunnel) {
|
||||||
|
updateMutation.mutate({
|
||||||
|
id: tunnel.id!,
|
||||||
|
updates: formData,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createMutation.mutate(formData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value, type } = e.target;
|
||||||
|
|
||||||
|
if (type === 'checkbox') {
|
||||||
|
const checked = (e.target as HTMLInputElement).checked;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: checked,
|
||||||
|
}));
|
||||||
|
} else if (type === 'number') {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: parseInt(value) || 0,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-overlay">
|
||||||
|
<div className="form-modal">
|
||||||
|
<div className="form-header">
|
||||||
|
<h2>{isEditing ? 'Edit Tunnel' : 'Create New Tunnel'}</h2>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="tunnel-form">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="name">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="e.g., Minecraft Server"
|
||||||
|
required
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="protocol">Protocol</label>
|
||||||
|
<select
|
||||||
|
id="protocol"
|
||||||
|
name="protocol"
|
||||||
|
value={formData.protocol}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<option value="TCP">TCP</option>
|
||||||
|
<option value="UDP">UDP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="localIp">Local IP</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="localIp"
|
||||||
|
name="localIp"
|
||||||
|
value={formData.localIp}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="127.0.0.1"
|
||||||
|
required
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="localPort">Local Port</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="localPort"
|
||||||
|
name="localPort"
|
||||||
|
value={formData.localPort}
|
||||||
|
onChange={handleChange}
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
required
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="remotePort">Remote Port</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="remotePort"
|
||||||
|
name="remotePort"
|
||||||
|
value={formData.remotePort}
|
||||||
|
onChange={handleChange}
|
||||||
|
min="1"
|
||||||
|
max="65535"
|
||||||
|
required
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="enabled"
|
||||||
|
checked={formData.enabled}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
<span>Enable this tunnel</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending && <Loader2 className="animate-spin" size={16} />}
|
||||||
|
<Save size={16} />
|
||||||
|
{isEditing ? 'Update' : 'Create'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TunnelForm;
|
||||||
69
app/src/client/index.css
Normal file
69
app/src/client/index.css
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/src/client/main.tsx
Normal file
12
app/src/client/main.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
213
app/src/client/pages/Dashboard.tsx
Normal file
213
app/src/client/pages/Dashboard.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { tunnelApi, frpcApi, nodeApi } from '../api/client';
|
||||||
|
import { CheckCircle, XCircle, AlertCircle, RefreshCw, Wifi, WifiOff } from 'lucide-react';
|
||||||
|
|
||||||
|
const Dashboard: React.FC = () => {
|
||||||
|
const { data: tunnels, isLoading: tunnelsLoading } = useQuery({
|
||||||
|
queryKey: ['tunnels'],
|
||||||
|
queryFn: tunnelApi.getAllTunnels,
|
||||||
|
refetchInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: tunnelStatuses, isLoading: statusLoading } = useQuery({
|
||||||
|
queryKey: ['tunnel-statuses'],
|
||||||
|
queryFn: tunnelApi.getAllTunnelStatuses,
|
||||||
|
refetchInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: frpcStatus, isLoading: frpcLoading } = useQuery({
|
||||||
|
queryKey: ['frpc-status'],
|
||||||
|
queryFn: frpcApi.getStatus,
|
||||||
|
refetchInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: nodeConnection } = useQuery({
|
||||||
|
queryKey: ['node-connection'],
|
||||||
|
queryFn: nodeApi.getConnection,
|
||||||
|
refetchInterval: 30000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: nodeStatus } = useQuery({
|
||||||
|
queryKey: ['node-status'],
|
||||||
|
queryFn: nodeApi.getStatus,
|
||||||
|
refetchInterval: 30000,
|
||||||
|
retry: false,
|
||||||
|
enabled: !!nodeConnection?.isReachable,
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTunnels = tunnels?.filter(t => t.enabled) || [];
|
||||||
|
const activeTunnelStatuses = tunnelStatuses?.filter(s => s.active) || [];
|
||||||
|
|
||||||
|
if (tunnelsLoading || statusLoading || frpcLoading) {
|
||||||
|
return (
|
||||||
|
<div className="dashboard">
|
||||||
|
<div className="loading">
|
||||||
|
<RefreshCw className="animate-spin" size={24} />
|
||||||
|
<span>Loading dashboard...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard">
|
||||||
|
<div className="dashboard-header">
|
||||||
|
<h1>FRP Manager Dashboard</h1>
|
||||||
|
<p>Manage your tunnel configurations and monitor their status</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stats-grid">
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-icon">
|
||||||
|
<CheckCircle className="text-green-500" size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="stat-content">
|
||||||
|
<h3>Active Tunnels</h3>
|
||||||
|
<p className="stat-number">{activeTunnelStatuses.length}</p>
|
||||||
|
<p className="stat-description">Currently running</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-icon">
|
||||||
|
<AlertCircle className="text-yellow-500" size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="stat-content">
|
||||||
|
<h3>Total Tunnels</h3>
|
||||||
|
<p className="stat-number">{tunnels?.length || 0}</p>
|
||||||
|
<p className="stat-description">Configured tunnels</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-icon">
|
||||||
|
{frpcStatus?.running ? (
|
||||||
|
<CheckCircle className="text-green-500" size={24} />
|
||||||
|
) : (
|
||||||
|
<XCircle className="text-red-500" size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="stat-content">
|
||||||
|
<h3>FRPC Service</h3>
|
||||||
|
<p className="stat-number">{frpcStatus?.running ? 'Running' : 'Stopped'}</p>
|
||||||
|
<p className="stat-description">Service status</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-icon">
|
||||||
|
<CheckCircle className="text-blue-500" size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="stat-content">
|
||||||
|
<h3>Enabled Tunnels</h3>
|
||||||
|
<p className="stat-number">{activeTunnels.length}</p>
|
||||||
|
<p className="stat-description">Ready to connect</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-icon">
|
||||||
|
{nodeConnection?.isReachable ? (
|
||||||
|
<Wifi className="text-green-500" size={24} />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="text-red-500" size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="stat-content">
|
||||||
|
<h3>Node Status</h3>
|
||||||
|
<p className="stat-number">{nodeConnection?.isReachable ? 'Online' : 'Offline'}</p>
|
||||||
|
<p className="stat-description">Home server agent</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dashboard-sections">
|
||||||
|
<div className="section">
|
||||||
|
<h2>Recent Tunnels</h2>
|
||||||
|
<div className="tunnel-list">
|
||||||
|
{activeTunnels.slice(0, 5).map(tunnel => (
|
||||||
|
<div key={tunnel.id} className="tunnel-item">
|
||||||
|
<div className="tunnel-info">
|
||||||
|
<h4>{tunnel.name}</h4>
|
||||||
|
<p>{tunnel.protocol} • {tunnel.localIp}:{tunnel.localPort} → :{tunnel.remotePort}</p>
|
||||||
|
</div>
|
||||||
|
<div className="tunnel-status">
|
||||||
|
{tunnelStatuses?.find(s => s.id === tunnel.id)?.active ? (
|
||||||
|
<span className="status-badge status-active">Active</span>
|
||||||
|
) : (
|
||||||
|
<span className="status-badge status-inactive">Inactive</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{activeTunnels.length === 0 && (
|
||||||
|
<p className="empty-state">No active tunnels configured</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="section">
|
||||||
|
<h2>System Status</h2>
|
||||||
|
<div className="status-list">
|
||||||
|
<div className="status-item">
|
||||||
|
<div className="status-indicator">
|
||||||
|
{frpcStatus?.running ? (
|
||||||
|
<CheckCircle className="text-green-500" size={20} />
|
||||||
|
) : (
|
||||||
|
<XCircle className="text-red-500" size={20} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="status-content">
|
||||||
|
<h4>FRPC Service</h4>
|
||||||
|
<p>{frpcStatus?.running ? 'Service is running normally' : 'Service is stopped'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status-item">
|
||||||
|
<div className="status-indicator">
|
||||||
|
<CheckCircle className="text-green-500" size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="status-content">
|
||||||
|
<h4>API Server</h4>
|
||||||
|
<p>API is responding normally</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status-item">
|
||||||
|
<div className="status-indicator">
|
||||||
|
<CheckCircle className="text-green-500" size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="status-content">
|
||||||
|
<h4>Database</h4>
|
||||||
|
<p>Database connection is healthy</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status-item">
|
||||||
|
<div className="status-indicator">
|
||||||
|
{nodeConnection?.isReachable ? (
|
||||||
|
<Wifi className="text-green-500" size={20} />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="text-red-500" size={20} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="status-content">
|
||||||
|
<h4>Home Server Node</h4>
|
||||||
|
<p>
|
||||||
|
{nodeConnection?.isReachable
|
||||||
|
? `Connected • Last seen: ${nodeConnection.lastConnectionTime ? new Date(nodeConnection.lastConnectionTime).toLocaleTimeString() : 'Now'}`
|
||||||
|
: 'Disconnected or unreachable'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
253
app/src/client/pages/ServerStatus.tsx
Normal file
253
app/src/client/pages/ServerStatus.tsx
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { frpcApi } from '../api/client';
|
||||||
|
import { Server, Play, Square, RotateCcw, RefreshCw, FileText, Activity } from 'lucide-react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
const ServerStatus: React.FC = () => {
|
||||||
|
const [logsLines, setLogsLines] = useState(50);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: frpcStatus, isLoading: statusLoading } = useQuery({
|
||||||
|
queryKey: ['frpc-status'],
|
||||||
|
queryFn: frpcApi.getStatus,
|
||||||
|
refetchInterval: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: logs, isLoading: logsLoading } = useQuery({
|
||||||
|
queryKey: ['frpc-logs', logsLines],
|
||||||
|
queryFn: () => frpcApi.getLogs(logsLines),
|
||||||
|
refetchInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const startMutation = useMutation({
|
||||||
|
mutationFn: frpcApi.start,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
|
||||||
|
toast.success('FRPC service started');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Start error:', error);
|
||||||
|
toast.error('Failed to start FRPC service');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopMutation = useMutation({
|
||||||
|
mutationFn: frpcApi.stop,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
|
||||||
|
toast.success('FRPC service stopped');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Stop error:', error);
|
||||||
|
toast.error('Failed to stop FRPC service');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const restartMutation = useMutation({
|
||||||
|
mutationFn: frpcApi.restart,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
|
||||||
|
toast.success('FRPC service restarted');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Restart error:', error);
|
||||||
|
toast.error('Failed to restart FRPC service');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const regenerateMutation = useMutation({
|
||||||
|
mutationFn: frpcApi.regenerate,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
|
||||||
|
toast.success('FRPC configuration regenerated');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Regenerate error:', error);
|
||||||
|
toast.error('Failed to regenerate FRPC configuration');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleStart = () => startMutation.mutate();
|
||||||
|
const handleStop = () => stopMutation.mutate();
|
||||||
|
const handleRestart = () => restartMutation.mutate();
|
||||||
|
const handleRegenerate = () => regenerateMutation.mutate();
|
||||||
|
|
||||||
|
const isLoading = statusLoading || logsLoading;
|
||||||
|
const isAnyMutationPending =
|
||||||
|
startMutation.isPending ||
|
||||||
|
stopMutation.isPending ||
|
||||||
|
restartMutation.isPending ||
|
||||||
|
regenerateMutation.isPending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="server-status">
|
||||||
|
<div className="server-header">
|
||||||
|
<h1>Server Status</h1>
|
||||||
|
<div className="server-info">
|
||||||
|
<div className="status-indicator">
|
||||||
|
<Activity
|
||||||
|
size={24}
|
||||||
|
className={frpcStatus?.running ? 'text-green-500' : 'text-red-500'}
|
||||||
|
/>
|
||||||
|
<span className={`status-text ${frpcStatus?.running ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
{frpcStatus?.running ? 'Running' : 'Stopped'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="server-controls">
|
||||||
|
<div className="control-section">
|
||||||
|
<h2>Service Controls</h2>
|
||||||
|
<div className="control-buttons">
|
||||||
|
<button
|
||||||
|
className="btn btn-success"
|
||||||
|
onClick={handleStart}
|
||||||
|
disabled={isAnyMutationPending || frpcStatus?.running}
|
||||||
|
>
|
||||||
|
<Play size={18} />
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={handleStop}
|
||||||
|
disabled={isAnyMutationPending || !frpcStatus?.running}
|
||||||
|
>
|
||||||
|
<Square size={18} />
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-warning"
|
||||||
|
onClick={handleRestart}
|
||||||
|
disabled={isAnyMutationPending}
|
||||||
|
>
|
||||||
|
<RotateCcw size={18} />
|
||||||
|
Restart
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
disabled={isAnyMutationPending}
|
||||||
|
>
|
||||||
|
<RefreshCw size={18} />
|
||||||
|
Regenerate Config
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status-section">
|
||||||
|
<h2>Service Information</h2>
|
||||||
|
<div className="info-grid">
|
||||||
|
<div className="info-item">
|
||||||
|
<span className="info-label">Status:</span>
|
||||||
|
<span className={`info-value ${frpcStatus?.running ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
{frpcStatus?.running ? 'Running' : 'Stopped'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-item">
|
||||||
|
<span className="info-label">Container:</span>
|
||||||
|
<span className="info-value">frpc</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-item">
|
||||||
|
<span className="info-label">Last Updated:</span>
|
||||||
|
<span className="info-value">
|
||||||
|
{new Date().toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="logs-section">
|
||||||
|
<div className="logs-header">
|
||||||
|
<h2>
|
||||||
|
<FileText size={20} />
|
||||||
|
Service Logs
|
||||||
|
</h2>
|
||||||
|
<div className="logs-controls">
|
||||||
|
<select
|
||||||
|
value={logsLines}
|
||||||
|
onChange={(e) => setLogsLines(parseInt(e.target.value))}
|
||||||
|
className="logs-select"
|
||||||
|
>
|
||||||
|
<option value={25}>Last 25 lines</option>
|
||||||
|
<option value={50}>Last 50 lines</option>
|
||||||
|
<option value={100}>Last 100 lines</option>
|
||||||
|
<option value={200}>Last 200 lines</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-secondary"
|
||||||
|
onClick={() => queryClient.invalidateQueries({ queryKey: ['frpc-logs'] })}
|
||||||
|
disabled={logsLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="logs-container">
|
||||||
|
{logsLoading ? (
|
||||||
|
<div className="logs-loading">
|
||||||
|
<RefreshCw className="animate-spin" size={20} />
|
||||||
|
<span>Loading logs...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre className="logs-content">
|
||||||
|
{logs?.logs || 'No logs available'}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="system-info">
|
||||||
|
<h2>System Information</h2>
|
||||||
|
<div className="info-cards">
|
||||||
|
<div className="info-card">
|
||||||
|
<div className="info-card-header">
|
||||||
|
<Server size={24} />
|
||||||
|
<h3>FRPC Service</h3>
|
||||||
|
</div>
|
||||||
|
<div className="info-card-content">
|
||||||
|
<p>Fast Reverse Proxy Client for tunneling services</p>
|
||||||
|
<p className="info-detail">
|
||||||
|
Status: <span className={frpcStatus?.running ? 'text-green-500' : 'text-red-500'}>
|
||||||
|
{frpcStatus?.running ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="info-card">
|
||||||
|
<div className="info-card-header">
|
||||||
|
<Activity size={24} />
|
||||||
|
<h3>API Server</h3>
|
||||||
|
</div>
|
||||||
|
<div className="info-card-content">
|
||||||
|
<p>RESTful API for managing tunnel configurations</p>
|
||||||
|
<p className="info-detail">
|
||||||
|
Status: <span className="text-green-500">Running</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="info-card">
|
||||||
|
<div className="info-card-header">
|
||||||
|
<FileText size={24} />
|
||||||
|
<h3>Configuration</h3>
|
||||||
|
</div>
|
||||||
|
<div className="info-card-content">
|
||||||
|
<p>Tunnel configurations stored in SQLite database</p>
|
||||||
|
<p className="info-detail">
|
||||||
|
Auto-generated FRPC config from active tunnels
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerStatus;
|
||||||
244
app/src/client/pages/TunnelManager.tsx
Normal file
244
app/src/client/pages/TunnelManager.tsx
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { tunnelApi, nodeApi, TunnelConfig } from '../api/client';
|
||||||
|
import { Plus, Edit2, Trash2, Power, PowerOff, Settings, Send, RefreshCw } from 'lucide-react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import TunnelForm from '../components/TunnelForm';
|
||||||
|
|
||||||
|
const TunnelManager: React.FC = () => {
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editingTunnel, setEditingTunnel] = useState<TunnelConfig | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { data: tunnels, isLoading } = useQuery({
|
||||||
|
queryKey: ['tunnels'],
|
||||||
|
queryFn: tunnelApi.getAllTunnels,
|
||||||
|
refetchInterval: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: tunnelStatuses } = useQuery({
|
||||||
|
queryKey: ['tunnel-statuses'],
|
||||||
|
queryFn: tunnelApi.getAllTunnelStatuses,
|
||||||
|
refetchInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: nodeConnection } = useQuery({
|
||||||
|
queryKey: ['node-connection'],
|
||||||
|
queryFn: nodeApi.getConnection,
|
||||||
|
refetchInterval: 30000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: tunnelApi.deleteTunnel,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||||
|
toast.success('Tunnel deleted successfully');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Delete error:', error);
|
||||||
|
toast.error('Failed to delete tunnel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ id, updates }: { id: string; updates: Partial<TunnelConfig> }) =>
|
||||||
|
tunnelApi.updateTunnel(id, updates),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||||
|
toast.success('Tunnel updated successfully');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Update error:', error);
|
||||||
|
toast.error('Failed to update tunnel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pushToNodeMutation = useMutation({
|
||||||
|
mutationFn: nodeApi.pushAndRestart,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast.success(`Successfully pushed ${data.tunnelCount} tunnels to node and restarted frpc`);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['node-connection'] });
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
console.error('Push to node error:', error);
|
||||||
|
toast.error(error.response?.data?.error || 'Failed to push configuration to node');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
if (window.confirm('Are you sure you want to delete this tunnel?')) {
|
||||||
|
deleteMutation.mutate(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (tunnel: TunnelConfig) => {
|
||||||
|
setEditingTunnel(tunnel);
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleEnabled = (tunnel: TunnelConfig) => {
|
||||||
|
updateMutation.mutate({
|
||||||
|
id: tunnel.id!,
|
||||||
|
updates: { enabled: !tunnel.enabled },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormClose = () => {
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingTunnel(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = () => {
|
||||||
|
handleFormClose();
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePushToNode = () => {
|
||||||
|
if (window.confirm('Push current tunnel configuration to node and restart frpc?')) {
|
||||||
|
pushToNodeMutation.mutate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="tunnel-manager">
|
||||||
|
<div className="loading">Loading tunnels...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tunnel-manager">
|
||||||
|
<div className="tunnel-header">
|
||||||
|
<h1>Tunnel Manager</h1>
|
||||||
|
<div className="header-actions">
|
||||||
|
{nodeConnection && (
|
||||||
|
<div className="node-status">
|
||||||
|
<span className={`node-indicator ${nodeConnection.isReachable ? 'online' : 'offline'}`}>
|
||||||
|
Node: {nodeConnection.isReachable ? 'Online' : 'Offline'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handlePushToNode}
|
||||||
|
disabled={pushToNodeMutation.isPending || !nodeConnection.isReachable}
|
||||||
|
title="Push configuration to node and restart frpc"
|
||||||
|
>
|
||||||
|
{pushToNodeMutation.isPending ? (
|
||||||
|
<RefreshCw size={18} className="spinning" />
|
||||||
|
) : (
|
||||||
|
<Send size={18} />
|
||||||
|
)}
|
||||||
|
Push to Node
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
>
|
||||||
|
<Plus size={18} />
|
||||||
|
Add Tunnel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tunnels-grid">
|
||||||
|
{tunnels?.map(tunnel => {
|
||||||
|
const status = tunnelStatuses?.find(s => s.id === tunnel.id);
|
||||||
|
return (
|
||||||
|
<div key={tunnel.id} className="tunnel-card">
|
||||||
|
<div className="tunnel-card-header">
|
||||||
|
<div className="tunnel-title">
|
||||||
|
<h3>{tunnel.name}</h3>
|
||||||
|
<div className="tunnel-badges">
|
||||||
|
<span className={`badge ${tunnel.enabled ? 'badge-success' : 'badge-secondary'}`}>
|
||||||
|
{tunnel.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</span>
|
||||||
|
<span className={`badge ${status?.active ? 'badge-active' : 'badge-inactive'}`}>
|
||||||
|
{status?.active ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="tunnel-actions">
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${tunnel.enabled ? 'btn-warning' : 'btn-success'}`}
|
||||||
|
onClick={() => handleToggleEnabled(tunnel)}
|
||||||
|
disabled={updateMutation.isPending}
|
||||||
|
>
|
||||||
|
{tunnel.enabled ? <PowerOff size={16} /> : <Power size={16} />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-secondary"
|
||||||
|
onClick={() => handleEdit(tunnel)}
|
||||||
|
>
|
||||||
|
<Edit2 size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-danger"
|
||||||
|
onClick={() => handleDelete(tunnel.id!)}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tunnel-info">
|
||||||
|
<div className="info-row">
|
||||||
|
<span className="info-label">Protocol:</span>
|
||||||
|
<span className="info-value">{tunnel.protocol}</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-row">
|
||||||
|
<span className="info-label">Local:</span>
|
||||||
|
<span className="info-value">{tunnel.localIp}:{tunnel.localPort}</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-row">
|
||||||
|
<span className="info-label">Remote:</span>
|
||||||
|
<span className="info-value">:{tunnel.remotePort}</span>
|
||||||
|
</div>
|
||||||
|
<div className="info-row">
|
||||||
|
<span className="info-label">Created:</span>
|
||||||
|
<span className="info-value">
|
||||||
|
{tunnel.createdAt ? new Date(tunnel.createdAt).toLocaleDateString() : 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status?.error && (
|
||||||
|
<div className="tunnel-error">
|
||||||
|
<span className="error-text">Error: {status.error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tunnels?.length === 0 && (
|
||||||
|
<div className="empty-state">
|
||||||
|
<Settings size={64} className="empty-icon" />
|
||||||
|
<h3>No tunnels configured</h3>
|
||||||
|
<p>Get started by adding your first tunnel configuration</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowForm(true)}
|
||||||
|
>
|
||||||
|
<Plus size={18} />
|
||||||
|
Add Your First Tunnel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showForm && (
|
||||||
|
<TunnelForm
|
||||||
|
tunnel={editingTunnel}
|
||||||
|
onClose={handleFormClose}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TunnelManager;
|
||||||
8
app/src/client/tsconfig.json
Normal file
8
app/src/client/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
app/src/client/vite-env.d.ts
vendored
Normal file
1
app/src/client/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
179
app/src/server/database.ts
Normal file
179
app/src/server/database.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { TunnelConfig, TunnelConfigSchema, TunnelConfigUpdate } from './types.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export class TunnelDatabase {
|
||||||
|
private db: Database.Database;
|
||||||
|
|
||||||
|
constructor(dbPath: string = 'data/tunnels.db') {
|
||||||
|
this.db = new Database(dbPath);
|
||||||
|
this.initializeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeDatabase() {
|
||||||
|
// Create tunnels table
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS tunnels (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
protocol TEXT NOT NULL CHECK(protocol IN ('TCP', 'UDP')),
|
||||||
|
local_ip TEXT NOT NULL,
|
||||||
|
local_port INTEGER NOT NULL,
|
||||||
|
remote_port INTEGER NOT NULL,
|
||||||
|
enabled BOOLEAN DEFAULT 1,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create trigger to update updated_at column
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TRIGGER IF NOT EXISTS update_tunnels_updated_at
|
||||||
|
AFTER UPDATE ON tunnels
|
||||||
|
BEGIN
|
||||||
|
UPDATE tunnels SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||||
|
END
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all tunnel configurations
|
||||||
|
getAllTunnels(): TunnelConfig[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
protocol,
|
||||||
|
local_ip as localIp,
|
||||||
|
local_port as localPort,
|
||||||
|
remote_port as remotePort,
|
||||||
|
enabled,
|
||||||
|
created_at as createdAt,
|
||||||
|
updated_at as updatedAt
|
||||||
|
FROM tunnels
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
return stmt.all() as TunnelConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tunnel by ID
|
||||||
|
getTunnelById(id: string): TunnelConfig | null {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
protocol,
|
||||||
|
local_ip as localIp,
|
||||||
|
local_port as localPort,
|
||||||
|
remote_port as remotePort,
|
||||||
|
enabled,
|
||||||
|
created_at as createdAt,
|
||||||
|
updated_at as updatedAt
|
||||||
|
FROM tunnels
|
||||||
|
WHERE id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
const result = stmt.get(id) as TunnelConfig | undefined;
|
||||||
|
return result || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new tunnel configuration
|
||||||
|
createTunnel(config: Omit<TunnelConfig, 'id' | 'createdAt' | 'updatedAt'>): TunnelConfig {
|
||||||
|
const id = uuidv4();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
INSERT INTO tunnels (id, name, protocol, local_ip, local_port, remote_port, enabled)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run(id, config.name, config.protocol, config.localIp, config.localPort, config.remotePort, config.enabled);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...config,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tunnel configuration
|
||||||
|
updateTunnel(id: string, updates: Partial<TunnelConfig>): TunnelConfig | null {
|
||||||
|
const existing = this.getTunnelById(id);
|
||||||
|
if (!existing) return null;
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
if (updates.name !== undefined) {
|
||||||
|
fields.push('name = ?');
|
||||||
|
values.push(updates.name);
|
||||||
|
}
|
||||||
|
if (updates.protocol !== undefined) {
|
||||||
|
fields.push('protocol = ?');
|
||||||
|
values.push(updates.protocol);
|
||||||
|
}
|
||||||
|
if (updates.localIp !== undefined) {
|
||||||
|
fields.push('local_ip = ?');
|
||||||
|
values.push(updates.localIp);
|
||||||
|
}
|
||||||
|
if (updates.localPort !== undefined) {
|
||||||
|
fields.push('local_port = ?');
|
||||||
|
values.push(updates.localPort);
|
||||||
|
}
|
||||||
|
if (updates.remotePort !== undefined) {
|
||||||
|
fields.push('remote_port = ?');
|
||||||
|
values.push(updates.remotePort);
|
||||||
|
}
|
||||||
|
if (updates.enabled !== undefined) {
|
||||||
|
fields.push('enabled = ?');
|
||||||
|
values.push(updates.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.length === 0) return existing;
|
||||||
|
|
||||||
|
values.push(id);
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
UPDATE tunnels
|
||||||
|
SET ${fields.join(', ')}
|
||||||
|
WHERE id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run(...values);
|
||||||
|
return this.getTunnelById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete tunnel configuration
|
||||||
|
deleteTunnel(id: string): boolean {
|
||||||
|
const stmt = this.db.prepare('DELETE FROM tunnels WHERE id = ?');
|
||||||
|
const result = stmt.run(id);
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get enabled tunnels only
|
||||||
|
getEnabledTunnels(): TunnelConfig[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
protocol,
|
||||||
|
local_ip as localIp,
|
||||||
|
local_port as localPort,
|
||||||
|
remote_port as remotePort,
|
||||||
|
enabled,
|
||||||
|
created_at as createdAt,
|
||||||
|
updated_at as updatedAt
|
||||||
|
FROM tunnels
|
||||||
|
WHERE enabled = 1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
return stmt.all() as TunnelConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close database connection
|
||||||
|
close() {
|
||||||
|
this.db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
145
app/src/server/frpc-manager.ts
Normal file
145
app/src/server/frpc-manager.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { TunnelConfig, FrpcConfig } from './types.js';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
export class FrpcManager {
|
||||||
|
private configPath: string;
|
||||||
|
private containerName: string;
|
||||||
|
|
||||||
|
constructor(configPath: string = 'data/frpc.toml', containerName: string = 'frpc') {
|
||||||
|
this.configPath = configPath;
|
||||||
|
this.containerName = containerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate frpc.toml configuration from tunnel configs
|
||||||
|
async generateConfig(tunnels: TunnelConfig[], serverAddr: string, serverPort: number = 7000, token?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const config: FrpcConfig = {
|
||||||
|
serverAddr,
|
||||||
|
serverPort,
|
||||||
|
token,
|
||||||
|
proxies: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add enabled tunnels to config
|
||||||
|
for (const tunnel of tunnels.filter(t => t.enabled)) {
|
||||||
|
config.proxies[tunnel.name] = {
|
||||||
|
type: tunnel.protocol.toLowerCase() as 'tcp' | 'udp',
|
||||||
|
localIP: tunnel.localIp,
|
||||||
|
localPort: tunnel.localPort,
|
||||||
|
remotePort: tunnel.remotePort
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const tomlContent = this.generateTomlContent(config);
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
const dir = path.dirname(this.configPath);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
|
await fs.writeFile(this.configPath, tomlContent);
|
||||||
|
logger.info(`Generated frpc configuration with ${Object.keys(config.proxies).length} tunnels`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to generate frpc configuration:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateTomlContent(config: FrpcConfig): string {
|
||||||
|
let toml = `[common]
|
||||||
|
server_addr = "${config.serverAddr}"
|
||||||
|
server_port = ${config.serverPort}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (config.token) {
|
||||||
|
toml += `token = "${config.token}"\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toml += '\n';
|
||||||
|
|
||||||
|
// Add proxy configurations
|
||||||
|
for (const [name, proxy] of Object.entries(config.proxies)) {
|
||||||
|
toml += `[${name}]
|
||||||
|
type = "${proxy.type}"
|
||||||
|
local_ip = "${proxy.localIP}"
|
||||||
|
local_port = ${proxy.localPort}
|
||||||
|
remote_port = ${proxy.remotePort}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if frpc container is running
|
||||||
|
async isRunning(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(`docker ps --filter "name=${this.containerName}" --format "{{.Names}}"`);
|
||||||
|
return stdout.trim() === this.containerName;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to check frpc container status:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start frpc container
|
||||||
|
async start(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await execAsync(`docker start ${this.containerName}`);
|
||||||
|
logger.info(`Started frpc container: ${this.containerName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to start frpc container:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop frpc container
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await execAsync(`docker stop ${this.containerName}`);
|
||||||
|
logger.info(`Stopped frpc container: ${this.containerName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to stop frpc container:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart frpc container
|
||||||
|
async restart(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await execAsync(`docker restart ${this.containerName}`);
|
||||||
|
logger.info(`Restarted frpc container: ${this.containerName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to restart frpc container:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get frpc container logs
|
||||||
|
async getLogs(lines: number = 50): Promise<string> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(`docker logs --tail ${lines} ${this.containerName}`);
|
||||||
|
return stdout;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get frpc container logs:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tunnel status by attempting to connect
|
||||||
|
async checkTunnelStatus(tunnel: TunnelConfig): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// This is a basic implementation - you might want to implement actual connectivity checks
|
||||||
|
// For now, we'll just check if the container is running
|
||||||
|
return await this.isRunning();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to check tunnel status for ${tunnel.name}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/src/server/logger.ts
Normal file
42
app/src/server/logger.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import winston from 'winston';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Create logs directory if it doesn't exist
|
||||||
|
const logDir = 'logs';
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.errors({ stack: true }),
|
||||||
|
winston.format.json()
|
||||||
|
),
|
||||||
|
defaultMeta: { service: 'frpc-manager' },
|
||||||
|
transports: [
|
||||||
|
// Write all logs with level 'error' and below to error.log
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logDir, 'error.log'),
|
||||||
|
level: 'error',
|
||||||
|
handleExceptions: true,
|
||||||
|
handleRejections: true
|
||||||
|
}),
|
||||||
|
// Write all logs with level 'info' and below to combined.log
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logDir, 'combined.log'),
|
||||||
|
handleExceptions: true,
|
||||||
|
handleRejections: true
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we're not in production, also log to console with simple format
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
logger.add(new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.simple()
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default logger;
|
||||||
55
app/src/server/main.ts
Normal file
55
app/src/server/main.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import express from "express";
|
||||||
|
import ViteExpress from "vite-express";
|
||||||
|
import cors from "cors";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
import routes from "./routes.js";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Get __dirname in ES modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// Logging middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
logger.info(`${req.method} ${req.path}`, {
|
||||||
|
method: req.method,
|
||||||
|
path: req.path,
|
||||||
|
userAgent: req.get('User-Agent'),
|
||||||
|
ip: req.ip
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
app.use("/api", routes);
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get("/health", (_, res) => {
|
||||||
|
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legacy hello endpoint
|
||||||
|
app.get("/hello", (_, res) => {
|
||||||
|
res.send("Hello Vite + React + TypeScript!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.error('Unhandled error:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.PORT || '3000');
|
||||||
|
|
||||||
|
ViteExpress.listen(app, PORT, () => {
|
||||||
|
logger.info(`Server is listening on port ${PORT}`);
|
||||||
|
console.log(`Server is listening on port ${PORT}...`);
|
||||||
|
});
|
||||||
114
app/src/server/node-client.ts
Normal file
114
app/src/server/node-client.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
|
export interface NodeStatus {
|
||||||
|
status: string;
|
||||||
|
timestamp: string;
|
||||||
|
uptime?: number;
|
||||||
|
memory?: {
|
||||||
|
used: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
cpu?: {
|
||||||
|
usage: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeConfig {
|
||||||
|
url: string;
|
||||||
|
token: string;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodeClient {
|
||||||
|
private config: NodeConfig;
|
||||||
|
private lastConnectionTime: Date | null = null;
|
||||||
|
private isOnline: boolean = false;
|
||||||
|
|
||||||
|
constructor(config: NodeConfig) {
|
||||||
|
this.config = {
|
||||||
|
...config,
|
||||||
|
timeout: config.timeout || 5000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async makeRequest<T>(
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||||
|
endpoint: string,
|
||||||
|
data?: any
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<T> = await axios({
|
||||||
|
method,
|
||||||
|
url: `${this.config.url}${endpoint}`,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.config.token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
timeout: this.config.timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastConnectionTime = new Date();
|
||||||
|
this.isOnline = true;
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.isOnline = false;
|
||||||
|
logger.error(`Node request failed: ${method} ${endpoint}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get node status
|
||||||
|
async getStatus(): Promise<NodeStatus> {
|
||||||
|
return this.makeRequest<NodeStatus>('GET', '/api/status');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send updated frpc.toml config to the node
|
||||||
|
async updateConfig(config: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
return this.makeRequest<{ success: boolean; message: string }>('POST', '/api/frpc/update-config', {
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the FRP client on the node
|
||||||
|
async restartFrpc(): Promise<{ success: boolean; message: string }> {
|
||||||
|
return this.makeRequest<{ success: boolean; message: string }>('POST', '/api/frpc/restart');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if node is reachable
|
||||||
|
async isReachable(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.makeRequest<any>('GET', '/health');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get connection info
|
||||||
|
getConnectionInfo() {
|
||||||
|
return {
|
||||||
|
url: this.config.url,
|
||||||
|
isOnline: this.isOnline,
|
||||||
|
lastConnectionTime: this.lastConnectionTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory function to create node client with environment variables
|
||||||
|
export function createNodeClient(): NodeClient {
|
||||||
|
const nodeUrl = process.env.NODE_URL;
|
||||||
|
const nodeToken = process.env.NODE_TOKEN;
|
||||||
|
|
||||||
|
if (!nodeUrl || !nodeToken) {
|
||||||
|
throw new Error('NODE_URL and NODE_TOKEN environment variables are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NodeClient({
|
||||||
|
url: nodeUrl,
|
||||||
|
token: nodeToken,
|
||||||
|
timeout: parseInt(process.env.NODE_TIMEOUT || '5000'),
|
||||||
|
});
|
||||||
|
}
|
||||||
376
app/src/server/routes.ts
Normal file
376
app/src/server/routes.ts
Normal file
|
|
@ -0,0 +1,376 @@
|
||||||
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
|
import { TunnelDatabase } from './database.js';
|
||||||
|
import { FrpcManager } from './frpc-manager.js';
|
||||||
|
import { createNodeClient } from './node-client.js';
|
||||||
|
import { TunnelConfigSchema, TunnelConfigUpdateSchema, TunnelConfig, TunnelStatus } from './types.js';
|
||||||
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
const db = new TunnelDatabase();
|
||||||
|
const frpcManager = new FrpcManager();
|
||||||
|
|
||||||
|
// Initialize node client if configured
|
||||||
|
let nodeClient: ReturnType<typeof createNodeClient> | null = null;
|
||||||
|
try {
|
||||||
|
nodeClient = createNodeClient();
|
||||||
|
logger.info('Node client initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Node client not configured:', error instanceof Error ? error.message : 'Unknown error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async handler wrapper
|
||||||
|
const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) => {
|
||||||
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
Promise.resolve(fn(req, res, next)).catch(next);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all tunnels
|
||||||
|
router.get('/tunnels', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const tunnels = db.getAllTunnels();
|
||||||
|
res.json(tunnels);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get tunnel by ID
|
||||||
|
router.get('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const tunnel = db.getTunnelById(req.params.id);
|
||||||
|
if (!tunnel) {
|
||||||
|
return res.status(404).json({ error: 'Tunnel not found' });
|
||||||
|
}
|
||||||
|
res.json(tunnel);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create new tunnel
|
||||||
|
router.post('/tunnels', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const validation = TunnelConfigSchema.safeParse(req.body);
|
||||||
|
if (!validation.success) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid tunnel configuration',
|
||||||
|
details: validation.error.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tunnel = db.createTunnel(validation.data);
|
||||||
|
|
||||||
|
// Regenerate frpc config if tunnel is enabled
|
||||||
|
if (tunnel.enabled) {
|
||||||
|
await regenerateFrpcConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(tunnel);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update tunnel
|
||||||
|
router.put('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const validation = TunnelConfigUpdateSchema.safeParse({
|
||||||
|
...req.body,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid tunnel configuration',
|
||||||
|
details: validation.error.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tunnel = db.updateTunnel(req.params.id, validation.data);
|
||||||
|
if (!tunnel) {
|
||||||
|
return res.status(404).json({ error: 'Tunnel not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate frpc config
|
||||||
|
await regenerateFrpcConfig();
|
||||||
|
|
||||||
|
res.json(tunnel);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Delete tunnel
|
||||||
|
router.delete('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const deleted = db.deleteTunnel(req.params.id);
|
||||||
|
if (!deleted) {
|
||||||
|
return res.status(404).json({ error: 'Tunnel not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate frpc config
|
||||||
|
await regenerateFrpcConfig();
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get tunnel status
|
||||||
|
router.get('/tunnels/:id/status', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const tunnel = db.getTunnelById(req.params.id);
|
||||||
|
if (!tunnel) {
|
||||||
|
return res.status(404).json({ error: 'Tunnel not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const active = await frpcManager.checkTunnelStatus(tunnel);
|
||||||
|
const status: TunnelStatus = {
|
||||||
|
id: tunnel.id!,
|
||||||
|
name: tunnel.name,
|
||||||
|
active,
|
||||||
|
lastChecked: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(status);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get all tunnel statuses
|
||||||
|
router.get('/tunnels-status', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const tunnels = db.getAllTunnels();
|
||||||
|
const statuses: TunnelStatus[] = [];
|
||||||
|
|
||||||
|
for (const tunnel of tunnels) {
|
||||||
|
try {
|
||||||
|
const active = await frpcManager.checkTunnelStatus(tunnel);
|
||||||
|
statuses.push({
|
||||||
|
id: tunnel.id!,
|
||||||
|
name: tunnel.name,
|
||||||
|
active,
|
||||||
|
lastChecked: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
statuses.push({
|
||||||
|
id: tunnel.id!,
|
||||||
|
name: tunnel.name,
|
||||||
|
active: false,
|
||||||
|
lastChecked: new Date().toISOString(),
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(statuses);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Control frpc service
|
||||||
|
router.post('/frpc/:action', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const { action } = req.params;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'start':
|
||||||
|
await frpcManager.start();
|
||||||
|
break;
|
||||||
|
case 'stop':
|
||||||
|
await frpcManager.stop();
|
||||||
|
break;
|
||||||
|
case 'restart':
|
||||||
|
await frpcManager.restart();
|
||||||
|
break;
|
||||||
|
case 'regenerate':
|
||||||
|
await regenerateFrpcConfig();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return res.status(400).json({ error: 'Invalid action' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: `frpc ${action} completed successfully` });
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get frpc status
|
||||||
|
router.get('/frpc/status', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const running = await frpcManager.isRunning();
|
||||||
|
res.json({ running });
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get frpc logs
|
||||||
|
router.get('/frpc/logs', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const lines = parseInt(req.query.lines as string) || 50;
|
||||||
|
const logs = await frpcManager.getLogs(lines);
|
||||||
|
res.json({ logs });
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Helper function to regenerate frpc config and restart if needed
|
||||||
|
async function regenerateFrpcConfig() {
|
||||||
|
const enabledTunnels = db.getEnabledTunnels();
|
||||||
|
|
||||||
|
// Get server configuration from environment variables
|
||||||
|
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
|
||||||
|
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
|
||||||
|
const token = process.env.FRPC_TOKEN;
|
||||||
|
|
||||||
|
await frpcManager.generateConfig(enabledTunnels, serverAddr, serverPort, token);
|
||||||
|
|
||||||
|
// Restart frpc if it's running
|
||||||
|
if (await frpcManager.isRunning()) {
|
||||||
|
await frpcManager.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node management endpoints
|
||||||
|
router.get('/node/status', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
if (!nodeClient) {
|
||||||
|
return res.status(503).json({ error: 'Node client not configured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const status = await nodeClient.getStatus();
|
||||||
|
const connectionInfo = nodeClient.getConnectionInfo();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
...status,
|
||||||
|
connection: connectionInfo
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get node status:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to connect to node',
|
||||||
|
connection: nodeClient.getConnectionInfo()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/node/connection', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
if (!nodeClient) {
|
||||||
|
return res.status(503).json({ error: 'Node client not configured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionInfo = nodeClient.getConnectionInfo();
|
||||||
|
const isReachable = await nodeClient.isReachable();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
...connectionInfo,
|
||||||
|
isReachable
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/node/push-config', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
if (!nodeClient) {
|
||||||
|
return res.status(503).json({ error: 'Node client not configured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Regenerate frpc config
|
||||||
|
await regenerateFrpcConfig();
|
||||||
|
|
||||||
|
// Read the generated config
|
||||||
|
const enabledTunnels = db.getEnabledTunnels();
|
||||||
|
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
|
||||||
|
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
|
||||||
|
const token = process.env.FRPC_TOKEN;
|
||||||
|
|
||||||
|
// Generate config content
|
||||||
|
let tomlContent = `[common]
|
||||||
|
server_addr = "${serverAddr}"
|
||||||
|
server_port = ${serverPort}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
tomlContent += `token = "${token}"\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tomlContent += '\n';
|
||||||
|
|
||||||
|
// Add proxy configurations
|
||||||
|
for (const tunnel of enabledTunnels) {
|
||||||
|
tomlContent += `[${tunnel.name}]
|
||||||
|
type = "${tunnel.protocol.toLowerCase()}"
|
||||||
|
local_ip = "${tunnel.localIp}"
|
||||||
|
local_port = ${tunnel.localPort}
|
||||||
|
remote_port = ${tunnel.remotePort}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send config to node
|
||||||
|
const result = await nodeClient.updateConfig(tomlContent);
|
||||||
|
|
||||||
|
logger.info('Configuration pushed to node successfully');
|
||||||
|
res.json({
|
||||||
|
message: 'Configuration pushed to node successfully',
|
||||||
|
tunnelCount: enabledTunnels.length,
|
||||||
|
nodeResponse: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to push config to node:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to push configuration to node',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/node/restart-frpc', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
if (!nodeClient) {
|
||||||
|
return res.status(503).json({ error: 'Node client not configured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await nodeClient.restartFrpc();
|
||||||
|
|
||||||
|
logger.info('frpc restarted on node successfully');
|
||||||
|
res.json({
|
||||||
|
message: 'frpc restarted on node successfully',
|
||||||
|
nodeResponse: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to restart frpc on node:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to restart frpc on node',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/node/push-and-restart', asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
if (!nodeClient) {
|
||||||
|
return res.status(503).json({ error: 'Node client not configured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First push the config
|
||||||
|
await regenerateFrpcConfig();
|
||||||
|
|
||||||
|
// Read the generated config
|
||||||
|
const enabledTunnels = db.getEnabledTunnels();
|
||||||
|
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
|
||||||
|
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
|
||||||
|
const token = process.env.FRPC_TOKEN;
|
||||||
|
|
||||||
|
// Generate config content
|
||||||
|
let tomlContent = `[common]
|
||||||
|
server_addr = "${serverAddr}"
|
||||||
|
server_port = ${serverPort}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
tomlContent += `token = "${token}"\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tomlContent += '\n';
|
||||||
|
|
||||||
|
// Add proxy configurations
|
||||||
|
for (const tunnel of enabledTunnels) {
|
||||||
|
tomlContent += `[${tunnel.name}]
|
||||||
|
type = "${tunnel.protocol.toLowerCase()}"
|
||||||
|
local_ip = "${tunnel.localIp}"
|
||||||
|
local_port = ${tunnel.localPort}
|
||||||
|
remote_port = ${tunnel.remotePort}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send config to node and restart in one call
|
||||||
|
const result = await nodeClient.updateConfig(tomlContent);
|
||||||
|
const restartResult = await nodeClient.restartFrpc();
|
||||||
|
|
||||||
|
logger.info('Configuration pushed and frpc restarted on node successfully');
|
||||||
|
res.json({
|
||||||
|
message: 'Configuration pushed and frpc restarted on node successfully',
|
||||||
|
tunnelCount: enabledTunnels.length,
|
||||||
|
configResponse: result,
|
||||||
|
restartResponse: restartResult
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to push config and restart frpc on node:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Failed to push configuration and restart frpc on node',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default router;
|
||||||
43
app/src/server/types.ts
Normal file
43
app/src/server/types.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Zod schemas for validation
|
||||||
|
export const TunnelConfigSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string().min(1, 'Name is required'),
|
||||||
|
protocol: z.enum(['TCP', 'UDP']),
|
||||||
|
localIp: z.string().min(1, 'Local IP is required'),
|
||||||
|
localPort: z.number().int().min(1).max(65535),
|
||||||
|
remotePort: z.number().int().min(1).max(65535),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
createdAt: z.string().optional(),
|
||||||
|
updatedAt: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TunnelConfigUpdateSchema = TunnelConfigSchema.partial().extend({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TunnelConfig = z.infer<typeof TunnelConfigSchema>;
|
||||||
|
export type TunnelConfigUpdate = z.infer<typeof TunnelConfigUpdateSchema>;
|
||||||
|
|
||||||
|
export interface TunnelStatus {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
lastChecked: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrpcConfig {
|
||||||
|
serverAddr: string;
|
||||||
|
serverPort: number;
|
||||||
|
token?: string;
|
||||||
|
proxies: {
|
||||||
|
[key: string]: {
|
||||||
|
type: 'tcp' | 'udp';
|
||||||
|
localIP: string;
|
||||||
|
localPort: number;
|
||||||
|
remotePort: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
19
app/tsconfig.json
Normal file
19
app/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
app/vite.config.ts
Normal file
7
app/vite.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
15
node/.dockerignore
Normal file
15
node/.dockerignore
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
|
.nyc_output
|
||||||
|
coverage
|
||||||
|
.vscode
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
26
node/.env.example
Normal file
26
node/.env.example
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Home Server Agent Configuration
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Authentication (set a strong token for production)
|
||||||
|
API_TOKEN=your-secret-token-here
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Docker Configuration
|
||||||
|
DOCKER_HOST=unix:///var/run/docker.sock
|
||||||
|
|
||||||
|
# FRP Configuration
|
||||||
|
FRPC_CONFIG_PATH=/app/data/frpc.toml
|
||||||
|
FRPC_CONTAINER_NAME=frpc
|
||||||
|
|
||||||
|
# Game Server Defaults
|
||||||
|
MINECRAFT_MEMORY=2G
|
||||||
|
VALHEIM_SERVER_NAME=My Valheim Server
|
||||||
|
VALHEIM_WORLD_NAME=MyWorld
|
||||||
|
VALHEIM_SERVER_PASS=secret123
|
||||||
|
TERRARIA_WORLD=MyWorld
|
||||||
|
TERRARIA_PASSWORD=secret123
|
||||||
34
node/.gitignore
vendored
Normal file
34
node/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
380
node/DEPLOYMENT.md
Normal file
380
node/DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
# Home Server Agent Deployment Guide
|
||||||
|
|
||||||
|
This guide will help you deploy the Home Server Agent on your home server with Docker.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose installed on your home server
|
||||||
|
- At least 4GB of RAM (for running game servers)
|
||||||
|
- Open ports for the game servers you want to run
|
||||||
|
- Basic understanding of Docker and networking
|
||||||
|
|
||||||
|
## Quick Deployment
|
||||||
|
|
||||||
|
### 1. Clone and Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone <repository-url>
|
||||||
|
cd home-server-agent
|
||||||
|
|
||||||
|
# Create environment configuration
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit the .env file with your settings
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Environment
|
||||||
|
|
||||||
|
Edit `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# IMPORTANT: Change this to a secure token
|
||||||
|
API_TOKEN=your-very-secure-token-here
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=production
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Docker socket (usually default)
|
||||||
|
DOCKER_HOST=unix:///var/run/docker.sock
|
||||||
|
|
||||||
|
# Game server settings (optional)
|
||||||
|
MINECRAFT_MEMORY=2G
|
||||||
|
VALHEIM_SERVER_NAME=Your Valheim Server
|
||||||
|
VALHEIM_WORLD_NAME=YourWorld
|
||||||
|
VALHEIM_SERVER_PASS=your-secure-password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy with Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start all services (agent + game servers)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Or start just the agent for testing
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
## Port Configuration
|
||||||
|
|
||||||
|
The following ports will be used:
|
||||||
|
|
||||||
|
| Service | Port | Protocol | Purpose |
|
||||||
|
|---------|------|----------|---------|
|
||||||
|
| Agent API | 3000 | TCP | REST API |
|
||||||
|
| Minecraft | 25565 | TCP | Minecraft server |
|
||||||
|
| Valheim | 2456-2457 | UDP | Valheim server |
|
||||||
|
| Terraria | 7777 | TCP | Terraria server |
|
||||||
|
| Portainer | 9000 | TCP | Docker management UI |
|
||||||
|
|
||||||
|
### Firewall Configuration
|
||||||
|
|
||||||
|
**Ubuntu/Debian:**
|
||||||
|
```bash
|
||||||
|
# Allow agent API
|
||||||
|
sudo ufw allow 3000/tcp
|
||||||
|
|
||||||
|
# Allow game server ports
|
||||||
|
sudo ufw allow 25565/tcp # Minecraft
|
||||||
|
sudo ufw allow 2456:2457/udp # Valheim
|
||||||
|
sudo ufw allow 7777/tcp # Terraria
|
||||||
|
|
||||||
|
# Optional: Portainer
|
||||||
|
sudo ufw allow 9000/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
**CentOS/RHEL:**
|
||||||
|
```bash
|
||||||
|
# Allow agent API
|
||||||
|
sudo firewall-cmd --permanent --add-port=3000/tcp
|
||||||
|
|
||||||
|
# Allow game server ports
|
||||||
|
sudo firewall-cmd --permanent --add-port=25565/tcp
|
||||||
|
sudo firewall-cmd --permanent --add-port=2456-2457/udp
|
||||||
|
sudo firewall-cmd --permanent --add-port=7777/tcp
|
||||||
|
|
||||||
|
sudo firewall-cmd --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## VPS Integration
|
||||||
|
|
||||||
|
To allow your VPS to control the home server agent:
|
||||||
|
|
||||||
|
### 1. Secure the API Token
|
||||||
|
|
||||||
|
Use a strong, unique API token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate a secure token
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Or use UUID
|
||||||
|
uuidgen
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. VPS Configuration
|
||||||
|
|
||||||
|
On your VPS app, configure it to make requests to your home server:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Example VPS integration
|
||||||
|
const HOME_SERVER_URL = 'https://your-home-server.com:3000'; // Use HTTPS in production
|
||||||
|
const API_TOKEN = 'your-secure-token-here';
|
||||||
|
|
||||||
|
// Start a game server from VPS
|
||||||
|
const startGameServer = async (serverName) => {
|
||||||
|
const response = await fetch(`${HOME_SERVER_URL}/api/gameserver/start/${serverName}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Network Setup
|
||||||
|
|
||||||
|
**Option A: Port Forwarding**
|
||||||
|
- Forward port 3000 on your router to your home server
|
||||||
|
- Consider using a non-standard port for security
|
||||||
|
|
||||||
|
**Option B: VPN/Tunnel**
|
||||||
|
- Use a VPN to connect your VPS to your home network
|
||||||
|
- More secure but requires additional setup
|
||||||
|
|
||||||
|
**Option C: Reverse Proxy**
|
||||||
|
- Use nginx or Apache to proxy requests
|
||||||
|
- Can add SSL termination and additional security
|
||||||
|
|
||||||
|
## SSL/HTTPS Setup
|
||||||
|
|
||||||
|
For production use, enable HTTPS:
|
||||||
|
|
||||||
|
### Using nginx as reverse proxy:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/home-server-agent
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/your/certificate.pem;
|
||||||
|
ssl_certificate_key /path/to/your/private.key;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Management
|
||||||
|
|
||||||
|
### Docker Management with Portainer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable Portainer
|
||||||
|
docker-compose --profile management up -d portainer
|
||||||
|
|
||||||
|
# Access at http://your-server:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View agent logs
|
||||||
|
docker-compose logs -f home-server-agent
|
||||||
|
|
||||||
|
# View specific game server logs
|
||||||
|
docker-compose logs -f minecraft
|
||||||
|
|
||||||
|
# View all logs
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Monitoring
|
||||||
|
|
||||||
|
Set up monitoring with the included health check scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux health check
|
||||||
|
./health-check.sh health
|
||||||
|
|
||||||
|
# Windows health check
|
||||||
|
./health-check.ps1 -CheckType health
|
||||||
|
|
||||||
|
# Check specific game server
|
||||||
|
./health-check.sh gameserver minecraft
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatic Updates
|
||||||
|
|
||||||
|
Enable Watchtower for automatic updates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable Watchtower
|
||||||
|
docker-compose --profile management up -d watchtower
|
||||||
|
|
||||||
|
# Or manually update
|
||||||
|
docker-compose pull
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup Strategy
|
||||||
|
|
||||||
|
### Game Data Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup game data volumes
|
||||||
|
docker run --rm -v minecraft-data:/data -v $(pwd):/backup ubuntu tar czf /backup/minecraft-backup.tar.gz /data
|
||||||
|
|
||||||
|
# Restore game data
|
||||||
|
docker run --rm -v minecraft-data:/data -v $(pwd):/backup ubuntu tar xzf /backup/minecraft-backup.tar.gz -C /
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup configuration
|
||||||
|
cp .env .env.backup
|
||||||
|
cp docker-compose.yml docker-compose.yml.backup
|
||||||
|
|
||||||
|
# Backup logs
|
||||||
|
tar czf logs-backup.tar.gz logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
- [ ] Changed default API token
|
||||||
|
- [ ] Configured firewall rules
|
||||||
|
- [ ] Enabled SSL/HTTPS (production)
|
||||||
|
- [ ] Restricted Docker socket access
|
||||||
|
- [ ] Set up log rotation
|
||||||
|
- [ ] Configured backup strategy
|
||||||
|
- [ ] Tested health monitoring
|
||||||
|
- [ ] Documented port assignments
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**1. Permission Denied (Docker Socket)**
|
||||||
|
```bash
|
||||||
|
# Add user to docker group
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
|
||||||
|
# Or adjust socket permissions
|
||||||
|
sudo chmod 666 /var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Port Already in Use**
|
||||||
|
```bash
|
||||||
|
# Check what's using the port
|
||||||
|
sudo netstat -tlnp | grep :3000
|
||||||
|
|
||||||
|
# Stop conflicting service
|
||||||
|
sudo systemctl stop <service-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Game Server Won't Start**
|
||||||
|
```bash
|
||||||
|
# Check Docker logs
|
||||||
|
docker logs gameserver-minecraft
|
||||||
|
|
||||||
|
# Check system resources
|
||||||
|
docker system df
|
||||||
|
free -h
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. API Not Responding**
|
||||||
|
```bash
|
||||||
|
# Check agent status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Check agent logs
|
||||||
|
docker-compose logs home-server-agent
|
||||||
|
|
||||||
|
# Test local connectivity
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Follow all logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Search for errors
|
||||||
|
docker-compose logs | grep -i error
|
||||||
|
|
||||||
|
# Check specific timeframe
|
||||||
|
docker-compose logs --since=1h
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Resource Limits
|
||||||
|
|
||||||
|
Add resource limits to `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
minecraft:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 2G
|
||||||
|
cpus: '2.0'
|
||||||
|
reservations:
|
||||||
|
memory: 1G
|
||||||
|
cpus: '1.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Requirements
|
||||||
|
|
||||||
|
**Minimum:**
|
||||||
|
- 2GB RAM
|
||||||
|
- 2 CPU cores
|
||||||
|
- 20GB storage
|
||||||
|
|
||||||
|
**Recommended:**
|
||||||
|
- 8GB RAM
|
||||||
|
- 4 CPU cores
|
||||||
|
- 100GB storage
|
||||||
|
- SSD storage for better performance
|
||||||
|
|
||||||
|
## Production Checklist
|
||||||
|
|
||||||
|
Before deploying to production:
|
||||||
|
|
||||||
|
- [ ] Secure API token configured
|
||||||
|
- [ ] Firewall rules applied
|
||||||
|
- [ ] SSL/HTTPS enabled
|
||||||
|
- [ ] Monitoring configured
|
||||||
|
- [ ] Backup strategy implemented
|
||||||
|
- [ ] Resource limits set
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] Testing completed
|
||||||
|
- [ ] Network security reviewed
|
||||||
|
- [ ] Access controls verified
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues:
|
||||||
|
1. Check the troubleshooting section
|
||||||
|
2. Review logs for error messages
|
||||||
|
3. Check Docker and system status
|
||||||
|
4. Verify network connectivity
|
||||||
|
5. Open an issue with detailed information
|
||||||
56
node/Dockerfile
Normal file
56
node/Dockerfile
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Use the official Bun image as base
|
||||||
|
FROM oven/bun:1 as builder
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY bun.lockb* ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY src/ ./src/
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Install security updates
|
||||||
|
RUN apk update && apk upgrade && apk add --no-cache dumb-init
|
||||||
|
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nodeuser -u 1001
|
||||||
|
|
||||||
|
# Copy built application from builder stage
|
||||||
|
COPY --from=builder --chown=nodeuser:nodejs /app/dist ./dist
|
||||||
|
COPY --from=builder --chown=nodeuser:nodejs /app/package.json ./
|
||||||
|
|
||||||
|
# Install production dependencies
|
||||||
|
RUN npm ci --only=production && npm cache clean --force
|
||||||
|
|
||||||
|
# Create logs directory
|
||||||
|
RUN mkdir -p logs && chown nodeuser:nodejs logs
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER nodeuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:3000/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1))"
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
285
node/README.md
Normal file
285
node/README.md
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
# node
|
||||||
|
|
||||||
|
# Home Server Agent
|
||||||
|
|
||||||
|
A lightweight Node.js agent for managing game servers on your home server through Docker containers. This agent provides a secure REST API to start, stop, and monitor game servers remotely.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎮 **Game Server Management**: Start, stop, and monitor popular game servers (Minecraft, Valheim, Terraria)
|
||||||
|
- 🐳 **Docker Integration**: Leverages Docker for containerized game server deployment
|
||||||
|
- 🔒 **Security**: Token-based authentication for API access
|
||||||
|
- 📊 **Monitoring**: Real-time status monitoring and system statistics
|
||||||
|
- 🌐 **REST API**: Clean RESTful API for remote management
|
||||||
|
- 📝 **Logging**: Comprehensive logging with Winston
|
||||||
|
- 🔄 **Auto-restart**: Containers automatically restart on failure
|
||||||
|
- 💾 **Data Persistence**: Game data persisted in Docker volumes
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Node.js 18+ (for development)
|
||||||
|
- Bun (optional, for development)
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd home-server-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
# or
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set up environment**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start development server**
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
# or
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
1. **Build and start with Docker Compose**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Or start with just the agent for testing**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
All API endpoints (except `/health`) require authentication via Bearer token or `X-API-Key` header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Bearer token
|
||||||
|
curl -H "Authorization: Bearer your-secret-token-here" http://localhost:3000/api/status
|
||||||
|
|
||||||
|
# Using API key header
|
||||||
|
curl -H "X-API-Key: your-secret-token-here" http://localhost:3000/api/status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/health` | GET | Health check (no auth required) |
|
||||||
|
| `/api/status` | GET | Get overall server status |
|
||||||
|
| `/api/status/ports` | GET | Get active ports and services |
|
||||||
|
| `/api/gameserver/list` | GET | List all available game servers |
|
||||||
|
| `/api/gameserver/start/:serviceName` | POST | Start a game server |
|
||||||
|
| `/api/gameserver/stop/:serviceName` | POST | Stop a game server |
|
||||||
|
| `/api/gameserver/restart/:serviceName` | POST | Restart a game server |
|
||||||
|
| `/api/gameserver/:serviceName/status` | GET | Get specific server status |
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get server status
|
||||||
|
curl -H "Authorization: Bearer your-secret-token-here" \
|
||||||
|
http://localhost:3000/api/status
|
||||||
|
|
||||||
|
# Start Minecraft server
|
||||||
|
curl -X POST -H "Authorization: Bearer your-secret-token-here" \
|
||||||
|
http://localhost:3000/api/gameserver/start/minecraft
|
||||||
|
|
||||||
|
# Stop Minecraft server
|
||||||
|
curl -X POST -H "Authorization: Bearer your-secret-token-here" \
|
||||||
|
http://localhost:3000/api/gameserver/stop/minecraft
|
||||||
|
|
||||||
|
# Get Minecraft server status
|
||||||
|
curl -H "Authorization: Bearer your-secret-token-here" \
|
||||||
|
http://localhost:3000/api/gameserver/minecraft/status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Game Servers
|
||||||
|
|
||||||
|
### Minecraft
|
||||||
|
- **Image**: `itzg/minecraft-server:latest`
|
||||||
|
- **Default Port**: 25565
|
||||||
|
- **Features**: Vanilla Minecraft server with configurable settings
|
||||||
|
|
||||||
|
### Valheim
|
||||||
|
- **Image**: `lloesche/valheim-server:latest`
|
||||||
|
- **Default Ports**: 2456-2457 (UDP)
|
||||||
|
- **Features**: Dedicated Valheim server with world persistence
|
||||||
|
|
||||||
|
### Terraria
|
||||||
|
- **Image**: `ryshe/terraria:latest`
|
||||||
|
- **Default Port**: 7777
|
||||||
|
- **Features**: Terraria server with world management
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `PORT` | Server port | 3000 |
|
||||||
|
| `NODE_ENV` | Environment mode | development |
|
||||||
|
| `API_TOKEN` | Authentication token | Required |
|
||||||
|
| `LOG_LEVEL` | Logging level | info |
|
||||||
|
| `DOCKER_HOST` | Docker socket path | unix:///var/run/docker.sock |
|
||||||
|
|
||||||
|
### Game Server Configuration
|
||||||
|
|
||||||
|
Game servers can be configured by modifying the environment variables in `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
minecraft:
|
||||||
|
environment:
|
||||||
|
EULA: "TRUE"
|
||||||
|
TYPE: "VANILLA"
|
||||||
|
MEMORY: "2G"
|
||||||
|
DIFFICULTY: "normal"
|
||||||
|
MAX_PLAYERS: "20"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Authentication**: All API endpoints require token authentication
|
||||||
|
- **Docker Security**: Containers run as non-root users where possible
|
||||||
|
- **Network Isolation**: Services run in isolated Docker networks
|
||||||
|
- **Read-only Docker Socket**: Docker socket is mounted read-only for security
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
The agent provides comprehensive monitoring capabilities:
|
||||||
|
|
||||||
|
- **System Stats**: Docker version, container counts, resource usage
|
||||||
|
- **Port Monitoring**: Active ports and their associated services
|
||||||
|
- **Container Status**: Real-time container health and uptime
|
||||||
|
- **Logs**: Structured logging with Winston
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # Main application entry point
|
||||||
|
├── middleware/
|
||||||
|
│ └── auth.ts # Authentication middleware
|
||||||
|
├── routes/
|
||||||
|
│ ├── status.ts # Status and monitoring routes
|
||||||
|
│ └── gameServer.ts # Game server management routes
|
||||||
|
├── services/
|
||||||
|
│ └── dockerManager.ts # Docker container management
|
||||||
|
└── utils/
|
||||||
|
└── logger.ts # Logging configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
bun run dev # Start development server with hot reload
|
||||||
|
bun run build # Build the application
|
||||||
|
bun run start # Start production server
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
bun run docker:build # Build Docker image
|
||||||
|
bun run docker:run # Run with Docker Compose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Volumes
|
||||||
|
|
||||||
|
The following volumes are used for data persistence:
|
||||||
|
|
||||||
|
- `minecraft-data`: Minecraft world data and configuration
|
||||||
|
- `valheim-data`: Valheim world data and configuration
|
||||||
|
- `terraria-data`: Terraria world data and configuration
|
||||||
|
- `./logs`: Application logs (mounted from host)
|
||||||
|
|
||||||
|
## Optional Services
|
||||||
|
|
||||||
|
The Docker Compose file includes optional management services:
|
||||||
|
|
||||||
|
### Portainer (Docker Management UI)
|
||||||
|
```bash
|
||||||
|
docker-compose --profile management up -d portainer
|
||||||
|
```
|
||||||
|
Access at: http://localhost:9000
|
||||||
|
|
||||||
|
### Watchtower (Auto-updates)
|
||||||
|
```bash
|
||||||
|
docker-compose --profile management up -d watchtower
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Permission Denied (Docker Socket)**
|
||||||
|
```bash
|
||||||
|
# Ensure Docker socket has proper permissions
|
||||||
|
sudo chmod 666 /var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Port Already in Use**
|
||||||
|
```bash
|
||||||
|
# Check what's using the port
|
||||||
|
netstat -tlnp | grep :3000
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Container Start Failures**
|
||||||
|
```bash
|
||||||
|
# Check container logs
|
||||||
|
docker logs gameserver-minecraft
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
Application logs are available in:
|
||||||
|
- `./logs/combined.log` - All logs
|
||||||
|
- `./logs/error.log` - Error logs only
|
||||||
|
- Console output (development mode)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests if applicable
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and questions:
|
||||||
|
1. Check the troubleshooting section
|
||||||
|
2. Review the logs for error messages
|
||||||
|
3. Open an issue on GitHub with detailed informationall dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.2.6. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||||
430
node/bun.lock
Normal file
430
node/bun.lock
Normal file
|
|
@ -0,0 +1,430 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "node",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dockerode": "^4.0.2",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"winston": "^3.11.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="],
|
||||||
|
|
||||||
|
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||||
|
|
||||||
|
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
|
||||||
|
|
||||||
|
"@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="],
|
||||||
|
|
||||||
|
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
|
||||||
|
|
||||||
|
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||||
|
|
||||||
|
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||||
|
|
||||||
|
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||||
|
|
||||||
|
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||||
|
|
||||||
|
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||||
|
|
||||||
|
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||||
|
|
||||||
|
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||||
|
|
||||||
|
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||||
|
|
||||||
|
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||||
|
|
||||||
|
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||||
|
|
||||||
|
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||||
|
|
||||||
|
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
|
||||||
|
|
||||||
|
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||||
|
|
||||||
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
|
|
||||||
|
"@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="],
|
||||||
|
|
||||||
|
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="],
|
||||||
|
|
||||||
|
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||||
|
|
||||||
|
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||||
|
|
||||||
|
"@types/morgan": ["@types/morgan@1.9.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@20.19.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA=="],
|
||||||
|
|
||||||
|
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||||
|
|
||||||
|
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||||
|
|
||||||
|
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
|
||||||
|
|
||||||
|
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
|
||||||
|
|
||||||
|
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||||
|
|
||||||
|
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||||
|
|
||||||
|
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
|
||||||
|
|
||||||
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||||
|
|
||||||
|
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||||
|
|
||||||
|
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||||
|
|
||||||
|
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
||||||
|
|
||||||
|
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||||
|
|
||||||
|
"buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
||||||
|
|
||||||
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
|
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
|
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||||
|
|
||||||
|
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||||
|
|
||||||
|
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
|
||||||
|
|
||||||
|
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||||
|
|
||||||
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
|
||||||
|
|
||||||
|
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||||
|
|
||||||
|
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
|
||||||
|
|
||||||
|
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
|
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||||
|
|
||||||
|
"docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="],
|
||||||
|
|
||||||
|
"dockerode": ["dockerode@4.0.7", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||||
|
|
||||||
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
|
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
|
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||||
|
|
||||||
|
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
|
||||||
|
|
||||||
|
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||||
|
|
||||||
|
"finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
|
||||||
|
|
||||||
|
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
|
||||||
|
|
||||||
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
|
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||||
|
|
||||||
|
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"helmet": ["helmet@7.2.0", "", {}, "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw=="],
|
||||||
|
|
||||||
|
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
|
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
|
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||||
|
|
||||||
|
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||||
|
|
||||||
|
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
||||||
|
|
||||||
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||||
|
|
||||||
|
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
||||||
|
|
||||||
|
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||||
|
|
||||||
|
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||||
|
|
||||||
|
"morgan": ["morgan@1.10.0", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.0.2" } }, "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
|
"nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="],
|
||||||
|
|
||||||
|
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
|
"on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
|
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
||||||
|
|
||||||
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
||||||
|
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
||||||
|
|
||||||
|
"protobufjs": ["protobufjs@7.5.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw=="],
|
||||||
|
|
||||||
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||||
|
|
||||||
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
|
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
||||||
|
|
||||||
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
|
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||||
|
|
||||||
|
"serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
|
||||||
|
|
||||||
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "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" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||||
|
|
||||||
|
"split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="],
|
||||||
|
|
||||||
|
"ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="],
|
||||||
|
|
||||||
|
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||||
|
|
||||||
|
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
|
||||||
|
|
||||||
|
"tar-stream": ["tar-stream@2.2.0", "", { "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" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||||
|
|
||||||
|
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||||
|
|
||||||
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
|
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
||||||
|
|
||||||
|
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
||||||
|
|
||||||
|
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
||||||
|
|
||||||
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
|
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
||||||
|
|
||||||
|
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
|
"@types/body-parser/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"@types/connect/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"@types/cors/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"@types/express-serve-static-core/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"@types/morgan/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"@types/send/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"@types/serve-static/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"docker-modem/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||||
|
|
||||||
|
"logform/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
|
||||||
|
|
||||||
|
"protobufjs/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||||
|
|
||||||
|
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||||
|
|
||||||
|
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"@types/body-parser/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"@types/connect/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"@types/cors/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"@types/morgan/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"@types/send/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"@types/serve-static/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"docker-modem/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"protobufjs/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
49
node/docker-compose.dev.yml
Normal file
49
node/docker-compose.dev.yml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Home Server Agent (Development)
|
||||||
|
home-server-agent:
|
||||||
|
build: .
|
||||||
|
container_name: home-server-agent-dev
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- API_TOKEN=dev-token-123
|
||||||
|
- LOG_LEVEL=debug
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./src:/app/src
|
||||||
|
networks:
|
||||||
|
- game-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Single Minecraft Server for Testing
|
||||||
|
minecraft:
|
||||||
|
image: itzg/minecraft-server:latest
|
||||||
|
container_name: gameserver-minecraft
|
||||||
|
ports:
|
||||||
|
- "25565:25565"
|
||||||
|
environment:
|
||||||
|
EULA: "TRUE"
|
||||||
|
TYPE: "VANILLA"
|
||||||
|
MEMORY: "1G"
|
||||||
|
DIFFICULTY: "peaceful"
|
||||||
|
MAX_PLAYERS: "5"
|
||||||
|
ONLINE_MODE: "false"
|
||||||
|
volumes:
|
||||||
|
- minecraft-dev-data:/data
|
||||||
|
networks:
|
||||||
|
- game-network
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "game-server=minecraft"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minecraft-dev-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
game-network:
|
||||||
|
driver: bridge
|
||||||
153
node/docker-compose.yml
Normal file
153
node/docker-compose.yml
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Home Server Agent
|
||||||
|
home-server-agent:
|
||||||
|
build: .
|
||||||
|
container_name: home-server-agent
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- API_TOKEN=${API_TOKEN:-your-secret-token-here}
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- FRPC_CONFIG_PATH=${FRPC_CONFIG_PATH:-/app/data/frpc.toml}
|
||||||
|
- FRPC_CONTAINER_NAME=${FRPC_CONTAINER_NAME:-frpc}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./data:/app/data
|
||||||
|
networks:
|
||||||
|
- game-network
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- minecraft
|
||||||
|
- valheim
|
||||||
|
- terraria
|
||||||
|
|
||||||
|
# Minecraft Server
|
||||||
|
minecraft:
|
||||||
|
image: itzg/minecraft-server:latest
|
||||||
|
container_name: gameserver-minecraft
|
||||||
|
ports:
|
||||||
|
- "25565:25565"
|
||||||
|
environment:
|
||||||
|
EULA: "TRUE"
|
||||||
|
TYPE: "VANILLA"
|
||||||
|
MEMORY: "2G"
|
||||||
|
DIFFICULTY: "normal"
|
||||||
|
SPAWN_PROTECTION: "0"
|
||||||
|
MAX_PLAYERS: "20"
|
||||||
|
ONLINE_MODE: "false"
|
||||||
|
ALLOW_NETHER: "true"
|
||||||
|
ANNOUNCE_PLAYER_ACHIEVEMENTS: "true"
|
||||||
|
ENABLE_COMMAND_BLOCK: "true"
|
||||||
|
FORCE_GAMEMODE: "false"
|
||||||
|
GENERATE_STRUCTURES: "true"
|
||||||
|
HARDCORE: "false"
|
||||||
|
MAX_BUILD_HEIGHT: "256"
|
||||||
|
MAX_TICK_TIME: "60000"
|
||||||
|
SPAWN_ANIMALS: "true"
|
||||||
|
SPAWN_MONSTERS: "true"
|
||||||
|
SPAWN_NPCS: "true"
|
||||||
|
VIEW_DISTANCE: "10"
|
||||||
|
volumes:
|
||||||
|
- minecraft-data:/data
|
||||||
|
networks:
|
||||||
|
- game-network
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "game-server=minecraft"
|
||||||
|
|
||||||
|
# Valheim Server
|
||||||
|
valheim:
|
||||||
|
image: lloesche/valheim-server:latest
|
||||||
|
container_name: gameserver-valheim
|
||||||
|
ports:
|
||||||
|
- "2456:2456/udp"
|
||||||
|
- "2457:2457/udp"
|
||||||
|
environment:
|
||||||
|
SERVER_NAME: "My Valheim Server"
|
||||||
|
WORLD_NAME: "MyWorld"
|
||||||
|
SERVER_PASS: "secret123"
|
||||||
|
SERVER_PUBLIC: "false"
|
||||||
|
ADMINLIST_IDS: ""
|
||||||
|
BANNEDLIST_IDS: ""
|
||||||
|
PERMITTEDLIST_IDS: ""
|
||||||
|
volumes:
|
||||||
|
- valheim-data:/config
|
||||||
|
networks:
|
||||||
|
- game-network
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "game-server=valheim"
|
||||||
|
|
||||||
|
# Terraria Server
|
||||||
|
terraria:
|
||||||
|
image: ryshe/terraria:latest
|
||||||
|
container_name: gameserver-terraria
|
||||||
|
ports:
|
||||||
|
- "7777:7777"
|
||||||
|
environment:
|
||||||
|
WORLD: "MyWorld"
|
||||||
|
PASSWORD: "secret123"
|
||||||
|
MAXPLAYERS: "16"
|
||||||
|
DIFFICULTY: "1"
|
||||||
|
AUTOCREATE: "2"
|
||||||
|
BANLIST: ""
|
||||||
|
SECURE: "1"
|
||||||
|
LANGUAGE: "en-US"
|
||||||
|
volumes:
|
||||||
|
- terraria-data:/world
|
||||||
|
networks:
|
||||||
|
- game-network
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "game-server=terraria"
|
||||||
|
|
||||||
|
# Optional: Portainer for Docker management
|
||||||
|
portainer:
|
||||||
|
image: portainer/portainer-ce:latest
|
||||||
|
container_name: portainer
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- portainer-data:/data
|
||||||
|
networks:
|
||||||
|
- game-network
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- management
|
||||||
|
|
||||||
|
# Optional: Watchtower for automatic updates
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower:latest
|
||||||
|
container_name: watchtower
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
environment:
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
- WATCHTOWER_SCHEDULE=0 0 2 * * * # 2 AM daily
|
||||||
|
networks:
|
||||||
|
- game-network
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- management
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minecraft-data:
|
||||||
|
driver: local
|
||||||
|
valheim-data:
|
||||||
|
driver: local
|
||||||
|
terraria-data:
|
||||||
|
driver: local
|
||||||
|
portainer-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
game-network:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/16
|
||||||
110
node/health-check.ps1
Normal file
110
node/health-check.ps1
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
# Health check script for the Home Server Agent (PowerShell)
|
||||||
|
# This script can be used with monitoring tools on Windows
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[ValidateSet('health', 'gameserver')]
|
||||||
|
[string]$CheckType,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$ServerName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$AgentUrl = "http://localhost:3000",
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$ApiToken = $env:API_TOKEN,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[int]$Timeout = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
# Function to check if agent is healthy
|
||||||
|
function Test-AgentHealth {
|
||||||
|
try {
|
||||||
|
$headers = @{}
|
||||||
|
if ($ApiToken) {
|
||||||
|
$headers["Authorization"] = "Bearer $ApiToken"
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = Invoke-RestMethod -Uri "$AgentUrl/health" -Headers $headers -TimeoutSec $Timeout -ErrorAction Stop
|
||||||
|
|
||||||
|
if ($response.status -eq "healthy") {
|
||||||
|
Write-Output "OK: Home Server Agent is healthy"
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
Write-Output "CRITICAL: Home Server Agent reported unhealthy status"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Output "CRITICAL: Home Server Agent is not responding - $($_.Exception.Message)"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check game server status
|
||||||
|
function Test-GameServerStatus {
|
||||||
|
param([string]$ServerName)
|
||||||
|
|
||||||
|
if (-not $ServerName) {
|
||||||
|
Write-Output "UNKNOWN: Server name not provided"
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $ApiToken) {
|
||||||
|
Write-Output "UNKNOWN: API_TOKEN not provided"
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$headers = @{
|
||||||
|
"Authorization" = "Bearer $ApiToken"
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = Invoke-RestMethod -Uri "$AgentUrl/api/gameserver/$ServerName/status" -Headers $headers -TimeoutSec $Timeout -ErrorAction Stop
|
||||||
|
|
||||||
|
switch ($response.status) {
|
||||||
|
"running" {
|
||||||
|
Write-Output "OK: $ServerName is running"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
"stopped" {
|
||||||
|
Write-Output "WARNING: $ServerName is stopped"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Output "CRITICAL: $ServerName status unknown ($($response.status))"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Output "CRITICAL: Cannot check $ServerName status - $($_.Exception.Message)"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
$exitCode = 0
|
||||||
|
|
||||||
|
switch ($CheckType) {
|
||||||
|
"health" {
|
||||||
|
$exitCode = Test-AgentHealth
|
||||||
|
}
|
||||||
|
"gameserver" {
|
||||||
|
$exitCode = Test-GameServerStatus -ServerName $ServerName
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Output "Usage: health-check.ps1 -CheckType {health|gameserver} [-ServerName <name>]"
|
||||||
|
Write-Output "Environment variables:"
|
||||||
|
Write-Output " API_TOKEN - Authentication token for API access"
|
||||||
|
Write-Output "Parameters:"
|
||||||
|
Write-Output " -AgentUrl - URL of the Home Server Agent (default: http://localhost:3000)"
|
||||||
|
Write-Output " -ApiToken - Authentication token for API access"
|
||||||
|
Write-Output " -Timeout - Request timeout in seconds (default: 10)"
|
||||||
|
$exitCode = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
98
node/health-check.sh
Normal file
98
node/health-check.sh
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Health check script for the Home Server Agent
|
||||||
|
# This script can be used with monitoring tools like Nagios, Zabbix, etc.
|
||||||
|
|
||||||
|
AGENT_URL="${AGENT_URL:-http://localhost:3000}"
|
||||||
|
API_TOKEN="${API_TOKEN:-}"
|
||||||
|
TIMEOUT="${TIMEOUT:-10}"
|
||||||
|
|
||||||
|
# Function to check if agent is healthy
|
||||||
|
check_health() {
|
||||||
|
local response
|
||||||
|
local status_code
|
||||||
|
|
||||||
|
if [ -n "$API_TOKEN" ]; then
|
||||||
|
response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \
|
||||||
|
-H "Authorization: Bearer $API_TOKEN" \
|
||||||
|
"$AGENT_URL/health" 2>/dev/null)
|
||||||
|
else
|
||||||
|
response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \
|
||||||
|
"$AGENT_URL/health" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
status_code="${response: -3}"
|
||||||
|
|
||||||
|
if [ "$status_code" = "200" ]; then
|
||||||
|
echo "OK: Home Server Agent is healthy"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "CRITICAL: Home Server Agent is not responding (HTTP $status_code)"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check game server status
|
||||||
|
check_gameserver() {
|
||||||
|
local server_name="$1"
|
||||||
|
local response
|
||||||
|
local status_code
|
||||||
|
|
||||||
|
if [ -z "$server_name" ]; then
|
||||||
|
echo "UNKNOWN: Server name not provided"
|
||||||
|
return 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$API_TOKEN" ]; then
|
||||||
|
echo "UNKNOWN: API_TOKEN not provided"
|
||||||
|
return 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \
|
||||||
|
-H "Authorization: Bearer $API_TOKEN" \
|
||||||
|
"$AGENT_URL/api/gameserver/$server_name/status" 2>/dev/null)
|
||||||
|
|
||||||
|
status_code="${response: -3}"
|
||||||
|
response_body="${response%???}"
|
||||||
|
|
||||||
|
if [ "$status_code" = "200" ]; then
|
||||||
|
# Parse JSON response to get status
|
||||||
|
status=$(echo "$response_body" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
|
||||||
|
case "$status" in
|
||||||
|
"running")
|
||||||
|
echo "OK: $server_name is running"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
"stopped")
|
||||||
|
echo "WARNING: $server_name is stopped"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "CRITICAL: $server_name status unknown ($status)"
|
||||||
|
return 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "CRITICAL: Cannot check $server_name status (HTTP $status_code)"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
case "$1" in
|
||||||
|
"health")
|
||||||
|
check_health
|
||||||
|
;;
|
||||||
|
"gameserver")
|
||||||
|
check_gameserver "$2"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {health|gameserver <server_name>}"
|
||||||
|
echo "Environment variables:"
|
||||||
|
echo " AGENT_URL - URL of the Home Server Agent (default: http://localhost:3000)"
|
||||||
|
echo " API_TOKEN - Authentication token for API access"
|
||||||
|
echo " TIMEOUT - Request timeout in seconds (default: 10)"
|
||||||
|
exit 3
|
||||||
|
;;
|
||||||
|
esac
|
||||||
23
node/home-server-agent.service
Normal file
23
node/home-server-agent.service
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Home Server Agent
|
||||||
|
Documentation=https://github.com/your-repo/home-server-agent
|
||||||
|
Requires=docker.service
|
||||||
|
After=docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=forking
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=/opt/home-server-agent
|
||||||
|
ExecStart=/usr/local/bin/docker-compose up -d
|
||||||
|
ExecStop=/usr/local/bin/docker-compose down
|
||||||
|
ExecReload=/usr/local/bin/docker-compose restart
|
||||||
|
TimeoutStartSec=0
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
User=docker
|
||||||
|
Group=docker
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
54
node/install-windows-service.ps1
Normal file
54
node/install-windows-service.ps1
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Windows Service Installation Script
|
||||||
|
# Run as Administrator
|
||||||
|
|
||||||
|
$serviceName = "HomeServerAgent"
|
||||||
|
$serviceDisplayName = "Home Server Agent"
|
||||||
|
$serviceDescription = "Lightweight agent for managing game servers"
|
||||||
|
$servicePath = "C:\Program Files\HomeServerAgent"
|
||||||
|
$dockerComposePath = "$servicePath\docker-compose.yml"
|
||||||
|
|
||||||
|
# Check if running as administrator
|
||||||
|
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
||||||
|
Write-Warning "Please run this script as Administrator"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create service directory
|
||||||
|
if (!(Test-Path $servicePath)) {
|
||||||
|
New-Item -ItemType Directory -Path $servicePath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy files to service directory
|
||||||
|
Copy-Item -Path ".\*" -Destination $servicePath -Recurse -Force
|
||||||
|
|
||||||
|
# Install NSSM (Non-Sucking Service Manager) if not present
|
||||||
|
$nssmPath = "$servicePath\nssm.exe"
|
||||||
|
if (!(Test-Path $nssmPath)) {
|
||||||
|
Write-Host "Downloading NSSM..."
|
||||||
|
Invoke-WebRequest -Uri "https://nssm.cc/release/nssm-2.24.zip" -OutFile "$servicePath\nssm.zip"
|
||||||
|
Expand-Archive -Path "$servicePath\nssm.zip" -DestinationPath $servicePath
|
||||||
|
Copy-Item -Path "$servicePath\nssm-2.24\win64\nssm.exe" -Destination $nssmPath
|
||||||
|
Remove-Item -Path "$servicePath\nssm.zip" -Force
|
||||||
|
Remove-Item -Path "$servicePath\nssm-2.24" -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create service
|
||||||
|
& $nssmPath install $serviceName "docker-compose"
|
||||||
|
& $nssmPath set $serviceName AppDirectory $servicePath
|
||||||
|
& $nssmPath set $serviceName AppParameters "up -d"
|
||||||
|
& $nssmPath set $serviceName DisplayName $serviceDisplayName
|
||||||
|
& $nssmPath set $serviceName Description $serviceDescription
|
||||||
|
& $nssmPath set $serviceName Start SERVICE_AUTO_START
|
||||||
|
& $nssmPath set $serviceName AppStopMethodConsole 30000
|
||||||
|
& $nssmPath set $serviceName AppStopMethodWindow 30000
|
||||||
|
& $nssmPath set $serviceName AppStopMethodThreads 30000
|
||||||
|
& $nssmPath set $serviceName AppKillProcessTree 1
|
||||||
|
|
||||||
|
# Set service to restart on failure
|
||||||
|
& $nssmPath set $serviceName AppRestartDelay 10000
|
||||||
|
& $nssmPath set $serviceName AppNoConsole 1
|
||||||
|
|
||||||
|
Write-Host "Service installed successfully!"
|
||||||
|
Write-Host "To start the service: Start-Service $serviceName"
|
||||||
|
Write-Host "To stop the service: Stop-Service $serviceName"
|
||||||
|
Write-Host "To remove the service: & '$nssmPath' remove $serviceName confirm"
|
||||||
34
node/package.json
Normal file
34
node/package.json
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "home-server-agent",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Lightweight agent for managing game servers on home server",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --watch src/index.ts",
|
||||||
|
"build": "bun build src/index.ts --outdir dist --target node",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"docker:build": "docker build -t home-server-agent .",
|
||||||
|
"docker:run": "docker-compose up -d"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"winston": "^3.11.0",
|
||||||
|
"dockerode": "^4.0.2",
|
||||||
|
"dotenv": "^16.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/node": "^20.10.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
83
node/src/index.ts
Normal file
83
node/src/index.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import morgan from 'morgan';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { logger } from './utils/logger.js';
|
||||||
|
import { authMiddleware } from './middleware/auth.js';
|
||||||
|
import { statusRouter } from './routes/status.js';
|
||||||
|
import { gameServerRouter } from './routes/gameServer.js';
|
||||||
|
import { frpcRouter } from './routes/frpc.js';
|
||||||
|
import { DockerManager } from './services/dockerManager.js';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Initialize Docker manager
|
||||||
|
const dockerManager = new DockerManager();
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } }));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Authentication middleware for protected routes
|
||||||
|
app.use('/api', authMiddleware);
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api/status', statusRouter);
|
||||||
|
app.use('/api/gameserver', gameServerRouter);
|
||||||
|
app.use('/api/frpc', frpcRouter);
|
||||||
|
|
||||||
|
// Health check endpoint (no auth required)
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Root endpoint
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
name: 'Home Server Agent',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'Lightweight agent for managing game servers',
|
||||||
|
endpoints: {
|
||||||
|
'/health': 'Health check (no auth)',
|
||||||
|
'/api/status': 'Get server status',
|
||||||
|
'/api/gameserver/list': 'List game servers',
|
||||||
|
'/api/gameserver/start/:serviceName': 'Start a game server',
|
||||||
|
'/api/gameserver/stop/:serviceName': 'Stop a game server',
|
||||||
|
'/api/gameserver/:serviceName/status': 'Get specific server status',
|
||||||
|
'/api/frpc/status': 'Get frpc status',
|
||||||
|
'/api/frpc/update-config': 'Update frpc configuration',
|
||||||
|
'/api/frpc/restart': 'Restart frpc container',
|
||||||
|
'/api/frpc/push-and-restart': 'Update config and restart frpc'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.error(`Error: ${err.message}`, { stack: err.stack });
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
logger.info(`Home Server Agent listening on port ${PORT}`);
|
||||||
|
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
logger.info('Received SIGTERM, shutting down gracefully');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
logger.info('Received SIGINT, shutting down gracefully');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
25
node/src/middleware/auth.ts
Normal file
25
node/src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
|
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const token = req.headers.authorization?.replace('Bearer ', '') || req.headers['x-api-key'];
|
||||||
|
const expectedToken = process.env.API_TOKEN;
|
||||||
|
|
||||||
|
if (!expectedToken) {
|
||||||
|
logger.warn('No API_TOKEN configured, skipping authentication');
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
logger.warn(`Unauthorized request from ${req.ip} - no token provided`);
|
||||||
|
return res.status(401).json({ error: 'Authentication required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token !== expectedToken) {
|
||||||
|
logger.warn(`Unauthorized request from ${req.ip} - invalid token`);
|
||||||
|
return res.status(403).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Authenticated request from ${req.ip}`);
|
||||||
|
next();
|
||||||
|
};
|
||||||
158
node/src/routes/frpc.ts
Normal file
158
node/src/routes/frpc.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Path to frpc config file
|
||||||
|
const FRPC_CONFIG_PATH = process.env.FRPC_CONFIG_PATH || '/app/data/frpc.toml';
|
||||||
|
const FRPC_CONTAINER_NAME = process.env.FRPC_CONTAINER_NAME || 'frpc';
|
||||||
|
|
||||||
|
// Update frpc configuration
|
||||||
|
router.post('/update-config', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { config } = req.body;
|
||||||
|
|
||||||
|
if (!config || typeof config !== 'string') {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid config format'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
const dir = path.dirname(FRPC_CONFIG_PATH);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
|
// Write config to file
|
||||||
|
await fs.writeFile(FRPC_CONFIG_PATH, config);
|
||||||
|
|
||||||
|
logger.info('frpc configuration updated successfully');
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Configuration updated successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to update frpc configuration:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to update configuration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart frpc container
|
||||||
|
router.post('/restart', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
// Check if container exists
|
||||||
|
const { stdout: containers } = await execAsync(`docker ps -a --filter "name=${FRPC_CONTAINER_NAME}" --format "{{.Names}}"`);
|
||||||
|
|
||||||
|
if (!containers.includes(FRPC_CONTAINER_NAME)) {
|
||||||
|
logger.warn(`frpc container ${FRPC_CONTAINER_NAME} not found`);
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'frpc container not found'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the container
|
||||||
|
await execAsync(`docker restart ${FRPC_CONTAINER_NAME}`);
|
||||||
|
|
||||||
|
logger.info(`frpc container ${FRPC_CONTAINER_NAME} restarted successfully`);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'frpc restarted successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to restart frpc container:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to restart frpc'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get frpc status
|
||||||
|
router.get('/status', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(`docker ps --filter "name=${FRPC_CONTAINER_NAME}" --format "{{.Names}} {{.Status}}"`);
|
||||||
|
|
||||||
|
const isRunning = stdout.trim().includes(FRPC_CONTAINER_NAME);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
container: FRPC_CONTAINER_NAME,
|
||||||
|
running: isRunning,
|
||||||
|
status: isRunning ? 'running' : 'stopped',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get frpc status:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
running: false,
|
||||||
|
status: 'error',
|
||||||
|
message: 'Failed to get frpc status'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get frpc logs
|
||||||
|
router.get('/logs', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const lines = parseInt(req.query.lines as string) || 50;
|
||||||
|
const { stdout } = await execAsync(`docker logs --tail ${lines} ${FRPC_CONTAINER_NAME}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
logs: stdout,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get frpc logs:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
logs: '',
|
||||||
|
message: 'Failed to get frpc logs'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push and restart - convenience endpoint
|
||||||
|
router.post('/push-and-restart', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { config } = req.body;
|
||||||
|
|
||||||
|
if (!config || typeof config !== 'string') {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid config format'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config
|
||||||
|
const dir = path.dirname(FRPC_CONFIG_PATH);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
await fs.writeFile(FRPC_CONFIG_PATH, config);
|
||||||
|
|
||||||
|
// Restart frpc
|
||||||
|
await execAsync(`docker restart ${FRPC_CONTAINER_NAME}`);
|
||||||
|
|
||||||
|
logger.info('frpc configuration updated and container restarted');
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Configuration updated and frpc restarted successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to push config and restart frpc:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to update configuration and restart frpc'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { router as frpcRouter };
|
||||||
138
node/src/routes/gameServer.ts
Normal file
138
node/src/routes/gameServer.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { DockerManager } from '../services/dockerManager.js';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const dockerManager = new DockerManager();
|
||||||
|
|
||||||
|
// List all available game servers
|
||||||
|
router.get('/list', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const servers = await dockerManager.listGameServers();
|
||||||
|
res.json(servers);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error listing game servers:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to list game servers' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start a game server
|
||||||
|
router.post('/start/:serviceName', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { serviceName } = req.params;
|
||||||
|
logger.info(`Request to start game server: ${serviceName}`);
|
||||||
|
|
||||||
|
const result = await dockerManager.startServer(serviceName);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: result.message,
|
||||||
|
serviceName
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: result.message,
|
||||||
|
serviceName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error starting game server ${req.params.serviceName}:`, error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error',
|
||||||
|
serviceName: req.params.serviceName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop a game server
|
||||||
|
router.post('/stop/:serviceName', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { serviceName } = req.params;
|
||||||
|
logger.info(`Request to stop game server: ${serviceName}`);
|
||||||
|
|
||||||
|
const result = await dockerManager.stopServer(serviceName);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: result.message,
|
||||||
|
serviceName
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: result.message,
|
||||||
|
serviceName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error stopping game server ${req.params.serviceName}:`, error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error',
|
||||||
|
serviceName: req.params.serviceName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get status of a specific game server
|
||||||
|
router.get('/:serviceName/status', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { serviceName } = req.params;
|
||||||
|
const status = await dockerManager.getServerStatus(serviceName);
|
||||||
|
res.json(status);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error getting status for ${req.params.serviceName}:`, error);
|
||||||
|
res.status(500).json({ error: 'Failed to get server status' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart a game server
|
||||||
|
router.post('/restart/:serviceName', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { serviceName } = req.params;
|
||||||
|
logger.info(`Request to restart game server: ${serviceName}`);
|
||||||
|
|
||||||
|
// Stop first
|
||||||
|
const stopResult = await dockerManager.stopServer(serviceName);
|
||||||
|
if (!stopResult.success && !stopResult.message.includes('not running')) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Failed to stop ${serviceName}: ${stopResult.message}`,
|
||||||
|
serviceName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a moment
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Start again
|
||||||
|
const startResult = await dockerManager.startServer(serviceName);
|
||||||
|
|
||||||
|
if (startResult.success) {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `${serviceName} restarted successfully`,
|
||||||
|
serviceName
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Failed to restart ${serviceName}: ${startResult.message}`,
|
||||||
|
serviceName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error restarting game server ${req.params.serviceName}:`, error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Internal server error',
|
||||||
|
serviceName: req.params.serviceName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { router as gameServerRouter };
|
||||||
78
node/src/routes/status.ts
Normal file
78
node/src/routes/status.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { DockerManager } from '../services/dockerManager.js';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const dockerManager = new DockerManager();
|
||||||
|
|
||||||
|
// Get overall server status
|
||||||
|
router.get('/', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const [gameServers, systemStats, runningContainers] = await Promise.all([
|
||||||
|
dockerManager.listGameServers(),
|
||||||
|
dockerManager.getSystemStats(),
|
||||||
|
dockerManager.getRunningContainers()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const activePorts = new Set<number>();
|
||||||
|
runningContainers.forEach((container: any) => {
|
||||||
|
if (container.Ports) {
|
||||||
|
container.Ports.forEach((port: any) => {
|
||||||
|
if (port.PublicPort) {
|
||||||
|
activePorts.add(port.PublicPort);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'operational',
|
||||||
|
gameServers: {
|
||||||
|
available: gameServers.available,
|
||||||
|
running: gameServers.running.length,
|
||||||
|
runningServers: gameServers.running
|
||||||
|
},
|
||||||
|
activePorts: Array.from(activePorts).sort(),
|
||||||
|
systemStats,
|
||||||
|
containers: {
|
||||||
|
total: runningContainers.length,
|
||||||
|
running: runningContainers.filter((c: any) => c.State === 'running').length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting server status:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get server status' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get active ports and services
|
||||||
|
router.get('/ports', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const containers = await dockerManager.getRunningContainers();
|
||||||
|
const portMappings: { [key: number]: string } = {};
|
||||||
|
|
||||||
|
containers.forEach((container: any) => {
|
||||||
|
if (container.Ports && container.State === 'running') {
|
||||||
|
container.Ports.forEach((port: any) => {
|
||||||
|
if (port.PublicPort) {
|
||||||
|
const serviceName = container.Labels?.['game-server'] ||
|
||||||
|
container.Names[0]?.replace('/', '') ||
|
||||||
|
'unknown';
|
||||||
|
portMappings[port.PublicPort] = serviceName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
activePorts: portMappings,
|
||||||
|
totalPorts: Object.keys(portMappings).length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting port information:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get port information' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { router as statusRouter };
|
||||||
247
node/src/services/dockerManager.ts
Normal file
247
node/src/services/dockerManager.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
import Docker from 'dockerode';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
|
export interface GameServerConfig {
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
ports: { [key: string]: number };
|
||||||
|
environment?: { [key: string]: string };
|
||||||
|
volumes?: string[];
|
||||||
|
restart?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerStatus {
|
||||||
|
name: string;
|
||||||
|
status: 'running' | 'stopped' | 'not-found';
|
||||||
|
containerId?: string;
|
||||||
|
ports?: { [key: string]: number };
|
||||||
|
uptime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DockerManager {
|
||||||
|
private docker: Docker;
|
||||||
|
private gameServers: Map<string, GameServerConfig>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.docker = new Docker();
|
||||||
|
this.gameServers = new Map();
|
||||||
|
this.initializeGameServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeGameServers() {
|
||||||
|
// Define available game servers
|
||||||
|
const servers: GameServerConfig[] = [
|
||||||
|
{
|
||||||
|
name: 'minecraft',
|
||||||
|
image: 'itzg/minecraft-server:latest',
|
||||||
|
ports: { '25565': 25565 },
|
||||||
|
environment: {
|
||||||
|
EULA: 'TRUE',
|
||||||
|
TYPE: 'VANILLA',
|
||||||
|
MEMORY: '2G'
|
||||||
|
},
|
||||||
|
volumes: ['/data/minecraft:/data'],
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'valheim',
|
||||||
|
image: 'lloesche/valheim-server:latest',
|
||||||
|
ports: { '2456': 2456, '2457': 2457 },
|
||||||
|
environment: {
|
||||||
|
SERVER_NAME: 'My Valheim Server',
|
||||||
|
WORLD_NAME: 'MyWorld',
|
||||||
|
SERVER_PASS: 'secret123'
|
||||||
|
},
|
||||||
|
volumes: ['/data/valheim:/config'],
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'terraria',
|
||||||
|
image: 'ryshe/terraria:latest',
|
||||||
|
ports: { '7777': 7777 },
|
||||||
|
environment: {
|
||||||
|
WORLD: 'MyWorld',
|
||||||
|
PASSWORD: 'secret123'
|
||||||
|
},
|
||||||
|
volumes: ['/data/terraria:/world'],
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
servers.forEach(server => {
|
||||||
|
this.gameServers.set(server.name, server);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRunningContainers(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const containers = await this.docker.listContainers();
|
||||||
|
return containers;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error listing containers:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getServerStatus(serviceName: string): Promise<ServerStatus> {
|
||||||
|
try {
|
||||||
|
const containers = await this.docker.listContainers({ all: true });
|
||||||
|
const container = containers.find(c =>
|
||||||
|
c.Names.some(name => name.includes(serviceName)) ||
|
||||||
|
c.Labels?.['game-server'] === serviceName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return {
|
||||||
|
name: serviceName,
|
||||||
|
status: 'not-found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = container.State === 'running' ? 'running' : 'stopped';
|
||||||
|
const ports: { [key: string]: number } = {};
|
||||||
|
|
||||||
|
if (container.Ports) {
|
||||||
|
container.Ports.forEach(port => {
|
||||||
|
if (port.PublicPort) {
|
||||||
|
ports[port.PrivatePort.toString()] = port.PublicPort;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: serviceName,
|
||||||
|
status,
|
||||||
|
containerId: container.Id,
|
||||||
|
ports,
|
||||||
|
uptime: status === 'running' ? this.calculateUptime(container.Created) : undefined
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error getting status for ${serviceName}:`, error);
|
||||||
|
return {
|
||||||
|
name: serviceName,
|
||||||
|
status: 'not-found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startServer(serviceName: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const config = this.gameServers.get(serviceName);
|
||||||
|
if (!config) {
|
||||||
|
return { success: false, message: `Unknown game server: ${serviceName}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if container already exists
|
||||||
|
const status = await this.getServerStatus(serviceName);
|
||||||
|
if (status.status === 'running') {
|
||||||
|
return { success: false, message: `${serviceName} is already running` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.containerId) {
|
||||||
|
// Container exists but is stopped, start it
|
||||||
|
const container = this.docker.getContainer(status.containerId);
|
||||||
|
await container.start();
|
||||||
|
logger.info(`Started existing container for ${serviceName}`);
|
||||||
|
} else {
|
||||||
|
// Create new container
|
||||||
|
const containerOptions = {
|
||||||
|
Image: config.image,
|
||||||
|
name: `gameserver-${serviceName}`,
|
||||||
|
Labels: {
|
||||||
|
'game-server': serviceName
|
||||||
|
},
|
||||||
|
Env: config.environment ? Object.entries(config.environment).map(([k, v]) => `${k}=${v}`) : [],
|
||||||
|
ExposedPorts: Object.keys(config.ports).reduce((acc, port) => {
|
||||||
|
acc[`${port}/tcp`] = {};
|
||||||
|
return acc;
|
||||||
|
}, {} as any),
|
||||||
|
HostConfig: {
|
||||||
|
PortBindings: Object.entries(config.ports).reduce((acc, [privatePort, publicPort]) => {
|
||||||
|
acc[`${privatePort}/tcp`] = [{ HostPort: publicPort.toString() }];
|
||||||
|
return acc;
|
||||||
|
}, {} as any),
|
||||||
|
Binds: config.volumes || [],
|
||||||
|
RestartPolicy: {
|
||||||
|
Name: config.restart || 'unless-stopped'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = await this.docker.createContainer(containerOptions);
|
||||||
|
await container.start();
|
||||||
|
logger.info(`Created and started new container for ${serviceName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: `${serviceName} started successfully` };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error starting ${serviceName}:`, error);
|
||||||
|
return { success: false, message: `Failed to start ${serviceName}: ${error}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopServer(serviceName: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const status = await this.getServerStatus(serviceName);
|
||||||
|
if (status.status !== 'running') {
|
||||||
|
return { success: false, message: `${serviceName} is not running` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.containerId) {
|
||||||
|
const container = this.docker.getContainer(status.containerId);
|
||||||
|
await container.stop();
|
||||||
|
logger.info(`Stopped container for ${serviceName}`);
|
||||||
|
return { success: true, message: `${serviceName} stopped successfully` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: `Could not find container for ${serviceName}` };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error stopping ${serviceName}:`, error);
|
||||||
|
return { success: false, message: `Failed to stop ${serviceName}: ${error}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listGameServers(): Promise<{ available: string[]; running: ServerStatus[] }> {
|
||||||
|
const available = Array.from(this.gameServers.keys());
|
||||||
|
const running: ServerStatus[] = [];
|
||||||
|
|
||||||
|
for (const serverName of available) {
|
||||||
|
const status = await this.getServerStatus(serverName);
|
||||||
|
if (status.status === 'running') {
|
||||||
|
running.push(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { available, running };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSystemStats(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const info = await this.docker.info();
|
||||||
|
const version = await this.docker.version();
|
||||||
|
return {
|
||||||
|
dockerVersion: version.Version,
|
||||||
|
containers: info.Containers,
|
||||||
|
containersRunning: info.ContainersRunning,
|
||||||
|
containersPaused: info.ContainersPaused,
|
||||||
|
containersStopped: info.ContainersStopped,
|
||||||
|
images: info.Images,
|
||||||
|
memoryLimit: info.MemoryLimit,
|
||||||
|
swapLimit: info.SwapLimit,
|
||||||
|
cpus: info.NCPU,
|
||||||
|
architecture: info.Architecture
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting system stats:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateUptime(created: number): string {
|
||||||
|
const now = Date.now();
|
||||||
|
const uptime = now - (created * 1000);
|
||||||
|
const hours = Math.floor(uptime / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
node/src/utils/logger.ts
Normal file
26
node/src/utils/logger.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import winston from 'winston';
|
||||||
|
|
||||||
|
const { combine, timestamp, errors, json, simple, colorize } = winston.format;
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
|
format: combine(
|
||||||
|
timestamp(),
|
||||||
|
errors({ stack: true }),
|
||||||
|
json()
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||||
|
new winston.transports.File({ filename: 'logs/combined.log' })
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// If not in production, log to console as well
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
logger.add(new winston.transports.Console({
|
||||||
|
format: combine(
|
||||||
|
colorize(),
|
||||||
|
simple()
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
67
node/test-api.js
Normal file
67
node/test-api.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Simple test script to demonstrate the Home Server Agent functionality
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
|
const API_BASE = 'http://localhost:3000';
|
||||||
|
const API_TOKEN = 'dev-token-123';
|
||||||
|
|
||||||
|
console.log('🎮 Home Server Agent Test Script\n');
|
||||||
|
|
||||||
|
// Helper function to make API requests
|
||||||
|
function makeRequest(endpoint, method = 'GET', needsAuth = true) {
|
||||||
|
const url = `${API_BASE}${endpoint}`;
|
||||||
|
let cmd;
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows PowerShell
|
||||||
|
if (needsAuth) {
|
||||||
|
cmd = `powershell "Invoke-RestMethod -Uri '${url}' -Method ${method} -Headers @{'Authorization'='Bearer ${API_TOKEN}'}"`;
|
||||||
|
} else {
|
||||||
|
cmd = `powershell "Invoke-RestMethod -Uri '${url}' -Method ${method}"`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unix-like systems
|
||||||
|
if (needsAuth) {
|
||||||
|
cmd = `curl -s -X ${method} -H "Authorization: Bearer ${API_TOKEN}" ${url}`;
|
||||||
|
} else {
|
||||||
|
cmd = `curl -s -X ${method} ${url}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = execSync(cmd, { encoding: 'utf8' });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error making request to ${endpoint}:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test sequence
|
||||||
|
console.log('1. Testing health check (no auth required)...');
|
||||||
|
const healthResult = makeRequest('/health', 'GET', false);
|
||||||
|
console.log(' ✅ Health check result:', healthResult ? 'OK' : 'FAILED');
|
||||||
|
|
||||||
|
console.log('\n2. Testing server status...');
|
||||||
|
const statusResult = makeRequest('/api/status');
|
||||||
|
console.log(' ✅ Server status retrieved');
|
||||||
|
|
||||||
|
console.log('\n3. Testing game server list...');
|
||||||
|
const listResult = makeRequest('/api/gameserver/list');
|
||||||
|
console.log(' ✅ Available game servers retrieved');
|
||||||
|
|
||||||
|
console.log('\n4. Testing API documentation...');
|
||||||
|
const rootResult = makeRequest('/', 'GET', false);
|
||||||
|
console.log(' ✅ API documentation retrieved');
|
||||||
|
|
||||||
|
console.log('\n5. Testing specific game server status...');
|
||||||
|
const minecraftStatus = makeRequest('/api/gameserver/minecraft/status');
|
||||||
|
console.log(' ✅ Minecraft server status retrieved');
|
||||||
|
|
||||||
|
console.log('\n🎉 All tests completed successfully!');
|
||||||
|
console.log('\nNext steps:');
|
||||||
|
console.log('- Start a game server: POST /api/gameserver/start/minecraft');
|
||||||
|
console.log('- Stop a game server: POST /api/gameserver/stop/minecraft');
|
||||||
|
console.log('- Check active ports: GET /api/status/ports');
|
||||||
|
console.log('\nNote: Game servers require Docker to be running.');
|
||||||
43
node/tsconfig.json
Normal file
43
node/tsconfig.json
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"noUncheckedSideEffectImports": false,
|
||||||
|
"types": ["node", "bun-types"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue